Buy

The iterable Pseudo-Type

I want to talk about yet another PHP 7.1 feature. I know, most of this tutorial is actually about PHP 7.1 features... not PHP 7.0. What can I say? They killed it with 7.1.

To show off this feature, open your Genus class and find the bottom. Add a fun new function called feed() with an array $food argument. Set the return type to a string.

I'm going to paste in some code. Basically, we pass an array with some food, and this returns a message. And if we pass no food, our genus looks at us funny...

233 lines src/AppBundle/Entity/Genus.php
... lines 1 - 18
class Genus
{
... lines 21 - 223
public function feed(array $food): string
{
if (count($food) === 0) {
return sprintf('%s is looking at you in a funny way', $this->getName());
}
return sprintf('%s recently ate: %s', $this->getName(), implode(', ', $food));
}
}

Let's go use this! In showAction(), create a $food array set to, how about, shrimp, clams, lobsters, and a shark! Pass a new recentlyAte variable into the template set to $genus->feed($food). Then, open the genus/show.html.twig template, add a new "Diet" key, and print recentlyAte.

163 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 15
class GenusController extends Controller
{
... lines 18 - 94
public function showAction(Genus $genus)
{
... lines 97 - 107
$food = ['shrimp', 'clams', 'lobsters', 'shark'];
... line 109
return $this->render('genus/show.html.twig', array(
... lines 111 - 112
'recentNoteCount' => count($recentNotes),
'recentlyAte' => $genus->feed($food),
));
}
... lines 117 - 161
}

94 lines app/Resources/views/genus/show.html.twig
... lines 1 - 4
{% block body %}
... lines 6 - 7
<div class="sea-creature-container">
... line 9
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 19
<dt>Diet</dt>
<dd>{{ recentlyAte }}</dd>
... lines 22 - 47
</dl>
</div>
</div>
... lines 51 - 92
{% endblock %}

Nice! Go back and refresh the show page. There it is! Aurelia recently ate shrimp, clams, lobsters, shark.

Creating an Iterable Object

Now, here's the challenge. Imagine that this feed() function is part of a re-usable library that we're creating, and we want to make it as flexible as possible. Right now, we are requiring the $foods argument be an array. But... is that necessary? Really, if you passed me anything that I could loop over, we could make this function work.

Let's try it! In GenusController, rename the $food variable to $foodArray. Then, add $food = new \ArrayObject() and pass it $foodArray.

165 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 15
class GenusController extends Controller
{
... lines 18 - 94
public function showAction(Genus $genus)
{
... lines 97 - 107
$foodArray = ['shrimp', 'clams', 'lobsters', 'shark'];
$foodObject = new \ArrayObject($foodArray);
... lines 110 - 117
}
... lines 119 - 163
}

If you're not familiar with ArrayObject, it's a PHP core object, but it looks and acts like an array. Most importantly, you can foreach over it. So in theory, our feed() function should be able to use this, right?

If you refresh... huge error!

The iterable Pseudo-type

Argument one passed to feed() must be an array, object given

Of course: we're requiring an array with the type-hint. Well... that's kind of lame. Change this to iterable: a new pseudo-type - like array - from PHP 7.1. This is perfect when all you care about is that an argument can be used in foreach.

233 lines src/AppBundle/Entity/Genus.php
... lines 1 - 18
class Genus
{
... lines 21 - 223
public function feed(iterable $food): string
... lines 225 - 231
}

Notice, PHPStorm doesn't like this at all. My version of PHPStorm still doesn't think that iterable exists. But, it is valid, and this will probably, hopefully be fixed soon.

Of course, if you refresh now, a new error!

Warning, implode, invalid arguments passed

Our function now allows an array or any iterable object. But... the second argument to implode() must be an array. Remember, when you type-hint with iterable, the only thing you know is that you can foreach over that value. It's not even legal to use count() like this!

If I want this to be more flexible, we need to do some refactoring. Create a new variable called $foodItems set to an empty array. Then, foreach over $food as $foodItem. This is legal! Inside, put each item into the $foodItems array.

239 lines src/AppBundle/Entity/Genus.php
... lines 1 - 18
class Genus
{
... lines 21 - 223
public function feed(iterable $food): string
{
$foodItems = [];
... line 227
foreach ($food as $foodItem) {
$foodItems[] = $foodItem;
}
... lines 231 - 236
}
}

Finally, update the count to use $foodItems and the same for implode().

239 lines src/AppBundle/Entity/Genus.php
... lines 1 - 18
class Genus
{
... lines 21 - 223
public function feed(iterable $food): string
{
... lines 226 - 231
if (count($foodItems) === 0) {
... line 233
}
... line 235
return sprintf('%s recently ate: %s', $this->getName(), implode(', ', $foodItems));
}
}

And just like that, this function can accept any value that we can loop over.

Now, I wouldn't necessarily do this in my code unless I needed it. If you're always going to pass an array, just type-hint with array! But, you will start seeing this more and more in libraries that you use.

Leave a comment!

  • 2017-06-15 Andrew M.

    The comment on the typo is just so good (missed the remainder of the video due to laughing) :D

  • 2017-06-15 Victor Bocharsky

    Hey DevDonkey ,

    Yes, we can, nice tip! But what if we have not an array and not an \ArrayObject instance? :) We just covered more cases in this screencast - actually, we covered all available options ;)

    Cheers!

  • 2017-06-15 DevDonkey

    you could also do:


    if ($food instanceof \ArrayObject) {
    $food = $food->getArrayCopy();
    }

    :)