A lot of the time you find yourself writing a lot of the same code over and over. Not only is this annoying, but it’s also prone to copy / paste errors. That is why you should stop repeating yourself and leverage service classes in your applications. Today I’ll share some of my services for Symfony which I find very useful in my day to day life as a developer. Feel free to use them in your applications.
Logging service
Logging is essential to see what happened in your application when and error occurred. Because I didn’t want to scatter my application with log statements enclosed in null checks to see if the logger is actually available, I created this LogService.
The Symfony workflow component is a very powerful component. Especially when you need to make sure your entities maintain a valid state. This can be very important when, for example, you build an ecommerce system with order handling. In this post we will use the processing of an order as an example.
Setting up the Symfony Workflow Component
To use the workflow component in your application you first need to add it using composer.
In this example we’ve defined a workflow for our order. We’ve used a state_machine because our orders can only have one state at a time. Because we’ve enabled the audit_trail we’ll get detailed logs about the transitions, which can be very useful when debugging the workflow.
The next thing we configure is the supported entity (App\Entity\Order) and the property in the entity to store the current state in (status). We set the default status of our orders to pending using the initial_marking option. Next we set the possible states (places) and the transitions we can do using this workflow.
Of course we need to create the order entity, so here it is, in a very basic form.
namespaceApp\Entity;
classOrder{
privatearray $products;
// This property will be managed by the workflow componentprivate string $status;
// These getters and setters need to exist for the workflow componentpublicfunctiongetStatus(): string{
return$this->status;
}
publicfunctionsetStatus(string $status, $context = []): void{
$this->status= $status;
}
}Code language:PHP(php)
Using the symfony workflow component
Now that the workflow component is set up we can use it in our application. To make this easier for you, the developer, symfony creates a service for you named workflow.[workflowname] in our case this is workflow.order. You can inject this service, or you can use autowiring to access this service. Just like with the caching component in symfony you can also inject the service using a special parameter name. In this case we can use WorkflowInterface $orderWorkflow to inject the workflow.
Lets create a service to handle the orders for us.
namespaceApp\Service;
useApp\Entity\Order;
useApp\Exception\OrderStateException;
useDoctrine\ORM\EntityManagerInterface;
useSymfony\Component\Workflow\WorkflowInterface;
useSymfony\Component\Workflow\Exception\LogicException;
classOrderService{
private EntityManagerInterface $entityManager;
private WorkflowInterface $orderWorkflow;
publicfunction__construct(EntityManagerInterface $entityManager, WorkflowInterface $orderWorkflow){
$this->entityManager = $entityManager;
$this->orderWorkflow = $orderWorkflow;
}
publicfunctionpay(Order $order): void{
$this->doTransition('pay', $order);
// Do extra stuff on the order entity here, like setting payment data$this->entityManager->flush();
}
publicfunctionpack(Order $order): void{
$this->doTransition('pack', $order);
// Do extra stuff on the order entity here, like setting the packed item amount$this->entityManager->flush();
}
publicfunctionship(Order $order): void{
$this->doTransition('ship', $order);
// Do extra stuff on the order entity here, like setting the track and trace code$this->entityManager->flush();
}
publicfunctioncancel(Order $order): void{
$this->doTransition('cancel', $order);
$this->entityManager->flush();
}
privatefunctiondoTransition(string $transition, Order $order): void{
try {
$this->orderWorkflow->apply($order, $transition);
} catch (LogicException $e) {
// Throw a custom exception here and handle this in your controller,// to show an error message to the userthrownew OrderStateException(sprintf('Cannot change the state of the order, because %s', $e->getMessage()), 0, $e);
}
}
}Code language:PHP(php)
As you can see we’ve created a function for every transition possible, this makes it very easy for us to call these in our controllers. Because every transition change is basically the same, check if it’s possible and then apply it, we’ve created a private helper method doTransition. This method will throw custom exceptions which we can catch in our controller to show helpful messages to our users.
Because we make these methods so lean we can leverage the transactions of the doctrine entity manager. A transition is not stored in the database automatically by the workflow component. You will have to do this manually. Using this makes sure we can change other properties of our entity in the same database transaction. So if it fails it will never end up in a corrupted state. Like having status paid without having payment data stored.
Using the workflow component guarantees the entity is processed in the correct order. An unpaid order will not be able to transition to a packed order. We did this with a little bit of configuration and a few lines of code and of course the Symfony Workflow Component.
Using events to extend your workflow
Let’s go a step further updating our store’s total sales statistics. We could write that functionality in the ship method of the service, but that will add an extra dependency. To circumvent this we can leverage the power of the event dispatcher of symfony.
To be able to do this the workflow component sends out a lot of events when transitioning from one state to another: – workflow.guard – Checking if the state can be changed – workflow.leave – The entity leaves a state – workflow.transition – An entity is going through a transition – workflow.enter – The entity enters a new state – workflow.entered – An entity entered a new state – workflow.completed – The transition is complete
You can make these events more specific by appending the workflow name and the transition name like this: – workflow.order.transition.pay – workflow.order.completed.ship
Let’s create our event subscriber to update the statistics.
As you can see, it is really easy to extend the actions performed on an transition using the event system.
Using the guard events
The guard events are a special kind of events in the workflow component. These will be dispatched when the workflow component checks to see if you can execute a transition. With the guard events you can block the transition using external factors.
Lets create a guard event that makes sure only users with the role ROLE_PACKER can transition an order to packed.
This event listener will make sure only warehouse employees (user with the role ROLE_PACKER) can pack orders. Everyone else will get the error message.
Conclusion
The workflow component is essential when you need to maintain a certain workflow within your application. It makes it really easy to maintain the status of an entity. It’s also really easy to adapt or extend the workflow by changing the configuration.
Have you used the workflow component already in an application? What did you use it for?
Imagine you’re working on this big application which involves an API and a React frontend. You decide on using API Platform for your REST API because of its ease of use. Because the users security is a great concern you want to find an easy way to make sure a user can only access their own data. Ofcourse you can create custom actions where you manually filter the returned data from the database. But this can be really tedious if you have a lot of database entities. To combat this, we can leverage extensions in API Platform.
Extensions in API Platform
Introducing the extensions of API Platform. These classes are called for every request that comes in. These extension classes are able to change the DQL generated by the platform. To create an extension you need to create a class implementing ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface and / or ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface.
If you create a class implemting one or both of these interfaces they will automatically be picked up by the symfony dependency injection container. If you don’t use auto wiring you will need to tag the classes as follows:
In our large application we want to make sure users can only access their own data. To do this efficiently we can harness the power of extensions. Let’s say for example we have a Profile entity and an Address entity which are linked to a user. Both of these entities have a property called user which contains the user it belongs to. To make sure we don’t fetch the data of the wrong user we can use an extension:
We inject the Symfony\Component\Security\Core\Security class so we are able to get the current user of the request. We need to implement the applyToCollection and the applyToItem functions for the two extension interfaces. These methods are called by the API Platform when we do a GET collection request or a GET item request respectively. Because we want to perform the same action in both cases we created the method addWhere which receives the QueryBuilder and the resource class.
Because we only want to change the query for certain cases we check if the resourceClass is supported by the extension or if the user has the role ROLE_ADMIN. If the resourceClass is not supported or the user is an admin we simple exit the function, since we don’t need to add the filter. If it is a supported class and the user is not an admin we use the query builder to add a where clause making sure we only fetch data for the current user.
Protip
If you find yourself adding alot of resourceClass checks to your if statement, try using an interface. You can make an interface called UserDataInterface and change the if statement to check if the resourceClass is an implementation of this interface. For example:
namespaceApp\Domain\Interface;
useApp\Entity\User;
interfaceUserDataInterface{
publicfunctiongetUser(): User;
// ... More methods if needed
}Code language:PHP(php)
You can now change your if-statement to the following:
It’s a small change, with a large impact. The function is_subclass_of checks if the class (first parameter can be an instance or a string) is a subclass of another class, or interface (the second parameter). So now this extension will add the filter for every class we mark with implements UserDataInterface.
Do you have any useful tips or tricks using API Platform? Leave them in the comments!
In this post I will teach you how to inject multiple concrete implementations of an interface into a service. This can be useful when, for example, you need to call different API’s depending on a variable. Your code will be much cleaner when using this method instead of using a lot of if statements.
Define your interface
First we need to define the interface all of our concrete classes will be implementing:
<?phpnamespaceApp\Interfaces;
interfaceProviderInterface{
/**
* Function to check wether this implementation supports the $type
*/publicfunctionsupports(string $type): bool;
/**
* This function contains the actual logic
*/publicfunctionexecute(ProviderDto $data): ProviderResultDto;
}Code language:HTML, XML(xml)
This will be the interface for all our Provider classes. The supports function will make sure the correct implementation will be called when needed. We can put all of our logic in the execute function.
Creating our providers
Now I’ll create two simple providers implementing this interface.
<?phpnamespaceApp\Providers;
useApp\Interfaces\ProviderInterface;
classFirstProviderimplementsProviderInterface{
publicfunctionsupports(string $type): bool{
return'first' === $type;
}
publicfunctionexecute(ProviderDto $data): ProviderResultDto{
// Some logic here to return the ProviderResultDto
}
}Code language:HTML, XML(xml)
And the second provider.
<?phpnamespaceApp\Providers;
useApp\Interfaces\ProviderInterface;
classSecondProviderimplementsProviderInterface{
publicfunctionsupports(string $type): bool{
return'second' === $type;
}
publicfunctionexecute(ProviderDto $data): ProviderResultDto{
// Some logic here to return the ProviderResultDto
}
}Code language:HTML, XML(xml)
These are very simple implementations of the ProviderInterface. Ofcourse you can do what ever you need in these classes.
Create the service
We need a central point to inject these implementations into. We’ll use a service for this. This is a simple service that will act as a factory. We can call the execute function of this service with a few parameters and this service will make sure the correct Povider implementation will be called.
<?phpnamespaceApp\Services;
useApp\Interfaces\ProviderInterface;
classProviderService{
/**
* @var ProviderInterface[]
*/privatearray $providers;
publicfunction__construct(ProviderInterface ...$providers){
$this->providers = $providers;
}
publicfunctionexecute(string $type, ProviderDto $data): ?ProviderResultDto{
foreach($this->providers as $provider) {
if ($provider->supports($type)) {
return $provider->execute($data);
}
}
returnnull; // Or throw an ProviderNotSupportedException or something like that
}
}Code language:HTML, XML(xml)
As you can see this is a very simple service. The constructor will accept an array of providers and stores this in a local parameter. When we want to execute one of the providers we can simple inject this service where you need it and call it’s execute function. This function will then call the correct provider and return the result.
Autowiring our ProviderService
We just need to do one final thing. That is making sure the providers are autowired in the ProviderService. We can use symfony service tags for this. First we need to make sure our implementations of the ProviderInterface are tagged correctly. We can do this by adding the following code to our service.yml file.
This will make sure all of our providers created now and in the future will have the app.provider tag applied to it. Now that the providers are tagged correctly, we can inject them into our service.
We are done! Our service will receive all of our providers and can access them accordingly. When we need to execute a provider we can simple inject our ProviderService and call the execute method.