Buy
Buy

Bonus! LoggerTrait & Setter Injection

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

What if we wanna send Slack messages from somewhere else in our app? This is the same problem we had before with markdown processing. Whenever you want to re-use some code - or just organize things a bit better - take that code and move it into its own service class.

Since this is such an important skill, let's do it: in the Service/ directory - though we could put this anywhere - create a new class: SlackClient:

... lines 1 - 2
namespace App\Service;
... lines 4 - 7
class SlackClient
{
... lines 10 - 30
}

Give it a public function called, how about, sendMessage() with arguments $from and $message:

... lines 1 - 7
class SlackClient
{
... lines 10 - 18
public function sendMessage(string $from, string $message)
{
... lines 21 - 29
}
}

Next, copy the code from the controller, paste, and make the from and message parts dynamic. Oh, but let's rename the variable to $slackMessage - having two $message variables is no fun.

... lines 1 - 7
class SlackClient
{
... lines 10 - 18
public function sendMessage(string $from, string $message)
{
... lines 21 - 24
$message = $this->slack->createMessage()
->from($from)
->withIcon(':ghost:')
->setText($message);
$this->slack->sendMessage($message);
}
}

At this point, we just need the Slack client service. You know the drill: create a constructor! Type-hint the argument with Client from Nexy\Slack:

... lines 1 - 5
use Nexy\Slack\Client;
class SlackClient
{
... lines 10 - 13
public function __construct(Client $slack)
{
... line 16
}
... lines 18 - 30
}

Then press Alt+Enter and select "Initialize fields" to create that property and set it:

... lines 1 - 5
use Nexy\Slack\Client;
class SlackClient
{
... lines 10 - 11
private $slack;
public function __construct(Client $slack)
{
$this->slack = $slack;
}
... lines 18 - 30
}

Below, celebrate! Use $this->slack:

... lines 1 - 5
use Nexy\Slack\Client;
class SlackClient
{
... lines 10 - 11
private $slack;
public function __construct(Client $slack)
{
$this->slack = $slack;
}
public function sendMessage(string $from, string $message)
{
... lines 21 - 28
$this->slack->sendMessage($message);
}
}

In about one minute, we have a completely functional new service. Woo! Back in the controller, type-hint the new SlackClient:

... lines 1 - 5
use App\Service\SlackClient;
... lines 7 - 13
class ArticleController extends AbstractController
{
... lines 16 - 36
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack)
{
... lines 39 - 75
}
... lines 77 - 88
}

And below... simplify: $slack->sendMessage() and pass it the from - Khan - and our message. Clean up the rest of the code:

... lines 1 - 5
use App\Service\SlackClient;
... lines 7 - 13
class ArticleController extends AbstractController
{
... lines 16 - 36
public function show($slug, MarkdownHelper $markdownHelper, SlackClient $slack)
{
if ($slug === 'khaaaaaan') {
$slack->sendMessage('Kahn', 'Ah, Kirk, my old friend...');
}
... lines 42 - 75
}
... lines 77 - 88
}

And I don't need to, but I'll remove the old use statement:

... lines 1 - 5
use Nexy\Slack\Client;
... lines 7 - 94

Yay refactoring! Does it work? Refresh! Of course - we rock!

Setter Injection

Now let's go a step further... In SlackClient, I want to log a message. But, we already know how to do this: add a second constructor argument, type-hint it with LoggerInterface and, we're done!

But... there's another way to autowire your dependencies: setter injection. Ok, it's just a fancy-sounding word for a simple concept. Setter injection is less common than passing things through the constructor, but sometimes it makes sense for optional dependencies - like a logger. What I mean is, if a logger was not passed to this class, we could still write our code so that it works. It's not required like the Slack client.

Anyways, here's how setter injection works: create a public function setLogger() with the normal LoggerInterface $logger argument:

... lines 1 - 5
use Psr\Log\LoggerInterface;
class SlackClient
{
... lines 10 - 21
public function setLogger(LoggerInterface $logger)
{
... line 24
}
... lines 26 - 38
}

Create the property for this: there's no shortcut to help us this time. Inside, say $this->logger = $logger:

... lines 1 - 5
use Psr\Log\LoggerInterface;
class SlackClient
{
... lines 10 - 14
private $logger;
... lines 16 - 21
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 26 - 38
}

In sendMessage(), let's use it! Start with if ($this->logger). And inside, $this->logger->info():

... lines 1 - 7
class SlackClient
{
... lines 10 - 14
private $logger;
... lines 16 - 26
public function sendMessage(string $from, string $message)
{
if ($this->logger) {
$this->logger->info('Beaming a message to Slack!');
}
... lines 32 - 37
}
}

Bah! No auto-complete: with setter injection, we need to help PhpStorm by adding some PHPDoc on the property: it will be LoggerInterface or - in theory - null:

... lines 1 - 5
use Psr\Log\LoggerInterface;
class SlackClient
{
... lines 10 - 11
/**
* @var LoggerInterface|null
*/
private $logger;
... lines 16 - 38
}

Now it auto-completes ->info(). Say, "Beaming a message to Slack!":

... lines 1 - 7
class SlackClient
{
... lines 10 - 11
/**
* @var LoggerInterface|null
*/
private $logger;
... lines 16 - 26
public function sendMessage(string $from, string $message)
{
if ($this->logger) {
$this->logger->info('Beaming a message to Slack!');
}
... lines 32 - 37
}
}

In practice, the if statement isn't needed: when we're done, Symfony will pass us the logger, always. But... I'm coding defensively because, from the perspective of this class, there's no guarantee that whoever is using it will call setLogger().

So... is Symfony smart enough to call this method automatically? Let's find out - refresh! Our class still works... but check out the profiler and go to "Logs". Bah! Nothing is logged yet!

The @required Directive

Yep, Symfony's autowiring is not that magic - and that's on purpose: it only autowires the __construct() method. But... it would be pretty cool if we could somehow say:

Hey container! How are you? Oh, I'm wonderful - thanks for asking. Anyways, after you instantiate SlackClient, could you also call setLogger()?

And... yeah! That's not only possible, it's easy. Above setLogger(), add /** to create PHPDoc. You can keep or delete the @param stuff - that's only documentation. But here's the magic: add @required:

... lines 1 - 7
class SlackClient
{
... lines 10 - 21
/**
* @required
*/
public function setLogger(LoggerInterface $logger)
{
... line 27
}
... lines 29 - 41
}

As soon as you put @required above a method, Symfony will call that method before giving us the object. And thanks to autowiring, it will pass the logger service to the argument.

Ok, move over and... try it! There's the Slack message. And... in the logs... yes! We are logging!

The LoggerTrait

But... I have one more trick to show you. I like logging, so I need this service pretty often. What if we used the @required feature to create... a LoggerTrait? That would let us log messages with just one line of code!

Check this out: in src/, create a new Helper directory. But again... this directory could be named anything. Inside, add a new PHP Class. Actually, change this to be a trait, and call it LoggerTrait:

... line 1
namespace App\Helper;
... lines 3 - 5
trait LoggerTrait
{
... lines 8 - 26
}

Ok, let's move the logger property to the trait... as well as the setLogger() method. I'll retype the "e" on LoggerInterface and hit Tab to get the use statement:

... lines 1 - 3
use Psr\Log\LoggerInterface;
trait LoggerTrait
{
/**
* @var LoggerInterface|null
*/
private $logger;
/**
* @required
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 20 - 26
}

Next, add a new function called logInfo() that has two arguments: a $message and an array argument called $context - make it optional:

... lines 1 - 5
trait LoggerTrait
{
... lines 8 - 20
private function logInfo(string $message, array $context = [])
{
... lines 23 - 25
}
}

We haven't used it yet, but all the log methods - like info() - have an optional second argument where you can pass extra information. Inside the method: let's keep coding defensively: if ($this->logger), then $this->logger->info($message, $context):

... lines 1 - 5
trait LoggerTrait
{
... lines 8 - 20
private function logInfo(string $message, array $context = [])
{
if ($this->logger) {
$this->logger->info($message, $context);
}
}
}

Now, go back to SlackClient. Thanks to the trait, if we ever need to log something, all we need to do is add use LoggerTrait:

... lines 1 - 4
use App\Helper\LoggerTrait;
... lines 6 - 7
class SlackClient
{
use LoggerTrait;
... lines 11 - 30
}

Then, below, use $this->logInfo(). Pass the message... and, let's even pass some extra information - how about a message key with our text:

... lines 1 - 4
use App\Helper\LoggerTrait;
... lines 6 - 7
class SlackClient
{
use LoggerTrait;
... lines 11 - 18
public function sendMessage(string $from, string $message)
{
$this->logInfo('Beaming a message to Slack!', [
'message' => $message
]);
... lines 24 - 29
}
}

And that's it! Thanks to the trait, Symfony will automatically call the setLogger() method. Try it! Move over and... refresh!

We get the Slack message and... in the profiler, yes! And this time, the log message has a bit more information.

I hope you love the LoggerTrait idea.

Leave a comment!

  • 2018-10-05 weaverryan

    Hey mickaël!

    Sorry my slow reply on this - I got a little behind.

    No, you’re totally right that once you have @required Symfony will call this 100% of the time. So, in practice, you don’t need to check for the existence of the property before you call a method on it.

    It’s just that, from an object oriented standpoint, if you look st this class, nothing enforces that this settee was called. This could have practical implications in unit tests: if you forget to call setLogger, your code will fail. And actually, because the Logger really shouldn’t be needed to make the class work, you shouldn’t *need* to call setLogger(). So, by checking for existence, it just feels better from an OO perspective. And, I don’t need to unnecessarily call setLogger in a unit test.

    Anyways, your idea about adding a little bit of logic in @required makes total sense! And that would totally be possible to implement. But, I don’t think it’s probably something that we need. Side note: if, for some reason, the Logger service were available in debug mode but not in prod mode, you could just make the argument optional (Logger $logger = null) as then it would not be set if the service didn’t exist (when the arg is required and the service doesn’t exist, you get an error).

    Cheers!

  • 2018-10-04 Serge Boyko

    Cool, thanks!

  • 2018-10-03 Diego Aguiar

    Hey Serge Boyko

    > But why make a setter required, if you could just add it to the constructor and achieve the same result?

    Yep, you're correct but it was done that way so you end up with a reusable trait for logging messages and for teaching purposes.

    > Is there a way to only inject dependancies that you actually used when calling service functions?

    That's totally doable! it's called "Lazy Services", you can find more detailed info at the docs: https://symfony.com/doc/cur...

    Cheers!

  • 2018-10-03 Serge Boyko

    But why make a setter required, if you could just add it to the constructor and achieve the same result?
    Is there a way to only inject dependancies that you actually used when calling service functions?

  • 2018-09-25 mickaël andrieu

    Hello weaverryan!

    I'm thinking about something like:

    /**
    * @required("kernel.debug == true") // need to think about what we could make available here...
    */
    public function setLogger(LoggerInterface $logger = null) {}

    Because the way I understand it, once you add the @required annotation the injection is not optional anymore!

    So the only valid code (for me) would be:

    /**
    * @required()
    */
    public function setLogger(LoggerInterface $logger) // will never be null in reality
    {
    // so no need to check for existence here!
    }

    Am I right, or there is some edge cases when - even with the @required annotation, the Logger could be null.?

  • 2018-09-25 weaverryan

    Hey mickaël andrieu!

    Yea, this is the key thing that I don't like about setter injection too. When I use it, I *still* code defensively - i.e. I check that the property IS populated before using it. And so, I only use it for true optional dependencies. Honestly, the logger is one of the few :). You could totally create a system full of traits like this so that you could easily "fetch" them via setter injection... but even for me, I think that's not a great world :).

    > Any chance to see @required annotation accept somehow a condition using expression language or a fonction from the class itself?

    Can you explain this a bit more? What kind of expressions would you use? The @required is really more of a flag to Symfony's container that says: "Hey! I *actually* want you to call this setter and autowire it". The annotation was invented so that the container wouldn't try to call & autowire EVERY method starting with "set" :)

    Cheers!

  • 2018-09-22 mickaël andrieu

    I'm not that "excited" about as setter injection are supposed to be optional and now you are making it required.

    Any chance to see @required annotation accept somehow a condition using expression language or a fonction from the class itself?

  • 2018-08-07 Diego Aguiar

    Hey Mike

    Thanks for the compliment man :)

    I would say no because the sendMessage() method is a "command" method, it just does something without worrying about returning something. But, if you need to return the response because you want to check the status for example, then just add that return statement, it depends on your needs.

    Cheers!

  • 2018-08-07 Mike

    It was awesome that you repeated how to code an service (Service/SlackClient.php), it felt so great to do it on my own, without looking something up in the video/script and it worked instantly. Really motivating way of teaching Ryan!

    One question, is it best practice to use:

    return $this->slack->sendMessage($slackMessage);

    instead of

    $this->slack->sendMessage($slackMessage);

    as last line inside the public sendMessage() function? (Video: 6:13 minutes left)

  • 2018-02-26 weaverryan

    Hey David!

    Sorry for my slow reply! Ah, this help! The *key* thing is that the service needs to be autowired - the whole automatic setter injection thing happens thanks to autowiring. So, it makes me wonder if you might not be using autowiring in your custom bundle (actually, we usually don't use autowiring for shareable bundles, but if it's just your own code, it's totally fine). To find out, try running:


    php bin/console debug:container id_of_your_service --show-private

    This has an "Autowired" line to tell you if your service is autowired :).

    Cheers!

  • 2018-02-21 David Patterson

    Oops. Failed to mention that the service, command, and trait are all part of a custom bundle (more about how I'm making that happen later).
    I can post some code or turn this into a SO post. What would be best?
    Thanks

  • 2018-02-21 weaverryan

    Hey David Patterson!

    Hmm. That is indeed strange - it should *not* work that way. Very simply, for each service (and both your console command AND service Y are services), the container looks to see if any of the public methods have the @required annotation above it. If it does, then it adds that as a "call" to the service. The *only* reason that it would not be doing this, is if service Y was not set to be autowired. But if you're using the default Symfony 4 config, all services that you register are autowired.

    So, I'm at a loss, but I *am* interested: I just can't think of why it would *not* work. Would you mind posting some real (or at least realistic - you can change names) code? I might be able to spot the problem then :).

    Cheers!

  • 2018-02-21 David Patterson

    I'm running into a problem using this concept where I have a console command that uses trait X and service Y.

    Service Y also uses trait X.
    Trait X has a setXxx() method just like here.
    The problem is that setXxx() is only called for the instatiation of the command, not for the service.

    This results in all of the trait properties in the instantiated Y service being null.