14/04/2018 - SYMFONY
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.
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.
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.
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;
}
}
}
}
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;
}
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);
}
}
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);
}
}
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);
}
}
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)]);
}
}
}
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());
}
}
services:
App\Strategy\EmailStrategy:
tags:
- { name: message_strategy }
App\Strategy\SmsStrategy:
tags:
- { name: message_strategy }
App\Strategy\VoiceStrategy:
tags:
- { name: message_strategy }
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');
}
}
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"
}
$ 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