If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Our goal is to make ShipLoader
load things from the database or from a JSON file.
In the resources directory I've already created a JsonFileShipStorage
class.
Copy that into the service directory and let's take a look inside of here:
... lines 1 - 2 | |
class JsonFileShipStorage | |
{ | |
private $filename; | |
public function __construct($jsonFilePath) | |
{ | |
$this->filename = $jsonFilePath; | |
} | |
public function fetchAllShipsData() | |
{ | |
$jsonContents = file_get_contents($this->filename); | |
return json_decode($jsonContents, true); | |
} | |
public function fetchSingleShipData($id) | |
{ | |
$ships = $this->fetchAllShipsData(); | |
foreach ($ships as $ship) { | |
if ($ship['id'] == $id) { | |
return $ship; | |
} | |
} | |
return null; | |
} | |
} |
It has all of the same methods as PdoShipStorage
. Except that this loads from a JSON file
instead of querying from a database. Let's try and use this in our project.
First, head over to bootstrap
of course and require JsonFileShipStorage.php
:
... lines 1 - 15 | |
require_once __DIR__.'/lib/Service/JsonFileShipStorage.php'; | |
... lines 17 - 19 |
In theory since this class has all the same methods as PdoShipStorage
we
should be able to pass a JsonFileShipStorage
object into ShipLoader
and everything
should just work. Really, the only thing ShipLoader
should care about is that
it's passed an object that has the two methods it's calling: fetchAllShipsData()
and
fetchSingleShipData()
:
... lines 1 - 2 | |
class ShipLoader | |
{ | |
... lines 5 - 31 | |
public function findOneById($id) | |
{ | |
$shipArray = $this->shipStorage->fetchSingleShipData($id); | |
... lines 35 - 36 | |
} | |
... lines 38 - 54 | |
private function queryForShips() | |
{ | |
return $this->shipStorage->fetchAllShipsData(); | |
} | |
} | |
... lines 60 - 61 |
In Container
let's give this a try. Down in getShipStorage()
let's say,
$this->shipStorage = new JsonFileShipStorage()
. And we'll give it a path to our JSON
of __DIR__.'/../../resources/ships.json'
:
... lines 1 - 2 | |
class Container | |
{ | |
... lines 5 - 49 | |
public function getShipStorage() | |
{ | |
if ($this->shipStorage === null) { | |
//$this->shipStorage = new PdoShipStorage($this->getPDO()); | |
$this->shipStorage = new JsonFileShipStorage(__DIR__.'/../../resources/ships.json'); | |
} | |
... lines 56 - 57 | |
} | |
... lines 59 - 70 | |
} |
From this directory I'm going up a couple of levels, into resources
and pointing
at this ships.json
file which holds all of our ship info:
... 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" | |
} | |
] |
Back to the browser and refresh. Ok no success yet, but as they say, try try again. Before we do that, let's check out this error:
Argument 1 passed to
ShipLoader::__construct()
must be an instance ofPdoShipStorage
, instance ofJsonFileShipStorage
given.
What's happening here is that in ShipLoader
we have this type-hint which says that
we only accept PdoShipStorage
and our Json file is not an instance of that:
... lines 1 - 2 | |
class ShipLoader | |
{ | |
... lines 5 - 6 | |
public function __construct(PdoShipStorage $shipStorage) | |
{ | |
... line 9 | |
} | |
... lines 11 - 58 | |
} | |
... lines 60 - 61 |
The easiest way to fix this is to say extends PdoShipStorage
in JsonFileShipStorage
:
... lines 1 - 2 | |
class JsonFileShipStorage extends PdoShipStorage | |
... lines 4 - 32 |
This makes the json file an instance of PdoShipStorage
. Try refreshing that again.
Perfect, our site is working.
But having to put that extends in our JSON file kinda sucks, when we do this we're overriding every single method and getting some extra stuff that we aren't going to use.
Instead, you should be thinking, "This is a good spot for Abstract Ship Storage!" And well, I
agree so let's create that. Inside the Service
directory add a new PHP Class called
AbstractShipStorage
. The two methods that this is going to need to have are: fetchSingleShipData()
and fetchAllShipsData()
so I'll copy both of those and paste them over to our new class.
Of course we don't have any body in these methods, so remove that. Now, set this as an abstract
class.
Make both of the functions abstract
as well:
... lines 1 - 2 | |
abstract class AbstractShipStorage | |
{ | |
abstract public function fetchAllShipsData(); | |
abstract public function fetchSingleShipData($id); | |
} |
Cool!
Now, JsonFileShipStorage
can extend AbstractShipStorage
:
... lines 1 - 2 | |
class JsonFileShipStorage extends AbstractShipStorage | |
... lines 4 - 32 |
And the same thing for PdoShipStorage
:
... lines 1 - 2 | |
class PdoShipStorage extends AbstractShipStorage | |
... lines 4 - 33 |
With this setup we know that if we have a AbstractShipStorage
it will definitely have both of those
methods so we can go into the ShipLoader
and change this type hint to AbstractShipStorage
because
we don't care which of the two storage classes are actually passed:
... lines 1 - 2 | |
class ShipLoader | |
{ | |
... lines 5 - 6 | |
public function __construct(AbstractShipStorage $shipStorage) | |
... lines 8 - 58 | |
} | |
... lines 60 - 61 |
To be very well behaved developers, we'll go into our Container
and above getShipStorage()
change
the type hint to AbstractShipStorage
. Not a requirement, but it is a good idea.
Go back to the browser and refresh... oh, class AbstractShipStorage
not found because we forgot to require it
in our bootstrap
file. We will eventually fix the need to have all of these require statements:
... lines 1 - 14 | |
require_once __DIR__.'/lib/Service/AbstractShipStorage.php'; | |
... lines 16 - 20 |
Refresh again and now it works perfectly.
We created an AbstractShipStorage
because it allows us to make our ShipLoader
more generic. It now
doesn't care which one is passed, as long as it extends AbstractShipStorage
.
But there's an even better way to handle this... interfaces!
You're 100% right :). And isn't that annoying? You'll love episode 4 where we show how this is solved with "autoload" ;) https://symfonycasts.com/sc...
Cheers!
Hi,
After the AbstractShipStorage i get the following error
"Argument 1 in de __constuct() must be an instance of AbstractShopStorage, array given, called in index.php on line 6"
In my index.php it's the following line:
"$container = new Container($configuration);"
So i don't quite get why i have this error.
Kind regards,
Emin
UPDATE
I accedently put in mu __construct AbstractShipStorage instead array so once i replaced those it works :)
Hey Emin,
Glad you had got it working before we replied to you! That's why typehinting is great, it helps to discover the problem earlier. :)
Cheers!
Hey,
Since we're creating an AbstractShipStorage class, shouldn't the title of this video be: AbstractShipStorage?
Kind regards,
Ben
Hey Ben,
Yes, you're totally right! I fixed it in https://github.com/knpunive... - thanks for letting us know!
Cheers!
Why can't I make the concrete function getRadius() in AbstractPlanet like:
abstract class AbstractPlanet
{
private $radius;
abstract public function getHexColor();
public function getRadius()
{
return $this->radius;
}
}
SolidPlanet uses it just happily as-is.
GasPlanet stores radius as the property (rather than uselessly storing diameter - which seemed just to be different):
class GasPlanet extends AbstractPlanet
{
private $mainElement;
public function __construct($mainElement, $diameter)
{
$this->radius = $diameter / 2;
$this->mainElement = $mainElement;
}
public function getHexColor()
{
// a "fake" map of elements to colors
// code unchanged here
}
}
But the exercise tells me that getRadius() must be declared abstract in AbstractPlanet. I thought that it was perfectly fine to have some concrete methods in an abstract class??
That would happen when there is a method body that applies appropriately to all (or most) subclasses that extend the abstract class.
Hey Phil,
Yeah, good question! Actually, you totally can do it on practice. The error you see is not a PHP internal one, just a user error triggered by our validation system - we just do not cover all possible cases. In this challenge we suppose do not change the current implementation much. Let's say, for simplicity, we need to store diameter for GasPlanet and radius for SolidPlaned, i.e. we want to keep current implementation, just make common methods abstract and extend the AbstractPlanet class. Anyway, you just overthought this challenge a bit, our tasks were much easier. But it's good you think about it, great job!
Cheers!
Thanks - good to know it is what I thought, not an actual PHP error but just a bit of a course code sanity check.
Reminder :
Pay attention to the order of "require" in bootstrap.php.
AbstractShipStorage must be included before PdoShipStorage and JsonFileShipStorage.