In this example we are going to use Compiler Passes to implement Strategy Pattern in our symfony application. This will help us prevent code duplication by auto-magically running relevant code for given input.


Information


User wants to send a message by calling an endpoint. The request contains a message. The message type can be either email, sms or voice. If we are to send the message by using traditional way, our code would look like below.


if ($type === 'email') {
$emailSender->send($message);
} elseif ($type === 'sms') {
$smsSender->send($message);
} elseif ($type === 'voice') {
$voiceSender->send($message);
}

As you can see, this just looks very old-school. Also if we are to introduce another format, we would need to modify the code. In our example below, we will use a single line $sender->send($type, $message); to do the same thing. In this case, if we are to introduce another format, we would just need to introduce the relevant strategy class.


Design


In our design, we will just log the messages as if we are actually sending them. I am just keeping the code short, that's why! Strategy classes also don't return anything but depending on your needs, you can return something back.



Files


Strategy/Message.php


declare(strict_types=1);

namespace App\Strategy;

class Message
{
private $strategies;

public function addStrategy(StrategyInterface $strategy): void
{
$this->strategies[] = $strategy;
}

public function send(string $type, iterable $payload = []): void
{
/** @var StrategyInterface $strategy */
foreach ($this->strategies as $strategy) {
if ($strategy->isSendable($type, $payload)) {
$strategy->send($payload);

return;
}
}
}
}

Strategy/StrategyInterface.php


declare(strict_types=1);

namespace App\Strategy;

interface StrategyInterface
{
public const SERVICE_TAG = 'message_strategy';
public const MESSAGE_KEY = 'message';

public function isSendable(string $type, iterable $payload = []): bool;
public function send(iterable $payload = []): void;
}

Strategy/EmailStrategy.php


declare(strict_types=1);

namespace App\Strategy;

class EmailStrategy implements StrategyInterface
{
private $key = 'email';

public function isSendable(string $type, iterable $payload = []): bool
{
return $type === $this->key && isset($payload[self::MESSAGE_KEY]);
}

public function send(iterable $payload = []): void
{
file_put_contents($this->key.'.log', $payload[self::MESSAGE_KEY].PHP_EOL, FILE_APPEND);
}
}

Strategy/SmsStrategy.php


declare(strict_types=1);

namespace App\Strategy;

class SmsStrategy implements StrategyInterface
{
private $key = 'sms';

public function isSendable(string $type, iterable $payload = []): bool
{
return $type === $this->key && isset($payload[self::MESSAGE_KEY]);
}

public function send(iterable $payload = []): void
{
file_put_contents($this->key.'.log', $payload[self::MESSAGE_KEY].PHP_EOL, FILE_APPEND);
}
}

Strategy/VoiceStrategy.php


declare(strict_types=1);

namespace App\Strategy;

class VoiceStrategy implements StrategyInterface
{
private $key = 'voice';

public function isSendable(string $type, iterable $payload = []): bool
{
return $type === $this->key && isset($payload[self::MESSAGE_KEY]);
}

public function send(iterable $payload = []): void
{
file_put_contents($this->key.'.log', $payload[self::MESSAGE_KEY].PHP_EOL, FILE_APPEND);
}
}

Strategy/MessageCompilerPass.php


declare(strict_types=1);

namespace App\Strategy;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class MessageCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$resolverService = $container->findDefinition(Message::class);

$strategyServices = array_keys($container->findTaggedServiceIds(StrategyInterface::SERVICE_TAG));

foreach ($strategyServices as $strategyService) {
$resolverService->addMethodCall('addStrategy', [new Reference($strategyService)]);
}
}
}

Kernel.php


namespace App;

use App\Strategy\MessageCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
...

class Kernel extends BaseKernel
{
use MicroKernelTrait;

...

protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new MessageCompilerPass());
}
}

config/services.yaml


services:

App\Strategy\EmailStrategy:
tags:
- { name: message_strategy }

App\Strategy\SmsStrategy:
tags:
- { name: message_strategy }

App\Strategy\VoiceStrategy:
tags:
- { name: message_strategy }

Controller/MessageController.php


declare(strict_types=1);

namespace App\Controller;

use App\Strategy\Message;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
* @Route("/messages")
*/
class MessageController
{
private $message;

public function __construct(Message $message)
{
$this->message = $message;
}

/**
* @Route("/{type}")
* @Method({"POST"})
*/
public function index(Request $request, string $type)
{
$payload = json_decode($request->getContent(), true);

$this->message->send($type, $payload);

return new Response('OK');
}
}

Test


POST /messages/email
{
"message": "This is the message 1"
}

POST /messages/sms
{
"message": "This is the message 2"
}

POST /messages/voice
{
"message": "This is the message 3"
}

Result


$ cat public/email.log 
This is the message 1

$ cat public/sms.log
This is the message 2

$ cat public/voice.log
This is the message 3