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:
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!
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!