Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Traits: "Horizontal" Reuse

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

Ok team: we need a new ship class - a BountyHunterShip. Start simple: in the model directory, add a new class: BountyHunterShip. Once again, PhpStorm already added the correct namespace for us:

... lines 1 - 2
namespace Model;
class BountyHunterShip extends AbstractShip
{
... lines 7 - 20
}

Like every other ship, extend AbstractShip. Ah, but we do not need a use statement for this: that class lives in the same namespace as us.

Just like with an interface, when you extend an abstract class, you usually need to implement some methods. Go back to "Code"->"Generate"->"Implement Methods". Select the 3 that this class needs:

... lines 1 - 4
class BountyHunterShip extends AbstractShip
{
public function getJediFactor()
{
// TODO: Implement getJediFactor() method.
}
public function getType()
{
// TODO: Implement getType() method.
}
public function isFunctional()
{
// TODO: Implement isFunctional() method.
}
}

Great!

Now, bounty hunter ships are interesting for a few reasons. First, they're never broken: those scrappy bounty hunters can always get the ship started. For isFunctional(), return true:

... lines 1 - 4
class BountyHunterShip extends AbstractShip
{
... lines 7 - 18
public function isFunctional()
{
return true;
}
... lines 23 - 27
}

For getType(), return Bounty Hunter:

... lines 1 - 4
class BountyHunterShip extends AbstractShip
{
... lines 7 - 13
public function getType()
{
return 'Bounty Hunter';
}
... lines 18 - 27
}

Simple. But the jediFactor will vary ship-by-ship. Add a JediFactor property and return that from inside getJediFactor():

... lines 1 - 4
class BountyHunterShip extends AbstractShip
{
private $jediFactor;
public function getJediFactor()
{
return $this->jediFactor;
}
... lines 13 - 27
}

At the bottom of the class add a public function setJediFactor() so that we can change this property: $this->jediFactor = $jediFactor:

... lines 1 - 4
class BountyHunterShip extends AbstractShip
{
... lines 7 - 23
public function setJediFactor($jediFactor)
{
$this->jediFactor = $jediFactor;
}
}

Cool!

To get one of these into our system, let's do something simple. Open ShipLoader. At the bottom of getShips(), add a new ship to the collection: $ships[] = new BountyHunterShip() called 'Slave I' - Boba Fett's famous ship:

... lines 1 - 10
class ShipLoader
{
... lines 13 - 22
public function getShips()
{
... lines 25 - 28
foreach ($shipsData as $shipData) {
$ships[] = $this->createShipFromData($shipData);
}
// Boba Fett's ship
$ships[] = new BountyHunterShip('Slave I');
... lines 34 - 35
}
... lines 37 - 74
}

Ok, head back and refresh! Yes! Slave I - Bounty Hunter, and it's not broken. That was easy.

Code Duplication

So, what's the problem? Look at BountyHunterShip and also look at Ship: there's some duplication. Both classes have a jediFactor property, a getJediFactor() method that returns this, and a setJediFactor that changes it.

Duplication is a bummer. How can we fix this? Well, we could use inheritance. But in this case, it's weird.

For example, we could make BountyHunterShip extend Ship, but then it would inherit this extra stuff that we don't really want or need. We could make it work, but I just don't like it.

Ok, what about making Ship extend BountyHunterShip? That just completely feels wrong: philosophically, not all Ships are BountyHunterShips - it's just not the right way to model these classes.

Are we stuck? What we want is a way to just share these 3 things: the jediFactor property, getJediFactor() and setJediFactor(). When you only need to share a few things, the right answer might be a trait.

Hello Mr Trait

Let's see what this trait thing is. In the Model directory, create a new PHP class called SettableJediFactorTrait. Now, change the class keyword to trait. Traits look and feel exactly like a normal class:

... lines 1 - 2
namespace Model;
trait SettableJediFactorTrait
{
... lines 7 - 17
}

In fact, open up BountyHunterShip and move the property and first method into the trait. Also grab setJediFactor() and put that in the trait too:

... lines 1 - 4
trait SettableJediFactorTrait
{
private $jediFactor;
public function getJediFactor()
{
return $this->jediFactor;
}
public function setJediFactor($jediFactor)
{
$this->jediFactor = $jediFactor;
}
}

The only difference between classes and traits is that traits can't be instantiated directly. Their purpose is for sharing code.

In BountyHunterShip, we can effectively copy and paste the contents of that trait into this class by going inside the class and adding use SettableJediFactorTrait:

... lines 1 - 4
class BountyHunterShip extends AbstractShip
{
use SettableJediFactorTrait;
... lines 8 - 17
}

That use statement has nothing to do with the namespace use statements: it's just a coincidence. As soon as we do this, when PHP runs, it will copy the contents of the trait and pastes them into this class right before it executes our code. It's as if all the code from the trait actually lives inside this class.

And now, we can do the same thing inside of Ship: remove the jediFactor property and the two methods. At the top, use SettableJediFactorTrait:

... lines 1 - 4
class Ship extends AbstractShip
{
use SettableJediFactorTrait;
... lines 8 - 27
}

Give it a try! Refresh. No errors! In fact, nothing changes at all. This is called horizontal reuse: because you're not extending a parent class, you're just using methods and properties from other classes.

This is perfect for when you have a couple of classes that really don't have that much in common, but do have some shared functionality. Traits are also cool because you cannot extend multiple classes, but you can use multiple traits.

Leave a comment!

4
Login or Register to join the conversation
Default user avatar
Default user avatar Diederik Depourcq | posted 5 years ago

Hi, I understand that if you want to explain traits, you have to use them in an example but I wondered what the difference is between the use of traits and adding another superclass above ship and bountyhuntership. Is the use of traits less OO than the latter? Is the practice of traits as accepted as adding another class to inherit from?
I presume an alternative is to create another abstract class jediShip that extends abstractShip. In that case ship and bountyhuntership are subclasses of jediship.

Reply

Hey Diederik Depourcq

Nice question, and here is what I think about traits

They were made to make code more reusable between multiple classes without affecting the system architecture, and to satisfy the need of multiple inheritance (In PHP you can inherit only from one class).
I usually don't create traits, because I use dependency injection a lot (Symfony makes it super easy), but I still think that traits can be great if you use them wisely :)

Cheers!

Reply
Default user avatar
Default user avatar Diederik Depourcq | MolloKhan | posted 5 years ago

Ok cool. So in a way Traits wants to solve the limitation of "only one inheritance allowed". Thanks!

Reply

Hey Diederik,

Yep, some kind of it. Or, in other words, they allow you to share code (and avoid code duplications) without inheritance at all - in some cases it makes sense.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

userVoice