Decoupling an Application

With Symfony Messenger


SymfonyCon Disneyland Paris - November 18th, 2022

© David Buchmann


We built a data kraken

flickr.com/photos/w-tommerdich/32823758672

Product API
massive central index of product data

First approach

Encapsulate Logic


Extract the logic out of commands and controllers.


=> Refactored into Importer classes that can handle a single item at a time.

Decouple with message queue

Reaping the benefits


Messages within the application


e.g. Compile data into Elasticsearch

Flexible on the outside,
consistent on the inside

Any questions so far?







@dbu




Did i hear «microservices»?

Une Bataille, ~1750, François-Joseph Casanova

Messaging != microservices





Symfony Messenger

Symfony Event System


... but decoupled


Symfony 6.3: External Events

Message System

Why Symfony Messenger?

The flow of Symfony Messenger

https://symfony.com/doc/current/components/messenger.html#concepts

Send Message

Receiving Message

Message

class UpdatePromotion
{
    public function __construct(
        public string $id
    ) {}
}

Bus and Middleware

private MessageBusInterface $bus;
...
$this->bus->dispatch(new UpdatePromotion($id));
            

Envelope and Stamps

$stamps[] = new DelayStamp(20);
$this->bus->dispatch(
    new UpdatePromotion($id),
    $stamps
);

Handler

class UpdatePromotionProcessor
    implements MessageHandlerInterface
{
  public function __invoke(
    UpdatePromotion $message
  ): void
  {
    // business logic update promotion on product
  }
}

Message Transport

Retry

Keep workers running

Autoscaler

What is going on?

Thank you!







@dbu


Middleware: Transaction ID

Middleware: Transaction ID

public function handle(Envelope $env, StackInterface $stack): Envelope {
  $stamp = $env->last(IdStamp::class);
  if (null === $stamp) {
    // sending message, set the transaction id
    $id = $this->transService->getId();
    $env = $env->with(new IdStamp($id));
  } else {
    // receiving message, record transaction id
    $this->transService->recordId($stamp->getId());
  }
  return $stack->next()->handle($env, $stack);
}
            

Transport Decorator: Message Priority

Transport Decorator: Message Priority

public function get(): iterable
{
 foreach ($this->wrappedTrans->get() as $env) {
   $stamp = $env->last(AmqpReceivedStamp::class);
   // error handling if stamp not found...

   $this->priority =
     $stamp->getAmqpEnvelope()->getPriority();

   yield $env;
 }
}
          

Transport Decorator: Message Priority

private function send(Envelope $env): Envelope
{
  $stamp = $envelope->last(AmqpStamp::class);
  $attr = $stamp ? $stamp->getAttributes() : [];
  if (!array_key_exists('priority', $attr)) {
    $stamp = AmqpStamp::createWithAttributes(
      ['priority' => $this->getPriority()],
      $stamp
    );
    $env = $env->with($stamp);
  }

  return $this->wrappedTrans->send($env);
}
          

Transport Decorator: Deduplicate

Transport Decorator: Amqp Routing Key