Lazy Services
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 SubscribeTime to talk about one of my favorite Symfony features: lazy services. Many times, you inject a service, but it's only used under certain conditions. Here's an example:
public function calculateSomething(ExpensiveService $service)
{
if ($this->isCached()) {
return $this->getCachedValue();
}
return $service->doSomethingExpensive();
}
In this case, most of the time, the ExpensiveService
is not used. But, because it's injected, it's always instantiated.
A "lazy service" gets to relax on the couch, eating potato chips, until it's actually needed.
Create a New Button
Let's do one! Create a new PHP class in src/Remote/
called ParentalControls
. This will send us alerts when the kids are doing something they totally know they shouldn't. The little rascals. Mark the class as final
and add a constructor with: private MailerInterface $mailer
so we can send the alerts via email:
// ... lines 1 - 6 | |
final class ParentalControls | |
// ... line 8 | |
public function __construct( | |
private MailerInterface $mailer, | |
) { | |
} | |
// ... lines 13 - 17 | |
} |
Add a new public method called volumeTooHigh()
with a void
return type. Inside, to represent sending the email, just write dump('send volume alert email')
:
// ... lines 1 - 6 | |
final class ParentalControls | |
{ | |
// ... lines 9 - 13 | |
public function volumeTooHigh(): void | |
{ | |
dump('send volume alert email'); | |
} | |
} |
Next, open up VolumeUpButton
, add a constructor here and inject private ParentalControls $parentalControls
:
// ... lines 1 - 8 | |
final class VolumeUpButton implements ButtonInterface | |
{ | |
public function __construct( | |
private ParentalControls $parentalControls, | |
) { | |
} | |
// ... lines 15 - 23 | |
} |
In the press()
method, pretend we're detecting when the volume is too high. Add an if (true)
statement (with a comment to remind us what this represents), then $this->parentalControls->volumeTooHigh()
:
// ... lines 1 - 8 | |
final class VolumeUpButton implements ButtonInterface | |
{ | |
// ... lines 11 - 15 | |
public function press(): void | |
{ | |
if (true) { // determine if volume is too high | |
$this->parentalControls->volumeTooHigh(); | |
} | |
// ... lines 21 - 22 | |
} | |
} |
Spin over to our app, refresh, press the "volume up" button, and check the profiler. We can see our ParentalControls
service is being used and working!
Back in VolumeUpButton
, switch true
to false
to pretend we didn't detect a high volume. Below the if statement, write dump($this->parentalControls)
:
// ... lines 1 - 8 | |
final class VolumeUpButton implements ButtonInterface | |
{ | |
// ... lines 11 - 15 | |
public function press(): void | |
{ | |
if (false) { // determine if volume is too high | |
$this->parentalControls->volumeTooHigh(); | |
} | |
// ... line 21 | |
dump($this->parentalControls); | |
// ... lines 23 - 24 | |
} | |
} |
Spin back, refresh, press "volume up" and check the profiler. Even though we didn't use ParentalControls
, it was still instantiated! So was the mailer service it depends on, the mailer transport, and so on. This is a long chain of dependencies that were instantiated but not used!
#[Lazy]
Class Attribute
The fix? Make ParentalControls
a lazy service. Open that class and add the #[Lazy]
attribute:
// ... lines 1 - 7 | |
final class ParentalControls | |
// ... lines 10 - 21 |
Back in our app, refresh, and... an error!
Cannot generate lazy proxy for service
ParentalControls
.
Check the previous exception to see why:
Class
ParentalControls
is final.
This is a minor downside of using lazy services: the class cannot be final. We'll see why in a second.
Open ParentalControls
, remove final...
// ... lines 1 - 8 | |
class ParentalControls | |
// ... lines 10 - 21 |
and refresh the app. We're good!
Press "volume up" and check the profiler.
Ghost Proxies
Whoa! What's this? ParentalControlsGhost
with a random string after it? This is called a "ghost proxy" and it's generated by Symfony. It extends our ParentalControls
class (which is why it can't be final) and, until it's actually used, is not fully instantiated - a ghost! Spooky!
But what if we didn't "own" ParentalControls
? What if it was part of a 3rd party package? How could we make it lazy? We can't edit vendor code, but the #[Lazy]
attribute can be added to an argument to make it lazy on a per-use basis.
#[Lazy]
Argument Attribute
In ParentalControls
, remove #[Lazy]
:
// ... lines 1 - 8 | |
class ParentalControls | |
// ... lines 10 - 21 |
and in VolumeUpButton
, add #[Lazy]
above the $parentalControls
argument:
// ... lines 1 - 9 | |
final class VolumeUpButton implements ButtonInterface | |
{ | |
public function __construct( | |
#[Lazy] | |
private ParentalControls $parentalControls, | |
) { | |
} | |
// ... lines 17 - 27 | |
} |
In our app, refresh, press "volume up", and check the profiler. It's still lazy!
When you add the #[Lazy]
attribute to a class, all instances of that service are lazy. When you add it to an argument, it's lazy only when used in that context.
What if it existed in a 3rd party package and was final? Are we out of luck?
Nope! Symfony has a few other tricks - and attributes - up its sleeve to help. Let's check those out next!