In this article we will be pushing refactor of our Symfony Application to the boundaries.
We will focus on dropping boilerplate completely so we can write only the code that matters, allowing us for easy modifications, maintenance and future extensions.
We will start with example functionality, which we will be refactoring step by step by extending our Symfony application with Ecotone.
Prepare a good coffee or tea and enjoy the ride :)
Our application will be having two functionalities:
- Registering new user
- Activating the user after the registration was done
Our UsersApiController receiving Request and calls UserService to register new user.
Our UserService, begins transaction and stores entity using Entity Manager.
And this is how our User looks like.
Before we start, let's install Ecotone for Symfony:
composer require ecotone/symfony-bundle
Drop redundant transaction management
What hurts eyes is the transaction management. We are doing it for each of the action (registerUser, activateUser) and the future actions will also need it.
Let's remove the boilerplate and place it one place.
To make it we will build a pipeline, where before running any action, we will be handling the transaction.
Let's first start by registering our actions as Command Handlers. This will allow us to intercept their execution by wrapping it in transaction.
We have registered two Command Handlers, under given names: registerUser and activateUser.
If you are familiar with Symfony Messenger, you know the concept of Message Handler.
A Command Handler is higher level concept and describes Message Handler which is responsible for actions that change the data or provide side effects (e.g. sending email).
To execute the command handlers, we need to replace our UserService with CommandBus in the Controller.
This execute Command Handler registered under name "registerUser". The second argument is payload, which will be passed as first argument to our Handler.
In this example, payload is actually a single parameter. We could easily provide array or object. Ecotone allows you to choose freely, depending on your preferences and needs.
Let's wrap our command handlers in transaction now:
#[Around(pointcut: CommandHandler::class)] public function transactional(MethodInvocation $methodInvocation)
Around is interceptor that allows us to add logic before and after execution of the Handler is performed. This is great for things like Transactions.
Pointcut tells what do we want to intercept. In this scenario, we have intercepted all Command Handlers.
For proceeding with the Command Handler invocation we use $methodInvocation->proceed();
Pushing to the limits
Let's compare activate method with new deactivate method.
If you look closely you will see boilerplate code:
/** @var User $user */ $user = $this->entityManager->find(User::class, $id); // Run some action on the user $this->entityManager->persist($user);
In order to fetch the user and persist the changes, we need to create separate class and method and all what we want to do is to execute method on the user.
Wouldn't it be easier, if we could call the Entity's method directly?
Lucky Ecotone solves this, as we are allowed to mark Entities as Command Handlers.
To get support for Doctrine ORM, we will install Ecotone Dbal:
# install ecotone/dbal: composer require ecotone/dbal # Add in services.yaml, so Ecotone can discover database connection: Enqueue\Dbal\DbalConnectionFactory: factory: [ 'Ecotone\Dbal\DbalConnection', 'createForManagerRegistry' ] arguments: [ "@doctrine","default" ]
If you install ecotone/dbal, transaction management will be handled by default.
You can remove TransactionWrapper.
And we need to enable Doctrine ORM Repositories for Ecotone:
Right now we are ready to mark our User Entity with Command Handlers:
- In Ecotone we are calling Entity as #[Aggregate]
- Just like with Doctrine ORM, we need to mark identifier #[AggregateIdentifier]
- And we mark methods as Command Handlers: #[CommandHandler("activateUser")]
The only change we need to make in Controller is to tell activate method Entity's identifier:
$this->commandBus->sendWithRouting("activateUser", $id, metadata: ["aggregate.id" => $id]);
We have dropped all of boilerplate code, leaving only the business logic code in our application.
Now we can produce new functionalities with minimum amount of code and things to test.
All the glue code was moved to Ecotone and Symfony, letting us focus on what matters.
Visit main Ecotone Github for more information about the Framework.
Visit Example Application to see implementation of the article.