Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Type Hinting?!

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

When we submit the form it goes to battle.php and we see this nasty error:

Argument 1 passed to battle() must be of the type array, object given

It comes from functions.php on line 74 and called on battle.php on line

  1. Let's start with battle.php, sure enough we can see the problem is with the battle function, let me show you why. But first, let's dump $ship1 and $ship2:

99 lines battle.php
... lines 1 - 25
$ship1 = $ships[$ship1Name];
$ship2 = $ships[$ship2Name];
var_dump($ship1, $ship2);die;
... lines 30 - 99

Let's see here, up top we call getShips() which returns an array of objects. Then we read the $_POST data to figure out which two ships are fighting. Then, down here, we get the ship objects off this array:

99 lines battle.php
... lines 1 - 3
$ships = get_ships();
... line 5
$ship1Name = isset($_POST['ship1_name']) ? $_POST['ship1_name'] : null;
$ship1Quantity = isset($_POST['ship1_quantity']) ? $_POST['ship1_quantity'] : 1;
$ship2Name = isset($_POST['ship2_name']) ? $_POST['ship2_name'] : null;
$ship2Quantity = isset($_POST['ship2_quantity']) ? $_POST['ship2_quantity'] : 1;
... lines 10 - 25
$ship1 = $ships[$ship1Name];
$ship2 = $ships[$ship2Name];
... lines 28 - 99

So this should dump two objects. And it does: we have the Jedi Starfighter and the Super Star Destroyer.

What is Type-Hinting?

Next, let's look in the battle() function which lives in functions.php:

128 lines functions.php
... lines 1 - 73
function battle(array $ship1, $ship1Quantity, array $ship2, $ship2Quantity)
{
... lines 76 - 120
}
... lines 122 - 128

Here's the issue the $ship1 and $ship2 arguments have "array" in front of them. This tells PHP that this argument must be an array and if someone passes something other than an array, I want you to throw a huge error. So let's see that error again, it says:

Argument 1 passed to battle() must be of the type array, object given

This is called a type hint and the only purpose of a type hint in PHP is to get better errors: it doesn't change the behavior. We can just take the type hint off like this and that will fix the error:

128 lines functions.php
... lines 1 - 73
function battle($ship1, $ship1Quantity, $ship2, $ship2Quantity)
{
... lines 76 - 120
}
... lines 122 - 128

And down here, knowing that $ship1 is actually an object, instead of using the array syntax we can call the getStrength() method. Let's go ahead and dump $ship1Health to make sure it's working:

129 lines functions.php
... lines 1 - 73
function battle($ship1, $ship1Quantity, $ship2, $ship2Quantity)
{
$ship1Health = $ship1->getStrength() * $ship1Quantity;
var_dump($ship1Health);die;
... lines 78 - 121
}
... lines 123 - 129

Just by removing the type hint it tells PHP to stop making sure it's an array, just let anything in and be ok with it. Refresh! This time it's printing out 60 which means it's printing out the ship's mighty strength correctly.

Type-Hinting Saves your Butt

The type hint is a useful thing, not from a functionality standpoint, but for knowing when you're doing something wrong. Let's go back to battle.php and pretend that something went wrong here by changing our object $ship1 to the string foo:

97 lines battle.php
... lines 1 - 28
$outcome = battle('foo', $ship1Quantity, $ship2, $ship2Quantity);
... lines 30 - 97

When we refresh this time, we get this really weird error:

Call to a member function getStrength() on a non-object

You're going to see this error a lot and it's coming from line 76:

129 lines functions.php
... lines 1 - 73
function battle($ship1, $ship1Quantity, $ship2, $ship2Quantity)
{
$ship1Health = $ship1->getStrength() * $ship1Quantity;
var_dump($ship1Health);die;
... lines 78 - 121
}
... lines 123 - 129

It happens whenever you use the arrow syntax on something that isn't an object. It's a fatal error and PHP just dies immediately. We know because I just passed foo, that $ship1 is no longer an object, it's just a string. And when we call this on it, everything dies. The issue is that from the error message, it isn't exactly clear where the mistake is. It's telling us that the problem is on line 76 in functions.php. And sure, that is where the error occurred. But the real problem is in battle.php where we are passing in a bad value to the battle() function.

Type-Hinting with a Class

So in addition to type-hinting with the array, when we use objects we can type hint with the class name. Which means we can actually type Ship here and we can do that here as well:

129 lines functions.php
... lines 1 - 73
function battle(Ship $ship1, $ship1Quantity, Ship $ship2, $ship2Quantity)
... lines 75 - 129

That is the exact same thing. It says, "Hey, PHP, if something is passed to this argument that's not a Ship object, I want you to throw a very clear error." So let's go see this new error! Refresh and there it is:

Argument 1 passed to battle() must be an instance of Ship, string given
on line 29 `battle.php`

This time it's very clear: it says it should have been a Ship object, but you're passing a string and it points us to the exact right spot. So type-hinting is optional, but it's a really good idea because it will make your code easier to debug later. It also has a second benefit: as soon as I type hinted this $ship1 variable here, all of a sudden my editor knew what type of object $ship1 was and offered me autocomplete. So it knows about getStrength() and all the other methods on that object.

Now that we know that these are objects, let's fix this method for all the array syntaxes. Let's see here we have a few more:

129 lines functions.php
... lines 1 - 73
function battle(Ship $ship1, $ship1Quantity, Ship $ship2, $ship2Quantity)
{
$ship1Health = $ship1->getStrength() * $ship1Quantity;
$ship2Health = $ship2->getStrength() * $ship2Quantity;
... lines 78 - 96
$ship1Health = $ship1Health - ($ship2->getWeaponPower() * $ship2Quantity);
$ship2Health = $ship2Health - ($ship1->getWeaponPower() * $ship1Quantity);
... lines 99 - 120
}
... lines 122 - 129

And then down here, which is called from above we have one more:

129 lines functions.php
... lines 1 - 123
function didJediDestroyShipUsingTheForce(array $ship)
{
$jediHeroProbability = $ship['jedi_factor'] / 100;
return mt_rand(1, 100) <= ($jediHeroProbability*100);
}

And notice that this one is not giving me autocomplete because it's being type hinted as an array. This function is called all the way up here, it's passing a $ship1 and $ship2, so it's passing a ship object:

129 lines functions.php
... lines 1 - 73
function battle(Ship $ship1, $ship1Quantity, Ship $ship2, $ship2Quantity)
{
... lines 76 - 82
if (didJediDestroyShipUsingTheForce($ship1)) {
... lines 84 - 87
}
if (didJediDestroyShipUsingTheForce($ship2)) {
... lines 90 - 93
}
... lines 95 - 120
}
... lines 122 - 129

Let's change that type hint to be a Ship instead of an array:

129 lines functions.php
... lines 1 - 122
function didJediDestroyShipUsingTheForce(Ship $ship)
{
$jediHeroProbability = $ship->getJediFactor() / 100;
return mt_rand(1, 100) <= ($jediHeroProbability*100);
}

And now we will get that nice autocompletion which will make sure the object is being passed there. Awesome, this function looks good!

Let's go back and refresh. And of course I get that same error because I forgot to go back and put $ship1 here:

97 lines battle.php
... lines 1 - 28
$outcome = battle($ship1, $ship1Quantity, $ship2, $ship2Quantity);
... lines 30 - 97

Fixing the Objects inside $outcome

Let's try that again. We still get an error, but if you look closely you'll see that it is happening farther down the page. The battle function is being called and it's all working. This new error is from line 61, at this point you can probably even spot what that is:

Cannot use object of type Ship as an array

That's another syntax thing that we need to change.

So, let's go down to line 61 and sure enough there it is:

97 lines battle.php
... lines 1 - 60
<?php echo $ship1Quantity; ?> <?php echo $ship1['name']; ?><?php echo $ship1Quantity > 1 ? 's': ''; ?>
... lines 62 - 97

We'll call getName() on our $ship1 and $ship2 objects:

98 lines battle.php
... lines 1 - 60
<?php echo $ship1Quantity; ?> <?php echo $ship1->getName(); ?><?php echo $ship1Quantity > 1 ? 's': ''; ?>
VS.
<?php echo $ship2Quantity; ?> <?php echo $ship2->getName(); ?><?php echo $ship2Quantity > 1 ? 's': ''; ?>
... lines 64 - 98

Now, real quick back up on battle(), what it returns is this $outcome variable, and I'm going to show you what that actually is. Down here, let's do a var_dump() on $outcome, put a die statement and refresh:

98 lines battle.php
... lines 1 - 65
<?php var_dump($outcome); ?>
... lines 67 - 98

So the battle() function returns an array with three different keys on it: winning_ship which is a Ship object, losing_ship which is a Ship object and whether or not Jedi powers were used to have a really awesome comeback win (used_jedi_powers).

The important part is that winning_ship and losing_ship are Ship objects. Let's just remove this var_dump real quick. Down here, when we reference $outcome['winning_ship'] we know that this is an object:

98 lines battle.php
... lines 1 - 70
<?php echo $outcome['winning_ship']['name']; ?>
... lines 72 - 98

And we want to call getName() on it. The same thing here. And then we'll do the same thing here as well:

97 lines battle.php
... lines 1 - 69
<?php echo $outcome['winning_ship']->getName(); ?>
... lines 71 - 78
The <?php echo $outcome['winning_ship']->getName(); ?>
... lines 80 - 82
overpowered and destroyed the <?php echo $outcome['losing_ship']->getName() ?>s
... lines 84 - 97

We're converting from that array syntax to the object syntax.

Moment of truth, do we have a working battle page? SUCCESS! Super Star Destoyer won. Let's try it again. We'll throw 10 Jedi Star Ships at our one Super Star Destroyer and it wins again. Come one Jedi's get it together! If you try enough times the Jedis do come out with a victory.

The key take away here is because we have a Ship class, when we have a Ship object, we know exactly what we can do with it. This is cool because whenever we pass around a Ship, object we can type hint it with Ship and our editor instantly knows what that is and what methods we can call on it. We're giving definition to our data instead of passing around arrays which have unknown and probably inconsistent keys.

Leave a comment!

9
Login or Register to join the conversation
Max S. Avatar

Hey!
At 6:11 you remove the array key and replace it with the getter-function. PHPStorm not seems to recognize it as it's yellow afterwards and does not suppose autocompletion. Is there a possibility to fix that?

1 Reply

Yo Max S.!

There is a fix! And great question - I'm kind of obsessed with getting auto-completion in PhpStorm (it also helps me know when I'm doing something right versus wrong).

So, the $ship1 and $ship2 variables come from the $ships variable near the top of the file. The summary looks like this:


$ships = get_ships();
// ...
$ship1 = $ships[$ship1Name];
$ship2 = $ships[$ship2Name];

If we could somehow tell PhpStorm that the get_ships() method returns an array of Ship objects, then it would be smart enough to know that $ship1 and $ship2 must themselves be Ship objects (since we fetch them from $ships with array keys). To do that, add this above get_ships()``

/**

  • @return Ship[]
    */
    get_ships()
    {
    // ...
    }
    
    

The [] after is what says "this is an array of Ship" objects.

Let me know if that helps!

1 Reply
Max S. Avatar

Super awesome! I think I am going to join your obsession :D Thank you very much!

1 Reply
Si Y. Avatar

I was getting an error on 'quantity". I changed the isset check in lines 8 and 10 to !empty.
$ship1Quantity = !empty($_POST['ship1_quantity']) ? $_POST['ship1_quantity'] : 1;

Reply

Hey Si Y.

What error were you getting? You need to add the isset() check because it's possible that the field ship1_quantity is not submitted.

Cheers!

Reply
Si Y. Avatar

The code wasn't actually erroring. It was working by redirecting back to index with a $GET suffix because the 'ship1_quantity' field in the $POST was an empty string. Changing the isset check to a !empty check catches the empty string as well as the existence of the variable. (Did I get that right?) You can check it in the code, it fails if the quantity fields are left blank (empty strings rather than nulls).

Reply

Ohh, yes, you are totally right, the empty() allow you to "access" to undefined index of arrays without throwing. That's nice :)

So, yes, if you do that change then, $ship1Quantity will default to 1. Nice debugging! Cheers!

Reply
Default user avatar
Default user avatar doc_anonymouse | posted 5 years ago

What chrome extension are you using that gives you the nicely formatted var_dump and error messages?

Reply

Actually, it's XDebug - it's a PHP extension. I highly recommend installing it on your development machine :)

Reply
Cat in space

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

userVoice