How to bind your favorite JS framework with Symfony 4 ?

Few months ago i wrote this article (https://medium.com/@rebolon/symfony-is-not-dead-thanks-to-vuejs-99cdf75f57b) about Symfony 4 and VueJS (and more globally all modern Javascript framework) . Since this time, i work on part time on a proof of concept which aim is to explain how to build API with Symfony4 for a JS frontend. Because when you work with a frontend on Javascript, you need an API on your backend to retrieve informations and persists data. So now that symfony 4 is available since few months, how would i do to build an API ?

Historically, Symfony developpers used to integrate FOSRestBundle to implement web services. But problem is that FOSRest requires a lot of configurations and was usually overkill for tha huge majority of projects. 
2 years ago, a new project named ApiPlatform come on the place. The aim of this project was to provide a Symfony distribution built for API projects. A lot of people embraces this project because it did a lot of thing out of the box. So i decided to give it a chance.

Api Platform is really dead simple to install using new Flex architecture. Just have a look at this file for more explanations.

Then the first thing you may need, is to configure the prefix path. If your project is only for API, then you might go to next section, but if it’s part of a project where you have backend and frontend, so read this:

#open the following file and change the prefix to make ApiPlatform available on /api route
#config/routes/api_platform.yaml
api_platform:
resource:
.
type: api_platform
prefix: api

Then you need to configure a little bit api_platform like this:

#config/packages/api_platform.yaml
api_platform:
title:
'My comics library'
description: 'Demo of an API built with Api-Platform v2 (use REST or GraphQL). My comics library allow to manage your comics collection in a simple way'
version: '1.0.0'
mapping: # path list to your entities which should be available on API
paths:
['%kernel.project_dir%/src/Entity']
formats: #which format is managed by your API
jsonld:
['application/ld+json']
json: ['application/json']
html: ['text/html']
graphql: #need GraphQL (instead of or with REST) ? just add this
graphiql:
enabled:
true
# more about configuration on this page: https://api-platform.com/docs/core/configuration

Then you will have to create your entities and don’t forget to set the constraints for the properties of thoses entities. Just remeber to create your entities in the paths you specified in the config file (above) or add new path !!!
For example: (complete entity file available here)

<?php
namespace
App\Entity\Library;

use ...

/**
*
@ApiResource(
* iri="http://bib.schema.org/ComicStory",
* attributes={"access_control"="is_granted('ROLE_USER')"},
* collectionOperations={"get"={"method"="GET"},"post"={"method"="POST"}},
* itemOperations={
* "get"={"method"="GET"},"put"={"method"="PUT"},"delete"={"method"="delete"},
* "special_3"={"route_name"="book_special_sample3"},
* }
* )
*
@ORM\Entity
*/
class Book implements LibraryInterface
{
/**
*
@ApiProperty(
* iri="http://schema.org/identifier"
* )
*
*
@ORM\Id
* @ORM\Column(type="integer")
*
@ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
*
@ApiProperty(
* iri="http://schema.org/headline"
* )
*
*
@ORM\Column(type="string", length=255, nullable=false)
*
*
@Assert\NotBlank()
*
@Assert\Length(max="512")
*
*/
private $title;

/**
*
@ApiProperty(
* iri="http://schema.org/reviews"
* )
*
@ApiSubresource(maxDepth=1)
*
*
@ORM\OneToMany(targetEntity="App\Entity\Library\Review", mappedBy="book", orphanRemoval=true)
*/
private $reviews;
...

/**
* Book constructor.
*/
public function __construct()
{
$this->reviews = new ArrayCollection();
}
...
}

What is important in this Entity ?

  • To expose your entity with ApiPlatform you have to add an @ApiResource annotation above the class
  • This annotation will describe:
     * iri to describe the kind of entity based on schema.org dictionnary (optional)
     * access_control to protect your routes with Symfony Role system
     * collectionOperations to specify to ApiPlatform which verbs from GET/POST are allowed and to
    define custom Routes (special_3 on POST) for those verbs
     * itemsOperations to specify to ApiPlatform which verbs from GET/PUT/DELETE are allowed and to
    define custom Routes for those verbs (special_1 and 2 on GET)
  • To setup easier validation, use Symfony Constraints like it’s done here with `title` property (NotBlank and Length < 512)
  • Don’t be confused by @ ORM and @ Assert annotations because one is for the ORM (Doctrine) to setup the database schema, and the other one is to validate the entity using Symfony Validator component. A lot of developpers usually missed the Assert because they think that ORM could be used by the validator, but it’s a mistake.

That’s it ! just with this things, your entities will be exposed by ApiPlatform. But you will quickly find that this sample is too small. In the real life, you want custom Routes where you can add both books with their editors and authors. And you won’t send those informations with key/value system (which is usually done with Symfony Forms) but with a clean JSON object that containes nested entities. 
And this is where complex part will come. There is not so much informations about it except the fact that ApiPlatform is based on Action Domain Responder pattern (vs MVC) and that you can specify new endpoint from your entity (look at annotation ApiResource.collectionsOperations/itemsOperations with name special_1/2/3). But there is no much information (maybe there is but i didn’t find it…) about how to build those new endpoint. I wnated to add extras informations in Swagger, i also wanted to use the ApiPlatform listeners to serialize Exceptions into beautiful Json content…

So i had to explore the code and here is what i decided:
 * i use ParamConverter to rebuild object from json content retrieved in the Request
 * i use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException to encapsulate my exceptions. That way the listener of ApiPlatform will take the Exception and serialize it in the wished format.
 * i tryed to overload Swagger definition throught this explanation.

Now i can write new Action for my custom Routes like this:

// src\App\Action\Bookspecial.php
/**
* Custom route to do POST operation over Book entity with all nested relations
* It uses ParamConverter usage to reduce the responsability of the controller
*
*
@Route(
* name="book_special_sample3",
* path="/api/booksiu/special_3"
* )
*
@ParamConverter(name="book", converter="book")
*
@Method("POST")
*
*
@param Book $book
*
@return JsonResponse
*/
public function special3(Book $book)
{
if ($book) {
$this->em->persist($book);

$this->em->flush();

$iris = $this->router->generate('api_books_get_item', ['id' => $book->getId(), ]);

$response = $this->serializer->serialize($book, 'json');
} else {
return new Response('No Content', 204);
}

// todo return a 201 with iris to book, use the router to build the iris
return new JsonResponse($response, 201, [], true);
}

You can see that the controller doesn’t do a lot of thing except trying to persist the Book entity if it’s well provided by the request. Everything is managed by my ParamConverters.
Those ParamConverters are declared as service with the right tags like this:

# config/services.yaml
App\Request\ParamConverter\Library\BookConverter:
public:
true
arguments:
- '@validator'
tags:
- { name: request.param_converter, priority: -2, converter: book }

And here is how i decided to manage ParamConverter. First of all, i think that i shall have used Symfony Serializer to improve the process and i might use it in the future. Then you have to know that if you don’t use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException your Exception won’t be catched by ApiPlatform Listeners and you won’t have Serialized errors in your response. this might be quite annying if you expect JSON !
Each ParamConverter will extends an AbstractConverter. And all will have to implements a ParamConverterInterface.

What does it require ?

  • At least a NAME constant. This one will be used to identify the ParamConverter in Action/Controller annotation in the name key, but it will also used to identify the root property of the json object
  • Each ParamConverter will have to implements getEzPropsName / getManyRelPropsName / getOneRelPropsName and an initFromRequest.

getEzPropsName: this is just a list of string that represent the fields available in the json content from the Request. For a Book you have

/**
* {
@inheritdoc}
* for this kind of json:
* {
*
"author": {
* "firstname": "Paul",
* "lastname": "Smith"
* }
* }
*/
function getEzPropsName(): array
{
return ['id', 'firstname', 'lastname', ];
}

getManyRelPropsName / getOneRelPropsName : they can return a list of string, or an associative array where the key is the json property name, and the value may have those keys:

// src/App/Request/ParamConverter/Library/ProjectBookEditionConverter.php
/**
* {
@inheritdoc}
* for this kind of json:
* {
* "editors": {
* "editor": { ... }
* }
* }
*/
function getOneRelPropsName():array {
return [
'editor' => [
'converter' => $this->editorConverter,
'registryKey' => 'editor',
],
];
}
// src/App/Request/ParamConverter/Library/BookConverter.php
/**
* {
@inheritdoc}
* for this kind of json:
* {
* "book": {
* "
authors": { ... }
* }
* }
*/
function getManyRelPropsName():array
{
return [
'authors' => [
'converter' => $this->projectBookCreationConverter,
'setter' => 'setAuthor',
'cb' => function ($relation, $entity) {
$relation->setBook($entity);
},
],
];
}
  • converter: the converter instance to use for the related property (converter are injected using Symfony Dependancy Injection)
  • registryKey: where to store the object to prevent duplicate insert (when you post a new Book with 2 editions that are linked to the same editor per example).
  • setter: the method to call to add the new sub-entity to the parent
  • cb: a callable that allows to add the parent into the new sub-entity (or any other operations that require a callback)

Finally, the initRequest is the main organizer of the ParamConverter. It must create the related entity, check the validity of the json content, call the build* methods, validate the new entity and return this new entity. This is also in this method that you have to manage exceptions that will then be cathed by ApiPlatform Listeners.

What i still need to do:
 * manage iris or ids from json sent in the POST Request (for instance i can just create new Entities and deduplicate them from json but i don’t manage Json node that are not object but just a string with the id of an existing entity or an IRIS to this Entity)
 * play with filters because it seems powerful
 * search for native ApiPlatform validation: natively, ApiPlatform just use Orm anotation to validate a property but this is when you write your own custom routes that you may need Symfony Constraints (what i did). Maybe it would be possible to use ApiPlatform in my custom routes but i didn’t explore this, and may be i should have to.
 *
use security over JWT or ApiKey
 * play with GraphQL from ApiPlatform to do batch query and batch Mutations

So will i use ApiPlatform in future ? Maybe. The product seems interesting but for small companies it requires a lot of investigation to be fully usable in real life. Maybe if the company Les-Tilleuls, which behind this project, deliver advanced training i may us it. Or if there is more tutorials on How to use it efficiently with bigger project than the sample with a Book and Nth reviews i may also reconsider my position. Because very often in small companes you don’t have a lot of time to deliver, so you have to be efficient and you don’t need to brainstorm on how to use a tool. The small project i work on to demonstrate Symfony 4 capabilities on API is a beginning but not sure to get more time to explore all features of Api-Platform. I hope that this project may help you to use Symfony and ApiPlatform with a JS frontend. It certainly miss the authentification system. JWT vs ApiKey is my next task on the backend to be able to build efficient apps.

Curious about the project that illustrate my article ? Have a look here https://github.com/Rebolon/php-sf-flex-webpack-encore-vuejs

The pattern used here is certainly not perferct. I think i can improve the AbstractConverter and many other thing, but don’t forget that it’s the result of a Proof Of Concept. I didn’t contribute to ApiPlatform core, so this article is just my little contribution to help developers.

And if you have more information about how to improve my small skills on Symfony 4 and Api Platform, you are welcome.
Now i will certainly work on the frontend part with DevXpress / Sencha or even Quasar Framework. The aim is to show how to build complex frontends nowadays with less works on components.

[Edit 1 :] I extracted the AbstractParamConverter from the POC into a standalone repository. You can use it just by adding it with composer into your Symfony4 project: `rebolon/api-json-param-converter`

[Edit 2 :] I added DevXpress sample (with angular5) in the poc project and i can say that Sf4 + ApiPlatform + DevXpress is rather cool. I will more likely use it in the future.