Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

An Army of Service Classes

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Yay! We got rid of a flat function. Woh - not so fast: inside battle(), we're calling a flat function: didJediDestroyShipUsingTheForce():

... lines 1 - 2
class BattleManager
{
... lines 5 - 9
public function battle(Ship $ship1, $ship1Quantity, Ship $ship2, $ship2Quantity)
{
... lines 12 - 18
if (didJediDestroyShipUsingTheForce($ship1)) {
... lines 20 - 23
}
... lines 25 - 56
}
}

No bueno!

Refactoring to private Functions

This lives at the bottom of functions.php. In our app, this is only called from inside battle(), and since it obviously relates to battles, let's move it into BattleManager. Make it a private function:

... lines 1 - 2
class BattleManager
{
... lines 5 - 58
private function didJediDestroyShipUsingTheForce(Ship $ship)
{
$jediHeroProbability = $ship->getJediFactor() / 100;
return mt_rand(1, 100) <= ($jediHeroProbability*100);
}
}

Why did I make it private? Well, do we need use this function from outside of this class? No - the only code using it is up in battle(), so this is a perfect candidate to be private.

Above in battle(), update the calls to be $this->didJediDestroyShipUsingTheForce(). The "force" of our app is happy again:

... lines 1 - 2
class BattleManager
{
... lines 5 - 9
public function battle(Ship $ship1, $ship1Quantity, Ship $ship2, $ship2Quantity)
{
... lines 12 - 18
if ($this->didJediDestroyShipUsingTheForce($ship1)) {
... lines 20 - 23
}
if ($this->didJediDestroyShipUsingTheForce($ship2)) {
... lines 26 - 29
}
... lines 31 - 56
}
... lines 58 - 64
}

Now, if someday we did want to use this function from outside of BattleManager, then we could change it to public. Ok, so why not just make everything public - isn't that more flexible? Yes, but making this private is nice: it means that if I want to change this function - add arguments or even change what it returns - I know that the only code that will be affected will be right inside this class. If it's public, who knows what code I might break in my app?

Start with private, make it public only if you need. The same rule goes for protected - something we'll talk about later with inheritance.

Let's make sure we didn't bust things. Refresh!

Yes!

Service 2: ShipLoader

In functions.php, only the flat get_ships() function remains. You guys know what do to: move it into a class!

Should we move it into BattleManager? No - it doesn't relate to battles. Instead, create a new class for this - how about ShipLoader:

... lines 1 - 2
class ShipLoader
{
... lines 5 - 36
}

Let's work our magic: go grab get_ships() and move it into ShipLoader. Remove the old commented code and make the function public. Also, rename it from get_ships() to getShips() - that's a more common naming standard for methods in a class:

... lines 1 - 2
class ShipLoader
{
public function getShips()
{
$ships = array();
$ship = new Ship('Jedi Starfighter');
... lines 10 - 33
return $ships;
}
}

Yep, that's great! Now we need to update the code that calls this function. But first, open functions.php and require the new ShipLoader.php:

6 lines functions.php
... lines 1 - 4
require_once __DIR__.'/lib/ShipLoader.php';

getShips() is used in battle.php and index.php - start there. To call the method, create a $shipLoader variable and create a new ShipLoader() object. Now, just $shipLoader->getShips():

119 lines index.php
<?php
require __DIR__.'/functions.php';
$shipLoader = new ShipLoader();
$ships = $shipLoader->getShips();
... lines 6 - 119

Do the same thing in battle.php:

99 lines battle.php
<?php
require __DIR__.'/functions.php';
$shipLoader = new ShipLoader();
$ships = $shipLoader->getShips();
... lines 6 - 99

I think it's time to try it. Click to create a new battle. Looks pretty good. Setup a new battle and, Engage. Ok! battle.php works too!

No More functions.php

AND, all the flat functions are gone! Object-orient all the things! So if you look in functions.php, well, there aren't any functions here: just require statements, and even those we'll get rid of eventually. To celebrate, give this a more appropriate name: bootstrap.php. Update this in battle.php:

99 lines battle.php
<?php
require __DIR__.'/bootstrap.php';
... lines 3 - 99

and index.php:

119 lines index.php
<?php
require __DIR__.'/bootstrap.php';
... lines 3 - 119

Refresh once more! Let's keep going.

Leave a comment!

7
Login or Register to join the conversation
Tariq I. Avatar
Tariq I. Avatar Tariq I. | posted 3 years ago

Without including require_once __DIR__.'functions.php'; on top of lib/BattleManager.php file, how come the line 19 of lib/BattleManager.php file succeed to be executed ? As the didJediDestroyShipUsingTheForce() function was defined in functions.php ( before moving it to the BattleManager class ), wasn't it supposed to be needed to add require_once __DIR__.'functions.php'; on top of lib/BattleManager.php file ?

Reply

Yo Tariq I.!

You're definitely thinking the correct way. The key is that index.php is the file that's loaded first and it has require __DIR__.'/functions.php'; on the first line. This means that all of those functions are available to any other files for the rest of this request.

But, you do touch on an interesting problem with require. In this case, the didJediDestroyShipUsingTheForce() function is available inside BattleManager.php because an earlier file index.php required it. But... when you look at BattleManager.php, you can't see this - you don't know that this is happening unless you look at the whole request flow. And what if we create another file - otherPage.php - where we don't require functions.php and then we try to use BattleManager? In that case, the function would suddenly not exist inside of BattleManager.

This is a LONG way of saying that require statements can be a bit confusing (do I need a require here or not?) and error-prone. In OO episode 4, we talk about the solution to this: an "autoloader": https://symfonycasts.com/screencast/oo-ep4/autoloading-awesomeness - this is where you basically teach PHP to do the require statements automatically, so you don't need to worry about them.

I hope that helps!

Cheers!

Reply
Simón B. Avatar
Simón B. Avatar Simón B. | posted 3 years ago

Hi, You used two instances of the ShipLoader, so you might be battling ships that are on service. Right?

Reply

Hey Simón B.!

You used two instances of the ShipLoader

Do you mean that on the first request (index.php) there is one ShipLoader instance and when you submit the form (battle.php) that creates a second ShipLoader instance? if so, you are 100% correct! We are working with different ShipLoader instances on each request - that's just a property of how php works. So, within a single request, you typically only need one instance of a service (ShipLoader)... because (if you needed to) you could call $shipLoader->getShips() multiple times. But yes, on the next request, all your objects (including your services) will be instantiated fresh. Ideally, your service objects will behave the same each time they are instantiated - i.e. $shipLoader->getShips() will return the same array of Ship objects.

Does that help? Or... did I misunderstand your question entirely? :p

Cheers!

Reply
Tariq I. Avatar

But doesn't that change the underRepair property of the ship objects, as this property exploits the mt_rand() function ?

Reply

Yo Tariq I.!

Nice avatar, btw :). You're totally right - the mt_rand() makes this all a bit less realistic, and means that the individual Ship objects on one request won't actually perfectly match the objects on the next request. In a real app, where we would probably load all this data from the database, the Ship objects would have identical data between the requests (even though they are technically different instances in memory) because they would be loading data from the same place (and no randomness).

So, yea, you're definitely thinking correctly!

Cheers!

Reply

Good tip! I didn't show that here because I want them to understand what changes and why. But in real life, I absolutely use this :).

Reply
Cat in space

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

userVoice