Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Abstract Classes

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

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:

109 lines battle.php
... 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.

Trouble With Type Hints

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.

What Methods are really on AbstractShip?

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.

Abstract Functions to the Rescue

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.

Marking a Class as Abstract

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?

123 lines index.php
... 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:

123 lines index.php
... 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.

Leave a comment!

14
Login or Register to join the conversation
Hanane K. Avatar
Hanane K. Avatar Hanane K. | posted 3 years ago

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 ?

Reply

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!

Reply
Hanane K. Avatar

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.

Reply

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

Reply
Hanane K. Avatar

Hello, yes it's clear now. Thanks

Reply
Default user avatar
Default user avatar Kieran Mathieson | posted 5 years ago

The exercise for the previous video asked students to use the abstract keyword, but the keyword wasn't explained until this lesson.

Reply

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!

Reply
Default user avatar
Default user avatar Thrasos Thrasyvoulou | weaverryan | posted 5 years ago

The previous exercise throws an error if the class is not defined as abstract, which is a keyword learned on this chapter.

Reply

You're right! Thanks for pointing that out - we'll get that check for abstract taken out of there :)

Reply
Default user avatar

It's still there.

Reply

Yep... it's still a TODO internally :). We've just recently started pushing more on our challenges. Sorry for the delay!

Reply
Default user avatar
Default user avatar Kieran Mathieson | posted 5 years ago

Challenge 1 is a bit confusing. OtherClass can't be instantiated, so doSomething can't be called with an OtherClass object.

Reply

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!

Reply

Thanks again Kieran for the note - we've just updated the first challenge!

Reply
Cat in space

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

userVoice