Abstracting a Class into 2 Smaller Pieces
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.
To get our ships we use ShipLoader
which queries the database
and creates ship objects. This queryForShips()
goes out, selects all
the ships, and then later it is passed to this nice createShipFromData()
function down here:
// ... lines 1 - 2 | |
class ShipLoader | |
{ | |
// ... lines 5 - 14 | |
public function getShips() | |
{ | |
$ships = array(); | |
$shipsData = $this->queryForShips(); | |
foreach ($shipsData as $shipData) { | |
$ships[] = $this->createShipFromData($shipData); | |
} | |
return $ships; | |
} | |
// ... lines 27 - 58 | |
} | |
// ... lines 60 - 61 |
This is the one we've been working in that creates the objects.
- Step 1: Query the database
- Step 2: Turn that data into objects
Suppose that we have a new requirement, sometimes we're going to get the ship data from the database but other times it will come from a different source, like a JSON file.
In the resources directory there's a new ship.json
file, as you can see this
holds the same info as we have in the database:
// ... line 1 | |
[ | |
{ | |
"id": "1", | |
"name": "Jedi Starfighter", | |
"weapon_power": "5", | |
"jedi_factor": "15", | |
"strength": "30", | |
"team": "rebel" | |
}, | |
// ... lines 11 - 26 | |
{ | |
"id": "4", | |
"name": "RZ-1 A-wing interceptor", | |
"weapon_power": "4", | |
"jedi_factor": "4", | |
"strength": "50", | |
"team": "empire" | |
} | |
] |
Now why would we want our application to sometimes load from the database and other times from a JSON file? Say that when we're developing locally we don't have access to our database, so we use a JSON file. But when we push to production we'll switch back to the real database. Or, suppose that our ship library is so awesome that someone else wants to reuse it. However, this fan doesn't use a database, they only load them from JSON.
This leaves us needing to make our ShipLoader
more generic.
Right now, all of the logic of querying things from the database is hardcoded in here. So let's create a new class whose only job is to load ship data through the database, or PDO.
Create a new class called PdoShipStorage
:
// ... lines 1 - 2 | |
class PdoShipStorage | |
{ | |
// ... lines 5 - 31 | |
} |
Looking back inside ShipLoader
there are two types of queries that we make:
// ... lines 1 - 2 | |
class ShipLoader | |
{ | |
// ... lines 5 - 31 | |
public function findOneById($id) | |
{ | |
$statement = $this->getPDO()->prepare('SELECT * FROM ship WHERE id = :id'); | |
// ... lines 35 - 42 | |
} | |
// ... lines 45 - 68 | |
private function queryForShips() | |
{ | |
$statement = $this->getPDO()->prepare('SELECT * FROM ship'); | |
// ... lines 72 - 75 | |
} | |
} | |
// ... lines 78 - 79 |
Sometimes we query for all of the ships and sometimes we query for just one ship by ID.
Back to our PdoShipStorage
I'll create two methods, to cover both of those actions. First,
create a public function fetchAllShipsData()
which we'll fill out in just one second. Now,
add public function fetchSingleShipData()
and pass it the id that we want to query for:
// ... lines 1 - 2 | |
class PdoShipStorage | |
{ | |
// ... lines 5 - 11 | |
public function fetchAllShipsData() | |
{ | |
// ... lines 14 - 17 | |
} | |
public function fetchSingleShipData($id) | |
{ | |
// ... lines 22 - 30 | |
} | |
} |
Before we go any further head back to our bootstrap.php
file and make sure that we require this:
// ... lines 1 - 14 | |
require_once __DIR__.'/lib/Service/PdoShipStorage.php'; | |
// ... lines 16 - 19 |
Perfect!
What I want to do is move all the querying logic from ShipLoader
into this PdoShipStorage
class.
Let's start with the logic that queries for one ship and pasting that over here:
// ... lines 1 - 2 | |
class PdoShipStorage | |
{ | |
// ... lines 5 - 19 | |
public function fetchSingleShipData($id) | |
{ | |
$statement = $this->pdo->prepare('SELECT * FROM ship WHERE id = :id'); | |
$statement->execute(array('id' => $id)); | |
$shipArray = $statement->fetch(PDO::FETCH_ASSOC); | |
if (!$shipArray) { | |
return null; | |
} | |
return $shipArray; | |
} | |
} |
Notice, that we're not returning an object here this is just a really dumb class that returns data, an array in our case.
There is one problem, we have a getPdo()
function inside of ShipLoader
that references a pdo
property. Point being, our PDO storage needs access to the PDO object, so we're going to use
dependency injection, a topic we covered a lot in
episode 2 . Add
public function __construct(PDO $pdo)
and store it as a property with
$this->pdo = $pdo;
:
// ... lines 1 - 2 | |
class PdoShipStorage | |
{ | |
private $pdo; | |
public function __construct(PDO $pdo) | |
{ | |
$this->pdo = $pdo; | |
} | |
// ... lines 11 - 31 | |
} |
If this pattern is new to you just head back and watch the dependency injection video in episode 2 of the OO series.
Here we're saying, whomever creates our PDO ship storage class must pass in the pdo object. This is cool because we need it. Now I can just reference the property there directly.
Back in ShipLoader
copy the entire queryForShips()
and paste that
into fetchAllShipsData()
and once again reference the pdo property:
// ... lines 1 - 2 | |
class PdoShipStorage | |
{ | |
// ... lines 5 - 11 | |
public function fetchAllShipsData() | |
{ | |
$statement = $this->pdo->prepare('SELECT * FROM ship'); | |
$statement->execute(); | |
return $statement->fetchAll(PDO::FETCH_ASSOC); | |
} | |
// ... lines 19 - 31 | |
} |
Now we have a class whose only job is to query for ship stuff, we're not using it anywhere yet, but it's fully
ready to go. So let's use this inside of ShipLoader
instead of the PDO object. Since we don't need PDO to be
passed anymore swap that out for a PdoShipStorage
object. Let's update that in a few other places and change
the property to be called shipStorage
:
// ... lines 1 - 2 | |
class ShipLoader | |
{ | |
private $shipStorage; | |
public function __construct(PdoShipStorage $shipStorage) | |
{ | |
$this->shipStorage = $shipStorage; | |
} | |
// ... lines 11 - 58 | |
} | |
// ... lines 60 - 61 |
Cool!
Down in getShips()
we used to call $this->queryForShips();
but we don't need to do that anymore! Instead,
say $this->shipStorage->fetchAllShipsData();
:
// ... lines 1 - 2 | |
class ShipLoader | |
{ | |
// ... lines 5 - 54 | |
private function queryForShips() | |
{ | |
return $this->shipStorage->fetchAllShipsData(); | |
} | |
} | |
// ... lines 60 - 61 |
Perfect, now scroll down and get rid of the queryForShips()
function all together:
we're not using that anymore. And while we're cleaning things out also delete this getPDO()
function.
We can delete this because up here where we reference it in findOneById()
we'll do the same thing.
Remove all the pdo querying logic, and instead say shipArray = $this->shipStorage->fetchSingleShipData();
and pass it the ID:
// ... lines 1 - 2 | |
class ShipLoader | |
{ | |
// ... lines 5 - 31 | |
public function findOneById($id) | |
{ | |
$shipArray = $this->shipStorage->fetchSingleShipData($id); | |
// ... lines 35 - 36 | |
} | |
// ... lines 38 - 58 | |
} | |
// ... lines 60 - 61 |
This class now has no query logic anywhere.
All we know is that we're passed in some PdoShipStorage
object and we're able to call methods on it. It can
make the queries and talk to whatever database it wants to, that's it's responsibility. In here we're just
calling methods instead of actually querying for things.
ShipLoader
and PdoShipStorage
are now fully setup and functional. The last step is going into our container
which is responsible for creating all of our objects to make a couple of changes. For example, when we
have new ShipLoader
we don't want to pass a pdo object anymore we want to pass in PdoShipStorage
.
Just like before, create a new function called getShipStorage()
and make sure we have our property up above.
The getShipStorage()
method is going to do exactly what you expect it to do. Instantiate a new PdoShipStorage
and return it. The ship's storage class does need PDO as its first constructor argument which we do with
new PdoShipStorage($this->getPDO());
:
// ... lines 1 - 2 | |
class Container | |
{ | |
// ... lines 5 - 12 | |
private $shipStorage; | |
// ... lines 15 - 49 | |
public function getShipStorage() | |
{ | |
if ($this->shipStorage === null) { | |
$this->shipStorage = new PdoShipStorage($this->getPDO()); | |
} | |
return $this->shipStorage; | |
} | |
// ... lines 58 - 69 | |
} |
Up in getShipLoader()
, now pass $this->getShipStorage()
:
// ... lines 1 - 2 | |
class Container | |
{ | |
// ... lines 5 - 40 | |
public function getShipLoader() | |
{ | |
if ($this->shipLoader === null) { | |
$this->shipLoader = new ShipLoader($this->getShipStorage()); | |
} | |
// ... lines 46 - 47 | |
} | |
// ... lines 49 - 69 | |
} |
Everything used to be in ShipLoader
, including the query logic. We've now split things up so that the query
logic is in PdoShipStorage
and in ShipLoader
you're just calling methods on the shipStorage
. Its real
job is to create the objects from the data, wherever that data came from. In Container.php
we've wired
all this stuff up.
Phew, that was a lot of coding we just did, but when we go to the browser and refresh,
everything still works exactly the same as before. That was a lot of internal refactoring.
In index.php
as always we still have $shipLoader->getShips()
:
// ... lines 1 - 6 | |
$ships = $shipLoader->getShips(); | |
// ... lines 8 - 126 |
And that function still works as it did before, but the logic is now separated into two pieces.
The cool thing about this is that our classes are now more focused and broken into smaller pieces. Initially we didn't need to do this, but once we had the new requirement of needing to load ships from a JSON file this refactoring became necessary. Now let's see how to actually load things from JSON instead of PDO.
All challenges from this course are gone (
What happened?