Buy Access to Course
17.

Bonus! LoggerTrait & Setter Injection

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

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:

32 lines | src/Service/SlackClient.php
// ... 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:

32 lines | src/Service/SlackClient.php
// ... 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:

32 lines | src/Service/SlackClient.php
// ... 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);
}
}

Oh, but let's rename the variable to $slackMessage - having two $message variables is no fun.

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:

32 lines | src/Service/SlackClient.php
// ... 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:

32 lines | src/Service/SlackClient.php
// ... 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:

32 lines | src/Service/SlackClient.php
// ... 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:

90 lines | src/Controller/ArticleController.php
// ... 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:

90 lines | src/Controller/ArticleController.php
// ... 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:

94 lines | src/Controller/ArticleController.php
// ... 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:

40 lines | src/Service/SlackClient.php
// ... 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:

40 lines | src/Service/SlackClient.php
// ... 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():

40 lines | src/Service/SlackClient.php
// ... 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:

40 lines | src/Service/SlackClient.php
// ... 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!":

40 lines | src/Service/SlackClient.php
// ... 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:

43 lines | src/Service/SlackClient.php
// ... 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:

28 lines | src/Helper/LoggerTrait.php
// ... 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:

28 lines | src/Helper/LoggerTrait.php
// ... 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:

28 lines | src/Helper/LoggerTrait.php
// ... 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):

28 lines | src/Helper/LoggerTrait.php
// ... 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:

32 lines | src/Service/SlackClient.php
// ... 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:

32 lines | src/Service/SlackClient.php
// ... 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.