More Laziness Attributes
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 SubscribeThe awesome thing about "lazy services" is they require no changes to your code (as long as services aren't final). But what if the ParentalControls service lived inside a 3rd-party package and was final? Tricky! But we do have some options.
#[AutowireServiceClosure]
Pretend that ParentalControls is final and lives in a 3rd-party package. In VolumeUpButton, replace #[Lazy] with #[AutowireServiceClosure] passing ParentalControls::class:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 14 | |
| public function __construct( | |
| #[AutowireServiceClosure(ParentalControls::class)] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 20 - 28 | |
| } |
This will inject a closure that returns a ParentalControls instance when invoked (and it will only be instantiated when invoked).
To help our IDE, add a docblock above the constructor: @param \Closure():ParentalControls $parentalControls:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| /** | |
| * @param \Closure():ParentalControls $parentalControls | |
| */ | |
| public function __construct( | |
| // ... lines 16 - 17 | |
| ) { | |
| } | |
| // ... lines 20 - 28 | |
| } |
Now, down in the press() method's if statement, switch false to true so we always detect that the volume is too high. Because $parentalControls is a closure, we need to wrap $this->parentalControls in braces and invoke it with () before calling ->volumeTooHigh():
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 20 | |
| public function press(): void | |
| { | |
| if (true) { // determine if volume is too high | |
| ($this->parentalControls)()->volumeTooHigh(); | |
| } | |
| dump('Change the volume up'); | |
| } | |
| } |
Check this out! Because we added the docblock, our IDE provides auto-completion and lets us click through (with CMD+click) to the volumeTooHigh() method. Awesome!
Remove the dump(), spin over to our app, refresh, and press the "volume up" button. Jump into the profiler. We see that the volumeTooHigh() logic is being called. Great! The ParentalControls service is only instantiated when the closure is invoked - and we only invoke it when needed.
#[AutowireCallable]
Let's look at another way to do the same thing. In VolumeUpButton, replace #[AutowireServiceClosure] with #[AutowireCallable]. Keep ParentalControls::class as the first argument but prefix it with service:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 14 | |
| public function __construct( | |
| #[AutowireCallable( | |
| service: ParentalControls::class, | |
| // ... lines 18 - 19 | |
| )] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 24 - 32 | |
| } |
#[AutowireCallable] also injects a closure. But instead of returning the full service object, it instantiates the service, calls a single method on it, then returns the result.
Make this multiline to give us some more room. Add a second argument: method: 'volumeTooHigh':
| // ... lines 1 - 14 | |
| public function __construct( | |
| #[AutowireCallable( | |
| // ... line 17 | |
| method: 'volumeTooHigh', | |
| // ... line 19 | |
| )] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 24 - 34 |
When Symfony instantiates a service that uses #[AutowireCallable], by default, it will instantiate its service. It's an eager beaver! To avoid this, add a third argument: lazy: true:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 14 | |
| public function __construct( | |
| #[AutowireCallable( | |
| // ... lines 17 - 18 | |
| lazy: true, | |
| )] | |
| private \Closure $parentalControls, | |
| ) { | |
| } | |
| // ... lines 24 - 32 | |
| } |
Now, ParentalControls will only be instantiated when the closure is invoked.
In the docblock above, change the closure return type to void to match the return type of volumeTooHigh():
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| /** | |
| * @param \Closure():void $parentalControls | |
| */ | |
| public function __construct( | |
| // ... lines 16 - 21 | |
| ) { | |
| } | |
| // ... lines 24 - 32 | |
| } |
Down in press(), remove the ->volumeTooHigh() call:
| // ... lines 1 - 9 | |
| final class VolumeUpButton implements ButtonInterface | |
| { | |
| // ... lines 12 - 24 | |
| public function press(): void | |
| // ... line 26 | |
| if (true) { // determine if volume is too high | |
| ($this->parentalControls)(); | |
| } | |
| // ... lines 30 - 31 | |
| } | |
| } |
This is now called by the closure when invoked.
Spin back to the app, refresh, press the "volume up" button, and jump into the profiler. The ParentalControls::volumeTooHigh() logic is still being called. Perfect!
#[AutowireCallable] is certainly cool, but for most cases, I prefer using #[AutowireServiceClosure] because:
- It's lazy by default.
- More flexible because it returns the full service object.
- And, with proper docblocks, we get: auto-completion, method navigation, refactoring support, and better static analysis with tools like PhpStan.
Ok team, that's it for this course! Put a #[TimeForVacation] attribute on your code and go relax!
YAML service config isn't going away entirely, but these attributes improve your developer experience by keeping your code and service configuration together.
More attributes are being added in almost every new Symfony version. Follow the Symfony blog to stay up-to-date! Check this out, in Symfony 7.2, there's a new #[WhenNot] attribute! It's basically the opposite of the #[When] attribute we discussed earlier. Cool!
Check out the "Dependency Injection" section of the Symfony Attributes Overview doc to see a list of all the dependency injection attributes that are currently available and how they work.
'Til next time! Happy coding!
11 Comments
Hello,
In the script, using
method: 'volumeToHigh', gives the same error for me: Cannot create lazy closure because its corresponding callable is invalid.I replaced it with volumeTooHigh and it worked !
Hey alpernuage,
Yes, it should be
volumeTooHighinstead ofvolumeToHigh, you have a type in 1 char in the first version. I don't see we usevolumeToHighsomewhere in the video or scripts. If you see we use it somewhere - please, refer me to the exact code block or a specific time in the video and I will fix it :)Cheers!
Hi Victor,
Thanks for your reply!
Actually, I didn’t follow the video since I don’t have a SymfonyCasts subscription🙈 — I was just following the code shown on the script page. That’s where I noticed the volumeToHigh typo.
Cheers,
Alper
Hey @alpernuage ,
Hm, I can't find any
volumeToHighin the scripts or code 🙃 Ok, if you find one - definitely point me to that and I will fix. Otherwise, it either was already fixed or you just made a typo I guess?Cheers!
Sorry for not being clear earlier — I wanted to share a screenshot but couldn’t upload it before.
Here it is: https://ibb.co/YFwhC5St
After the sentence
“Make this multiline to give us some more room. Add a second argument: method: 'volumeTooHigh':”
the usage is correct: method: 'volumeTooHigh',
However, 4 code blocks on this page contain the wrong version volumeToHigh: in this section
👉 https://symfonycasts.com/screencast/dependency-injection-attributes/more-laziness-attributes#code-autowirecallable-code
They appear after expanding the code sections (with the two arrows) in these parts:
Hope that helps clarify it! 🙈
Hey @alpernuage,
I see it now, thanks for sharing more details on it. I fixed the affected code blocks on that page. Yes, as you figured out, the correct version should be
volumeTooHigh:)Cheers!
Hello:
Everything was going well until class 11, when I finished coding according to the instructions, and when I tried to run the application, the following error appeared:
Cannot create lazy closure because its corresponding callable is invalid.
Hey @giorgiocba,
Ak, I did see this error a while ago and made a note to come back and check it out but now I can't re-recreate it! Let's see if we can figure this out.
startdirectory?composer.locklike runcomposer update?lazy: truedoes it work?--Kevin
If you remove lazy: true does it work? This is the solution
Thanks for the update! I think somehow there was a bug with some combination of Symfony/PHP versions. I've tried a bunch of permutations but can't re-create.
Thanks, Kevin. I'll check out all the options.
"Houston: no signs of life"
Start the conversation!