This course is still being released! Check back later for more chapters.
List Buttons with AutowireIterator
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe'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!
// ... 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!
// ... 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
.
// ... lines 1 - 23 | |
public function buttons(): iterable | |
{ | |
foreach ($this->buttons as $name => $button) { | |
yield $name; | |
} | |
} | |
// ... lines 30 - 31 |
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()
:
// ... 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:
// ... 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
:
// ... 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
.
// ... 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
:
// ... 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:
// ... 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:
// ... 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:
// ... 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
:
// ... lines 1 - 6 | |
'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
:
// ... lines 1 - 6 | |
'channel-up', priority: 40) | (|
final class ChannelUpButton implements ButtonInterface | |
// ... lines 9 - 15 |
The "Channel Down" button, a priority of 30
:
// ... lines 1 - 6 | |
'channel-down', priority: 30) | (|
final class ChannelDownButton implements ButtonInterface | |
// ... lines 9 - 15 |
"Volume Up" a priority of 20
:
// ... lines 1 - 6 | |
'volume-up', priority: 20) | (|
final class VolumeUpButton implements ButtonInterface | |
// ... lines 9 - 15 |
and "Volume Down", a priority of 10
:
// ... lines 1 - 6 | |
'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.