Salut, bienvenue dans ce tutoriel sur Symfony et Api platform, nous allons ensemble développer une API REST pour gérer un blog (j'aime bien l'exemple du blog). Dans ce tutoriel, nous aurons à gérer 4 entités: ArticleUserComment et Tag. On commence tout de suite.

API Platform

API Platform est une librairie qui permet de faire des API REST et GraphQL. Elle est développée et maintenue par une entreprise française, Les tilleuls coop.

Dans ce tutoriel, nous allons l'utiliser avec Symfony pour faire une API REST.

Si vous ne connaissez pas Symfony ou que vous avez besoin d'un petit rappel, vous pouvez lire cet extraordinaire article sur Symfony.

Installation

Nous allons d'abord commencer par installer Symfony, je vais dans ce tutoriel utiliser le CLI de Symfony pour l'installation. Si vous ne l'avez pas encore, vous pouvez le télécharger sur le site de Symfony, ou vous pouvez aussi utiliser directement composer. Mais vous aurez besoin du CLI de Symfony pour lancer l'application.

$ symfony new symfony-rest-api

Ou avec composer:

$ composer create-project symfony/skeleton symfony-rest-api

Vous remarquerez que nous n'avons pas utiliser l'option --full (symfony/website-skeleton avec composer).

Déplacez-vous dans le votre nouveau dossier et lancer le serveur avec Symfony:

$ cd symfony-rest-api
$ symfony serve -d --no-tls

J'utilise le CLI de Symfony pour lancer le serveur, les options -d et --no-tls ne sont pas obligatoires. L'option -d permet de lancer la commande en mode detach pour que je puisse réutiliser le même terminal et --no-tls sert a désactiver l'encryption TLS qui est par défaut utiliser. Vous pouvez maintenant naviguer sur le lien qui vous a été donner par la commande, dans mon cas http://127.0.0.1:8000

J'utilise la version 5.0.6 de Symfony dans ce tutoriel. Une vraie coïncidence, j'ai écris cette partie du tutoriel le jour même du lancement de cette version, je viens à peine de suivre Fabien et Nicolas faire la release lors du premier SymfonyLive Online et c'était super dope. Allez voir la vidéo.

Maintenant que nous avons installer Symfony, nous pouvons installer API Platform avec composer:

$ composer require api

Et c'est tout. Allez sur le lien http://127.0.0.1:8000/api et vous devez avoir cette page:

API Platform utilise le format OpenAPI pour décrire les endpoints de notre API et Swagger UI pour afficher la documentation de l'API.

Nous allons commencer par rajouter le nom de notre application à cette vue. Pour cela , ouvrez le fichier api_platform.yaml qui se trouve dans config/packages et mettez-y le contenu suivant, j'ai juste rajouté le titledescription et version:

# config/packages/api_platform.yaml
api_platform:
    title: 'Symfony REST API'
    description: 'A Symfony API to manage a simple blog app.'
    version: '1.0.0'
    mapping:
        paths: ['%kernel.project_dir%/src/Entity']
    patch_formats:
        json: ['application/merge-patch+json']
    swagger:
        versions: [3]

Si vous actualiser la page http://127.0.0.1:8000/api vous devez voir le changement:

C'est bien beau, mais on n'a toujours pas notre API.

Le concept fondamental de toute API REST c'est la Ressource. Une ressource est un objet avec un type, des données associées, des relations avec d'autres ressources et un ensemble de méthodes qui opèrent dessus. La ressource est similaire à un objet en programmation orienté objet, avec la différence importante que seules quelques méthodes standard sont définies pour la ressource (correspondant aux méthodes HTTP GETPOSTPUT et DELETE standard), tandis qu'un objet a généralement de nombreuses méthodes.

Une API REST va manipuler des ressources (entités) et comme nous l'avons vu, avec notre application, nous allons utiliser 4 entités: ArticleUserComment et Tag.

Nous allons donc créer l'entité Article, pour cela nous allons utiliser la commande make:entity de Symfony.

Mais avant, il nous faut installer MakerBundle:

$ composer require symfony/maker-bundle --dev

Nous pouvons maintenant créer l'entité Article avec:

$ php bin/console make:entity

Le nom de l'entité c'est Article, le terminal nous demande ensuite si on veut faire de cette classe une ressource, cela est dû au package api-platform que nous avons installer, on répond par yes

L'entité Article sera donc exposer comme ressource par notre API. Vous pouvez ensuite ajouter le reste des champs de l'entité Article.

Une fois terminer, ouvrez le fichier src/Entity/Article.php, vous verrez avant la définition de la classe l'annotation @ApiResource():

<?php

// src/Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource()
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $title;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $slug;

    /**
     * @ORM\Column(type="text", nullable=true)
     */
    private $content;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $picture;

    /**
     * @ORM\Column(type="boolean")
     */
    private $isPublished;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $publishedAt;

    /**
     * @ORM\Column(type="datetime")
     */
    private $updatedAt;

    // ...
}

Cette annotation est présente parce que nous avons répondu par yes à la question qui demandait s'il faut ou pas exposer cette entité comme étant une ressource. Mais qu'est-ce que cela veut vraiment dire? Ouvrez la page http://127.0.0.1:8000/api

API Platform nous génère toutes le opérations pour la ressource Article, et ces opérations sont fonctionnelles, nous allons les tester tout de suite. Vous pouvez cliquer sur un endpoint pour voir sa documentation.

Essayez juste de retirer l'annotation @ApiResource() de la classe et actualiser la page pour voir, il n'y a plus rien. Vous voyez donc un peu ce que fait API Platform.

Avant de tester les endpoints, il faut installer le package doctrine migration bundle qui nous permettra de faire les migrations:

$ composer require migrations

Pour tester les endpoints, nous allons commencer par créer notre base de données, créer un fichier .env.local à la racine de votre projet et mettez y le contenu suivant:

DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7

Remplacez db_user par le nom d'utilisateur de base de données, db_password par son mot de passe et db_name par le nom de la base de données.

Créez la base de données avec:

$ php bin/console doctrine:database:create

Créez et faites les migrations avec:

$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate

Repondez par yes à la question qui sera poser.

Maintenant que nous avons créer la base de données, nous pouvons tester notre API.

Nous pouvons directement tester notre API sur la page http://127.0.0.1:8000/api, pour ceux qui veulent, vous pouvez aussi utiliser des logiciels comme Postman ou aussi Postwoman.

Rendez-vous sur la page http://127.0.0.1:8000/api et cliquez sur le premier endpoint (GET /api/articles). Cet endpoint va permettre de récupérer tous les articles de notre application. Dans la section Parameters, il est mentionné que la requête prend un paramètre page de type entier (integer) et que ce paramètre va être dans la requête (query). En face, la description dit que ce paramètre page représente le numéro de page de la collection et la valeur par défaut est 1.

Une collection est un ensemble de ressources. Un article est une ressource. La liste des articles est une collection.

Au fait, je ne vous ai pas tout dit sur API Platform. Imaginez votre blog il contient 100 articles, ou même 1 000 articles, si vous faites un GET /api/articles, votre API va vous renvoyez tous les articles, 1 000 articles, c'est trop, beaucoup trop et va à coup sûr faire ramer votre API et vous faire perdre beaucoup d'utilisateurs. Comment faire donc pour éviter cela, c'est de mettre en place un système de pagination, disons par exemple nous allons retourner juste 20 articles au maximum à chaque appel de l'endpoint GET /api/articles. Quand l'utilisateur fera appel à cet endpoint, on lui dira, écoute il y a 1 000 articles dans la base de données, mais voici les 20 premiers, j'ai réparti les 1 000 articles sur des pages de 20 articles par page. Si tu as besoin de la suite, envoie moi la même requête en me spécifiant la page dont tu as besoin dans la requête.

Nous aurons donc combien de pages dans ce cas? Sortez les calculatrices. Nous avons 1 000 articles, repartis sur x pages, chaque page contient 20 articles, combien de pages avons-nous? ... Vous avez tout le temps... On a 50 pages, bravo!

Quand l'utilisateur aura donc besoin des articles 21 à 40, il enverra la requête GET /api/articles?page=2 et ainsi de suite.

Par défaut API Platform renvoie 30 ressources par page, et vous pouvez modifier cette valeur ou même désactiver la pagination. Vous en saurez plus sur la documentation.

Revenons sur la page http://127.0.0.1:8000/api et toujours sur l'endpoint GET /api/articles, regardez maintenant dans la section Responses, cette section définit les valeurs de retour possible quand on appelle cette requête. Nous avons le code HTTP, une description de la réponse et un exemple du type de retour. Pour l'endpoint GET /api/articles, nous avons une seule réponse possible, c'est la liste des articles, le code est 200, ce qui veut dire tout est bon. Par défaut, API Platform retourne du JSON-LD, c'est du JSON un peu particulier, avec des champs en plus quoi, si vous voulez que votre API retourne du JSON, il faut le spécifier dans l’entête de votre requête avec le paramètre Accept, pour l'exemple sur la page http://127.0.0.1:8000/api, il suffit de sélectionner application/json dans le select box Media Type.

Pour tester notre endpoint, regarder en face du titre Parameters, il y a un bouton Try it out, cliquez dessus. On laisse la valeur par défaut du paramètre page à 1, cliquez ensuite sur le bouton Execute.

Nous avons la requête qui a été exécuté avec l'URL et juste en dessous, nous avons la réponse:

Nous avons 0 articles dans la base de données, donc c'est logique que nous n'ayons rien. Si vous regardez hydra:totalItems vaut 0. Il faut donc ajouter un article.

Pour ajouter un article, on ouvre l'endpoint POST /api/articles, on clique sur le bouton Try it out, on saisit les données de notre nouvel article:

Les valeurs de publishedAt et updatedAt sont générés automatiquement, elles peuvent donc être différentes chez vous.

On clique ensuite sur Execute.

Nous avons une réponse 201, qui veut dire que la ressource a été créer, et dans le corps de la réponse, nous avons l'article que nous venons de créer.

Essayez maintenant de rappeler l'endpoint GET /api/articles

Nous avons bien l'article que nous venons d'ajouter et cette fois-ci hydra:totalItems vaut 1, nous avons une ressource.

Les opérations

Une opération représente le lien entre une ressource, une route et le contrôleur associé.

Par défaut, API Platform défini les opérations CRUD sur toutes nos ressources, nous l'avons vu avec notre ressource Article, on peut créer un article, le modifier, le supprimer et lire la liste des articles.

Il faut savoir qu'il y a deux types d'opérations: les opérations sur les collections (collectionOperations) et les opérations sur une entité/une ressource (itemOperations).

Opérations sur les collections

Les opérations sur les collections sont des opérations qui s'applique aux collections de ressources, deux méthodes HTTP sont implémentées par défaut:

  • GET — GET /api/articles cette méthode permet de récupérer la liste des ressources
  • POST — POST /api/articles cette méthode permet d'ajouter une nouvelle ressource

Opérations sur une ressource

Les opérations sur une ressource ne s'applique que sur une ressource (logique quand même non) et là aussi 4 méthodes HTTP sont implémentées:

  • GET — GET /api/articles/{id} cette méthode permet de récupérer une seule ressource dont l'id est mentionné
  • PUT — PUT /api/articles/{id} cette méthode va remplacer la ressource dont l'id est mentionné
  • PATCH — PATCH /api/articles/{id} cette méthode va modifier la ressource dont l'id est mentionné
  • DELETE — DELETE /api/articles/{id} cette méthode va supprimer la ressource dont l'id est mentionné

Par défaut, API Platform définit toutes ces méthodes et nous pouvons désactiver celles que nous ne voulons pas.

Activer ou désactiver des opérations

Pour configurer nos opérations, nous allons utiliser les annotations directement dans le fichier PHP de la ressource, dans notre cas nous allons travailler avec la ressource Article qui est défini dans src/Entity/Article.php.

Pour désactiver les opérations de collections, nous allons utiliser collectionOperations comme suit:

<?php

// src/Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    // ...
}

Et si vous actualiser la page http://127.0.0.1:8000/api, vous remarquerez qu'il n'y a plus aucune opérations sur la collection d'articles.

Nous n'avons défini aucune opération, donc API Platform désactive toutes les deux. Et si nous voulons juste désactiver l'opération POST, c'est à dire que notre ressource est juste en mode lecture:

<?php

// src/Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={"get"}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    // ...
}

Et maintenant nous avons bien l'opération GET /api/articles. Si vous voulez juste activer l'opération POST, il faut mettre post à la place de get, si vous voulez des deux, n'utilisez pas collectionOperations, parce que nous avons les deux par défaut.

Maintenant pour désactiver les opérations sur une ressource, là aussi il faut utiliser itemOperations:

<?php

// src/Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={"get"},
 *     itemOperations={}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    // ...
}

La aussi nous n'avons aucune item operations sur article:

Si je veux juste désactiver la méthode PATCH par exemple, il faut mentionner celles que je veux activer:

<?php

// src/Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={"get"},
 *     itemOperations={"get", "put", "delete"}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    // ...
}

Et voilà.

Dans notre cas, pour l'entité Article, nous n'allons désactiver aucune opération, je vais donc retirer le collectionOperations et itemOperations.

Sérialisation / Désérialisation

API Platform utilise le composant Serializer de Symfony pour convertir les objets au format JSON, XML, ... Pour un peu comprendre le processus de sérialisation et de désérialisation, regarder cette image que j'ai prise sur le site de API Platform.

Comme vous le voyez sur cette image, la sérialisation c'est le processus pour passer de l'objet au tableau puis au format JSON et la désérialisation c'est l'inverse.

Ce que nous voulons faire c'est de contrôler les attributs qui interviennent pendant la sérialisation. Tout à l'heure lorsque nous avons créé notre premier article, nous avons nous même renseigner le slug qui doit être normalement être généré à partir du titre de l'article, les attributs publishedAt et updtedAt doivent aussi être générés automatiquement, elles ne doivent pas être saisi.

Nous allons donc utiliser les annotations normalizationContext et denormalizationContext pour définir ce qu'on appelle des groupes et ensuite nous définirons pour chaque attribut dans quel groupe il appartient.

  • Les attributs qui se trouveront dans le groupe du normalizationContext seront accessible en mode lecture (GET)
  • Les attributs qui se trouveront dans le groupe du denormalizationContext seront accessible en mode écriture (POST, PUT, PATCH)
  • Les attributs qui auront les deux groupes seront accessible en mode lecture et écriture
  • Les attributs qui n'auront aucun des groupes ne seront pas pris en compte
<?php

// src/Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"article:read"}},
 *     denormalizationContext={"groups"={"article:write"}}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    // ...
}

Ici je définis les groupes article:read et article:write, ces noms sont facultatifs et vous pouvez appeler vos groupes comme vous voulez.

Si vous regardez maintenant vos endpoints, vous verrez qu'il n'y a plus aucun attribut qui est défini, ici avec l'endpoint POST /api/articles:

Et si vous regardez plus bas, dans la section Schemas, vous verrez que nous avons maintenant 4 entrées et elles ne contiennent rien au fait:

Cela est dû au fait que nous avons défini des groupes, mais pour aucun attribut, nous l'avons mis dans un groupe, il faut renseigner les groupes pour chaque attribut:

<?php

// src/Entity

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"article:read"}},
 *     denormalizationContext={"groups"={"article:write"}}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     *
     * @Groups("article:read")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     *
     * @Groups({"article:read", "article:write"})
     */
    private $title;

    /**
     * @ORM\Column(type="string", length=255)
     *
     * @Groups("article:read")
     */
    private $slug;

    /**
     * @ORM\Column(type="text", nullable=true)
     *
     * @Groups({"article:read", "article:write"})
     */
    private $content;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     *
     * @Groups({"article:read", "article:write"})
     */
    private $picture;

    /**
     * @ORM\Column(type="boolean")
     *
     * @Groups("article:read")
     */
    private $isPublished;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     *
     * @Groups("article:read")
     */
    private $publishedAt;

    /**
     * @ORM\Column(type="datetime")
     *
     * @Groups("article:read")
     */
    private $updatedAt;

    // ...
}

Les attributs idslugisPublishedpublishedAt et updatedAt sont en accessible en lecture seule, les autres sont accessibles en lecture et en écriture.

Si vous regardez maintenant les schemas ont changer:

Vous pouvez remarquer le readOnly: true sous idslug, ...

Et l'endpoint POST /api/articles ne prend plus que 3 élements, titlecontent et picture:

Sauf qu'à ce niveau, si vous essayez d'ajouter un article, vous devez avoir une erreur qui dit que le champ slug ne peut pas être null, et c'est vrai, nous allons corriger cela avec les Data Persister.

Data Persister

Pour l'instant, quand nous faisons une requête POST, API Platform gère automatiquement l'enregistrement de notre entité en base de données, ce qu'il fait au fait, c'est prendre le JSON que nous lui envoyons, instancier un objet avec ce JSON et faire la persistence avec Doctrine. Mais maintenant que nous avons défini des attributs en lecture seule comme le slug, nous n'envoyons plus cet attribut dans le JSON et dans notre table article dans la base de données, nous avons défini le slug comme un champ qui ne peut pas être null, c'est ce qui donne donc l'erreur de tout à l'heure.

Pour corriger cela, nous allons nous même implémenter notre méthode POST, au lieu d'appeler celle par défaut de API Platform, nous dirons à l'application d'utiliser notre méthode, et dans notre méthode, nous prendrons le soin de remplir les champs obligatoires.

Nous allons donc créer un data persister. Pour cela, nous allons créer un fichier ArticleDataPersister.php dans src/DataPersister/ et cette classe va implementer l'interface ContextAwareDataPersisterInterface d'API Platform:

<?php

// src/DataPersister

namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;

class ArticleDataPersister implements ContextAwareDataPersisterInterface
{
    public function supports($data, array $context = []): bool
    {
        // TODO: Implement supports() method.
    }

    public function persist($data, array $context = [])
    {
        // TODO: Implement persist() method.
    }

    public function remove($data, array $context = [])
    {
        // TODO: Implement remove() method.
    }
}

L'interface ContextAwareDataPersisterInterface défini 3 méthodes:

  • supports — cette méthode défini si ce persister supporte l'entité. Au fait c'est cette méthode qui dira si ce persister est pour l'entité Article ou pas
  • persist — cette méthode va créer ou modifier les données, c'est donc cette méthode qui sera appelée à chaque opération POST, PUT ou PATCH
  • remove — cette méthode sera appelée pour l'opération DELETE

Le paramètre $data est un objet qui représente l'entité, dans notre cas, nous voulons que ce persister soit appelé pour l'entité Article, nous allons donc définir cela avec la méthode supports():

<?php

// src/DataPersister

namespace App\DataPersister;

use App\Entity\Article;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;

class ArticleDataPersister implements ContextAwareDataPersisterInterface
{
    public function supports($data, array $context = []): bool
    {
        return $data instanceof Article;
    }

    // ...
}

Voilà, on vérifie juste que le paramètre $data est une instance de la classe Article, si c'est le cas, ce persister sera appelé.

Nous pouvons maintenant implémenter les méthodes persist() et remove(). Nous allons utiliser le composant String Component de Symfony pour générer le slug, il faut donc commencer par l'installer:

$ composer require symfony/string

Et maintenant le code du data persister ArticleDataPersister.php:

<?php

// src/DataPersister

namespace App\DataPersister;

use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\String\Slugger\SluggerInterface;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;

/**
 *
 */
class ArticleDataPersister implements ContextAwareDataPersisterInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $_entityManager;
    private $_slugger;

    public function __construct(
        EntityManagerInterface $entityManager,
        SluggerInterface $slugger
    ) {
        $this->_entityManager = $entityManager;
        $this->_slugger = $slugger;
    }


    /**
     * {@inheritdoc}
     */
    public function supports($data, array $context = []): bool
    {
        return $data instanceof Article;
    }

    /**
     * @param Article $data
     */
    public function persist($data, array $context = [])
    {
        $data->setSlug(
            $this
                ->_slugger
                ->slug(strtolower($data->getTitle())). '-' .uniqid()
        );

        $this->_entityManager->persist($data);
        $this->_entityManager->flush();
    }

    public function remove($data, array $context = [])
    {
        $this->_entityManager->remove($data);
        $this->_entityManager->flush();
    }
}

Et on modifie un peu la classe Article.php pour initialiser la valeur de isPublished à false, j'ai aussi ajouter un attribut createdAt et mis updatedAt a null par défaut:

<?php

// src/Entity

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"article:read"}},
 *     denormalizationContext={"groups"={"article:write"}}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     *
     * @Groups("article:read")
     */
    private $id;

    // ...

    /**
     * @ORM\Column(type="datetime", nullable=true)
     *
     * @Groups("article:read")
     */
    private $updatedAt;

    /**
     * @ORM\Column(type="datetime")
     *
     * @Groups("article:read")
     */
    private $createdAt;

    public function __construct()
    {
        $this->isPublished = false;
        $this->createdAt = new \DateTime();
    }

    // ...

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeInterface $createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }
}

Et n'oubliez pas de faire les migrations avec:

$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate

Si vous avez des erreurs, vider la table article dans la base de données et réessayer.

Vous pouvez maintenant essayer d'ajouter un article:

Et cette fois-ci, ça marche bien et le slug est bien généré:

Comme nous l'avons dit, la méthode persist() est appelée pour les opérations de POST, PUT et PATCH. Dans le cas d'une opération de PUT ou PATCH, nous devons modifier l'attribut updatedAt, et le slug n'est modifier que si l'article n'est pas encore publier. Nous allons donc modifier la méthode persist() pour prendre en compte ces règles:

<?php

// src/DataPersister

namespace App\DataPersister;

use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\String\Slugger\SluggerInterface;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;

/**
 *
 */
class ArticleDataPersister implements ContextAwareDataPersisterInterface
{
    /**
     * @var EntityManagerInterface
     */
    private $_entityManager;

    /**
     * @param SluggerInterface
     */
    private $_slugger;

    /**
     * @param Request
     */
    private $_request;

    public function __construct(
        EntityManagerInterface $entityManager,
        SluggerInterface $slugger,
        RequestStack $request
    ) {
        $this->_entityManager = $entityManager;
        $this->_slugger = $slugger;
        $this->_request = $request->getCurrentRequest();
    }


    /**
     * {@inheritdoc}
     */
    public function supports($data, array $context = []): bool
    {
        return $data instanceof Article;
    }

    /**
     * @param Article $data
     */
    public function persist($data, array $context = [])
    {
        // Update the slug only if the article isn't published
        if (!$data->getIsPublished()) {
            $data->setSlug(
                $this
                    ->_slugger
                    ->slug(strtolower($data->getTitle())). '-' .uniqid()
            );
        }

        // Set the updatedAt value if it's not a POST request
        if ($this->_request->getMethod() !== 'POST') {
            $data->setUpdatedAt(new \DateTime());
        }

        $this->_entityManager->persist($data);
        $this->_entityManager->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function remove($data, array $context = [])
    {
        $this->_entityManager->remove($data);
        $this->_entityManager->flush();
    }
}

Et voilà, nous avons un data persister qui marche bien.

Pour terminer cette première partie, je vous laisse définir les deux autres entités Comment et Tag, c'est super facile au fait, vous utilisez make:entity, vous répondez par yes à la question de API Platform et c'est tout, pas besoin de data persister pour l'instant.

Voici ma classe Comment.php:

<?php

// src/Entity

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"comment:read"}},
 *     denormalizationContext={"groups"={"comment:write"}}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\CommentRepository")
 */
class Comment
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     *
     * @Groups("comment:read")
     */
    private $id;

    /**
     * @ORM\Column(type="text")
     *
     * @Groups({"comment:read", "comment:write"})
     */
    private $content;

    /**
     * @ORM\Column(type="datetime")
     *
     * @Groups("comment:read")
     */
    private $createdAt;

    public function __construct()
    {
        $this->createdAt = new \DateTime();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getContent(): ?string
    {
        return $this->content;
    }

    public function setContent(string $content): self
    {
        $this->content = $content;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeInterface $createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }
}

et Tag.php:

<?php

// src/Entity

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"tag:read"}},
 *     denormalizationContext={"groups"={"tag:write"}}
 * )
 * @ORM\Entity(repositoryClass="App\Repository\TagRepository")
 */
class Tag
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     *
     * @Groups("tag:read")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     *
     * @Groups({"tag:read", "tag:write"})
     */
    private $label;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getLabel(): ?string
    {
        return $this->label;
    }

    public function setLabel(string $label): self
    {
        $this->label = $label;

        return $this;
    }
}

N'oubliez pas faire les migrations avec:

$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate

Rendez-vous dans la prochaine partie, nous allons parler des relations entre entités. D'ici là, pratiquez, encore et encore, c'est le seul secret. Merci, à bientôt.


Partager cet article

alioukahere

Mamadou Aliou Diallo

@alioukahere

Développeur web fullstack avec une passion pour l’entrepreneuriat et les nouvelles technologies. Fondateur de Kaherecode.