
A few words about the Symfony REST API in the FOSRestBundle + JMSSerializerBundle bundle
Hello! Let's talk about building a REST API solution on FOSRestBundle + JMSSerializerBundle.
Our path to the development of the REST API began about four years ago (we are GLAVVEB ). The first attempt was to write your own bike on the Yii framework. Happened. And in the future, with minor modifications, we applied this solution to several small projects. Since I am not a third-party of my own “bicycles”, in the following projects we will already use one of the restful extensions of the Yii framework, periodically updating it.
Then Symfony came to our company. We did not take any new projects on Yii. We started to look at what REST solutions exist for Symfony. Of course, the first thing we started experimenting with was the standard assembly FOSRestBundle + JMSSerializer (+ NelmioApiDocBundle for generating documentation). If you are interested, the documentation is here. What I liked here was the lack of magic with the controllers (I mean the dynamic generation of routes based on models and the processing of all requests in the base controller) and the presence of magic with the generation of documentation.
The solution based on FOSRestBundle + JMSSerializer proved to be quite good in our projects. But before you develop a project more than your own one, you need to decide on the following questions:
Let's give more details on each of them.
How to implement an access rights management system?
The symphony out of the box has a couple of solutions in this regard:
- use an ACL, you can read here ;
- organize a system of separation of access rights based on roles + votes (voter), read here .
For ourselves, we chose the second option.
How to organize list filtering?
First, like most developers probably, we created a basic controller. It implemented the basic methods for filtering, creating and updating entities. The method that implements filtering dynamically generated the queribalder based on the parameters passed in the request. We transferred this controller from project to project. Somewhere it was modified as needed. Ultimately, in different projects, this basic controller had significant differences.
Next, we decided to arrange it a bit. They took out filtering in a special service, logic with adding and updating entities in a separate class (action pattern). So GlavwebRestBundle was born . At that time, he looked something like this .
How to determine the boundaries of nesting entities in each arc?
And I mean, that situation when one entity contains a collection of others. To solve this problem, the JMSSerializer has the attribute "MaxDepth", in essence it looks something like this:
But there are pitfalls. Depth is calculated from the beginning of the json object resulting from, and not based on the entity. Those. if our object is nested in the collection, then the depth should be 3, and if we return our object in a single copy, then depth = 2. When entities are nested many times, we get terrible things like: JMS \ MaxDepth (depth = 7) . Below I will show how we got rid of MaxDepth.
How to return only certain fields of an entity?
Suppose we have an entity "user", the user contains a number of fields, including a password that we do not want to show in api. The ExclusionPolicy strategy and the Expose attribute in the JMSSerializer will help us with this.
For the class, define the ExclusionPolicy strategy:
And specify Expose for the fields that we need in the api, all the rest will be skipped by JMSSerializer
How to return a specific set of fields depending on the request?
Often, for a list of objects, we need a limited set of data, and to view a specific object, a complete one. This can be implemented using the “Groups” attribute in the JMSSerializer. For each entity, we defined at least two groups: entity_list and entity_view.
In the controller with through the request parameters we get the necessary values and pass them to the serializer SerializerContext.
This solved the nesting problem, we no longer need to specify MaxDepth for the fields. Now the client, turning to api, could configure the necessary nesting for himself and choose one of two sets of fields (list or view).
How to modify return values?
Here, too, the JMSSerializer comes to the rescue, we determine the listener and change the output in it as we want.
How to organize file upload in PUT?
Because the PUT method does not allow submitting a form; there were options for updating files using POST or encoding files in base64. Neither one nor the other option did not suit us. We decided to upload and delete files using separate requests for api for each field. Suppose a user has an “avatar” field, accordingly, two additional methods must be implemented: POST / api / user / {user} / avatar to upload a new avatar (submit a form with one file field) and DELETE / api / user / {user} / avatar to delete an existing avatar.
How to test the REST API?
A very important issue, at least for us. There are enough nuances here, I will describe them in more detail in one of the following articles. In short, we used LiipFunctionalTestBundle + fixtures in conjunction with AliceBundle. And they wrote their own class in which we implemented the functions we needed. This component was also defined in the GlavwebRestBundle .
As practice has shown, the FOSRestBundle + JMSSerializer solution is generally working. But the world is dictating more and more requirements. This forced us to revise the concept of implementing the REST API on Symfony. We will talk about this in the next article .
A bit of our story.
Our path to the development of the REST API began about four years ago (we are GLAVVEB ). The first attempt was to write your own bike on the Yii framework. Happened. And in the future, with minor modifications, we applied this solution to several small projects. Since I am not a third-party of my own “bicycles”, in the following projects we will already use one of the restful extensions of the Yii framework, periodically updating it.
Then Symfony came to our company. We did not take any new projects on Yii. We started to look at what REST solutions exist for Symfony. Of course, the first thing we started experimenting with was the standard assembly FOSRestBundle + JMSSerializer (+ NelmioApiDocBundle for generating documentation). If you are interested, the documentation is here. What I liked here was the lack of magic with the controllers (I mean the dynamic generation of routes based on models and the processing of all requests in the base controller) and the presence of magic with the generation of documentation.
So FOSRestBundle + JMSSerializerBundle
The solution based on FOSRestBundle + JMSSerializer proved to be quite good in our projects. But before you develop a project more than your own one, you need to decide on the following questions:
- How to implement an access rights management system?
- How to organize filtering of lists?
- how to determine the boundaries of nesting entities in each arc?
- How to return only certain fields of an entity?
- How to return a specific set of fields depending on the request?
- how to modify return values?
- How to organize file upload in PUT?
- How to test the REST API?
Let's give more details on each of them.
How to implement an access rights management system?
The symphony out of the box has a couple of solutions in this regard:
- use an ACL, you can read here ;
- organize a system of separation of access rights based on roles + votes (voter), read here .
For ourselves, we chose the second option.
How to organize list filtering?
First, like most developers probably, we created a basic controller. It implemented the basic methods for filtering, creating and updating entities. The method that implements filtering dynamically generated the queribalder based on the parameters passed in the request. We transferred this controller from project to project. Somewhere it was modified as needed. Ultimately, in different projects, this basic controller had significant differences.
Next, we decided to arrange it a bit. They took out filtering in a special service, logic with adding and updating entities in a separate class (action pattern). So GlavwebRestBundle was born . At that time, he looked something like this .
How to determine the boundaries of nesting entities in each arc?
And I mean, that situation when one entity contains a collection of others. To solve this problem, the JMSSerializer has the attribute "MaxDepth", in essence it looks something like this:
/**
* @JMS\MaxDepth(depth=2)
*/
private $groups;
But there are pitfalls. Depth is calculated from the beginning of the json object resulting from, and not based on the entity. Those. if our object is nested in the collection, then the depth should be 3, and if we return our object in a single copy, then depth = 2. When entities are nested many times, we get terrible things like: JMS \ MaxDepth (depth = 7) . Below I will show how we got rid of MaxDepth.
How to return only certain fields of an entity?
Suppose we have an entity "user", the user contains a number of fields, including a password that we do not want to show in api. The ExclusionPolicy strategy and the Expose attribute in the JMSSerializer will help us with this.
For the class, define the ExclusionPolicy strategy:
use JMS\Serializer\Annotation as JMS;
/**
* @JMS\ExclusionPolicy("all")
*/
class MedicalEscortType {
And specify Expose for the fields that we need in the api, all the rest will be skipped by JMSSerializer
/**
* @JMS\Expose
* @var integer
*/
private $name;
How to return a specific set of fields depending on the request?
Often, for a list of objects, we need a limited set of data, and to view a specific object, a complete one. This can be implemented using the “Groups” attribute in the JMSSerializer. For each entity, we defined at least two groups: entity_list and entity_view.
In the controller with through the request parameters we get the necessary values and pass them to the serializer SerializerContext.
$scopes = array_map('trim', explode(',', $request->get('_scope')));
$serializationContext = SerializationContext::create()
->setGroups(array_merge($scopes, [GroupsExclusionStrategy::DEFAULT_GROUP]))
;
$view = $this->view($data, $statusCode, $headers);
$view->setSerializationContext($serializationContext)
return $view;
This solved the nesting problem, we no longer need to specify MaxDepth for the fields. Now the client, turning to api, could configure the necessary nesting for himself and choose one of two sets of fields (list or view).
How to modify return values?
Here, too, the JMSSerializer comes to the rescue, we determine the listener and change the output in it as we want.
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
/**
* Class SerializationListener
* @package AppBundle\Listener
*/
class SerializationListener implements EventSubscriberInterface
{
/**
* @var UploaderHelper
*/
private $uploaderHelper;
/**
* @param UploaderHelper $uploaderHelper
*/
public function __construct(UploaderHelper $uploaderHelper)
{
$this->uploaderHelper = $uploaderHelper;
}
/**
* @inheritdoc
*/
static public function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'class' => 'AppBundle\Entity\User', 'method' => 'onPostSerializeUserAvatar')
);
}
/**
* @param ObjectEvent $event
*/
public function onPostSerializeUserAvatar(ObjectEvent $event)
{
$url = $this->uploaderHelper->asset($event->getObject(), 'avatarFile');
$event->getVisitor()->addData('avatarUrl', $url);
}
How to organize file upload in PUT?
Because the PUT method does not allow submitting a form; there were options for updating files using POST or encoding files in base64. Neither one nor the other option did not suit us. We decided to upload and delete files using separate requests for api for each field. Suppose a user has an “avatar” field, accordingly, two additional methods must be implemented: POST / api / user / {user} / avatar to upload a new avatar (submit a form with one file field) and DELETE / api / user / {user} / avatar to delete an existing avatar.
How to test the REST API?
A very important issue, at least for us. There are enough nuances here, I will describe them in more detail in one of the following articles. In short, we used LiipFunctionalTestBundle + fixtures in conjunction with AliceBundle. And they wrote their own class in which we implemented the functions we needed. This component was also defined in the GlavwebRestBundle .
Conclusion
As practice has shown, the FOSRestBundle + JMSSerializer solution is generally working. But the world is dictating more and more requirements. This forced us to revise the concept of implementing the REST API on Symfony. We will talk about this in the next article .