If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
There is one more thing that is special about the Rebel Ships. Since, they're the good guys we're going to give them some extra Jedi power.
Inside of Ship
we have a jediFactor
which is a value that is set from the
database and a getJediFactor()
function:
... lines 1 - 2 | |
class Ship | |
{ | |
... lines 5 - 10 | |
private $jediFactor = 0; | |
... lines 13 - 89 | |
public function getJediFactor() | |
{ | |
return $this->jediFactor; | |
} | |
... lines 94 - 138 | |
} |
In the BattleManager
this is used to figure out if some super awesome Jedi powers
are used during the battle.
For Rebel Ships, the Jedi Powers work differently than Empire ships. They always
have at least some Jedi Power, sometimes there's a lot and sometimes it's lower,
depending on what side of the galaxy they woke up on that day. So, instead of making
this a dynamic value that we set in the datbase let's create a public function getJediFactor()
that returns the rand()
function with levels between 10 and 30:
... lines 1 - 2 | |
class RebelShip extends Ship | |
{ | |
... lines 5 - 30 | |
public function getJediFactor() | |
{ | |
return rand(10, 30); | |
} | |
} |
Setting it up like this overrides the function in the Ship
parent class.
Back in the browser, when we refresh we can see the Jedi Factor keeps changing on the first two Rebel ships only.
Over in PhpStorm, when we look at this function now, Ship
has a Jedi Factor property
but RebelShip
doesn't need that at all. Since RebelShip
is extending Ship
it is
still inheriting that property. While this doesn't hurt anything it is a bit weird to have
this extra property on our class that we aren't using at all. And this is also true for
the isFunctional()
method. In RebelShip
it's always true:
... lines 1 - 2 | |
class RebelShip extends Ship | |
{ | |
... lines 5 - 17 | |
public function isFunctional() | |
{ | |
return true; | |
} | |
... lines 22 - 34 | |
} |
But in Ship
it reads from an underRepair
property, and again that's just not
needed in RebelShip
:
... lines 1 - 2 | |
class Ship | |
{ | |
... lines 5 - 14 | |
private $underRepair; | |
... lines 16 - 23 | |
public function isFunctional() | |
{ | |
return !$this->underRepair; | |
} | |
... lines 28 - 138 | |
} |
The point being, Ship
comes with extra stuff that we are inheriting but not using
in RebelShip
.
These classes are like blueprints, so maybe, instead of having RebelShip
extend
Ship
and inherit all these things it won't use, we should have a third class that
would hold the properties and methods that actually overlap between the two called
AbstractShip
. From here, Ship
and RebelShip
would both extend AbstractShip
to get access to those common things.
This is a way of changing the class heirachy so that each class has only what it actually needs.
Let's start this! Create a new PHP Class called AbstractShip
:
... lines 1 - 2 | |
class AbstractShip | |
{ | |
... lines 5 - 138 | |
} |
Since it is the most abstract idea of a ship in our project. To start, I'm going
to copy everything out of the Ship
class and paste it into AbstractShip
:
... lines 1 - 2 | |
class AbstractShip | |
{ | |
private $id; | |
private $name; | |
private $weaponPower = 0; | |
... lines 10 - 16 | |
public function __construct($name) | |
{ | |
$this->name = $name; | |
// randomly put this ship under repair | |
$this->underRepair = mt_rand(1, 100) < 30; | |
} | |
public function isFunctional() | |
{ | |
return !$this->underRepair; | |
} | |
... lines 28 - 138 | |
} |
I know this looks like where we just were, but trust me we're going somewhere with this.
Now, let's write Ship extends AbstractShip
:
... lines 1 - 2 | |
class Ship extends AbstractShip | |
{ | |
} |
And do the same thing in RebelShip
changing it from Ship
to AbstractShip
:
... lines 1 - 2 | |
class RebelShip extends AbstractShip | |
... lines 4 - 36 |
Then in bootstrap
add our require line for our new class:
... lines 1 - 9 | |
require_once __DIR__.'/lib/Model/AbstractShip.php'; | |
require_once __DIR__.'/lib/Model/Ship.php'; | |
require_once __DIR__.'/lib/Model/RebelShip.php'; | |
... lines 13 - 16 |
Perfecto!
After just that change, refresh the browser and see what's happening. Hey nothing is broken, which makes sense since nothing has really changed in our code's functionality -- yet.
Let's trim down AbstractShip
to only the items that are truly shared between our
two ships.
First, jediFactor
is specific to Ship
so let's move it over there:
... lines 1 - 2 | |
class Ship extends AbstractShip | |
{ | |
private $jediFactor = 0; | |
... lines 6 - 21 | |
} |
And then we'll update the references to it in AbstractShip
to what the two classes share,
which is a getJediFactor()
function:
... lines 1 - 2 | |
class AbstractShip | |
{ | |
... lines 5 - 50 | |
public function getNameAndSpecs($useShortFormat = false) | |
{ | |
if ($useShortFormat) { | |
return sprintf( | |
'%s: %s/%s/%s', | |
$this->name, | |
$this->weaponPower, | |
$this->getJediFactor(), | |
$this->strength | |
); | |
} else { | |
return sprintf( | |
'%s: w:%s, j:%s, s:%s', | |
$this->name, | |
$this->weaponPower, | |
$this->getJediFactor(), | |
$this->strength | |
); | |
} | |
} | |
... lines 71 - 120 | |
} |
So let's copy and paste that function into Ship
:
... lines 1 - 2 | |
class Ship extends AbstractShip | |
{ | |
... lines 5 - 9 | |
public function getJediFactor() | |
{ | |
return $this->jediFactor; | |
} | |
... lines 14 - 21 | |
} |
RebelShip
already has one so that class is good to go already. Now in AbstractShip
the getJediFactor()
function will either call the version of the function in Ship
or RebelShip
depending on what is being loaded. There are a few other things I
want to share with you about this, but we'll get to those later.
Now let's move setJediFactor()
from AsbtractShip
into Ship
:
... lines 1 - 2 | |
class Ship extends AbstractShip | |
{ | |
... lines 5 - 17 | |
public function setJediFactor($jediFactor) | |
{ | |
$this->jediFactor = $jediFactor; | |
} | |
} |
and that should do it! Now, Ship
still has all the functionality that it had before,
it extends AbstractShip
, and only contains its unique code. And RebelShip
no
longer inherits the jediFactor
property and anything that works with it. Now each
file is simpler, and only has the code that it actually needs. Back to the browser
to test that everything still works. Oh look an error!
Call to undefined method RebelShip::setJediFactor() on ShipLoader line 55.
Let's check that out.
Ah, it's because down here when we create a ship object from the database, we always
call setJediFactor()
on it, and that doesn't make sense anymore. So we'll move this
up and only call it for the Ship
class:
... lines 1 - 2 | |
class ShipLoader | |
{ | |
... lines 5 - 44 | |
private function createShipFromData(array $shipData) | |
{ | |
if ($shipData['team'] == 'rebel') { | |
$ship = new RebelShip($shipData['name']); | |
} else { | |
$ship = new Ship($shipData['name']); | |
$ship->setJediFactor($shipData['jedi_factor']); | |
} | |
$ship->setId($shipData['id']); | |
$ship->setWeaponPower($shipData['weapon_power']); | |
$ship->setStrength($shipData['strength']); | |
return $ship; | |
} | |
... lines 60 - 76 | |
} | |
... lines 78 - 79 |
Refresh again, no error, perfect!
Back to AbstractShip
, we have the underRepair
property which is only used by Ship
,
so let's move that over:
... lines 1 - 2 | |
class Ship extends AbstractShip | |
{ | |
... lines 5 - 6 | |
private $underRepair; | |
... lines 8 - 32 | |
public function isFunctional() | |
{ | |
return !$this->underRepair; | |
} | |
} |
And, let's also move over the isFunctional()
method from AbstractShip
as well,
since RebelShip
has its own isFunctional()
method already. Finally, the last
place that this is used is in the construct function. The random number for under
repair is set here, so just remove that one piece but leave the $this->name = $name;
where it is since it is shared by both types of ships. In the Ship
class we'll
override the construct function, I'll keep the same argument. Using our trick from
earlier I'll call the parent::__construct($name);
and then paste in the under repair
calculation line:
... lines 1 - 2 | |
class Ship extends AbstractShip | |
{ | |
... lines 5 - 8 | |
public function __construct($name) | |
{ | |
parent::__construct($name); | |
... lines 12 - 13 | |
$this->underRepair = mt_rand(1, 100) < 30; | |
} | |
... lines 16 - 36 | |
} |
The last thing that's extra right now in the AbstractShip
class is the getType()
method. Both ships need a getType()
function, but this one here is specific to
the Ship
class so we'll cut and paste that over:
... lines 1 - 2 | |
class Ship extends AbstractShip | |
{ | |
... lines 5 - 37 | |
public function getType() | |
{ | |
return 'Empire'; | |
} | |
} |
Back to the browser and refresh, everything looks great. The Rebel Ships aren't breaking and Jedi Factors are random, awesome!
This is the same functionality we had a second ago but the RebelShip
class is a
lot simpler. It only inherits what it actually uses from AbstractShip
. Which means
that our new class truly is the blueprint for the things that are shared by all the
ship classes. Ship
extends AbstractShip
as does RebelShip
and then each add
their own specific code.
While this isn't a new concept, it is a new way of thinking of how to organize your "class hierarchy".
Hey theNaschkatze,
Yea, in this example, it seems like the makeFirignNoise()
does not do too much, but if you would have to add more DS derivatives, they all can share the same logic by extending from their abstract class. Besides that, type-hinting for an abstract class is better than type-hinting for a concrete implementation because it decouples your code and makes the callers agnostic from the implementation details
Cheers!
Why should we keep getJediFactor() in AbstractShip ? ("So let's copy and paste that function into Ship:")
This class doesn't own the $jediFactor property.
Is it because we have $this->getJediFactor() in the getNameAndSpecs() method ?
Do we have to refer to a method that must be existing even if this method is referring to a nonexistent property ?
(sorry for my english)
Hey Marc R. !
Excellent question :). Part of the answer is explained in the next chapter, but it can still be a bit confusing. Let's focus first on just this chapter - and you already are thinking the right thing!
Here's what we know: AbstractShip has a getNameAndSpecs()
method and that needs to know the "jedi factor" to do its job. And so, it calls a getJediFactor()
method to do that. If EVERY ship type calculated their jedi factor the same way (by returning the jediFactor property), then we should put both the jediFactor
property and the getJediFactor()
method in AbstractShip
. But in reality, the way that the "jedi factor" is calculated is different between Ship
and RebelShip
. For example, only Ship</cod> needs a
jediFactor` property, which is why it lives there.
Because of this, the tricky part is balancing these two things:
1) AbstractShip needs to guarantee that it has a getJediFactor()
method... because it's calling it in getNameAndSpecs()
!
2) But... AbstractShip
can't have a getJediFactor
method... because each sub-class determines this in a different way.
So, we have 2 options really:
A) Add getJediFactor()
to AbstractShip
with some default implementation. This makes sense if we have several classes that extend AbstractShip and maybe only one of them behaves differently. So, we add getJediFactor()
to AbstractShip
with the most common implementation and then override it in the one sub-class that needs to behave differently.
B) OR, do what we do in the next chapter: add a abstract public function getJediFactor()
to AbstractShip. This will guarantee that this method must be implemented in every class. This allows AbstractShip
to safely use the method: we know it will exist, and each sub-class can figure out its own code for it.
Neither of these options is always right or always wrong - it depends on the situation. But it is true that if AbstractShip
is calling a method like $this->getJediFactor()
, then we SHOULD have that method in AbstractShip
either as a real or abstract method.
Phew! I hope that helps. If I completely answered the wrong question, please let me know :).
Cheers!
Thank you for that answer.
I finished the next chapter and I understand the use of abstract methods.
So I suppose that the choice to have kept the getJediFactor() method in AbstractShip in this chapter (even if the $jediFactor property is not there and that it can be disturbing) is made on purpose in order to introduce later the notion of abstract methods.
Hey Marc R.!
I finished the next chapter and I understand the use of abstract methods.
Excellent :). Nice work
So I suppose that the choice to have kept the getJediFactor() method in AbstractShip in this chapter (even if the $jediFactor property is not there and that it can be disturbing) is made on purpose in order to introduce later the notion of abstract methods
Well actually, by the end of this chapter, the getJediFactor()
method is not inside AbstractShip
anymore. We separated all the "different" code between Ship
and RebelShip
and this ultimately meant that each class had its own getJediFactor()
. And, functionally, this worked ok: the getSpecs()
method calls getJediFactor()
and, because both sub-classes have this, the code runs. But, this is "weird": there is nothing "enforcing" that every sub-class of AbstractShip
must have a getJediFactor()
method. If we created a 3rd sub-class today and forgot to add that method, the getSpecs()
method would blow up :).
So this chapter was all about: how can we share some code in this parent AbstractShip
method but move "specific" code into each sub-class. The next chapter is all about "Hey! It's weird that there is nothing enforcing that each sub-class has a getJediFactor() method. Let's enforce that with an abstract method.
It sounds like things were already making sense to you, but I hope this can clarify even more :).
Cheers!
In challenge 5.1, how public function setCrewSize($numberOfPeople) of DeathStar class can access the private $crewSize property of AbstractDeathStar class ? Isn't it required that the $crewSize property be a protected one for accessing it from the DeathStar sub-class ?
=============== AbstractDeathStar.php ==============
`(php tag)
class AbstractDeathStar
{
private $crewSize;
private $weaponPower;
public function getCrewSize()
{
return $this->crewSize;
}
public function setWeaponPower($power)
{
$this->weaponPower = $power;
}
public function getWeaponPower()
{
return $this->weaponPower;
}
public function makeFiringNoise()
{
echo 'BOOM x '.$this->weaponPower;
}
}`
=========== DeathStar.php ===========
`(php tag)
class DeathStar extends AbstractDeathStar
{
public function setCrewSize($numberOfPeople)
{
$this->crewSize = $numberOfPeople;
echo $this->crewSize;
}
}`
Hey Tariq I.!
Awesome discovery :). Here's what's going on. In PHP, it's legal (but not recommended) to set a property on a class that doesn't exist. Let me explain. Here are two facts:
1) Check out this code:
class Foo
{
// no properties
public function setName($name)
{
$this->name = $name;
echo $name; // This WILL print the value! The property *was* set
}
}
In PHP, if you say $this->name = $name
and that class has no name property... PHP simply creates a name
property in the background and sets it. Basically, you don't technically need to say private $name
on the class - PHP allows you to set properties that don't exist. This is not recommended... it's more of a "left over" feature of PHP from earlier versions.
2) Because crewSize
is private in AbstractDeathStar
, when you're inside <DeathStar`, that property basically doesn't exist.
If you combine these two facts, here's what's happening:
class DeathStar extends AbstractDeathStar
{
public function setCrewSize($numberOfPeople)
{
// this creates a NEW crewSize property on ONLY this class (DeathStar)
$this->crewSize = $numberOfPeople;
// this prints that NEW crewSize property
echo $this->crewSize;
}
}
So basically, AbstractDeathStar has a crewSize
property AND DeathStar
has a crewSize
property...but they are two totally different properties! You can see this in the coding challenge - if you re-add the code that you printed above and hit "Check", on the browser, you will see that the "Crew Size" of "DeathStar 1" is empty. That's because this is coming from $deathStar1->getCrewSize()
. And because the getCrewSize
method lives in AbstractDeathStar
, it references its crewSize
property, which was never set (only the crewSize
property in DeathStar1
was set.
Phew! Does that make sense? It's actually a super fun, little PHP trivia question :).
Cheers!
Is the AbstractShip class supposed to be a true abstract class? I did not see it get declared as abstract.
In the code challenge, why are we defining the
makeFiringNoise()
method in the abstract deathstar (ds)? Is basically a different method in DSI and DSII, so I would prefer defining it in every star than having one star overriding it.