Alias an Interface with AsAlias
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 add a new feature! I want to add logging to the button presses so we can keep track of what our minions are doing!
In ButtonRemote
, we could inject the logger service and log the button presses right here. But technically... this violates something called the "single responsibility principle". That's just a fancy way of saying that a class should only do one thing. Right now, this class handles pressing buttons. Adding logging here would make it do two things. And that's probably fine, but let's challenge ourselves!
Decorator Pattern
Instead, we'll use a design pattern called "Decoration" by creating a new class that wraps, or "decorates", ButtonRemote
.
In src/Remote/
, create a new PHP class called LoggerRemote
. This is our "decorator" and it needs the same methods as the class it's decorating. Copy the two methods from ButtonRemote
, paste them here and remove their guts. Add a constructor, and inject the logger service with private LoggerInterface
(the one from Psr\Log
) $logger
. Now, inject the object we're decorating: private ButtonRemote $inner
. I like to use $inner
as the parameter name when creating decorators.
// ... lines 1 - 6 | |
final class LoggerRemote | |
{ | |
public function __construct( | |
private LoggerInterface $logger, | |
private ButtonRemote $inner, | |
) { | |
} | |
// ... lines 14 - 34 | |
} |
In each method, first, defer to the inner object. In press()
, write $this->inner->press($name);
and in buttons()
, return $this->inner->buttons()
:
// ... lines 1 - 6 | |
final class LoggerRemote | |
{ | |
// ... lines 9 - 14 | |
public function press(string $name): void | |
{ | |
// ... lines 17 - 20 | |
$this->inner->press($name); | |
// ... lines 22 - 25 | |
} | |
// ... line 27 | |
/** | |
* @return string[] | |
*/ | |
public function buttons(): iterable | |
{ | |
return $this->inner->buttons(); | |
} | |
} |
Now let's add the logging. Before the inner press, add $this->logger->info('Pressing button {name}')
and add a context array with 'name' => $name
. This curly brace stuff is a mini-templating system used by Monolog, Symfony's logger. Copy this, paste below the inner press and change "Pressing" to "Pressed":
// ... lines 1 - 6 | |
final class LoggerRemote | |
{ | |
// ... lines 9 - 14 | |
public function press(string $name): void | |
{ | |
$this->logger->info('Pressing button {name}', [ | |
'name' => $name | |
]); | |
$this->inner->press($name); | |
$this->logger->info('Pressed button {name}', [ | |
'name' => $name | |
]); | |
} | |
// ... lines 27 - 34 | |
} |
Decorator done!
To actually use this decorator, in RemoteController
, instead of injecting ButtonRemote
, inject LoggerRemote
:
// ... lines 1 - 12 | |
final class RemoteController extends AbstractController | |
// ... line 14 | |
'/', name: 'home', methods: ['GET', 'POST']) | (|
public function index(Request $request, LoggerRemote $remote): Response | |
{ | |
// ... lines 18 - 32 | |
} | |
} |
Let's try it! Back in our app, press the power button and jump into the profiler for the last request. We can see that the logic from ButtonRemote
is still being called. And if we check out the "Logs" panel, we see the messages!
Decorator Interface
The two remote classes have the same methods, this is a sign that we could use a common interface. Create a new class in src/Remote/
called RemoteInterface
. Copy the press()
method stub from LoggerRemote
and paste it here. Do the same for buttons()
:
// ... lines 1 - 4 | |
interface RemoteInterface | |
{ | |
public function press(string $name): void; | |
/** | |
* @return string[] | |
*/ | |
public function buttons(): iterable; | |
} |
Next, make both remote classes implement this interface. In ButtonRemote
, add implements RemoteInterface
:
// ... lines 1 - 8 | |
final class ButtonRemote implements RemoteInterface | |
// ... lines 10 - 30 |
... and do the same in LoggerRemote
:
// ... lines 1 - 6 | |
final class LoggerRemote implements RemoteInterface | |
// ... lines 8 - 36 |
In LoggerRemote
's constructor, Change ButtonRemote
to RemoteInterface
:
// ... lines 1 - 6 | |
final class LoggerRemote implements RemoteInterface | |
{ | |
public function __construct( | |
// ... line 10 | |
private RemoteInterface $inner, | |
) { | |
} | |
// ... lines 14 - 34 | |
} |
We don't have to do this, but now that we have an interface, that's really the best thing to type-hint.
Back in the app, refresh and... Error!
Cannot autowire service
LoggerRemote
: argument$inner
of method__construct()
references interfaceRemoteInterface
but no such service exists.
This happens when Symfony tries to autowire an interface but there are multiple implementations. We need to tell Symfony which of our two services to use when we type-hint RemoteInterface
. And the error even gives us a hint!
You should maybe alias this interface to one of these existing services: "ButtonRemote", "LoggerRemote".
Ah, we need to "alias" our interface. Technically, this will create a new service whose id is App\Remote\RemoteInterface
, but is really just an alias - or a pointer - to one of our real remote services.
#[AsAlias]
Do this with, you guessed it, another attribute: #[AsAlias]
. In ButtonRemote
, our inner-most class, add #[AsAlias]
:
// ... lines 1 - 5 | |
use Symfony\Component\DependencyInjection\Attribute\AsAlias; | |
// ... lines 7 - 9 | |
final class ButtonRemote implements RemoteInterface | |
{ | |
// ... lines 13 - 30 | |
} |
This tells Symfony:
Hey! When you need to autowire a
RemoteInterface
, useButtonRemote
.
Back in our app, refresh and... the error is gone! Press "channel up" and check the profiler. The button logic is still being called and if we check the "Logs" panel, there's our messages.
Open up RemoteController
: we're still injecting a concrete instance of our service. That's fine, but we can be fancier now and use RemoteInterface
:
// ... lines 1 - 4 | |
use App\Remote\RemoteInterface; | |
// ... lines 6 - 12 | |
final class RemoteController extends AbstractController | |
{ | |
// ... line 15 | |
public function index(Request $request, RemoteInterface $remote): Response | |
{ | |
// ... lines 18 - 32 | |
} | |
} |
Back in the app, press "channel down" and check the profiler. The button logic is working, but our logs are gone!
Because we aliased RemoteInterface
to ButtonRemote
, Symfony doesn't know about our decoration! When it sees the RemoteInterface
type-hint, it injects ButtonRemote
, not LoggerRemote
.
Next: Let's fix this by using service decoration, and of course, another attribute!