📜 ⬆️ ⬇️

A few words about REST API on symfony in the FOSRestBundle + JMSSerializerBundle bundle

Hello! Let's talk about building a REST API solution on the FOSRestBundle + JMSSerializerBundle.


A bit of our history.


Our journey to the development of the REST API began about four years ago (we are GLAVWEB ). 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 on several small projects. Since I do not have third-party “bicycles” of my own, in the following projects we already use one of the restful extensions of the Yii framework, periodically finishing it.

Then symfony came to our company. New projects on Yii, we have not taken. Began to look what exist REST solutions for symfony. Of course, the first thing we started experimenting with is the standard FOSRestBundle + JMSSerializer build (+ NelmioApiDocBundle for generating documentation). If anyone is interested, the documentation is here . What was pleasant about it is the absence of magic with controllers (I mean the dynamic generation of routes based on the models and the processing of all requests in the basic controller) and the presence of magic with the generation of documentation.
')

So, FOSRestBundle + JMSSerializerBundle


The solution based on FOSRestBundle + JMSSerializer has a good reputation in our projects. But before you develop a project more than your own blog, you need to decide on the following issues:

Let's take a closer look at each of them.

How to implement an access rights management system?

A symphony out of the box has a couple of solutions to this effect:
- use ACL, you can read here ;
- to organize a system of separation of access rights based on roles + voter (voter), read here .

For ourselves, we chose the second option.

How to organize filtering of lists?

At first, as most developers probably did, we created a basic controller. It implemented the basic methods for filtering, creating and updating entities. A method that implements filtering dynamically generated a queribiler based on the parameters passed in the request. We transferred this controller from project to project. Somewhere it was finished as needed. Ultimately, in different projects this basic controller had significant differences.

Next, we decided to make it a little. Brought filtering to a special service, logic with the addition and updating of entities in a separate class (action pattern). This is how GlavwebRestBundle was born. At the 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 an attribute “MaxDepth”, in essence it looks like this:

/** * @JMS\MaxDepth(depth=2) */ private $groups; 


But there are pitfalls. The depth is considered from the beginning of the json object of the resulting, and not based on the essence. Those. if our object is nested in a collection, then the depth should be 3, and if we return our object in a single copy, then depth = 2. When entities are nested into each other 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 the “user” entity, the user contains a number of fields, including the password that we don’t want to show in api. The ExclusionPolicy strategy and the Expose attribute in the JMSSerializer will help us with this.

For the class, we define the ExclusionPolicy strategy:

 use JMS\Serializer\Annotation as JMS; /** * @JMS\ExclusionPolicy("all") */ class MedicalEscortType { 


And we specify Expose for those fields that we need in api, all the rest will be skipped by the JMSSerializer

  /** * @JMS\Expose * @var integer */ private $name; 


How to return a specific set of fields depending on the query?

Often, for a list of objects, we need a limited set of data, and to view a specific object - a complete one. This is possible using the “Groups” attribute in the JMSSerializer. For each entity, we defined at least two groups: entity_list and entity_view.

In the controller via the request parameters, we get the necessary values ​​and transfer them to the SerializerContext serializer.

 $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 fields. Now the client, turning to the api, could configure the nesting he needed and select one of two sets of fields (list or view).

How to modify return values?

Here, too, the JMSSerializer comes to the rescue, we define the listener and change the output in it as we like.

 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 uploading files to PUT?

Since the PUT method does not allow to send the form, there were options to update the files, use POST or encode the files in base64. Neither one nor the other did not suit us. It was decided to load and delete files with the help of separate requests to api for each field. Suppose a user has an “avatar” field, respectively, you need to implement two additional methods: POST / api / user / {user} / avatar to upload a new avatar (submit a form with one file field) and DELETE / api / user / {user} / avatar to remove the existing avatar.

How to test REST API?

A very important question, at least for us. There are enough nuances, I will describe them in more detail in one of the following articles. In short, we used LiipFunctionalTestBundle + fixtures in conjunction with AliceBundle. And we wrote our own class in which we implemented the necessary functions. This component was also defined in the GlavwebRestBundle .

Conclusion


As practice has shown, the FOSRestBundle + JMSSerializer solution is generally working. But the world dictates more and more demands. This forced us to revise the concept of implementing REST API on symfony. We will talk about this in the next article .

Source: https://habr.com/ru/post/302902/


All Articles