Command bus in Symfony application

Creating a command bus in a Symfony project is really easy. Most of the work is done by the composer recipes and the default configuration is usually almost enough to run a command bus.

Let’s get started

I created a simple project template to save you time. To install it, just run the below commands.

git clone https://github.com/karol-dabrowski/messenger-symfony-example.git -b project_template
cd ./messenger-symfony-example
composer install

Afterward, paste your MySQL credentials in .env file. Then execute commands that create a database and run migrations.

php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate

Finally, run a development server.

php -S localhost:8000 -t public

Bus configuration

First, we need to install the Messenger component. If you want to learn more about Messenger check this post. During installation, Symfony Flex is going to create configuration files automatically.

composer require messenger

When the component is installed, we need to configure a command bus and set it as a default bus. Our command bus is going to work synchronously so we don’t have to configure any transports.

// config/packages/messenger.yaml

messenger:
    default_bus: command.bus
        buses:
            command.bus:
                middleware:
                    - doctrine_transaction

We also need to ensure that our command handlers are available only for the command bus.

// config/services.yaml

services:
    // ...
    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    App\Command\Handler\:
        autoconfigure: false
        resource: '../src/Command/Handler'
        tags: [{ name: messenger.message_handler, bus: command.bus }]

Command and command handler

Now let’s create our first command and command handler.

// src/Command/AddNote.php

<?php
namespace App\Command;

use Ramsey\Uuid\Uuid;

class AddNote
{
    private $id;
    private $title;
    private $description;

    public function __construct(Uuid $noteId, string $noteTitle, string $noteDescription)
    {
        $this->id = $noteId;
        $this->title = $noteTitle;
        $this->description = $noteDescription;
    }

    public function getId(): Uuid
    {
        return $this->id;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getDescription(): string
    {
        return $this->description;
    }
}
// src/Command/Handler/AddNoteHandler.php

<?php
namespace App\Command\Handler;

use App\Command\AddNote;
use App\Entity\Note;
use App\Repository\NoteRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

class AddNoteHandler implements MessageHandlerInterface
{
    private $notes;
    private $em;

    public function __construct(NoteRepository $noteRepository, EntityManagerInterface $em)
    {
        $this->notes = $noteRepository;
        $this->em = $em;
    }

    public function __invoke(AddNote $command)
    {
        $noteId = $command->getId();
        $noteTitle = $command->getTitle();
        $noteDescription = $command->getDescription();

        if($this->notes->noteWithTitleExists($noteTitle)) {
            throw new \LogicException("Note title has to be unique");
        }

        $note = new Note($noteId);
        $note->setTitle($noteTitle);
        $note->setDescription($noteDescription);

        $this->em->persist($note);
        $this->em->flush();
    }
}

Then, we have to add a method, which we used in the handler, to our repository class.

// src/Repository/NoteRepository.php

<?php

namespace App\Repository;

use App\Entity\Note;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class NoteRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Note::class);
    }

    public function noteWithTitleExists(string $title): bool
    {
        return !is_null($this->getNoteByTitle($title));
    }

    public function getNoteByTitle(string $title): ?Note
    {
        return $this->findOneBy(['title' => $title]);
    }
}

In the end, we just need to implement command dispatching in NoteController.

// src/Controller/NoteController.php

// ...
class NoteController extends AbstractController
{
    /**
     * @Route("/note/add", methods={"POST"})
     */
    public function add(Request $request, MessageBusInterface $commandBus)
    {
        $noteId = Uuid::uuid4();
        $noteTitle = $request->get("noteTitle");
        $noteDescription = $request->get("noteDescription");

        try {
            $commandBus->dispatch(
                new AddNote($noteId, $noteTitle, $noteDescription)
            );
        } catch (\Exception $e) {
            //TODO Handle exception
            dd($e->getMessage());
        }

        return $this->redirectToRoute('index');
    }

Deleting notes

In the same way, we can implement deleting notes. All required changes are contained in this commit.

Summary

All code up to this point is available here. You can also check the next posts about creating buses in Symfony project:

You May Also Like

2 Comments to “Command bus in Symfony application”

  1. Gratz, great post !. Just a little comment … if you use the “middleware doctrine_transaction” then you don’t need to do “$this->em->flush()” on handler!

    bye!

    1. Thank you for your comment!
      You are totally right that DoctrineTransactionMiddleware wraps the entire handler in a transaction and there’s no need to execute flush() manually. However, I decided to leave it as is because using the middleware is not necessary and it might be confusing for devs who don’t use it. In addition, executing flush() method twice is harmless.
      Nevertheless, I will add an alert box with that information soon. Thanks for the suggestion!

Leave a Reply

Your email address will not be published. Required fields are marked *