If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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.
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?
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.
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.
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!
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!
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!
Hey Brian!
We are glad to hear that you find useful our tutorials, we make them with passion ;)
Cheers!
Hi, what is the difference with dependency injection? That dependencies are required to work, while composition is just a wrapper?
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!
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!!!.
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!
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 !
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!
So in this example our class LoggableShipStorage is actually implements a Proxy pattern?
I wish someday your Design Patterns course will be completed :)
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!
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!
Typo in challenge one:
But here's the challenge: the existing PlanetRenderer returns a div **aroudn** the plane.
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!
And another remark: at 2:30 you are saying return (as it's written in the script), but fetchSingleShipData does not return anything... :)
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!
Thank you for this series of tutorials. This has done wonders for my understanding of OO PHP. Truly helpful!