diff --git a/sfcasts/asalias.md b/sfcasts/asalias.md index ce78c18..676a868 100644 --- a/sfcasts/asalias.md +++ b/sfcasts/asalias.md @@ -1,107 +1,40 @@ # Alias an Interface with AsAlias -Alright so now that we've successfully refactored our buttons to make it much -easier to add new buttons and have them automatically listed in our remote, let's -look at now adding features. So let's look at now adding a feature. So I think it -would be good in our button remote to add some logging. So before and after a -button press, we will log, we will add a little log message. So we could inject our -logger here, the Symfony logger here, which is from the Symfony logger here, but -that would technically violate the single responsibility principle of this class. -And what that means is it would mean the class is doing too much. So already we're, -you know, clicking the proper button here and running the button logic. If we add -logging here, technically that's doing something unrelated to pressing a button. So -what we're going to do instead is we're going to decorate this button remote with a -new remote called logger, a new logger remote that is purely responsible for doing -the logging and then calling the logic of the button remote inside it, but it keeps -it separate. So the button pressing logic will still be in this and the logging -remote will only care about logging. - -So when using decoration, you could technically create another class, a logger -remote class that has the same API here, and that will work, but usually it's best -practice to add an interface and use decoration with interfaces. So the first thing -we're going to do is we're going to create a remote interface. So in our remote -folder, we'll create a new PHP class and we'll call this `RemoteInterface`. And -we're going to add our two methods from `ButtonRemote` in here. So we'll just copy -them. So we'll grab the press, our two public methods from here. So we'll copy them -and paste. So we have our press. And then down here, we also get our list of -buttons. So we'll copy the whole doc block here and then add a semicolon. - -And yeah, so this interface is done. So the first thing we'll do is implement the -interface here in our `ButtonRemote`. Perfect. And PHPStorm seems to be happy. We're -implementing the interface successfully. So now what we're going to do is we're -actually going to inject in our controller, instead of injecting the button remote, -we're going to inject the interface. Typically, Symfony allows auto-wiring concrete -classes, as well as interfaces. And technically, if possible, but when possible, you -should defer to using interfaces as it just makes your code easier to extend later -on. So if we go to our controller, and right here, instead of `ButtonRemote`, we're -going to do `RemoteInterface`. We're going to change this type hint to -`RemoteInterface`. - -Now let's jump back to our app and make sure everything's still working. So refresh. -And great, looks like it's all still working. So now what we want to do is add a new -implementation of `RemoteInterface` for the logger. So we'll add a logger button, a -`LoggerRemote`. All right, and we'll implement this interface, our -`RemoteInterface`. And then we'll use `Ctrl-Enter` here to implement the methods. - -All right, so what this service needs is it needs the logger, of course, but what it -also needs because we're using service decoration, we need to also inject another -instance of `RemoteInterface`, the one that we're decorating. So we'll add a -constructor here, and we'll just clean this up a little bit. - -And then, so first thing we're going to do is inject our `LoggerInterface`, the one -from PSR log, `Logger`. And then we're going to inject `RemoteInterface`. So just -like in our controller, it's always best if possible to inject the interface as -opposed to the concrete class. We could inject an instance of `ButtonRemote` here, -but let's use the interface to follow sort of best practices. - -So the first thing we want to do is basically buttons. We want to defer to our -decorated service. We're not going to do any logging here. So let's just call this, -this remote. Actually, you know what, what we're going to do is I'm going to rename -this inner. So it's super clear that this is the inner service that we're -decorating. So we'll call this `innerButtons` and we'll return this. Okay. So we're -done here. We'll do the same thing here. We will call this `innerPress` and then -pass the name. - -So now this decorator is deferring all the calls to the inner service that it's -decorating, but now we want to add our logging. So we will, before we press the -button, we'll add this logger. We'll do, we'll log to the info type, and then we'll -just do `pressing button`, and then we'll do `{{ name }}`. And we will add a context -array here and we will pass `name`. - -So what this is doing is this is a little syntax that monolog uses, which is the -default Symfony logger, and it allows you to inject in this, it's sort of like a -little templating string to inject your context variables into the string, just -saves you from having to use like a `sprintf` or something like that. So we'll copy -this and paste it after we execute the button press. And instead of pressing, we'll -call `pressed`. - -Okay. So let's go back to our app and see if everything's still working. So we'll -refresh and uh-oh, an error. So our controller requires a remote argument that could -not be resolved. Cannot auto-wire argument remote. It references interface -`RemoteInterface`, but no service exists. So what's happening here is we now have -our interface now has two implementations. There's a `ButtonRemote` implementation -and a `LoggerRemote` implementation. - -And basically what this error is telling us is Symfony doesn't know which one to use. -When we only had our `ButtonRemote`, because there's only one service that -implemented the `RemoteInterface`, Symfony knew to just use that, but now it -doesn't know. And if we look back at the error message, it gives us a little hint. -You should maybe alias this interface with one of these existing services, -`ButtonRemote`, or `LoggerRemote`. So what we need here is an alias. - -An alias in Symfony allows you to, for our purposes allows us to say, when someone -asks for this interface, this `RemoteInterface`, what service should be used and -that will solve this problem. So the way to do that is we're going to use our next -dependency injection attribute, and this one's called `asAlias`. So what you need to -do for this is find the service that you want to be aliased with `RemoteInterface` -and make sure that it does implement `RemoteInterface`. - -And at the top here, we can add the attribute `asAlias`. And now what this does is -it tells Symfony that anytime we ask for one of the interfaces that this service -implements, in our case, just `RemoteInterface`, inject this one. So if we go back -to our app and refresh, we should be good to go. Perfect. It's working. - -One thing we don't have now is logging. We added our `LoggerRemote`, but logging -isn't working yet. Right now, it's basically just ignored. The service isn't used. -So in the next chapter, we'll look at how we can decorate `ButtonRemote` with -`LoggerRemote` and actually make this all work together. That's next. +Now that we've successfully refactored our buttons so we can easily add *new* buttons that automatically appear on our remote, let's work on adding some new features. How about adding a little log message before and after pressing a button? + +We *could* inject the Symfony logger here, but that would *technically* violate the Single Responsibility Principle of this class. *Basically*, this class would be doing too much. We're already clicking the button and running the button logic. If we also add logging, we're doing something unrelated to pressing a button. + +*Instead*, we're going to decorate this button remote with a *new* logger remote who's only responsible for doing the logging and calling the button remote logic inside of it, while keeping it separate. So the button press logic will be *here* and the logging remote will only care about logging. + +When using decoration, you could *technically* create a logger remote class that has the same API here, and that *would* work, but it's best practice to add an *interface* and use decoration with interfaces. *So*, the first thing we're going to do is create a remote interface. In the `Remote/` folder, create a new PHP class called `RemoteInterface`. We need to add two methods from `ButtonRemote.php` here, so head over, copy the this method here, and... *paste*. So we have `press()`... and then, down here, we also need our list of buttons. Copy the whole block here... paste... and then add a semicolon. Nice! Interface *done*! + +Now we need to *implement* `RemoteInterface` in `ButtonRemote.php`. And... perfect! PHPStorm seems to be happy, so we're sucessfully implementing our interface. The next step is to inject the *interface* into our controller. Symfony allows autowiring concrete classes as well as interfaces but, whenever possible, you should stick to using interfaces, since it just makes your code easier to extend later on. Over in our controller... right here, we're going to replace `ButtonRemote` with `RemoteInterface`... and change this typehint to `$remoteInterface`. + +Okay, let's jump back to our app and make sure everything's still working. Refresh, and... looks great! Now we need to add a new implementation of `RemoteInterface` for the logger, so, back in our code, we'll add a new PHP class called `LoggerRemote`. *Implement* our `RemoteInterface`... and then use "command" + "enter" here to implement the methods. + +All right, our service needs the logger of course, but since we're using service decoration, we also need to inject another instance of `RemoteInterface` - the one we're decorating. So add a constructor here... and then we can clean this up a little. Next, we're going to inject `LoggerInterface` - the one from "Psr\Log" - and `RemoteInterface`. Just like with our controller, it's best to inject the *interface* instead of the concrete class whenever possible. + +Now we want to defer `buttons()` to our decorated service. We're not going to do any logging here, so let's just call `$this->remote`... and... hm... I'm actually going to rename `$remote` to `$inner` so it's *super* clear that this is the inner service we're decorating. So, say `$this->inner->buttons()` and `return` it. Done! We'll do the same thing up here. Call `$this->inner->press()` and pass the `$name`. Now this decorator is deferring all of the calls to the `$inner` service that it's decorating, but we *still* need to add our logging. So *before* we press the button, we'll add this logger with `$this->logger->info('Pressing button {name}.')` with a context array where we'll pass `'name' => $name`. + +This is a little syntax that monolog uses, which is the default Symfony logger, and it's basically a templating string to inject your context variables into the string. *So*, copy this and paste it *after* we execute the button press... and instead of `Pressing`, we'll call `pressed`. + +Okay! Let's go back to our app and see if everything's still working. Refresh and... uh-oh, an *error*. It looks like our controller requires a `$remote` argument that couldn't be resolved. + +`Cannot autowire argument $remote of +"App\Controller\RemoteController::index()": it +references interface "App\Remote\RemoteInterface" +but no such service exists.` + +This tells us that our interface has *two* implementations. There's a `ButtonRemote` implementation and a `LoggerRemote` implementation. Basically, Symfony doesn't know which one to use. When we were only using `ButtonRemote`, since there was only one service that implemented `RemoteInterface`, Symfony knew to use *that* one, but now we've complicated things a bit. How do we fix it? This error message gives us a little hint. + +`You should maybe alias this interface to one of +these existing services: "App\Remote\ButtonRemote", +"App\Remote\LoggerRemote"`. + +It looks like we could use an *alias*. In Symfony, an alias allows us to determine which service should be used when someone asks for the `RemoteInterface`. To do that, we need to deploy a new dependency injection attribute - `asAlias`. So let's find the service that we want to be aliased with `RemoteInterface` and make sure that it does, in fact, implement `RemoteInterface`. + +At the top here, we can add the attribute `asAlias`, which tells Symfony that any time we ask for an interface this service implements (in our case, just `RemoteInterface`), then it needs to inject *that* one. If we go back to our app and refresh... perfect! It's working again! + +Okay, that's done, but we're still not logging. We a + +Next: Let's look at how we can decorate `ButtonRemote` with `LoggerRemote` and make them work *together*.