Login to bookmark this video
Buy Access to Course
04.

List Buttons with AutowireIterator

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We've refactored our app to use the Command pattern to execute each button. Great! New goal: make the buttons more dynamic: as we add new button classes, I'd like to not have to edit our template.

Start inside ButtonRemote. We need a way to get a list of all the button names: the indexes from our container. To do that, create a public method here called buttons(), which will return an array. This will be an array of strings: our button names!

31 lines | src/Remote/ButtonRemote.php
// ... lines 1 - 7
final class ButtonRemote
{
// ... lines 10 - 20
/**
* @return string[]
*/
public function buttons(): iterable
{
// ... lines 26 - 28
}
}

#[AutowireIterator]

The mini-container is great for fetching individual services. But you can't loop over all the button services inside. To fix that, change #[AutowireLocator] to #[AutowireIterator]. This tells Symfony to inject an iterable of our services, so this will no longer be a ContainerInterface. Instead, use iterable and rename $container to $buttons here... and here. Nice!

31 lines | src/Remote/ButtonRemote.php
// ... lines 1 - 7
final class ButtonRemote
{
public function __construct(
#[AutowireIterator(ButtonInterface::class)]
private iterable $buttons,
) {
}
// ... line 15
public function press(string $name): void
{
$this->buttons->get($name)->press();
}
// ... lines 20 - 29
}

Now, below, loop over the buttons: foreach ($this->buttons as $name => $button). $button is the actual service, but we're going to ignore that completely and just grab the $name, and add it to this $buttons array. At the bottom, return $buttons.

35 lines | src/Remote/ButtonRemote.php
// ... lines 1 - 23
public function buttons(): iterable
{
$buttons = [];
foreach ($this->buttons as $name => $button) {
$buttons[] = $name;
}
return $buttons;
}
// ... lines 34 - 35

Passing Buttons to the Template

Back in the controller, we're already injecting ButtonRemote, so down where we render the template, pass a new buttons variable with 'buttons' => $remote->buttons():

35 lines | src/Controller/RemoteController.php
// ... lines 1 - 12
final class RemoteController extends AbstractController
{
// ... line 15
public function index(Request $request, ButtonRemote $remote): Response
{
// ... lines 18 - 29
return $this->render('index.html.twig', [
'buttons' => $remote->buttons(),
]);
}
}

Add a dd() to see what it returns:

37 lines | src/Controller/RemoteController.php
// ... lines 1 - 29
dd($remote->buttons());
return $this->render('index.html.twig', [
// ... lines 33 - 37

Okay, back at the browser, refresh the page and... hm... that's not quite what we want. Instead of a list of numbers, we want a list of button names. To fix this, back in ButtonRemote, find #[AutowireIterator]. #[AutowireLocator], the attribute we had before, automatically uses the $index property from #[AsTaggedItem] for the service keys. #[AutowireIterator] does not! It just gives us an iterable with integer keys.

#[AutowireIterator]'s indexAttribute

To tell it to key the iterable using #[AsTaggedItem]'s $index, add indexAttribute set to key:

31 lines | src/Remote/ButtonRemote.php
// ... lines 1 - 9
public function __construct(
#[AutowireIterator(ButtonInterface::class, indexAttribute: 'key')]
private iterable $buttons,
// ... lines 13 - 31

Now, when we loop over $this->buttons, $name will be the $index which in our case, is the button name.

Over in our controller, we still have this dd() so, back in our app, refresh and... there we go! We have the button names now! Pretty cool!

Remove the dd(), then open index.html.twig.

37 lines | src/Controller/RemoteController.php
// ... lines 1 - 29
dd($remote->buttons());
return $this->render('index.html.twig', [
// ... lines 33 - 37

Rendering Buttons Dynamically

Right here, we have a hardcoded list of buttons. Add some space, and then for button in buttons:

51 lines | templates/index.html.twig
// ... lines 1 - 4
{% block body %}
<div class="mx-auto max-w-5xl">
<div class="bg-[#1B1B1D] w-[477px] mx-auto rounded-xl p-6">
// ... lines 8 - 18
<form method="post">
<div class="flex justify-center">
<ul class="grid grid-cols-2 row-span-3 gap-8">
{% for button in buttons %}
// ... lines 23 - 35
{% endfor %}
</ul>
</div>
</form>
// ... lines 40 - 47
</div>
</div>
{% endblock %}

In the UI, you probably noticed that the first button - the "Power" button - looks different: it's red & larger. To keep that special styling, add an if loop.first here, and an else for the rest of the buttons:

51 lines | templates/index.html.twig
// ... lines 1 - 21
{% for button in buttons %}
{% if loop.first %}
// ... lines 24 - 28
{% else %}
// ... lines 30 - 34
{% endif %}
{% endfor %}
// ... lines 37 - 51

Copy the code for the first button and paste it here. Instead of hard-coding "power" as the button's value, render the button variable. Same for the Twig icon's name:

51 lines | templates/index.html.twig
// ... lines 1 - 22
{% if loop.first %}
<li class="col-span-2 flex justify-center -mb-4">
<button name="button" value="{{ button }}" class="flex rounded-full border border-[#3F4241] hover:border-[#C33E21] w-[100px] h-[100px] justify-center items-center focus:bg-[#C33E21] group">
<twig:ux:icon name="{{ button }}" width="184" height="184" class="fill-[#C33E21] group-focus:fill-[#ffffff]" />
</button>
</li>
{% else %}
// ... lines 30 - 51

For the rest of the buttons, copy the second button's code, paste, then replace the button's value attribute and icon name with the button variable:

51 lines | templates/index.html.twig
// ... lines 1 - 28
{% else %}
<li>
<button name="button" value="{{ button }}" class="flex rounded-full border border-[#3F4241] hover:border-white w-[80px] h-[80px] justify-center items-center focus:bg-[#ffffff] group">
<twig:ux:icon name="{{ button }}" width="36" height="36" class="fill-white group-focus:fill-[#0E0E0E]" />
</button>
</li>
{% endif %}
// ... lines 36 - 51

Nice. Celebrate by deleting the rest of the hard-coded buttons.

Let's try it! Spin back over to our app and refresh... hm... It's rendering the buttons, but they're not in the right order. We want this one at the top. So... what do we do?

Ordering Services with AsTaggedItem::$priority

We need to enforce the order of our buttons in the iterator. To do that, open PowerButton. #[AsTaggedItem] has a second argument: priority.

Before, with #[AutowireLocator], this wasn't important because we were just fetching services by their name. But now that we do care about the order, add priority and set it to, how about, 50:

15 lines | src/Remote/Button/PowerButton.php
// ... lines 1 - 6
#[AsTaggedItem('power', priority: 50)]
final class PowerButton implements ButtonInterface
// ... lines 9 - 15

Now we go to the "Channel Up" button and add a priority of 40:

15 lines | src/Remote/Button/ChannelUpButton.php
// ... lines 1 - 6
#[AsTaggedItem('channel-up', priority: 40)]
final class ChannelUpButton implements ButtonInterface
// ... lines 9 - 15

The "Channel Down" button, a priority of 30:

15 lines | src/Remote/Button/ChannelDownButton.php
// ... lines 1 - 6
#[AsTaggedItem('channel-down', priority: 30)]
final class ChannelDownButton implements ButtonInterface
// ... lines 9 - 15

"Volume Up" a priority of 20:

15 lines | src/Remote/Button/VolumeUpButton.php
// ... lines 1 - 6
#[AsTaggedItem('volume-up', priority: 20)]
final class VolumeUpButton implements ButtonInterface
// ... lines 9 - 15

and "Volume Down", a priority of 10:

15 lines | src/Remote/Button/VolumeDownButton.php
// ... lines 1 - 6
#[AsTaggedItem('volume-down', priority: 10)]
final class VolumeDownButton implements ButtonInterface
// ... lines 9 - 15

Any button without an assigned priority has a default priority of 0.

Head back to our app and refresh... all right! We're back in business! All the buttons are added automatically and in the right order.

But you may have noticed we have a big problem. Press any button and... Error!

Attempted to call an undefined method "get" of class RewindableGenerator.

Huh?

This RewindableGenerator is the iterable object Symfony injects with #[AutowireIterator]. We can loop over this, but it does not have a get() method. Boo!

Next, let's fix this by injecting an object that's both a service iterator and locator.