Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

AbstractShipStorage

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 $6.00

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:

19 lines bootstrap.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 of PdoShipStorage, instance of JsonFileShipStorage 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.

Creating a "Ship storage" contract

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:

20 lines bootstrap.php
... 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!

Leave a comment!

10
Login or Register to join the conversation
Marc R. Avatar
Marc R. Avatar Marc R. | posted 3 years ago

Reminder :
Pay attention to the order of "require" in bootstrap.php.
AbstractShipStorage must be included before PdoShipStorage and JsonFileShipStorage.

Reply

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!

Reply
Jeffrey C. Avatar
Jeffrey C. Avatar Jeffrey C. | posted 4 years ago

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 :)

Reply

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!

Reply
Default user avatar

Hey,

Since we're creating an AbstractShipStorage class, shouldn't the title of this video be: AbstractShipStorage?

Kind regards,

Ben

Reply

Hey Ben,

Yes, you're totally right! I fixed it in https://github.com/knpunive... - thanks for letting us know!

Cheers!

Reply
Default user avatar
Default user avatar Phil | posted 5 years ago | edited

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.

Reply

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!

Reply
Default user avatar

Thanks - good to know it is what I thought, not an actual PHP error but just a bit of a course code sanity check.

Reply
Cat in space

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

userVoice