Login to bookmark this video
Buy Access to Course
10.

Lazy Services

|

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

Time 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:

19 lines | src/Remote/ParentalControls.php
// ... 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'):

19 lines | src/Remote/ParentalControls.php
// ... 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:

25 lines | src/Remote/Button/VolumeUpButton.php
// ... 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():

25 lines | src/Remote/Button/VolumeUpButton.php
// ... 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):

27 lines | src/Remote/Button/VolumeUpButton.php
// ... 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:

21 lines | src/Remote/ParentalControls.php
// ... lines 1 - 7
#[Lazy]
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...

21 lines | src/Remote/ParentalControls.php
// ... 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]:

21 lines | src/Remote/ParentalControls.php
// ... lines 1 - 8
class ParentalControls
// ... lines 10 - 21

and in VolumeUpButton, add #[Lazy] above the $parentalControls argument:

29 lines | src/Remote/Button/VolumeUpButton.php
// ... 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!