If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Since everything seems to be working on our site, let's start a battle! Four Jedi Starfighters against three Super Star Destroyers. Engage.
Ahh an error!
Argument 1 passed to BattleManager::battle() must be an instance of Ship, instance of RebelShip given
And this is apparently happening on battle
line 32:
... lines 1 - 33 | |
$battleResult = $battleManager->battle($ship1, $ship1Quantity, $ship2, $ship2Quantity); | |
... lines 35 - 109 |
And BattleManager
line 10:
... lines 1 - 2 | |
class BattleManager | |
{ | |
... lines 5 - 9 | |
public function battle(Ship $ship1, $ship1Quantity, Ship $ship2, $ship2Quantity) | |
{ | |
... lines 12 - 56 | |
} | |
... lines 58 - 64 | |
} |
Back to our IDE and open up battle.php
.
Down on line 32, what we see is that $ship1
is actually a RebelShip
object, which makes sense since one of the ships I selected was a Rebel. But
it expected that to be a normal Ship
class. Over in BattleManager
look
at the battle function to see the problem! We type hinted our arguments with the
Ship
class:
... lines 1 - 2 | |
class BattleManager | |
{ | |
... lines 5 - 9 | |
public function battle(Ship $ship1, $ship1Quantity, Ship $ship2, $ship2Quantity) | |
{ | |
... lines 12 - 56 | |
} | |
... lines 58 - 64 | |
} |
Which tells PHP to only allow Ship
classes or subclasses to be passed here.
The issue is that RebelShip
is no longer a subclass of Ship
and so now we have this
error. The good news, the fix is simple! We don't care if we get a ship object in battle anymore.
What we actually care about is that we get an AbstractShip
object or any of its subclasses
which we know includes Ship
and RebelShip
:
... lines 1 - 2 | |
class BattleManager | |
{ | |
... lines 5 - 9 | |
public function battle(AbstractShip $ship1, $ship1Quantity, AbstractShip $ship2, $ship2Quantity) | |
{ | |
... lines 12 - 56 | |
} | |
... lines 58 - 64 | |
} |
Refresh and give this another try, we get the exact same error. Let's see we're being
notified about something in BattleManager
on line 58. Scroll down and look there:
... lines 1 - 2 | |
class BattleManager | |
{ | |
... lines 5 - 58 | |
private function didJediDestroyShipUsingTheForce(Ship $ship) | |
{ | |
... lines 61 - 63 | |
} | |
} |
Ah yes, it's this type hinting right here. This function is called up here, and we pass it the
ship object, so let's update this one to be expecting an AbstractShip
:
... lines 1 - 2 | |
class BattleManager | |
{ | |
... lines 5 - 58 | |
private function didJediDestroyShipUsingTheForce(AbstractShip $ship) | |
{ | |
... lines 61 - 63 | |
} | |
} |
Let's try this again! Cool, one more error! This one is having issues with BattleResult::__construct()
.
In our IDE we can see that when we instantiate the BattleResult
object we pass it the $winningShip
and
the $losingShip
:
... lines 1 - 9 | |
public function battle(AbstractShip $ship1, $ship1Quantity, AbstractShip $ship2, $ship2Quantity) | |
{ | |
... lines 12 - 55 | |
return new BattleResult($usedJediPowers, $winningShip, $losingShip); | |
} | |
... lines 58 - 66 |
Over in BattleResult
we see that these are also typehinted with Ship
. Update those two:
... lines 1 - 2 | |
class BattleResult | |
{ | |
... lines 5 - 13 | |
public function __construct($usedJediPowers, AbstractShip $winningShip = null, AbstractShip $losingShip = null) | |
{ | |
... lines 16 - 18 | |
} | |
... lines 20 - 53 | |
} |
This is nice, our code is a lot more flexible now. Before, it had to be a Ship
instance. Now
we don't care what class you have as long as it extends AbstractShip
.
Refresh again! Awesome, battling is back on.
Now we have a few minor, but interesting, problems. First, in AbstractShip
head down to
getNameAndSpecs()
and we see that getJediFactor()
is highlighted with an error that says
"Method getJediFactor()
not found in class AbstractShip". Now, this is working because we do have
a getJediFactor()
method in Ship
and RebelShip
. When we call getNameAndSpecs()
it's able to
call getJediFactor()
. But this should look a little fishy to you. There is no getJediFactor()
function inside of AbstractShip
, so just looking at this class you should feel suspicious and
question whether or not this works.
Here's what's going on, we have an implied rule that says, "Yo, every class that extends AbstractShip
must have a getJediFactor()
function." If it doesn't everything is going to break when we call this
function with a 'method not found' error. We aren't enforcing this rule. So we could easily create a new
ship class, extend AbstractShip
, and forget to add a getJediFactor()
function. Our application would
break and no battles would be happening. Sad times.
You're in luck, there's a feature called Abstract Classes that can handle this issue for us. I'll scroll
up, but really the position of this doesn't matter. Add a new abstract public function getJediFactor();
:
... lines 1 - 2 | |
abstract class AbstractShip | |
{ | |
... lines 5 - 15 | |
abstract public function getJediFactor(); | |
... lines 17 - 111 | |
} |
You may notice there are two different things about this. One is the word abstract
before public function
and the other is that I just have a semicolon on the end, I didn't actually make a function. The best part,
this line doesn't add any functionality to our app, but it does force any class that extends this
to have this method.
For example, if RebelShip
didn't have this getJediFactor()
method, then when we refresh the browser
we'll get a huge error that says: "Hey! RebelShip must have a getJediFactor function!". This is because
it has been defined as an abstract function inside of the parent class.
Up until now we could have instantiated an abstract ship directly with new AbstractShip()
we didn't
actually want to but it was possible. But, once you have an abstract function in here, that is no longer
an option, it's only purpose then becomes to be a blueprint for other classes to extend.
Up here at the top of the file you can see that there is an error highlight with a message that says
"Class must be declared abstract or implement method getJediFactor()
". Once your class has an
abstract function you need to add the abstract
keyword in front of it, which enforces the rule that
you can't say new AbstractShip()
:
... lines 1 - 2 | |
abstract class AbstractShip | |
... lines 4 - 113 |
Now when we scroll down, we can see that getJediFactor()
isn't highlighted anymore since we know that
inside AbstractShip
any subclasses will be forced to have that. Back to the browser and refresh!
Everything still works just fine.
Related to this, there is one more little thing we need to fix up. Start in ShipLoader
, notice that our
getShips()
and findOneById()
functions still have PHPDoc above them that say they return a ship object.
That's not the biggest deal, but it would be more accurate if it said AbstractShip
- because this actually
returns a mixture of RebelShip
and Ship
objects:
... lines 1 - 2 | |
class ShipLoader | |
{ | |
... lines 5 - 11 | |
/** | |
* @return AbstractShip[] | |
*/ | |
public function getShips() | |
{ | |
... lines 17 - 25 | |
} | |
/** | |
* @param $id | |
* @return AbstractShip | |
*/ | |
public function findOneById($id) | |
{ | |
... lines 34 - 42 | |
} | |
... lines 44 - 76 | |
} | |
... lines 78 - 79 |
Now check this out, inside of index.php
, remember this $ships
variable we get by calling
that getShips()
function?
... lines 1 - 6 | |
$ships = $shipLoader->getShips(); | |
... lines 8 - 123 |
So that returns an array of AbstractShip
objects. When we loop over it, the isFunctional()
and
the getType()
functions aren't found:
... lines 1 - 70 | |
<?php foreach ($ships as $ship): ?> | |
... lines 72 - 74 | |
<td><?php echo $ship->getJediFactor(); ?></td> | |
... line 76 | |
<td><?php echo $ship->getType(); ?></td> | |
... lines 78 - 85 | |
<?php endforeach; ?> | |
... lines 87 - 123 |
The message here says "Method getType()
not found in class AbstractShip
".
This is just like the getJediFactor()
problem we just fixed. We don't have a getType()
function inside of here.
Both of our subclasses do, which is why our app still works, but technically we're not enforcing that. Any new
subclasses to AbstractShip
could easily end up missing these functions which would again stop all the battles.
What we need is another abstract public function for getType()
and isFunctional()
:
... lines 1 - 2 | |
abstract class AbstractShip | |
{ | |
... lines 5 - 20 | |
abstract public function getType(); | |
... lines 23 - 25 | |
abstract public function isFunctional(); | |
... lines 27 - 121 | |
} |
This doesn't change anything in our application, it just forces our subclasses to have those methods.
And now index.php
is really happy again!
That's the power of abstract classes, you can have a whole bunch of shared logic in there, but if there are a couple of pieces that you can't fill in in your abstract class because they are specific to your subclasses, no problem! Just put them in there as abstract functions and your subclasses will be forced to have those.
In my example these are abstract public functions but you could also have abstract protected functions as well. Which one you use just depends on your use case. It's a very powerful feature of object oriented code.
Hey Hanane K.
In this case you can't pass objects of the class OtherClass
because it's an abstract class. PHP do not allow you to instantiate abstract classes, only concrete classes.
That's the reason why the other options are incorrect
I hope it made things clearer. Cheers!
Hello, thanks for your response. when you say « you can't pass objects of the class OtherClass because it's an abstract class » you mean that we can not type-hint an argument using AbstractClass ?
it’s not yet clear for me, because in the video, we passed an object of class AbstractShip to battle function for example and it works.
Oh, no, sorry, I didn't mean that. What I meant is that you can't create objects of an abstract class. e.g.
abstract class AbstractShip
{
}
$object = new AbstractShip(); // PHP won't allow you to do this
but you can type-hint arguments with such class so any other class that inherits from it can be passed in. Is it clearer now? If not, let me know :)
The exercise for the previous video asked students to use the abstract keyword, but the keyword wasn't explained until this lesson.
Hey Kieran!
Actually, it's subtle, but I think that's not true. On the previous chapter, we create class with the word "abstract" in it, but we don't actually have you use the "abstract" keyword in the challenges until this chapter. But if I'm wrong, please tell me! Or, if we can make some wording more clear for others, awesome too!
Cheers!
The previous exercise throws an error if the class is not defined as abstract, which is a keyword learned on this chapter.
You're right! Thanks for pointing that out - we'll get that check for abstract
taken out of there :)
Yep... it's still a TODO internally :). We've just recently started pushing more on our challenges. Sorry for the delay!
Challenge 1 is a bit confusing. OtherClass can't be instantiated, so doSomething can't be called with an OtherClass object.
Ah, fair point! I was thinking "instances / sub-classes" of OtherClass can be passed to doSomething(). But you're absolutely right - technically you cannot have an OtherClass object, so it should not be an answer. We'll update that - it makes the question even trickier I think, which is kinda cool :)
Thanks!
Hello, I have a question about challenge 1 related to chapter 6 “Abstract Classes”, the type of the object (argument)can be all classes except GreatClass.
I don t understand why we have only chosen MyClass and Puppy and not the others. if we have the possibility to select multi values from the answers, will be correct if we choose A,B, and C. The question is about type hinting the argument not about its instantiation. Am I missing something ?