Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Object Composition FTW!

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

In modern PHP, you're going to spend a lot of time working with other people's classes: via external libraries that you bring into your project to get things done faster. Of course, when you do that: you can't actually edit their code if you need to change or add some behavior.

Fortunately, OO code gives us some really neat ways to deal with this limitation.

Modifying a Class without Modifying it?

For the next few minutes, I want you to pretend like our PDOShipStorage is actually from a third-party library. In other words, we can't modify it.

Now, let's say whenever we call fetchAllShipsData(), it's really important for us to log to a file, how many ships were found. But if we can't edit this file, how can we do that?

Using Inheritance

There's actually two ways to do this, and both are pretty awesome. The first way is to create a new class that extends PDOShipStorage, like LoggablePDOShipStorage, and override some methods to add logging.

Nah, Use Composition

But forget that, let's skip to a better method called composition. First, create a new class in the Service directory called LoggableShipStorage, but do not extend PDOShipStorage:

... lines 1 - 2
namespace Service;
class LoggableShipStorage implements ShipStorageInterface
{
... lines 7 - 32
}

Now, the only rule for any ship storage object is that it needs to implement the ShipStorageInterface. Add that, and then go to our handy "Code"->"Generate" method to implement the 2 methods we need:

... lines 1 - 4
class LoggableShipStorage implements ShipStorageInterface
{
... lines 7 - 13
public function fetchAllShipsData()
{
... lines 16 - 20
}
public function fetchSingleShipData($id)
{
... line 25
}
... lines 27 - 32
}

So far, this is how every ship storage starts.

But LoggableShipStorage will not actually do any of the ship-loading work - it'll offload all that hard work to some other ship storage object, like PDOShipStorage. To do that, add a new private $shipStorage property and a public function __construct() method that accepts one ShipStorageInterface argument. Then, set that value onto the $shipStorage property:

... lines 1 - 4
class LoggableShipStorage implements ShipStorageInterface
{
private $shipStorage;
public function __construct(ShipStorageInterface $shipStorage)
{
$this->shipStorage = $shipStorage;
}
... lines 13 - 32
}

For fetchAllShipData(), just return $this->shipStorage->fetchAllShipsData(). Repeat for the other method: return $this->shipStorage->fetchSingleShipData():

... lines 1 - 4
class LoggableShipStorage implements ShipStorageInterface
{
... lines 7 - 22
public function fetchSingleShipData($id)
{
return $this->shipStorage->fetchSingleShipData($id);
}
... lines 27 - 32
}

We've now created a wrapper object that offloads all of the work to an internal ship storage object. This is composition: you put one object inside of another.

To use the new class, open up Container. Inside getShipStorage(), add $this->shipStorage = new LoggableShipStorage() and pass it $this->shipStorage, which is the PDOShipStorage object:

... lines 1 - 4
class Container
{
... lines 7 - 51
public function getShipStorage()
{
if ($this->shipStorage === null) {
$this->shipStorage = new PdoShipStorage($this->getPDO());
//$this->shipStorage = new JsonFileShipStorage(__DIR__.'/../../resources/ships.json');
// use "composition": put the PdoShipStorage inside the LoggableShipStorage
$this->shipStorage = new LoggableShipStorage($this->shipStorage);
}
... lines 61 - 62
}
... lines 64 - 75
}

We've just pulled a "fast one" on our application: our entire app thinks we're using PDOShipStorage, but we just changed that! If you refresh now, nothing is different: everything still eventually goes through the PDOShipStorage object.

But now, we have the opportunity to add more functionality - or to change functionality - in either of these methods.

Add some Logging!

To give a really simple example, replace the return statement with $ships = and add return $ships below that:

... lines 1 - 4
class LoggableShipStorage implements ShipStorageInterface
{
... lines 7 - 13
public function fetchAllShipsData()
{
$ships = $this->shipStorage->fetchAllShipsData();
... lines 17 - 19
return $ships;
}
... lines 22 - 32
}

Between, we could call some new log() method, passing it a string like: just fetched %s ships - passing that a count() of $ships:

... lines 1 - 4
class LoggableShipStorage implements ShipStorageInterface
{
... lines 7 - 13
public function fetchAllShipsData()
{
$ships = $this->shipStorage->fetchAllShipsData();
$this->log(sprintf('Just fetched %s ships', count($ships)));
return $ships;
}
... lines 22 - 32
}

Below, add a new private function log() with a $message argument:

... lines 1 - 4
class LoggableShipStorage implements ShipStorageInterface
{
... lines 7 - 27
private function log($message)
{
// todo - actually log this somewhere, instead of printing!
echo $message;
}
}

You should do something more intelligent in a real app, but to prove it's working, echo that message.

Let's refresh! There's our message!

Why is Composition Cool?

Wrapping one object inside of another like this is called composition. You see, when you want to change the behavior of an existing class, the first thing we always think of is

Oh, just extend that class and override some methods

But composition is another option, and it does have some subtle advantages. If we had extended PDOShipStorage and then later wanted to change back to our JsonFileShipStorage, then all of a sudden we would need to change our LoggableShipStorage to extend JsonFileShipStorage. But with composition, our wrapper class can work with any ShipStorageInterface. We could change just one line to go back to loading files from JSON and not lose our logging.

This isn't always a ground-breaking difference, but this is what people mean when they talk about "composition over inheritance".

Alright guys! I have tried to think of all the weird stuff that we haven't talked about with object oriented coding, and I've run out! You are now super qualified with this stuff - so get out there, find some classes, find some interfaces, make some traits, do some good, and just keep practicing. It's going to sink in more and more over time, and serve you for years to come, in many different languages.

See you next time!

Leave a comment!

23
Login or Register to join the conversation
Default user avatar
Default user avatar Jeremy Carlson | posted 5 years ago

Thank you for this series of tutorials. This has done wonders for my understanding of OO PHP. Truly helpful!

16 Reply

Wooo! Cheers Jeremy Carlson :)

Reply
Aristotele T. Avatar
Aristotele T. Avatar Aristotele T. | posted 3 years ago

Brilliant series, thank you so much guys! <3

1 Reply

Hey Aristotele,

Thank you for you feedback! It made our day! :)

Cheers!

Reply
Default user avatar
Default user avatar Brian Morris | posted 5 years ago

Thank you for the wonderful and informative series of tutorials. Coming from old-skool php land this has brought me up to speed in a hurry. Now to go code something!

1 Reply

Hey Brian!

We are glad to hear that you find useful our tutorials, we make them with passion ;)

Cheers!

Reply

Hi, what is the difference with dependency injection? That dependencies are required to work, while composition is just a wrapper?

Reply

Hey Naschkatze,

Composition and dependency injection could be seen as the same thing because you achieve the same goal, which is composing your classes. For example, a big method could be split into two different classes and just inject (adding a property field), let's say, classB into classA, so classA would make a call to some method on classB. Does it make sense to you?

Cheers!

1 Reply
Manuel rodrigo Z. Avatar
Manuel rodrigo Z. Avatar Manuel rodrigo Z. | posted 1 year ago

Finally done with OOP Full Course.

It's been a really nice experience full of fun.
Great examples, great way to explain the topics, but in some cases a few exercises were a little bit confusing!.

I'll definitely keep studying the rest of the courses!!!.

Reply

Hey Roi!

Congrats! You did a huge job finishing all the episodes in OOP :) And we're happy to hear it was useful for you! If something was confusing - feel free to leave a comment below the related chapter. Fairly speaking, yeah... there might be some tricky challenges, that's because we just wanted to make things not that easy at the fist sight ;)

Good luck with further learning!

Cheers!

Reply
Guillermo P. Avatar
Guillermo P. Avatar Guillermo P. | posted 3 years ago

absolutely brilliant!. thanks guys!

Reply
Default user avatar

Hi ! How come the log says "just fectched 4 ships", whereas there are 5 ships ? And why doesn't Slave I (Bounty Hunter) have any Jedi Factor ? Thank you in advance !

Reply

Hey @Ana

Nice question! If you look at ShipLoader::getShips() you will notice that it hardcodes an extra Ship into the array, so the Logger will never know about it. I hope it anwsers to your question :)

Cheers!

Reply
Default user avatar

So in this example our class LoggableShipStorage is actually implements a Proxy pattern?
I wish someday your Design Patterns course will be completed :)

Reply

Hey Ivan!

Actually, yes! There are many different use-cases for the proxy pattern, but this is certainly one of them: the LoggableShipStorage is effectively a proxy for whatever other ship storage we pass into it. In fact, it fits the "smart reference" proxy pattern that Marco (core Doctrine contributor) talks about in his presentation here: http://ocramius.github.io/p...

Cheers!

Reply
Default user avatar
Default user avatar Hakim Ch | posted 5 years ago

In this case, use the composition, but if I have only one handler, can I use the inheritance?

Reply

Totally :). I think using inheritance is a bit more common when people develop their *own* applications, as it's a bit more straightforward, and you probably don't need the flexibility that composition gives you. This happens a lot actually: if you're building a re-usable library, then you often need to code things "more correct" than you should need to in your own code :).

Cheers!

Reply
Max S. Avatar

Typo in challenge one:
But here's the challenge: the existing PlanetRenderer returns a div **aroudn** the plane.

Reply

Laaaame :). Thanks a lot for reporting that - fixed at https://github.com/knpunive... I wanted to give you a GitHub shout-out, but couldn't find your user!

Cheers!

Reply
Max S. Avatar

You couldn't as I wasn't registered until now ;)

Reply
Max S. Avatar

And another remark: at 2:30 you are saying return (as it's written in the script), but fetchSingleShipData does not return anything... :)

Reply

When I mess up (i.e. by forgetting the return) we keep the code and script correct :). But we'll add a note about the missing return.

Thanks!

Reply
Cat in space

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

userVoice