PHP     Symfony 2 et 3       AngularJS   Angular2       Cordova/Ionic 2
Daniel G.
Développeur d'applications Web et mobiles - Gironde

TUTORIEL

APi PLATFORM

Basé sur la version 1.2 d'APi PLATFORM (Symfony 4)


Avec APi PLATFORM, on a un ensemble d’outils pour créer rapidement une API REST.

Une API compatible REST, ou « RESTful », est une interface de programmation d’application qui fait appel à des requêtes HTTP pour obtenir (GET), modifier (PUT), publier (POST) et supprimer (DELETE) des données.

exemples :

GET /articles
GET /articles/{identifiant-unique}

APi PLATFORM ou FORESTBUNDLE ?
Les 2 sont bien, la différence est que API PLATFORM est un peu plus orienté “paramétrage”.
API PLATFORM est un projet officiel Symfony.


Pour écrire ce tutorial, je me suis basé sur la documentation officelle : https://api-platform.com/docs/distribution/
Parfois, J’ai copié des parties de textes explicatifs provenant de cette documentation.


Le projet

Histoire d’avoir du concret pour suivre le tutorial.
Nous allons partir d’un projet complet avec des entités, des relations…

à partir d’un projet Symfony Flex, nous allons petit à petit l’enrichir de composants.

composer create-project symfony/skeleton my-product-api  
cd my-product-api  
  • installer api platform :
composer req api

Lancer le projet

Plusieurs façons, au choix :

  • PHP server
php -S 127.0.0.1:8000 -t public
  • Symfony WebServerBundle
composer req server --dev  
php bin/console server:run
  • à partir d’un serveur apache ou nginx



Vous pouvez dès à present tester que tout va bien :

http://127.0.0.1:8000/api

Vous obtenez une page de la document de l’API totalement vide (avec une petite araignée sur la droite)

La documentation de l’API permet aux autres développeurs ou même pour soit meme de connaitre précisement toutes les fonctionnalités disponibles et aussi de pouvoir les tester.


security bundle

Il faut sécuriser notre API afin de controler les accès.

composer require symfony/security-bundle

ORM

  • doctrine…
composer require symfony/orm-pack  

configurer la base de donnée

copier .env en .env.local

.env.local

...
DATABASE_URL=mysql://root:@127.0.0.1:3306/db_my_product_api
...

  • Astuces :

un composant pour générer en ligne de commande des entités, des controlleurs, des fixtures…
et bien sur, utile qu’en dév !

composer require symfony/maker-bundle --dev  
  • pour créer une entité en ligne de commande :
php bin/console make:entity

astuce 1 :
mettre comme type : ManyToMany, ManyToOne ou OneToMany
cela va écrire directement les relations dans les entités (des 2 cotés).
Finis les erreurs de confusion avec inversedBy ou mappedBy…

astuce 2 :
Pour connaitre la liste des choses qu’il est possible de créer en ligne de commande :

php bin/console list make

LES DONNÉES

on ajoute 4 nouvelles entités : (Product, Offer, Author, User)

  • un Produit peut être lié à 0 ou plusieurs offres (OneToMany)
  • 1 ou plusieurs produits peut être lié à un auteur (ManyToOne)

Vous remarquerez dans les entités ci dessous l’annotation @ApiRessource() (nous verrons plus tard, son utilisation).

#App\Entity\Product.php

<?php
// api/src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use ApiPlatform\Core\Annotation\ApiSubresource;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Serializer\Filter\GroupFilter;
use Symfony\Component\Serializer\Annotation\Groups;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use App\Filter\RegexpFilter;



/**
 * @ApiResource()
 * @ORM\Entity
 */
class Product {

    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @var string $name A name property - this description will be available in the API documentation too.
     *
     * @ORM\Column
     * @Assert\NotBlank
     */
    public $name;

    /**
     * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
     * @Assert\NotBlank
     */
    public $author;   

    /**
     * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"})  
     */
    public $offers;

     /**
     * @ORM\Column(type="boolean", options={"default" : false}, nullable = true)
     */
    private $isActive;

     /**
     * @Assert\Date
     * @var string A "Y-m-d" formatted value
     * @ORM\Column(type="date", nullable = true)
     */
    public $createdAt;

    /**
     * @ORM\Column(type="float", nullable=true)
     * @Assert\Range(min=0, minMessage="The transportFees must be superior to 0.")
     * @Assert\Type(type="float")
     */
    public $transportFees;

    /**
     * @var string[] Describe the product
     *
     * @ORM\Column(type="json", nullable = true)
     */
    public $properties;


    public function __construct()
    {
        $this->offers = new ArrayCollection(); // Initialize $offers as an Doctrine collection
    }

    // Adding both an adder and a remover as well as updating the reverse relation are mandatory
    // if you want Doctrine to automatically update and persist (thanks to the "cascade" option) the related entity
    public function addOffer(Offer $offer): void
    {
        $offer->product = $this;
        $this->offers->add($offer);
    }

    public function removeOffer(Offer $offer): void
    {
        $offer->product = null;
        $this->offers->removeElement($offer);
    }

    public function getIsActive(): ?bool
    {
        return $this->isActive;
    }
 
    public function setIsActive(bool $isActive): self
    {
        $this->isActive = $isActive;
 
        return $this;
    }    

    /**
     * Get $name A name property - this description will be available in the API documentation too.
     *
     * @return  string
     */ 
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set $name A name property - this description will be available in the API documentation too.
     *
     * @param  string  $name  $name A name property - this description will be available in the API documentation too.
     *
     * @return  self
     */ 
    public function setName(string $name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get describe the product
     *
     * @return  string[]
     */ 
    public function getProperties()
    {
        return $this->properties;
    }

    /**
     * Set describe the product
     *
     * @param  string[]  $properties  Describe the product
     *
     * @return  self
     */ 
    public function setProperties(string $properties)
    {
        $this->properties = $properties;

        return $this;
    }
}

#App\Entity\Offer.php

<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use ApiPlatform\Core\Annotation\ApiFilter;        
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\NumericFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;

/**
 * An offer from my shop - this description will be automatically extracted form the PHPDoc to document the API.
 *
 * @ApiResource()
 * @ORM\Entity
 */
class Offer
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="text")
     */
    public $description;

    /**
     * @ORM\Column(type="float")
     * @Assert\NotBlank
     * @Assert\Range(min=0, minMessage="The price must be superior to 0.")
     * @Assert\Type(type="float")
     */
    public $price;

    /**
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="offers")
     */
    public $product;
}

#App\Entity\Author.php

<?php
// api/src/Entity/Author.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ORM\Entity
 */
class Author
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @var string $name A name property
     *
     * @ORM\Column
     * @Assert\NotBlank  
     */
    public $name;

         /**
     * @Assert\Date
     * @Assert\NotBlank
     * @var string A "Y-m-d" formatted value
     * @ORM\Column(type="date", nullable=true)
     */
    public $birthday;

    public function __construct() {}
}

#App\Entity\User.php

<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
 * @ORM\Table(name="users")
 * @ORM\Entity
 */
class User implements UserInterface
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @ORM\Column(type="string", length=25, unique=true)
     */
    private $username;
    /**
     * @ORM\Column(type="string", length=500)
     */
    private $password;
    /**
     * @ORM\Column(name="is_active", type="boolean")
     */
    private $isActive;
    /**
     * @ORM\Column(type="json", nullable=true)
     */
    private $roles = [];


    public function __construct($username)
    {
        $this->isActive = true;
        $this->username = $username;
    }
    public function getUsername()
    {
        return $this->username;
    }
    public function getSalt()
    {
        return null;
    }
    public function getPassword()
    {
        return $this->password;
    }
    public function setPassword($password)
    {
        $this->password = $password;
    }
    public function eraseCredentials()
    {
    }
    public function getRoles(): array
    {
        $roles = $this->roles;

        return json_decode($roles);
    }

    /**
     * Set the value of roles
     *
     * @return  self
     */ 
    public function setRoles($roles)
    {
        $this->roles = $roles;

        return $this;
    }
}
php bin/console doctrine:database:create
php bin/console doctrine:schema:update --force

fixtures


On va remplir la base avec des données fictifs, pour cela on utilise le composant Fixtures

composer require --dev doctrine/doctrine-fixtures-bundle

#/DataFixtures/AppFixtures

<?php

namespace App\DataFixtures;

use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use App\Entity\Product;
use App\Entity\Offer;
use App\Entity\Author;
use App\Entity\User;


class AppFixtures extends Fixture
{
    private $encoder;
    
    public function __construct(UserPasswordEncoderInterface $encoder)
    {
        $this->encoder = $encoder;
    }

    public function load(ObjectManager $manager)
    {
        $arr_authors_list = array();

        for($i=1; $i<8; $i++) {
            $author = new Author();
            $author->name = "auteur " . $i;
            $author->birthday = $this->randomDate("1901-01-31", "1999-12-31");            
            $manager->persist($author);
            $arr_authors_list[] = $author;
        }

        for($i=1; $i<16; $i++) {
            $arr_transport_fees = array();
            $arr_transport_fees[] = rand(400, 1000) / 100;
            $arr_transport_fees[] = null;

            $product = new Product();
            $product->name = "produit " . $i;
            $product->setIsActive(0 == $i % 2 ? false : true);
            $auth = $arr_authors_list[array_rand ($arr_authors_list, 1)];        
            $product->author = $auth;
            $product->createdAt = $this->randomDate("2015-01-31", "2018-12-31");
            $product->transportFees = $arr_transport_fees[array_rand($arr_transport_fees, 1)];
            $manager->persist($product);

            for($j=1; $j<random_int (0, 8); $j++) {
                $offer = new Offer();
                $offer->product = $product;
                $offer->description = "produit " . $i . " : description offre " . $j;    
                $offer->price = $i . $j;
                $manager->persist($offer);
            }    
        }

        $user1 = new User("reader");
        $user1->setPassword($this->encoder->encodePassword($user1, "reader"));
        $user1->setRoles(json_encode(array("ROLE_USER")));
        $manager->persist($user1);

        $user2 = new User("admin");
        $user2->setPassword($this->encoder->encodePassword($user2, "admin"));
        $user2->setRoles(json_encode(array("ROLE_ADMIN")));
        $manager->persist($user2);

        $manager->flush();
    }

    public function randomDate($start_date, $end_date)
    {
        $min = strtotime($start_date);
        $max = strtotime($end_date);
        $val = rand($min, $max);

        return new \DateTime(date('Y-m-d', $val));
    }
}

Étant donné que dans les fixtures nous utilisons “encoder” pour crypter le mot de passe, on va indiquer dans security.yaml le type d’encodage :

#/config/packages/security.yaml

        ...
        encoders:
            App\Entity\User:
                algorithm: bcrypt
        ...
  • plus qu’à charger les fixtures :
php bin/console doctrine:fixtures:load

Authentification JWT


L’authentification par token est bien adapté pour sécuriser une api.

https://api-platform.com/docs/core/jwt#jwt-authentication

JSON Web Token (JWT) est un standard ouvert basé sur JSON ( RFC 7519 ) permettant de créer des jetons d’accès qui revendiquent un certain nombre de revendications.
Par exemple, un serveur peut générer un jeton dont la revendication est “connectée en tant qu’administrateur” et le fournir à un client.
Le client peut ensuite utiliser ce jeton pour prouver qu’il est connecté en tant qu’administrateur.
Les jetons sont signés par la clé du serveur, ce qui permet au serveur de vérifier que le jeton est légitime.
Les jetons sont conçus pour être compacts, sécurisés pour les URL et utilisables notamment dans un contexte de connexion unique (SSO) sur un navigateur Web.

LexikJWTAuthenticationBundle

  • Installer LexikJWTAuthenticationBundle

    composer req lexik/jwt-authentication-bundle
    
  • Générer les clés SSH

    Il faut installer openSSL sur votre système.
    Je vous laisse chercher sur google…

    openSSL est installé ?

    openssl version
    

    Il faut générer une clé public et une clé privé qui vont servir pour crypter et decrypter le token :

    mkdir config/jwt
    openssl genrsa -out config/jwt/private.pem -aes256 4096
    openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
    

    private passphrase: !r5UAz4Y!o

    .env.local

    ...
    ###> lexik/jwt-authentication-bundle ###
    JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
    JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
    JWT_PASSPHRASE=!r5Az4oi!o0
    ###< lexik/jwt-authentication-bundle ###
    ...
    
    • Dans le cas où la première openssl commande vous oblige à saisir le mot de passe, utilisez ce qui suit pour obtenir la clé privée déchiffrée

      openssl rsa -in config/jwt/private.pem -out config/jwt/private2.pem
      mv config/jwt/private.pem config/jwt/private.pem-back
      mv config/jwt/private2.pem config/jwt/private.pem
      
  • configurer les routes

    • register : une route pour gérer l’enregistrement d’un nouvel utilisateur
    • login_check : une route pour envoyer les identifiants (login + password) et en retour on obtiendra le token
    • swagger_ui: redéfinir le chemin de la documentation de l’api
      au lieu de : http://127.0.0.1:8000/api
      on pourra mettre : http://127.0.0.1:8000/docs (c’est parceque je préfère comme ça)

    #/config/routes.yaml

    ...
    register:
        path: /register
        controller: App\Controller\AuthController::register
        methods: POST
    
    login_check:
        path:     /login_check
        methods:  [POST]
    
    swagger_ui:
        path: /docs
        controller: api_platform.swagger.action.ui
    ...
    
d'ailleurs, si vous allez  :  http://127.0.0.1:8000/docs  
vous aller voir qu'on a nos 4 entités et les requêtes qu'il est possible de faire.  
Ceci, grace à l'annotation  @ApiResource dans les entités, nous verrons ça plus loin.
  • User

    • l’entité User à déjà été créé dans le point précedent.

    • Rappel :

      user provider, 3 choix :

      • Doctrine user provider fourni par Symfony (recommandé)
      • créer un user provider personnalisé
      • utiliser l’user provider de FOSUserBundle

      user provider est un fournisseur d’utilisateur, c.a.d à partir de quelle source Symfony doit il aller chercher l’utilisateur (une base de données, un service web, un fichier, en dure…)

      nous utiliserons : Doctrine user provider fourni par Symfony (doctrine donc en base de données)

  • configuration

    #/config/packages/security.yaml

    informer le système de sécurité de Symfony de notre fournisseur et de notre authentificateur

    security:
        encoders:
            App\Entity\User:
                algorithm: bcrypt
    
        role_hierarchy:    
            ROLE_ADMIN: ROLE_USER
                
        providers:
            entity_provider:
                entity:
                    class: App\Entity\User
                    property: username
    
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
            login:
                pattern:  ^/login
                stateless: true
                anonymous: true
                json_login:
                    check_path: /login_check
                    success_handler: lexik_jwt_authentication.handler.authentication_success
                    failure_handler: lexik_jwt_authentication.handler.authentication_failure
    
            register:
                pattern:  ^/register
                stateless: true
                anonymous: true
    
            api:
                pattern:  ^/api
                stateless: true
                anonymous: false
                provider: entity_provider
                guard:
                    authenticators:
                        - lexik_jwt_authentication.jwt_token_authenticator
    
        access_control:
            - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/api, roles: IS_AUTHENTICATED_FULLY  }
            - { path: ^/docs, roles: IS_AUTHENTICATED_ANONYMOUSLY  }
    
    • providers :
      entity_provider -> basé sur l’entité donc sur la base de donnée

    • firewalls :

      • login:

        • reçoit les identifiants /login_check
        • en cas qu’il le trouve (via le providers) il execute : lexik_jwt_authentication.handler.authentication_success
          lexik redefini l’authentification reussi pour inclure la création du token qui sera envoyé en réponse
      • api :

        • guard:
          c’est l’authenticator de lexik qui sera utilisé. Celui ci va vérifier la validité du token et accorder l’accès à /api en conséquence.
    • access_control

      • /api et /docs doivent être protégés
        mais pour l’instant /docs est accessible à tous (Plus tard en prod, mettre /docs à : IS_AUTHENTICATED_FULLY)
#/config/packages/lexik_jwt_authentication.yaml
```
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 360000
```
J'ai mis un TTL à 10 heures pour les testes.
  • controlleur: enregistrement d’un utilisateur:

    php bin/console make:controller
    

    #/src/controller/AuthController.php

    <?php
    
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use App\Entity\User;
    use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
    use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
    use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    
    
    class AuthController extends AbstractController
    {
        public function register(Request $request, UserPasswordEncoderInterface $encoder)
        {
            $em = $this->getDoctrine()->getManager();
    
            $username = $request->request->get('username');
            $password = $request->request->get('password');
            $roles = $request->request->get('roles');
    
            if (!$roles) {
                $roles = json_encode([]);
            }
    
            $user = new User($username);
            $user->setPassword($encoder->encodePassword($user, $password));
            $user->setRoles(($roles));
            $em->persist($user);
            $em->flush();
    
            return new Response(sprintf('User %s successfully created', $user->getUsername()));
        }
    }
    

accès à la documentation de l’API

faisons un petit tour sur la documentation : http://127.0.0.1:8000/docs/

  • Vous constaterez, si vous avez repris les entités tel quelles si dessus que la doc presente Offer, product mais que l’Author n’y ai pas. c’est normal, j’ai déliberemment ommis l’annotation
    @ApiResource(), à vous de l’ajouter dans l’entité.

  • on va essayer de récupérer les produits :
    pour cela on va dans GET /api/products
    on clique sur “TRY IT OUT” et ensuite sur “EXECUTE”

    on obtiens en reponse :

    {
    "code": 401,
    "message": "JWT Token not found"
    }
    

    c’est normal, on a un firewall sur /api, nous n’avons pas fourni de token pour y accèder
    (voir security.yaml)

    La doc va juste nous servir pour prendre connaissance de ce que l’on peut faire avec l’api. Elle ne nous servira pas pour tester (et donc recuperer des choses). Pour cela, on utilisera curl ou postman (car on peut joindre un token)

    astuce :
    Si vous voulez tester en utilisant la doc :

    • normalement, doc : http://127.0.0.1:8000/docs/
      il est possible de joindre un token afin de pouvoir interroger les requêtes mais sur mon poste le bouton Authorization n’apparait pas. (peut être que chez vous, ça y est)

    • sinon, une façon pas propre du tout consiste à desactiver la sécurité sur l’api
      #/config/security.yaml

    firewalls:
        ...    
        api:
            pattern:  ^/api_xxxxxxxxxxx
            ...
    

    Ainsi, le firewall va securiser un pattern qui n’existe pas et vous pourrez intérroger /api sans token (à la fin, ne pas oublier de remettre comme avant)

    GET /api/products
    on clique sur “TRY IT OUT” et ensuite sur “EXECUTE”
    -> vous obtenez la liste des produits

la configuration de base est terminé, plus qu’à tester :

Pour pouvoir interroger l’api vu qu’il est sécurisé, il faut demander un token.
Pour demander un token, il faut s’enregistrer.

  • enregistrer un utilisateur :
    /register
    avec : username=“reader” et password=“reader”

    • curl
``` 
curl -X POST http://localhost:8000/register -d username=reader -d password=reader
``` 
  • postman

    POST http://localhost:8000/register
    HEADER
        Content-Type    application/json
    BODY form-data
        username        reader
        password        reader
    
  • demande jeton

    Maintenant qu’un utilisateur est en base de données, on va pouvoir demander au service d’authentification d’échanger l’identifiant + password contre un token

    • curl

      curl -X POST -H "Content-Type: application/json"  -d "{\"username\": \"reader\",\"password\": \"reader\"}" http://localhost:8000/login_check
      
    • postman

      POST http://localhost:8000/login_check
      HEADER
          Content-Type    application/json
      BODY raw
      {
          "username": "reader",
          "password": "reader"
      } 
      

      -> on obtient

      token "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NDY5NjAzMzEsImV4cCI6MTU0Njk2MzkzMSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoidG90byJ9.Oa2wiT5H-fRFqN0hQsGB5qpDz89vZ-6GkJ7UYPaNi3dVldg1Pd5t98boC76gJrxXGUyg6sv1QXzc-v9KtNzX1jz3pavjItthKHj6552YUYOh9YC5e97-cfNc3zTefA6he4Ks1U_6x_tJXPWlQEIEL9OT8r_F0N2eFx6hs-Iqn8FiudqtqL2TRDvDB0dlCEZnJVr0SbJPzRo-UYZjBr6GEkWGw7whV_AHcyObbWT0Ea8i6SDpN4G-LtBhE3XnUr6xN1fLjGBZLt0A2tgxcu3PEfpu30PhHn4u_6HmdH0_At_hlzR-J7BNEHF2nmPjgNtXStdj1cQ8163gpZnx1d_xJmpxwNNHlB2WVhfujHrs_woY82WXHf6zW5E8TvmocBVT63N7zCYJsQc0thwrW7hfnE7nAdl09POXf_AgHJFmdhA5BYQPzgB88f-kJd4b5W_W8kCK3wVEoNet-H6yHbXunrER1JaGPr7OEmEli3CcPPY0tMYMOrnZopf_7K_77LLmLtdGaAbGLkFymTqxCcC9n12pfDhdlNnwaNoAQhtVGF-XZGA9wPAD8FMCCwvYf-Q2vY3KmtFHBluwoYLtx21hG6M2I8ux7U-08N9lSQbNHr6Bz4FGsHz9UN1-te1l2qbzRXgnbYo3vEhfTmCWPmvHri3WHhuQz9-M64KOSznYIkA"
      

      si vous avez en retour un JWT TOKEN INVALID, c’est que soit :

      • le passPhrase indiqué dans .env.local ne correspond pas avec celui lors de la génération des clés publics et privés
      • /config/jwt et ces clés ne sont pas présents
      • /config/jwt n’est pas autorisé en lecture
  • accès à l’API sécurisé avec le token via curl

    curl -H “Authorization: Bearer [TOKEN]” http://localhost:8000/api/products

    curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NDY5NjAzMzEsImV4cCI6MTU0Njk2MzkzMSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoidG90byJ9.Oa2wiT5H-fRFqN0hQsGB5qpDz89vZ-6GkJ7UYPaNi3dVldg1Pd5t98boC76gJrxXGUyg6sv1QXzc-v9KtNzX1jz3pavjItthKHj6552YUYOh9YC5e97-cfNc3zTefA6he4Ks1U_6x_tJXPWlQEIEL9OT8r_F0N2eFx6hs-Iqn8FiudqtqL2TRDvDB0dlCEZnJVr0SbJPzRo-UYZjBr6GEkWGw7whV_AHcyObbWT0Ea8i6SDpN4G-LtBhE3XnUr6xN1fLjGBZLt0A2tgxcu3PEfpu30PhHn4u_6HmdH0_At_hlzR-J7BNEHF2nmPjgNtXStdj1cQ8163gpZnx1d_xJmpxwNNHlB2WVhfujHrs_woY82WXHf6zW5E8TvmocBVT63N7zCYJsQc0thwrW7hfnE7nAdl09POXf_AgHJFmdhA5BYQPzgB88f-kJd4b5W_W8kCK3wVEoNet-H6yHbXunrER1JaGPr7OEmEli3CcPPY0tMYMOrnZopf_7K_77LLmLtdGaAbGLkFymTqxCcC9n12pfDhdlNnwaNoAQhtVGF-XZGA9wPAD8FMCCwvYf-Q2vY3KmtFHBluwoYLtx21hG6M2I8ux7U-08N9lSQbNHr6Bz4FGsHz9UN1-te1l2qbzRXgnbYo3vEhfTmCWPmvHri3WHhuQz9-M64KOSznYIkA" http://localhost:8000/api/products
    
  • accès à l’API sécurisé avec le token via postman

    GET http://localhost:8000/api/products
    AUTHORIZATION
        Bearer Token
        Token   .................token.....................
    HEADER
        Content-Type    application/json
    


API PLATFORM



à partir de maintenant nous rentrons véritablement dans le vif du sujet d’API PLATFORM !

mapping

determiner les entités qui devront être soumis à la génération de point d’entrée

/packages/api_platform.yaml

api_platform:
    mapping:
        paths: 
            - '%kernel.project_dir%/src/Entity'
  • par defaut, toutes les entités se trouvant dans /src/Entity seront pris en compte.
    il est possible de préciser individuellement tel ou tel entité.

  • Ensuite, il faut également préciser l’annotation @ApiResource dans l’entité pour indiquer une opération de CRUD.
    (plus tard, on verra comment parametrer finement le CRUD)

dans toutes les entités (Product, Offer, Author, User), mettez l’annotation @ApiResource en début de classe (si vous avez repris mon code, c’est déjà fait mais aller vérifier tout de même)

...
...
use ApiPlatform\Core\Annotation\ApiResource;

/**
 * @ORM\Table(name="product")
 * @ORM\Entity
 *
 * @ApiResource()
 *
 */
class Product
{
    ...
    ...

avec @ApiResource(), on peut effectuer sur Product toutes les opérations : GET, POST, PUT


Formats

GraphQL, API JSON , JSON-LD, HAL , JSON brut , XML (expérimental) et même YAML et CSV
JSON-LD est le meilleur format par défaut pour une nouvelle API

Parametrer Les formats de l’API

/packages/api_platform.yaml  
...
...
    formats:
        jsonld:   ['application/ld+json']
        json:     ['application/json']
        html:     ['text/html']
        jsonhal:  ['application/hal+json']

JSON-LD

JSON-LD = JSON + structure

Le but de ces éléments de balisage est d’apporter des informations supplémentaires aux internautes et aux moteurs.
Il s’agit de données qu’il faut insérer dans le code source qui permettent un affichage riche dans les moteurs de recherche.
Cela peut être des informations sur un auteur, un artiste, des avis, des dates ou les caractéristiques d’un produit.
Cela améliore le SEO.

exemple en JSON-LD

GET http://127.0.0.1:8000/api/products/1

{
  "@context": "/api/contexts/Book",
  "@id": "/api/books/2",
  "@type": "Book",
  "isbn": "9781782164104",
  "title": "livre 1 : Persistence in PHP with the Doctrine ORM",
  "description": "livre 1 : This book is designed for PHP developers and architects who want to modernize their skills through better understanding of Persistence and ORM.",
  "author": "Kévin Dunglas",
  "publicationDate": "2016-11-21T00:00:00+00:00",
  "reviews": [
    "/api/reviews/20",
    "/api/reviews/21"
  ],
  "id": 2
}
  • chaque ressource est identifiée par un IRI (unique)
  • “@id”: “/api/books/2”,
    • l’adresse IRI avec l’identifiant
    • (Vous pouvez utiliser cet IRI pour référencer ce document à partir d’autres documents)

les dates :

  • API Platform comprend tout format de date pris en charge par PHP.
    nous vous recommandons vivement d’utiliser le format spécifié par la RFC 3339, mais, comme vous pouvez le constater, les formats les plus courants, y compris, September 21, 2016 peuvent être utilisés.

validation

  • La plateforme API est livrée avec un pont avec le composant de validation de Symfony
  • Ajouter certaines de ses nombreuses contraintes de validation (ou en créer des personnalisées ) à nos entités est suffisant pour valider les données soumises par les utilisateurs
@Assert\Isbn  
@Assert\NotBlank  
@Assert\NotNull   
@Assert\Range(min=0, max=5)    
...

lorsque l’on soumet des données à l’api (POST, PUT), la validation via les assert sont vérifiés.
en cas d’erreur de validation, les données ne sont pas soumis et l’api renvoi un message d’erreur de validation.


la pagination

https://api-platform.com/docs/core/pagination/

  • La pagination est activée par défaut pour toutes les collections.
  • Chaque collection contient 30 articles par page.
  • L’activation de la pagination et le nombre d’éléments par page peuvent être configurés à partir du :
    • coté serveur :
      • de façon spécifique : sur chaque entité
      • de façon globale : via une configuration yaml (s’applique à tous les entités)
    • coté client (désactivé par défaut) via l’url. ex: GET /books?itemsPerPage=20

http://127.0.0.1:8000/docs/

GET /api/offers

-> TRY IT OUT
page [ 1 ]
Executer

  • de façon spécifique : pour tel entité, x élements par page

    ```php
    ...
    * @ApiResource(attributes={"pagination_enabled"=true, pagination_items_per_page"=3})
    * @ORM\Entity
    */
    class Offer
    {
        ...
    ```
    

    GET http://127.0.0.1:8000/api/offers
    -> par defaut, page = 1
    -> 3 offres seront donc récupérées

    GET http://127.0.0.1:8000/api/offers?page=4
    -> 3 offres seront donc récupérées (à la page 4)

  • de façon globale :

    /packages/api_platform.yaml

        ...
        ...
        collection:
            pagination:
                items_per_page: 2
                client_items_per_page: true
    

    GET http://127.0.0.1:8000/api/offers
    -> par defaut, page = 1
    -> 2 offres seront donc récupérées

    GET http://127.0.0.1:8000/api/offers?page=2
    -> 2 offres seront donc récupérées (à la page 2)

remarque :
Dans les 2 cas, rien n’empeche de récuperer x produits par page en précisant ce que l’on veut dans l’url, par exemple :

GET http://127.0.0.1:8000/api/products?page=1&itemsPerPage=4
-> 4 produits par page sont récupérés (à la page 1)


Les opérations

Activation et désactivation des opérations

https://api-platform.com/docs/core/operations#enabling-and-disabling-operations

  • par defaut, toutes les opérations CRUD
  • La liste des opérations activées peut être configurée ressource par ressource.
  • La création d’opérations personnalisées sur des itinéraires spécifiques est également possible.
  • voici comment est classé les 2 types d’opérations :
    • les opérations de collection :
      1 … GET de la collection
      2 … POST création d’un element
    • les opérations d’élément :
      3 … GET d’un élément
      4 … PUT d’un élément
      5 … DELETE d’un élément
      6 … (PATCH) d’un élément

remarque : PATCH est pris en charge lors de l’ utilisation du format API JSON

1er exemple :

 * @ApiResource(
 *     collectionOperations={"get"},    // 1..
 *     itemOperations={"get"}           // 3..
 * )
 */
class Product
{

pour Product, on peut faire que les opérations suivantes :

  • GET de la collection /api/products // 1…
  • GET d’un élément /api/products/{id} // 3…

2eme exemple :

 * @ApiResource(
 *     collectionOperations={"get"},    // 1...
 *     itemOperations={}                // aucun
 * )
 */
class Product
{

pour Product, on peut faire que l’opération suivante :

  • GET de la collection /api/products // 1…

3eme exemple :

 * @ApiResource(
 *     collectionOperations={"get"},    // 1..
 * )
 */
class Product
{

pour Product, on peut faire que les opérations suivantes :

  • GET de la collection /api/products // 1…
  • GET d’un élément /api/products/{id} // 3…
  • PUT d’un élément /api/products/{id} // 4…
  • DELETE d’un élément /api/products/{id} // 5…

Remarque :
le fait de ne pas mentionner itemOperations, cela intègre toutes les opérations lui concernant soit : 3… 4… 5… (6…)

4eme exemple :

 * @ApiResource(
 *     collectionOperations={"post"},       // 2...
 *     itemOperations={"put"}               // 4...
 * )
 */
class Product
{

pour Product, on peut faire que les opérations suivantes :

  • POST de la collection /api/products // 2…
  • PUT d’un élément /api/products/{id} // 4…

Configuration des opérations

https://api-platform.com/docs/core/operations#configuring-operations

remplacer les URL générées par défaut par des URL personnalisées

 * @ApiResource(
     itemOperations={
 *     "get"={"method"="GET", "path"="/commodity/{id}", "requirements"={"id"="\d+"}, "defaults"={"color"="brown"}, "options"={"my_option"="my_option_value"}, "schemes"={"https"}, "host"="{subdomain}.api-platform.com"},
 *     "put"={"method"="PUT", "path"="/commodity/{id}/update", "hydra_context"={"foo"="bar"}},
 * }
 * )
 */
class Product
{
  • GET /products/{id} est remplacé par : /commodity/{id}
  • PUT /products/{id} est remplacé par : /commodity/{id}/update

remarque :

  • itemOperations est mentionné donc cela concerne uniquement que GET et PUT
  • collectionOperation n’est pas mentionné donc toutes les opérations lui concernant sont intégrées soit : GET et POST

Préfixe

https://api-platform.com/docs/core/operations#prefixing-all-routes-of-all-operations

Parfois, il est également utile de mettre une ressource entière dans son propre “espace de noms” concernant l’URI.
Disons que vous voulez mettre tout ce qui est lié à un Product dans un “entrepot” de sorte que les URIs deviennent : /warehouse/product/{id}

/**
 * @ApiResource(routePrefix="/warehouse")
 */
class Product
{

Sous-ressources

https://api-platform.com/docs/core/operations#subresources

  • Vous pouvez déclarer des sous-ressources (uniquement pour les GET)
  • Une sous-ressource est une collection ou un élément appartenant à une autre ressource.
  • Le point de départ d’une sous-ressource doit être une relation sur une ressource existante.

on a cette relation : un produit peut avoir plusieurs offres
Disons que nous voulons obtenir toutes les offres du produit dont l’ID=1
l’uri serait la suivante : /api/products/1/offers

la sous-ressource est ici : offers

pour cela on va utiliser l’annotation : ApiSubresource
afin d’avoir un nouveau point d’entrée dans l’API : GET /api/products/{id}/offers

...
use ApiPlatform\Core\Annotation\ApiSubresource;
...
/**
 * @ApiResource()
 * @ORM\Entity
 */
class Product // The class name will be used to name exposed resources
{
    ...

    /**
     * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"})
     * @ApiSubresource    
     */
    public $offers;

    ...

Aller vérifier la doc : http://127.0.0.1:8000/docs/
un nouveau point d’entrée apparait dans product : GET /api/products/{id}/offers

  • cela permet de récuperer les offres d’un produit

    GET /api/products/1/offers

    {
        "@context": "/api/contexts/Offer",
        "@id": "/api/products/145/offers",
        "@type": "hydra:Collection",
        "hydra:member": [
            {
                "@id": "/api/offers/249",
                "@type": "Offer",
                "id": 249,
                "description": "produit 1 : description offre 1",
                "price": 11,
                "product": "/api/products/145",
                "date": "2019-01-10T15:47:41+00:00"
            },
            {
                "@id": "/api/offers/250",
                "@type": "Offer",
                "id": 250,
                "description": "produit 1 : description offre 2",
                "price": 12,
                "product": "/api/products/145",
                "date": "2019-01-10T15:47:41+00:00"
            }
        ],
        "hydra:totalItems": 2
    }
    

sous-ressources imbriquées :

les sous-ressources peuvent être imbriquées

exemple :
un Produit peut avoir plusieurs offres
et une offre peut avoir plusieurs commentaires

afin d’obtenir la collection de commentaires pour le produit 42 et l’offre 10 :
GET /products/42/offers/10/comments

pour cela, il faut ajouter :

  • @ApiSubresource sur $offer de Product
  • @ApiSubresource sur $comments de Offer

des groupes personnalisés sur des sous-ressources :

https://api-platform.com/docs/core/operations#subresources


Contrôler le chemin des sous-ressources

https://api-platform.com/docs/core/operations#control-the-path-of-subresources


Contrôle d’accès des sous-ressources

https://api-platform.com/docs/core/operations#access-control-of-subresources

...
 * @ApiResource(
 *     subresourceOperations={
 *          "api_products_get_collection"= {
 *              "method"="GET",
 *              "access_control"="has_role('ROLE_USER')"
 *          }
 *      }
 * )
 */
 classProduct
 {
     ...

php bin/console debug:router
api_products_get_collection -> route

  • l’accès à Product est autorisé qu’avec le role ADMIN
    /**
     * @ApiResource(
     *     attributes={"access_control"="is_granted('ROLE_ADMIN')"},
     * )
     * @ORM\Entity
     */
    class Product {
        ...
    

Contrôler la profondeur des sous-ressources

https://api-platform.com/docs/core/operations#control-the-depth-of-subresources

  • Vous pouvez contrôler la profondeur des sous-ressources avec le paramètre maxDepth.

Par exemple, si Product a également une sous-ressource telle que offers et que vous ne voulez pas que la route api/products/{id}/offers/{id}/comments soit générée.
Vous pouvez le faire en ajoutant le paramètre maxDepth dans l’annotation ApiSubresource

<?php
// api/src/Entity/Question.php

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiSubresource;

/**
 * ...
 * @ApiResource
 */
class Product
{
    /**
     * ...
     * @ApiSubresource(maxDepth=1)
     */
    public $offer;

    // ...
}

Création d’opérations et de contrôleurs personnalisés

https://api-platform.com/docs/core/operations#creating-custom-operations-and-controllers

  • ce n’est pas recommandé mais Il est possible d’utiliser des controlleurs Symfony standard
    (y compris les contrôleurs Symfony standard étendant la Symfony\Bundle\FrameworkBundle\Controller\AbstractController)
  • API Platform recommande d’utiliser des classes d’action au lieu des contrôleurs Symfony classiques.
    En interne, API Platform implémente le modèle ADR ( Action-Domain-Responder ), un raffinement de MVC spécifique au Web .

Remarque :
le système d’événements doit être préféré aux contrôleurs personnalisés, le cas échéant.

  • API-platform facilite également la mise en œuvre du modèle ADR: elle enregistre automatiquement les classes d’actions stockées dans des src/App/Controller services autowired .
  • Grâce à la fonctionnalité d’autowiring du conteneur Symfony Dependency Injection, les services requis par une action peuvent être indexés dans son constructeur, ils seront automatiquement instanciés et injectés, sans avoir à la déclarer explicitement.
  • Si vous créez une opération personnalisée, vous voudrez probablement la documenter correctement. Voir la partie « swagger» de la documentation pour le faire.

opération personnalisée : méthode recommandé

https://api-platform.com/docs/core/operations#recommended-method

<?php
// api/src/Controller/CreateProductPublication.php

namespace App\Controller;

use App\Entity\Product;
use App\Operation\ProductPublishingHandler;

class CreateProductPublication
{
    private $productPublishingHandler;

    public function __construct(ProductPublishingHandler $productPublishingHandler)
    {
        $this->productPublishingHandler = $productPublishingHandler;
    }

    public function __invoke(Product $data): Product
    {
        //
        //  ici, on fait ce que l'on veut avec les données
        //  il est recommandé de faire appel à un handler pour traiter les donnés comme on veut
        $this->productPublishingHandler->handle($data);
        //
        //

        return $data;
    }
}
<?php
// src/Operation/ProductPublishingHandler.php
namespace App\Operation;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Entity\Product;

class ProductPublishingHandler 
{
    public function handle(Product $data)
    {
        // ...
        // ...

        return true;
    }
}

/config/services.yaml

    ...
    ...
    'App\Operation\ProductPublishingHandler':
        # arguments: [ '@doctrine', '@request_stack', '@?logger']  

remarque :

  • $this->productPublishingHandler->handle($data);
    Il faut créer le handler: ProductPublishingHandler.php
    (pour faire ce que l’on veut : enregistrer en base…)

  • Cette opération personnalisée se comporte exactement comme l’opération intégrée: elle renvoie un document JSON-LD correspondant à l’ID transmis dans l’URL.

  • Nous considérons ici que l’autowiring est activé pour les classes de contrôleur (la valeur par défaut lors de l’utilisation de la distribution API Platform).
    Cette action sera automatiquement enregistrée en tant que service (le nom du service est identique au nom de la classe:) App\Controller\CreateProductPublication.

  • API Platform récupère automatiquement l’entité PHP appropriée à l’aide du fournisseur de données, puis désérialise les données utilisateur qu’il contient POST et PUT met à jour l’entité avec les données fournies par l’utilisateur.

Attention:
le paramètre de __invoke() DOIT être appelé $data , sinon il ne sera pas renseigné correctement !

<?php
// src/Entity/Product.php

use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\CreateProductPublication;

/**
 * @ApiResource(
 *  itemOperations={
 *     "get",
 *     "post_publication"={
 *         "method"="POST",
 *         "path"="/products/{id}/publication",
 *         "controller"=CreateProductPublication::class,
 *     }
 *  }
 * )
 */
class Product
{
    //...
  • itemOperations -> donc il s’agit d’un élément
  • cela concerne plus précisement l’action POST
  • Il est obligatoire de définir les attributs method, path et controller.
    (Ils permettent à la plate-forme API de configurer le chemin de routage et le contrôleur associé, respectivement)

conclusion:
POST /products/{id}/publication



GROUPES DE SÉRIALISATION

https://api-platform.com/docs/core/operations#serialization-groups

à l’opération personnalisée du point précédent, on ajoute la possibilité de selectionner les champs qui deront être concernés par l’opération.

avec l’exemple précedent, on rajoute :

  • “normalization_context”={“groups”={“publication”}},
  • @Groups(“publication”)
/**
 * @ApiResource(
 *  itemOperations={
 *     "get",
 *     "post_publication"={
 *         "method"="POST",
 *         "path"="/products/{id}/publication",
 *         "controller"=CreateProductPublication::class,
 *         "normalization_context"={"groups"={"publication"}},
 *     }
 *  }
 * )
 * @ORM\Entity
 */
class Product {

    ...
    ...

    /**
     * @var string $name A name property - this description will be available in the API documentation too.
     *
     * @ORM\Column
     * @Assert\NotBlank
     * @Groups("publication")
     */
    public $name;

Ainsi, avec : POST /products/{id}/publication
seul le champ “name” sera pris en compte


RÉCUPÉRATION D’ENTITÉ

https://api-platform.com/docs/core/operations#entity-retrieval

Si vous souhaitez ignorer l’extraction automatique de l’entité dans votre opération personnalisée, vous pouvez définir le paramètre _api_receive sur false

De cette façon : Read, Deserialize et Validate ne seront pas executé

/**
 * @ApiResource(itemOperations={
 *     "get",
 *     "post_publication"={
 *         "method"="POST",
 *         "path"="/books/{id}/publication",
 *         "controller"=CreateBookPublication::class,
 *         "defaults"={"_api_receive"=false},
 *     }
 * })
 */
class Book
{
    //...
Attribute Type Default Description
_api_receive bool true Enables or disables the ReadListener, DeserializeListener and ValidateListener
_api_respond bool true Enables or disables SerializeListener, RespondListener
_api_persist bool true Enables or disables WriteListener

opération personnalisée : Méthode alternative

https://api-platform.com/docs/core/operations#alternative-method

  • Il existe un autre moyen de créer une opération personnalisée. Cependant, nous n’encourageons pas son utilisation.
  • En effet, celui-ci disperse la configuration à la fois dans le routage et la configuration des ressources.



Remplacement de l’ordre par défaut

https://api-platform.com/docs/core/default-order

Par défaut, les éléments de la collection sont classés par ordre croissant (ASC) sur ID

  • sur la propriété price avec la direction DESC

    ...
    /**
     * @ApiResource(attributes={"order"={"price": "DESC"}})
     */
    class Offer
    {
        ....
    
  • sur les propriétés price et description (ASC par defaut)

    ...
    /**
     * @ApiResource(attributes={"order"={"price", "description"}})
     */
    class Offer
    {
        ....
    
  • sur une propriété d’association (ASC par defaut)

    /**
     * @ApiResource(attributes={"order"={"author.name"}})
     */
    class Product
    {
        // ...
    
        /**
         * @var User
        */
        public $author;
        ...
    

    en précisant la direction:

    attributes={"order"={"author.name": "DESC"}},
    


Les filtres



https://api-platform.com/docs/core/filters

  • Par défaut, tous les filtres sont désactivés.

  • API Platform Core fournit un système générique pour appliquer des filtres sur les collections

  • Lorsqu’un filtre est activé, il est automatiquement documenté en tant que propriété hydra:search dans la réponse à la collection

  • les filtres ne peuvent être appliqués à une propriété que si:

    • la propriété existe :
      la valeur est supportée (ex: asc ou desc pour les filtres d’ordre).

    • Cela signifie que le filtre sera ignoré silencieusement si la propriété:

      • n’existe pas :
        • n’est pas activé
        • a une valeur invalide

Filtres Doctrine ORM

  • Doctrine ORM doit être activée.

Notions de base

https://api-platform.com/docs/core/filters#basic-knowledge

  • Les filtres sont des services (voir la section sur les filtres personnalisés ), et ils peuvent être liés à une ressource de deux manières:
    • à travers l’ApiResource via l’attribut filters

      # api/config/services.yaml
      services:
          # ...
          offer.price_filter:
              parent: 'api_platform.doctrine.orm.order_filter'
              arguments: [ { price: ~ } ]
              tags:  [ 'api_platform.filter' ]
              # The following are mandatory only if a _defaults section is defined
              # You may want to isolate filters in a dedicated file to avoid adding them
              autowire: false
              autoconfigure: false
              public: false
      
          /**
          * @ApiResource(
          *  attributes={"filters"={"offer.price_filter"}},
          * )
          * @ORM\Entity
          */
          class Offer
          {
              ...
      

      utilisation:
      GET http://127.0.0.1:8000/api/offers?order[price]=DESC

  • En utilisant l’annotation : @ApiFilter

    <?php
    // api/src/Entity/Offer.php
    
    namespace App\Entity;
    
    use ApiPlatform\Core\Annotation\ApiResource;
    use ApiPlatform\Core\Annotation\ApiFilter;        
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
    
    /**
     * @ApiResource
     * @ApiFilter(OrderFilter::class, properties={"price"})
     */
    class Offer
    {
        ...
    

    utilisation:
    http://127.0.0.1:8000/api/offers?order[price]=DESC


Filtre de recherche

https://api-platform.com/docs/core/filters#search-filter

exact pour rechercher des champs avec le texte exacte
partial utilise LIKE %text% pour rechercher des champs qui contiennent le texte
start utilise LIKE text% pour rechercher des champs qui commence par le texte
end utilise LIKE %text pour rechercher des champs qui se termine par le texte
word_start utilise LIKE text% OR LIKE % text% pour rechercher les champs contenant le mot commençant par text

insensibilité à la casse :

  • Ajoutez la lettre i au filtre si vous souhaitez qu’elle ne soit pas sensible à la casse :
    iexact
    ipartial
    istart
    iend
    iword_start

  • remarque :

    • avec utilisation i : aura un impact sur les performances s’il n’y a pas d’index approprié
    • L’insensibilité à la casse peut déjà être appliquée au niveau de la base de données, en fonction du classement utilisé.
      Si vous utilisez MySQL, notez que le utf8_unicode_ciclassement couramment utilisé (et son frère utf8mb4_unicode_ci) est déjà insensible à la casse, comme indiqué par la _cipartie dans leurs noms.
    • les filtres de recherche avec la stratégie exact peuvent avoir plusieurs valeurs pour une même propriété (dans ce cas, la condition sera similaire à une clause SQL IN).
      Syntaxe: ?property[]=foo&property[]=bar
<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;

/**
 * @ApiResource()
 * @ApiFilter(SearchFilter::class, properties={"id": "exact", "price": "exact", "description": "partial", "product": "exact"})
 */
class Offer
{
    // ...

utilisations:


Filtre de date

https://api-platform.com/docs/core/filters#date-filter

Syntaxe: ?property[<after|before|strictly_after|strictly_before>]=value

after, before filtreront en incluant la valeur
strictly_after, strictly_before filtreront en excluant la valeur
<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;

/**
 * @ApiResource
 * @ApiFilter(DateFilter::class, properties={"createdAt"})
 */
class Product
{
    // ...

utilisation :
http://localhost:8000/api/products?createdAt[after]=2018-03-19


GÉRER LES VALEURS NULL

https://api-platform.com/docs/core/filters#managing-null-values

Utiliser le comportement par défaut du SGBD null
Exclure des articles ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter::EXCLUDE_NULL ( exclude_null )
Considérer les articles comme les plus anciens ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE ( include_null_before )
Considérer les articles comme les plus jeunes ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_AFTER ( include_null_after )
Toujours inclure des articles ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER ( include_null_before_and_after )
  • exclure les élements ayant la propriété createdAt = NULL
<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;

/**
 * @ApiResource
 * @ApiFilter(DateFilter::class, properties={"createdAt": DateFilter::EXCLUDE_NULL})
 */
class Product
{
    // ...    

Filtre booléen

https://api-platform.com/docs/core/filters#boolean-filter

Le filtre booléen vous permet de rechercher des champs et des valeurs booléennes.

Syntaxe: ?property=<true|false|1|0>

  • filtre sur la propriété de type booléen : isActive

    <?php
    // api/src/Entity/Product.php
    
    namespace App\Entity;
    
    use ApiPlatform\Core\Annotation\ApiFilter;
    use ApiPlatform\Core\Annotation\ApiResource;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
    
    /**
     * @ApiResource
     * @ApiFilter(BooleanFilter::class, properties={"isActive"})
     */
    class Product
    {
        // ...        
    

    http://127.0.0.1:8000/api/products?isActive=true

    Il retournera tous les produits ayant isActive = true


Filtre numérique

https://api-platform.com/docs/core/filters#numeric-filter

Le filtre numérique vous permet de rechercher des champs et des valeurs numériques.

Syntaxe: ?property=<int|bigint|decimal…>

<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\NumericFilter;

/**
 * @ApiResource
 * @ApiFilter(NumericFilter::class, properties={"price"})
 */
class Offer
{
    // ...    

http://127.0.0.1:8000/api/offers?price=11

Il retournera toutes les offres ayant price = 11


Filtre de gamme

https://api-platform.com/docs/core/filters#range-filter

Le filtre de plage vous permet de filtrer par une valeur inférieure à, supérieure à, inférieure ou égale, supérieure ou égale et entre deux valeurs.

Syntaxe: ?property[<lt|gt|lte|gte|between>]=value

<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter;

/**
 * @ApiResource
 * @ApiFilter(RangeFilter::class, properties={"price"})
 */
class Offer
{
    // ...    

http://127.0.0.1:8000/api/offers?price[gt]=10&price[lt]=16

Il retournera toutes les offres dont le price est entre 10 et 16


Filtre existant

https://api-platform.com/docs/core/filters#exists-filter

Le filtre existe vous permet de sélectionner des éléments en fonction de la valeur de champ nullable.

Syntaxe: ?property[exists]=<true|false|1|0>

<?php
// api/src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter;

/**
 * @ApiResource
 * @ApiFilter(ExistsFilter::class, properties={"transportFees"})
 */
class Product
{
    // ...    
http://localhost:8000/api/products?transportFees[exists]=false

-> Il retournera tous les produits dont la propriété transportFees est null

?transportFees[exists]=false qui n’existe pas donc qui est à null
?transportFees[exists]=true qui existe donc n’importe quelle valeur

Filtre de commande (tri)

https://api-platform.com/docs/core/filters#order-filter-sorting

Le filtre d’ordre permet de trier une collection en fonction des propriétés données.

Syntaxe: ?order[property]=<asc|desc>

<?php
// api/src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;

/**
 * @ApiResource
 * @ApiFilter(OrderFilter::class, properties={"id": "ASC", "name": "DESC"})
 */
class Product
{
    // ...    

ici, par defaut id est en ASC et name en DESC

http://127.0.0.1:8000/api/products?order[name]=ASC

Il retournera tous les produits par ordre croissant sur le name


COMPARER AVEC LES VALEURS NULLES

https://api-platform.com/docs/core/filters#comparing-with-null-values

spécifier le mode de traitement du NULL des valeurs dans la comparaison:

Utiliser le comportement par défaut du SGBD null
Considérer les articles comme les plus petits ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter::NULLS_SMALLEST ( nulls_smallest )
Considérez les articles comme les plus gros ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter::NULLS_LARGEST ( nulls_largest )
  • ce point est lié avec le précedent : Filtre de commande (tri)
  • Si il existe des valeurs NULL parmis d’autres qui ne le sont pas. Pour trier suivant un ordre ASC ou DESC, on peut indiquer que la valeur NULL doit être considéré comme la plus petite valeur ou alors comme la valeur la plus grande ainsi lors du resultat du tri, les enregistrements avec des valeurs NULL se retrouverons en début ou en fin de liste.
<?php
// api/src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;

/**
 * @ApiResource
 * @ApiFilter(OrderFilter::class, properties={"transportFees": { "nulls_comparison": OrderFilter::NULLS_SMALLEST, "default_direction": "DESC" }})
 */
class Product
{
    // ...    

Il retournera tous les produits par ordre decroissant sur la propriété : transportFees. Les valeurs de transportFees à NULL seront en premier dans la liste ordonné.

attention:
semble ne pas fonctionner… à voir


UTILISATION D’UN NOM DE PARAMÈTRE DE REQUÊTE D’ORDRE PERSONNALISÉ

https://api-platform.com/docs/core/filters#using-a-custom-order-query-parameter-name

Un conflit se produira si order est également le nom d’une propriété avec le filtre de recherche activé. Heureusement, le nom du paramètre de requête à utiliser est configurable:

# api/config/packages/api_platform.yaml
api_platform:
    collection:
        order_parameter_name: '_order' # the URL query parameter to use is now "_order"

Filtrage sur les propriétés imbriquées

https://api-platform.com/docs/core/filters#filtering-on-nested-properties

Parfois, vous devez pouvoir effectuer un filtrage en fonction de certaines ressources liées (de l’autre côté d’une relation).

<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;

/**
 * @ApiResource
 * @ApiFilter(DateFilter::class, properties={"product.createdAt"})
 * @ApiFilter(SearchFilter::class, properties={"product.name": "partial"})
 */
class Offer
{
    // ...    
http://127.0.0.1:8000/api/offers?product.createdAt[after]=2018-01-01&product.name=produit 1
  • cela va rechercher des offres dont le produit :
    • à une date de creation : createdAt > 2018-01-01
    • et par le nom du produit (name) de façon partiel (mot contenu dans le name)
  • on obtient donc une liste de produit respectant ces 2 critères.
  • rappel:

    exact pour rechercher des champs avec le texte exacte
    partial utilise LIKE %text% pour rechercher des champs qui contiennent le texte
    start utilise LIKE text% pour rechercher des champs qui commence par le texte
    end utilise LIKE %text pour rechercher des champs qui se termine par le texte
    word_start utilise LIKE text% OR LIKE % text% pour rechercher les champs contenant le mot commençant par text

    insensibilité à la casse :

    • Ajoutez la lettre i au filtre si vous souhaitez qu’elle ne soit pas sensible à la casse :
      iexact, ipartial, istart, iend, iword_start

Activation d’un filtre pour toutes les propriétés d’une ressource

https://api-platform.com/docs/core/filters#enabling-a-filter-for-all-properties-of-a-resource

Si vous ne vous souciez pas de la sécurité et des performances (par exemple, une API avec un accès restreint), il est également possible d’activer les filtres intégrés pour toutes les propriétés:

<?php
// api/src/Entity/Offer.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;

/**
 * @ApiResource
 * @ApiFilter(OrderFilter::class)
 */
class Offer
{
    // ...    

Toutes les propriétés seront ordonnés par defaut avec la direction ASC



Filtres sérialiseurs


Filtre de groupe

https://api-platform.com/docs/core/filters#group-filter

Le filtre de groupe vous permet de filtrer par groupes de sérialisation.

Syntaxe: ?groups[]=<group>
parameterName est le nom du paramètre de requête (par défaut groups)
overrideDefaultGroups permet de remplacer les groupes de sérialisation par défaut (par défaut false)
whitelist liste blanche des groupes pour éviter une exposition incontrôlée des données (par defaut = NULL)
  • whitelist

    ne rien mettre équivaut à “whitelist”: NULL
    “whitelist”: NULL prise en compte de la demande
    “whitelist”: {“allowed_group”} je ne comprends pâs !
    “whitelist”: {“read”} que les champs ayant le groupe “read”
    “whitelist”: {“read”, “other”} que les champs ayant les grouges : “read” ou “other”
<?php
// api/src/Entity/Product.php
...
...
/**
 * @ApiResource(
 * )
 * @ApiFilter(
 *      GroupFilter::class, 
 *      arguments={
 *          "parameterName": "groups", 
 *          "overrideDefaultGroups": false, 
 *          "whitelist": NULL})
 * @ORM\Entity
 */
class Product {

    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @var string $name A name property - this description will be available in the API documentation too.
     *
     * @ORM\Column
     * @Assert\NotBlank
     * @Groups("write")
     */
    public $name;

    /**
     * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
     * @Assert\NotBlank
     * @Groups("read")
     */
    public $author;   

    /**
     * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"})
     * @ApiSubresource  
     * @Groups("other")    
     */
    public $offers;

     /**
     * @ORM\Column(type="boolean", options={"default" : false}, nullable = true)
     */
    private $isActive;
http://127.0.0.1:8000/api/products?groups[]=read&groups[]=write
  • Les produits seront récupérés avec uniquement les champs : name et author
  • donc les champs qui ont les groupes: “read” ou “write”

Filtre de propriété

https://api-platform.com/docs/core/filters#property-filter

Le filtre de propriétés ajoute la possibilité de sélectionner les propriétés à sérialiser (ensembles de champs épars).

Syntaxe: ?properties[]=<property>&properties[<relation>][]=<property>
parameterName est le nom du paramètre de requête (par défaut properties)
overrideDefaultProperties permet de remplacer les propriétés de sérialisation par défaut (par défaut false)
whitelist Liste blanche des propriétés pour éviter une exposition incontrôlée des données (par défaut null pour autoriser toutes les propriétés)
<?php

// api/src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;

/**
 * @ApiResource
 * @ApiFilter(PropertyFilter::class, arguments={"parameterName": "properties", "overrideDefaultProperties": false, "whitelist": {false}})
 */
class Product
{
  • exemple 1 :
    Avoir dans la liste récuperée que la propriété : name

http://localhost:8000/api/products?properties[]=name

  • exemple 2 :
    Avoir dans la liste récuperée les propriétés : name et createdAt

http://localhost:8000/api/products?properties[]=name,createdAt
http://127.0.0.1:8000/api/products?properties=name&properties=createdAt

  • exemple 3 :
    Avoir dans la liste récuperée que la propriété relationnelle : offer.description

http://localhost:8000/api/products?properties[offer][]=description



Création de filtres personnalisés

implementer : ApiPlatform\Core\Api\FilterInterface

Création de filtres ORM de Doctrine personnalisée

https://api-platform.com/docs/core/filters#creating-custom-doctrine-orm-filters

  • Les filtres de doctrine ont accès à la requête HTTP ( Requestobjet Symfony ) et à l’ QueryBuilder instance utilisée pour extraire des données de la base de données. Ils ne sont appliqués qu’aux collections.

  • Si vous souhaitez traiter la requête DQL générée pour récupérer des éléments ou si vous n’avez pas besoin d’accéder à la requête HTTP, les extensions sont la solution.

  • ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter
    une classe abstraite pratique implémentant cette interface et fournissant des méthodes utilitaires

exemple :

Dans l’exemple suivant, nous créons une classe pour filtrer une collection en appliquant une expression rationnelle à une propriété.
La REGEXP fonction DQL utilisée dans cet exemple se trouve dans la DoctrineExtensions bibliothèque.
Cette bibliothèque doit être correctement installée et enregistrée pour utiliser cet exemple (fonctionne uniquement avec MySQL).

<?php
// api/src/Filter/RegexpFilter.php

namespace App\Filter;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

final class RegexpFilter extends AbstractContextAwareFilter
{
    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        // otherwise filter is applied to order and page as well
        if (
            !$this->isPropertyEnabled($property, $resourceClass) ||
            !$this->isPropertyMapped($property, $resourceClass)
        ) {
            return;
        }

        $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
        $queryBuilder
            ->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName))
            ->setParameter($parameterName, $value);
    }

    // This function is only used to hook in documentation generators (supported by Swagger and Hydra)
    public function getDescription(string $resourceClass): array
    {
        if (!$this->properties) {
            return [];
        }

        $description = [];
        foreach ($this->properties as $property => $strategy) {
            $description["regexp_$property"] = [
                'property' => $property,
                'type' => 'string',
                'required' => false,
                'swagger' => [
                    'description' => 'Filter using a regex. This will appear in the Swagger documentation!',
                    'name' => 'Custom name to use in the Swagger documentation',
                    'type' => 'Will appear below the name in the Swagger documentation',
                ],
            ];
        }

        return $description;
    }
}
filterProperty dans cette méthode on indique le code métier du filtre
getDescription dans cette méthode on indique la doc
...
protected function filterProperty ...
...
...
->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName))
...
  • config pour appliquer le filtre personnalisé à n’importe quel propriété:
# api/config/services.yaml
services:
    # ...
    'App\Filter\RegexpFilter':
        # Uncomment only if autoconfiguration isn't enabled
        #tags: [ 'api_platform.filter' ]
  • config pour appliquer le filtre personnalisé pour certaines propriétés:
    (exemple : pour les propriétés : name et isActive)
# api/config/services.yaml
services:
    'App\Filter\RegexpFilter':
        arguments: [ '@doctrine', '@request_stack', '@?logger', { name: ~, isActive: ~ } ]
        # Uncomment only if autoconfiguration isn't enabled
        #tags: [ 'api_platform.filter' ]

plus qu’à appliquer le filtre :

<?php
// api/src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Filter\RegexpFilter;

/**
 * @ApiResource
 * @ApiFilter(RegexpFilter::class)
 */
class Product
{

Utiliser des filtres de doctrine

https://api-platform.com/docs/core/filters#using-doctrine-filters

Doctrine comporte un système de filtrage qui permet au développeur d’ajouter du code SQL aux clauses conditionnelles des requêtes, quel que soit l’endroit où le code SQL est généré (par exemple à partir d’une requête DQL ou en chargeant des entités associées).
Celles-ci sont appliquées aux collections et aux éléments, elles sont donc extrêmement utiles.

en savoir plus : http://blog.michaelperrin.fr/2014/12/05/doctrine-filters/

  • exemple :
    Supposons que nous ayons une entité User et une entité Product liée à celle-ci.
    Un utilisateur ne devrait voir que ses produits et pas ceux des autres.

  • le but est d’ajouter une annotation à l’entité

    • créer une annotation : @UserAware
      cette annotation contiendra le nom du champs de l’user
    • on va créer une classe de filtre doctrine : UserFilter :
      • va recuperer l’user en parametre :
        $userId = $this->getParameter(‘id’);
      • va ajouter la comparaison à la requete doctrine :
        return sprintf(’%s.%s = %s’, $targetTableAlias, $fieldName, $userId);
        pour avoir un truc du genre : table_order.fieldname(celui annotation) = $userId
      • declarer le filtre dans : api_platform.yaml
        dans la section doctrine
    • configurer une classe de configuration du filtre
      • créer une classe de configuration pour :
        • aller chercher le user
      • ajouter un écouteur dans : services.yaml
        afin que chaque demande initialise le filtre Doctrine UserFilter avec l’utilisateur actuel
  • Ainsi Doctrine filtrera automatiquement toutes les entités ayant l’annotation : UserAware

  • pratique:
    Un utilisateur ne devrait voir que ses produits et pas ceux des autres.
    L’idée est que toute requête sur la table des commandes doit ajouter une WHERE user_id = :user_idcondition.

    <?php
    // api/Annotation/UserAware.php
    
    namespace App\Annotation;
    
    use Doctrine\Common\Annotations\Annotation;
    
    /**
     * @Annotation
     * @Target("CLASS")
     */
    final class UserAware
    {
        public $userFieldName;
    }
    
    <?php
    // api/src/Filter/UserFilter.php
    
    namespace App\Filter;
    
    use App\Annotation\UserAware;
    use Doctrine\ORM\Mapping\ClassMetaData;
    use Doctrine\ORM\Query\Filter\SQLFilter;
    use Doctrine\Common\Annotations\Reader;
    
    final class UserFilter extends SQLFilter
    {
        private $reader;
    
        public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
        {
        
            if (null === $this->reader) {
                throw new \RuntimeException(sprintf('An annotation reader must be provided. Be sure to call "%s::setAnnotationReader()".', __CLASS__));
            }
    
            // The Doctrine filter is called for any query on any entity
            // Check if the current entity is "user aware" (marked with an annotation)
            $userAware = $this->reader->getClassAnnotation($targetEntity->getReflectionClass(), UserAware::class);
            if (!$userAware) {
                return '';
            }
    
            $fieldName = $userAware->userFieldName;
            try {
                // Don't worry, getParameter automatically escapes parameters
                $userId = $this->getParameter('id');
            } catch (\InvalidArgumentException $e) {
                // No user id has been defined
                return '';
            }
    
            if (empty($fieldName) || empty($userId)) {
                return '';
            }
    
            return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
        }
    
        public function setAnnotationReader(Reader $reader): void
        {
            $this->reader = $reader;
        }
    }
    
  • /config/packages/api_platform.yaml

    ...
    ...
    
    doctrine:
        orm:
            filters:
                user_filter:
                    class: App\Filter\UserFilter
    
  • ajoutez un écouteur pour chaque demande qui initialise le filtre Doctrine avec l’utilisateur actuel :

    <?php
    // api/EventListener/UserFilterConfigurator.php
    
    namespace App\EventListener;
    
    use Symfony\Component\Security\Core\User\UserInterface;
    use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
    use Doctrine\Orm\EntityManagerInterface;
    use Doctrine\Common\Annotations\Reader;
    
    final class UserFilterConfigurator
    {
        private $em;
        private $tokenStorage;
        private $reader;
    
        public function __construct(EntityManagerInterface $em, TokenStorageInterface $tokenStorage, Reader $reader)
        {
            $this->em = $em;
            $this->tokenStorage = $tokenStorage;
            $this->reader = $reader;
        }
    
        public function onKernelRequest(): void
        {
            if (!$user = $this->getUser()) {
                throw new \RuntimeException('There is no authenticated user.');
            }
    
            $filter = $this->em->getFilters()->enable('user_filter');
            $filter->setParameter('id', $user->getId());
            $filter->setAnnotationReader($this->reader);
        }
    
        private function getUser(): ?UserInterface
        {
            if (!$token = $this->tokenStorage->getToken()) {
                return null;
            }
    
            $user = $token->getUser();
            return $user instanceof UserInterface ? $user : null;
        }
    }
    
  • /config/services.yaml

        ...
        ...
    
        'App\EventListener\UserFilterConfigurator':
            arguments: [ '@doctrine.orm.default_entity_manager', '@security.token_storage', '@annotations.cached_reader']       
            tags:
                - { name: kernel.event_listener, event: kernel.request, priority: 5 }
            # Autoconfiguration must be disabled to set a custom priority
            autoconfigure: false   
    

    Il est essentiel de définir la priorité supérieure à la ApiPlatform\Core\EventListener\ReadListener« priorité s, sinon la Paginator ignorera le filtre Doctrine et retour incorrect total Items et des données (premier / dernier / suivant)

  • ajouter l’annotation “UserAware” aux entités:

    <?php
    // api/src/Entity/Product.php
    
    namespace App\Entity;
    
    ...
    ...
    
    use App\Annotation\UserAware;
    
    /**
     * @ApiResource(
     * )
     * @ORM\Entity
     * @UserAware(userFieldName="user_id")
     */
    class Product {
    
        ...
        ...
    
        /**
         * @ORM\ManyToOne(targetEntity="User")
         * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
         * @Assert\NotBlank
         */
        public $user;      
    
        // ...        
    

    remarque: -> user_id
    * @UserAware(userFieldName=“user_id”)

    * @ORM\JoinColumn(name=“user_id”, referencedColumnName=“id”)

  • pour que cela fonctionne, par rapport à la doc, j’ai du faire ces modifs :

    • sur la classe UserFilter
      • remplacer :
        public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string
        
      • par :
        public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string    
        
    • services.yaml
      • j’ai du ajouter les arguments :
        ...
        arguments: [ '@doctrine.orm.default_entity_manager', '@security.token_storage', '@annotations.cached_reader']
        
        liste des services disponible pour l’autowire :
        php bin/console debug:autowiring
  • utlisation :

    GET http://localhost:8000/api/products

    -> on récupère que les produits qui appartiennent à l’utilisateur


Remplacement de l’extraction des propriétés de la demande

https://api-platform.com/docs/core/filters#overriding-extraction-of-properties-from-the-request


Annotation ApiFilter

https://api-platform.com/docs/core/filters#apifilter-annotation

Annotation ApiFilter peut être utilisée sur propriété ou sur une classe

  • sur une propriété

    /**
     * @ORM\Column(type="string")
     * @ApiFilter(SearchFilter::class, strategy="partial")
     */
    private $name;
    
  • sur une classe

    ...
    ...
    use ApiPlatform\Core\Annotation\ApiFilter;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
    use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
    use ApiPlatform\Core\Serializer\Filter\GroupFilter;
    
    /**
     * @ApiResource
     * @ORM\Entity
     * @ApiFilter(BooleanFilter::class)
     * @ApiFilter(DateFilter::class, strategy=DateFilter::EXCLUDE_NULL)
     * @ApiFilter(SearchFilter::class, properties={"colors.prop": "ipartial", "name": "partial"})
     * @ApiFilter(PropertyFilter::class, arguments={"parameterName": "foobar"})
     * @ApiFilter(GroupFilter::class, arguments={"parameterName": "foobargroups"})
     */
    class Product
    {
        // ...
    }
    
    • @ApiFilter(BooleanFilter::class)
      sera appliqué sur chaque booléen de la classe
      GET http://localhost:8000/api/products?isActive=true&…=false&…=true
    • @ApiFilter(DateFilter::class, strategy=DateFilter::EXCLUDE_NULL)
      sera appliqué sur chaque date de la classe
    • @ApiFilter(SearchFilter::class, properties={“colors.prop”: “ipartial”, “name”: “partial”})
      GET http://localhost:8000/api/products?name=1
      remarque: on a précisé une propriété, on aurait pu mettre ce filtre directement au niveau de la propriété
    • @ApiFilter(PropertyFilter::class, arguments={“parameterName”: “name”})
      GET http://127.0.0.1:8000/api/products?name=1&name=2
    • @ApiFilter(GroupFilter::class, arguments={“parameterName”: “foobargroups”})
      -> je ne comprends pas !


Le processus de sérialisation



https://api-platform.com/docs/core/serialization


Utilisation de groupes de sérialisation

https://api-platform.com/docs/core/serialization#using-serialization-groups

  • il y a 2 contexts : la normalisation et la denormalisation

    • normalisation : processus qui passe de l’objet en un format (json, xml…)
    • denormalisation : processus qui passe d’un format (json, xml…) en objet
  • Avec un système de groupe, on peut indiquer quels champs sont concernés par la normalisation et quels champs sont concernés par la denormalisation

exemple :

  • lors de la recuperation des données : processus normalisation
    • on veut avoir que le champ “name”
  • lors de la modification (put, post) : processus de denormalisation
    • pouvoir modifier/créer les champs, n’avoir que les champs : name et author
<?php
// api/src/Entity/Book.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"read"}},
 *     denormalizationContext={"groups"={"write"}}
 * )
 * @ORM\Entity
 */
class Book
{
    // ...

    /**
     * @var string $name 
     *
     * @ORM\Column
     * @Assert\NotBlank
     * @Groups({"read", "write"})
     */
    public $name;

    /**
     * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
     * @Groups({"write"})
     */
    public $author;   

    // ...
}

remarque :
les nom “read” et “write” n’est pas important. il ne correspond pas à des mots clés.
On aurait pu mettre “xxx” et “yyy”

resultat :

  • ainsi avec : GET http://127.0.0.1:8000/api/products

    [
    {
        "name": "produit 1"
    },
    {
        "name": "produit 2"
    },
    ...
    
  • et avec :

    PUT "http://127.0.0.1:8000/api/products/84" -H "accept: application/ld+json" -H "Content-Type: application/ld+json" -d "{ \"name\": \"produit x\", \"author\": \"/api/authors/72\"}"
    

    -> on peut modifier que les champs : name et author

  • les modeles product dans la doc api
    http://127.0.0.1:8000/api/

    allez en bas, dans models
    vous verrez non plus un modele : product
    mais 2 modeles : product-read et product-write (avec les noms des groupes(“read” et “write”))

    • dans le model : product-read
      il y a que le champs “name” de présent
    • dans le model product-write
      il y a que les champs : name et author

Utilisation de groupes de sérialisation par opération

https://api-platform.com/docs/core/serialization#using-serialization-groups-per-operation

  • Dans le point précédent : Utilisation de groupes de sérialisation
    Nous avons vu comment configurer les champs de façon globale en fonction du contexte (normalisation et denormalisation).
  • ici, dans ce point. Nous allons voir comment configurer plus finement et ce, toujours en fonction du contexte (normalisation et d’une denormalisation) mais en plus précis, avec en plus, sur quelles opérations (GET, PUT, POST…)
  • si on configure en même temps, de façon globale (par contexte) et plus précise(par operation) alors “par opération” aura la priorité.
  • rappel :
    • itemOperations : GET (un élément /api/…/{id}), PUT, DELETE et (PATCH)
    • collectionOperation : GET (collection) et POST

exemple 1 :

  • On veut faire un GET sur un element (avec prise en compte de tous les champs)
  • exclure : DELETE, PATCH
*         "get",
  • et on veut faire un PUT, avec que le champ : name
*         "put"={
*             "normalization_context"={"groups"={"put"}}
*         }

code complet :

...

/**
 * @ApiResource(
 *     itemOperations={
 *         "get",
 *         "put"={
 *             "normalization_context"={"groups"={"put"}}
 *         }
 *     }
 * )
 * @ORM\Entity
 */
class Product // The class name will be used to name exposed resources
{
    ...

    /**
     * @var string $name A name 
    *
    * @ORM\Column
    * @Assert\NotBlank
    * @Groups({"get", "put"})
    */
    public $name;

    /**
     * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
     * @Groups({"get"})
     */
    public $author;   

    ...

attention:

  • le “get” se trouvant dans le groups : * @Groups({“get”})
    n’a aucun rapport avec le “get”, de itemOperations
  • pour cette exemple, ce “get” du groupe ne sert pas.

remarque:

  • Vu qu’on redefini itemOperations et que l’on mentionne dans celui ci que GET et PUT, les 2 autres : DELETE et (PATCH) ne sont pas pris en compte.
  • collectionOperations n’est pas mentionné dans ApiResource donc ses opérations(GET d’une collection et POST) sont prises en compte

Donc au final, on peut faire :

  • GET d’un element (sur tous les champs)
  • PUT d’un element (que sur le champ name)
  • GET d’une collection (sur tous les champs)
  • POST d’un element (sur tous les champs)

Relations d’intégration

https://api-platform.com/docs/core/serialization#embedding-relations

  • les relations entre les objets sont représentées à l’aide d’ identificateurs IRI non différenciables.
    “/api/offers/139”
  • Ils vous permettent de récupérer les détails des objets associés en émettant des requêtes HTTP supplémentaires.
  • pour des raisons de performances, il est parfois préférable d’éviter de forcer le client à émettre des requêtes HTTP supplémentaires et de récuperer ces données directement dans la réponse (au lieu de l’IRI)
  • pour cela, il suffit de jouer avec les groupes

exemple :
recuperer un produit et l’Auteur associé (avec le champ name)

/**
 * @ApiResource(
 *      normalizationContext={"groups"={"product"}}
 * )
 * @ORM\Entity
 */
class Product 
{
    ...
    ...

    /**
     * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
     * @Groups({"product"})
     */
    public $author; 
    
    ...

on met le groupe product sur la relation : Auteur

...
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 * )
 * @ORM\Entity
 */
class Author
{
    ...
    ...

    /**
     * @var string $name A name property 
     *
     * @ORM\Column
     * @Assert\NotBlank
     * @Groups({"product"})
     */
    public $name;

    ...

on met le groupe sur le champ : name

résultat:

http://127.0.0.1:8000/api/products

    {
      "@id": "/api/products/86",
      "@type": "Product",
      "author": {
        "@id": "/api/authors/77",
        "@type": "Author",
        "name": "auteur 7"
      }
    },
    ...
    ...

on a donc dans “author” :
“name”: “auteur 7”
au lieu de “/api/authors/72”


Dénormalisation

https://api-platform.com/docs/core/serialization#denormalization

à savoir:

  • la normalisation: quand on récupère des données : GET
  • la dénormalisation: quand on envoit des données au serveur : POST, PUT

exemple :
Pour PUT et POST, pouvoir modifier/créer uniquement sur les champs : name et isActive

...
...
/**
 * @ApiResource(
 *      denormalizationContext={"groups"={"product"}}
 * )
 * @ORM\Entity
 */
class Product {

    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @var string $name A name property - this description will be available in the API documentation too.
     *
     * @ORM\Column
     * @Assert\NotBlank
     * @Groups({"product"})
     */
    public $name;

    /**
     * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
     */
    public $author;   

    /**
     * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"})
     * @ApiSubresource    
     */
    public $offers;

     /**
     * @ORM\Column(type="boolean", options={"default" : false})
     * @Groups({"product"})
     */
    private $isActive;

    ...

on met un groupe “product” sur les champs : name et isActive

utilisation:
PUT http://127.0.0.1:8000/api/products/1

BODY
{
“name”: “pdouit 1”,
“isActive”: true
}

  • remarque:
    si on inclus un autre champ dans le BODY ?
    PUT http://127.0.0.1:8000/api/products/1
    {
        "name": "pdouit 1",
        "isActive": true,
        "createdAt": "2018-01-01"
    }
    
    la dénormalization concerne que les champs : “name” et “isActive”
    donc la mise à jour de createdAt ne sera pas effectué

Modification dynamique du contexte de la sérialisation

https://api-platform.com/docs/core/serialization#changing-the-serialization-context-dynamically

Jusqu’à maintenant, tous les utilisateurs peuvent récuperer ou modifier les champs.

Supposons que pour certains champs nous voulons qu’ils soient gérés(PUT, POST…) que par un administrateur (ROLE_ADMIN)

pour cela :

  • la première étape consiste à indiquer à l’aide des groupes quels champs sont concernés par la normalisation et/ou la denormalisation
  • dans une classe spécifique lié au context, en fonction du rôle de l’utilisateur et du context il faut autoriser ou pas tel ou tel groupe.

à savoir:

  • la normalisation: quand on récupère des données : GET
  • la dénormalisation: quand on envoit des données au serveur : POST, PUT

exemple :

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"product:output"}},
 *     denormalizationContext={"groups"={"product:input"}}
 * )
 
* @ORM\Entity
*/
class Product {


    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @var string $name A name property - this description will be available in the API documentation too.
    *
    * @ORM\Column
    * @Assert\NotBlank
    * @Groups({"product:output"})
    */
    public $name;

    /**
     * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
     * @Assert\NotBlank
     * @Groups({"product:input"})
     */
    public $author;   

    /**
     * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"})
     * @ApiSubresource  
     * @Groups({"product:input"})       
     */
    public $offers;

    /**
     * @ORM\Column(type="boolean", options={"default" : false}, nullable = true)
     * @Groups({"admin:input"})     
     */
    private $isActive;
    ...

à ce niveau :

  • remarque: pour l’instant, le groupe “admin:input” du champ “isActive” ne sert pas

  • avec un get (denormalisation)

    GET http://localhost:8000/api/products/1

    on récupère le produit avec le champ : “name” qui est marqué en : “product:output”

  • avec un PUT :

    PUT http://127.0.0.1:8000/api/products/1

    {
        "author": "string",
        "offers": [
            "string"
        ]
    }
    

    on peut renseigner que les champs: “author” et “offers” qui sont marqués par : “product:input”

maintenant, gérons des champs en fonction du role de l’utilisateur :

# api/config/services.yaml
    ...
    ...
    'App\Serializer\ProductContextBuilder':
        decorates: 'api_platform.serializer.context_builder'
        arguments: [ '@App\Serializer\ProductContextBuilder.inner' ]
        autoconfigure: false
<?php
// api/src/Serializer/ProductContextBuilder.php

namespace App\Serializer;

use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use App\Entity\Product;

final class ProductContextBuilder implements SerializerContextBuilderInterface
{
    private $decorated;
    private $authorizationChecker;

    public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->decorated = $decorated;
        $this->authorizationChecker = $authorizationChecker;
    }

    public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
    {
        $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
        $resourceClass = $context['resource_class'] ?? null;
        
        if ($resourceClass === Product::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && true === $normalization) {
            $context['groups'][] = 'admin:input';
        }

        return $context;
    }
}
  • avec ce bout de code :

            if ($resourceClass === Product::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && true === $normalization) {
                $context['groups'][] = 'admin:input';
            }
    

    Il y a 3 conditions importantes :

    • $resourceClass === Product::class
      (si la ressource concerné est : Produit)
    • $this->authorizationChecker->isGranted(‘ROLE_ADMIN’)
      (si le role de l’utilisateur est “ROLE_ADMIN”)
    • true === $normalization
      (si cela concerne la normalisation donc : POST ou PUT)

    si les 3 conditions sont ok alors :
    $context[‘groups’][] = ‘admin:input’
    alors on indique qu’il faut prendre en compte les champs marqués par : ‘admin:input’

    donc dans cet exemple, dans le cas d’un POST ou d’un PUT d’un Produit,
    le champ : isActive sera pris en compte si l’utilisateur à le role : ADMIN


Modification du contexte de la sérialisation par article

https://api-platform.com/docs/core/serialization#changing-the-serialization-context-on-a-per-item-basis

  • L’exemple précédent montre comment modifier le contexte de normalisation / dénormalisation en fonction des autorisations utilisateur actuelles pour tous les livres.

  • Parfois, cependant, les autorisations varient en fonction du livre en cours de traitement.

  • Pensez aux listes de contrôle d’accès: l’utilisateur “A” peut récupérer le livre “A” mais pas le livre “B”

  • Dans ce cas, nous devons tirer parti de la puissance du sérialiseur Symfony et enregistrer notre propre normalisateur qui ajoute le groupe à chaque élément (note: la priorité 64 est un exemple; il est toujours important de vous assurer que votre normalisateur est chargé en premier. la priorité à la valeur appropriée pour votre application (les valeurs les plus élevées sont chargées plus tôt):

  • exemple:

    • lors d’une normalisation d’un GET d’un element ou d’une collection

    • retoune un Produit / les produits

      • SI product.isActive == TRUE ALORS on affiche tous les champs
      • SI product.isActive == FALSE ALORS on affiche que certains champs ayant le groupe : “can_retrieve_product” (donc les champs : “name” et “isActive”)

      /config/services.yaml

          ...
          ...
      
          'App\Serializer\ProductAttributeNormalizer':
              arguments: [ '@security.token_storage' ]
              tags:
                  - { name: 'serializer.normalizer', priority: 64 }
      
      <?php
      // api/src/Serializer/ProductAttributeNormalizer.php
      
      namespace App\Serializer;
      
      use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
      use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
      use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
      use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
      use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
      use App\Entity\Product;
      
      class ProductAttributeNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
      {
          use NormalizerAwareTrait;
      
          private const ALREADY_CALLED = 'PRODUCT_ATTRIBUTE_NORMALIZER_ALREADY_CALLED';
      
          private $tokenStorage;
      
          public function __construct(TokenStorageInterface $tokenStorage)
          {
              $this->tokenStorage = $tokenStorage;
          }
      
          public function normalize($object, $format = null, array $context = [])
          {
              if ($this->userHasPermissionsForProduct($object)) {
                  $context['groups'][] = 'can_retrieve_product';
              }
      
              $context[self::ALREADY_CALLED] = true;
      
              return $this->normalizer->normalize($object, $format, $context);
          }
          
          public function supportsNormalization($data, $format = null, array $context = [])
          {
              // Make sure we're not called twice
              if (isset($context[self::ALREADY_CALLED])) {
                  return false;
              }
      
              return $data instanceof Product;
          }
          
          private function userHasPermissionsForProduct($object): bool
          {
              // Get permissions from user in $this->tokenStorage
              // for the current $object (Product) and
              // return true or false
      
              return !$object->getIsActive();
          }
      }
      
      /**
       * @ApiResource(
       * )
       * @ORM\Entity
       */
      class Product {
      
          /**
           * @ORM\Column(type="integer")
           * @ORM\Id
           * @ORM\GeneratedValue(strategy="AUTO")
           */
          public $id;
      
          /**
           * @var string $name A name property 
          *
          * @ORM\Column
          * @Assert\NotBlank
          * @Groups("can_retrieve_product")
          */
          public $name;
      
          /**
           * @ORM\Column(type="boolean", options={"default" : false}, nullable = true)
           * @Groups("can_retrieve_product")
           */
          public $isActive;
      
          /**
           * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"})
           * @Assert\NotBlank
           */
          public $author;   
      
          /**
           * @ORM\OneToMany(targetEntity="Offer", mappedBy="product", cascade={"persist"})
           * @ApiSubresource   
           */
          public $offers;
      
          ...
          ...
      

Conversion de nom

https://api-platform.com/docs/core/serialization#name-conversion

Le composant Serializer fournit un moyen pratique de mapper les noms de champs PHP avec des noms sérialisés. Voir la documentation Symfony associée .

exemple: convertir CamelCase en snake_case

  • actuellement, on est en CamelCase :
    http://127.0.0.1:8000/api/products
            ...  
            ...  
            "createdAt": "2018-01-01T00:00:00+00:00",
            "transportFees": 7.42,
            "isActive": true
            },
    
    -> createdAt, transportFees, isActive
# api/config/services.yaml
services:
    'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter': ~
# api/config/packages/api_platform.yaml
api_platform:
    name_converter: 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'
  • on obtient :
    http://127.0.0.1:8000/api/products
        ...
        ...
        "created_at": "2018-01-01T00:00:00+00:00",
        "transport_fees": 7.42,
        "is_active": true
        },
    
    -> created_at, transport_fees, is_active

Décorer un sérialiseur et ajouter des données supplémentaires

https://api-platform.com/docs/core/serialization#decorating-a-serializer-and-adding-extra-data

comment ajouter des informations supplémentaires à la sortie sérialisée

exemple: GET en plus des champs, ajouter une date sur chaque demande dans GET (pour par exemple avoir la date de la demande d’extraction des données de l’API)

# api/config/services.yaml
services:
    'App\Serializer\ApiNormalizer':
        decorates: 'api_platform.jsonld.normalizer.item'
        arguments: [ '@App\Serializer\ApiNormalizer.inner' ]
<?php
// api/src/Serializer/ApiNormalizer

namespace App\Serializer;

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;

final class ApiNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
    private $decorated;

    public function __construct(NormalizerInterface $decorated)
    {
        if (!$decorated instanceof DenormalizerInterface) {
            throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
        }

        $this->decorated = $decorated;
    }

    public function supportsNormalization($data, $format = null)
    {
        return $this->decorated->supportsNormalization($data, $format);
    }

    public function normalize($object, $format = null, array $context = [])
    {
        $data = $this->decorated->normalize($object, $format, $context);
        if (is_array($data)) {
            $data['date'] = date(\DateTime::RFC3339);
        }

        return $data;
    }

    public function supportsDenormalization($data, $type, $format = null)
    {
        return $this->decorated->supportsDenormalization($data, $type, $format);
    }

    public function denormalize($data, $class, $format = null, array $context = [])
    {
        return $this->decorated->denormalize($data, $class, $format, $context);
    }
    
    public function setSerializer(SerializerInterface $serializer)
    {
        if($this->decorated instanceof SerializerAwareInterface) {
            $this->decorated->setSerializer($serializer);
        }
    }
}

-> tout se passe dans la méthode : normalize, ou l’on ajoute une date à $data (qui represente Product ou Offer ou …)

GET http://127.0.0.1:8000/api/products

      "createdAt": "2018-01-01T00:00:00+00:00",
      "transportFees": 7.42,
      "isActive": true,
      "date": "2019-01-06T15:25:25+00:00"
    },

-> “date”: “2019-01-06T15:25:25+00:00”


Identifiant d’entité

https://api-platform.com/docs/core/serialization#entity-identifier-case

  • La plate-forme API est capable de deviner l’identificateur d’entité à l’aide des métadonnées Doctrine . Il supporte également les identifiants composites.
  • Si vous n’utilisez pas le fournisseur Doctrine ORM, vous devez explicitement marquer l’identificateur à l’aide de l’ identifierattribut de l’ ApiPlatform\Core\Annotation\ApiPropertyannotation.

Par exemple:

/**
 * @ApiResource()
 */
class Book
{
    // ...
    
    /**
     * @ApiProperty(identifier=true)
     */
    private $id;
    ...
    ...
  • Dans certains cas, vous souhaiterez définir l’identifiant d’une ressource à partir du client (par exemple, un UUID généré par le client ou un slug).

  • Dans ce cas, vous devez faire de la propriété identifier une propriété de classe accessible en écriture. Plus précisément, pour utiliser les identifiants générés par le client, vous devez procéder comme suit:

  • créer un passeur pour l’identifiant de l’entité (par exemple public function setId(string $id)) ou en faire une publicpropriété,
    ajouter le groupe de dénormalisation à la propriété (uniquement si vous utilisez un groupe de dénormalisation spécifique), et
    si vous utilisez Doctrine ORM, veillez à ne pas marquer cette propriété avec l’ @GeneratedValueannotation ni utiliser la NONE valeur


Incorporation du contexte JSON-LD

https://api-platform.com/docs/core/serialization#embedding-the-json-ld-context

Par défaut, le contexte JSON-LD généré ( @context) est uniquement référencé par un IRI. Un client qui utilise JSON-LD doit envoyer une seconde requête HTTP pour le récupérer:

{
  "@context": "/contexts/Book",
  "@id": "/books/62",
  "@type": "Book",
  "name": "My awesome book",
  "author": "/people/59"
}

Vous pouvez configurer API Platform pour incorporer le contexte JSON-LD dans le document racine comme en ajoutant l’ jsonld_embed_context attribut à l’ @ApiResourceannotation:

/**
 * @ApiResource(normalizationContext={"jsonld_embed_context"=true})
 * @ORM\Entity
 */
class Product {
    ...
    ...

GET http://127.0.0.1:8000/api/products

{
  "@context": {
    "@vocab": "http://127.0.0.1:8000/api/docs.jsonld#",
    "hydra": "http://www.w3.org/ns/hydra/core#",
    "id": "Product/id",
    "name": "Product/name",
    "author": {
      "@id": "Product/author",
      "@type": "@id"
    },
    "offers": {
      "@id": "Product/offers",
      "@type": "@id"
    },
    "createdAt": "Product/createdAt",
    "transportFees": "Product/transportFees",
    "isActive": "Product/isActive"
  },
 ...
 ...

-> le context de author et offers est incorporé !





Validation




https://api-platform.com/docs/core/validation#validation


Validation des données soumises

https://api-platform.com/docs/core/validation#validating-submitted-data

php bin/console doctrine:schema:update --force


Utilisation de groupes de validation

https://api-platform.com/docs/core/validation#using-validation-groups



Utilisation de groupes de validation sur des opérations

https://api-platform.com/docs/core/validation#using-validation-groups-on-operations

Vous pouvez avoir une validation différente pour chaque opération liée à votre ressource.

exemple :

sur le champ author plage de la taille
avec POST min = 2, max = 50
avec PUT min = 2, max = 70
<?php
// api/src/Entity/Book.php

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ApiResource(
 *     collectionOperations={
 *         "get",
 *         "post"={"validation_groups"={"Default", "postValidation"}}
 *     },
 *     itemOperations={
 *         "delete",
 *         "get",
 *         "put"={"validation_groups"={"Default", "putValidation"}}
 *     }
 * )
 * ...
 */
class Product
{
...
...

    /**
     * @Assert\NotNull
     * @Assert\Length(
     *     min = 2,
     *     max = 50,
     *     groups={"postValidation"}
     * )
     * @Assert\Length(
     *     min = 2,
     *     max = 70,
     *     groups={"putValidation"}
     * )
     */
    private $author;
    ...
    ...

remarque:

  • comme indiqué plus haut, il faut préciser “delete” et “get” dans itemOperations
    pour qu’il soit pris en compte
    *     itemOperations={
    *         "delete",
    *         "get",
    *         "put"={"validation_groups"={"Default", "putValidation"}}
    *     }
    
  • “Default”: contient les contraintes qui n’appartiennent à aucun autre groupe.

Groupes de validation dynamiques

https://api-platform.com/docs/core/validation#dynamic-validation-groups

  • Si vous devez déterminer de manière dynamique les groupes de validation à utiliser pour une entité dans différents scénarios, transmettez simplement un callable .

  • Le callback recevra l’objet entité comme premier argument et devrait renvoyer un tableau de noms de groupes ou une séquence de groupes .

  • avec une méthode statique

    <?php
    // api/src/Entity/Product.php
    
    use ApiPlatform\Core\Annotation\ApiResource;
    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @ApiResource(
     *     attributes={"validation_groups"={Product::class, "validationGroups"}}
     * )
     */
    class Product
    {
        /**
         * Return dynamic validation groups.
         *
         * @param self $product Contains the instance of Book to validate.
        *
        * @return string[]
        */
        public static function validationGroups(self $product)
        {
            //
            //  determine quoi retourner ['a'] ou ['b'] ou ['a', 'b']
            //
            return ['a'];
        }
    
        /**
         * @Assert\NotBlank(groups={"a"})
         */
        private $name;
    
        /**
         * @Assert\NotNull(groups={"b"})
         */
        private $author;
    
        // ...
    }
    
    • la méthode validationGroups retoune ‘a’, donc seul NotBlank sur le champ ‘name’ sera pris en compte.

    • c’est donc dans la méthode validationGroups que l’on determine quoi retourner ([‘a’] ou [‘b’] ou [‘a’, ‘b’] ou …), et ce, de façon dynamique.

    • utilisation:
      POST http://127.0.0.1:8000/api/products
      BODY

      {
      "id": 0,
      "name": "toto"
      }
      
      • avec : … return [‘a’];
        -> pas d’erreur de validation
      • avec : … return [‘a’, ‘b’];
        -> erreur de validation sur author car dans le BODY, le champ author n’est pas présent
    • remarque:

      • utilisation:
        avec : … return [‘a’];

        PUT http://127.0.0.1:8000/api/products
        BODY

          ```
          {
          "id": 0,
          "name": "toto"
          }
          ```
        

        -> il n’y a pas d’erreur de validation
        car c’est un PUT (une mise à jour) donc il y a déjà une valeur dans le champ : author

  • en utilisant un service pour récupérer les groupes à utiliser:

    Par exemple, en fonction de tel rôle, on valide tel champ

    <?php
    // api/src/Validator/AdminGroupsGenerator.php
    
    namespace App\Validator;
    
    use App\Entity\Product;
    use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
    
    final class AdminGroupsGenerator
    {
        private $authorizationChecker;
    
        public function __construct(AuthorizationCheckerInterface $authorizationChecker)
        {
            $this->authorizationChecker = $authorizationChecker;
        }
    
        public function __invoke(Product $product): array
        {
            return $this->authorizationChecker->isGranted('ROLE_ADMIN', $product) ? ['a', 'b'] : ['a'];
        }
    }
    
    <?php
    // api/src/Entity/Product.php
    
    namespace App\Entity;
    
    use ApiPlatform\Core\Annotation\ApiResource;
    use App\Validator\AdminGroupsGenerator;
    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @ApiResource(attributes={"validation_groups"=AdminGroupsGenerator::class})
     */
    class Product
    {
        /**
         * @Assert\NotBlank(groups={"a"})
         */
        private $name;
    
        /**
         * @Assert\NotNull(groups={"b"})
         */
        private $author;
    
        // ...
    }
    

    -> si le role = ‘ROLE_ADMIN’ alors [‘a’, ‘b’] sinon [‘a’]
    -> si le role = ‘ROLE_ADMIN’ alors [ validation sur name et author ] sinon [ validation sur name ]


Niveaux d’erreur et sérialisation de la charge utile

https://api-platform.com/docs/core/validation#error-levels-and-payload-serialization

  • Comme indiqué dans la documentation Symfony , vous pouvez utiliser le payload field pour définir les niveaux d’erreur.
  • Vous pouvez récupérer le payload field en définissant l’option: serialize_payload_fields = true dans la configuration de la plate-forme API:
  • voir la doc Symfony sur les payload : https://symfony.com/doc/current/validation/severity.html
# api/config/packages/api_platform.yaml

api_platform:
    validator:
        serialize_payload_fields: true

Ensuite, le sérialiseur renverra toutes les valeurs de payload dans la réponse d’erreur.

  • Si vous ne voulez sérialiser que certains champs de payload, définissez-les dans la configuration comme suit:
# api/config/packages/api_platform.yaml

api_platform:
    validator:
        serialize_payload_fields: [ severity, anotherPayloadField ]

Dans cet exemple, seul severity et anotherPayloadField sera sérialisé.




Gestion des erreurs




  • API Platform est livré avec un système d’erreur puissant.
  • Il gère les exceptions :
    • telles que les documents JSON défectueux envoyés par le client
    • les erreurs de validation
    • les erreurs inattendues (exceptions et erreurs PHP).
  • API Platform envoie automatiquement le code de statut HTTP approprié au client:
    • 400 pour les erreurs attendues,
    • 500 pour les imprévues.
  • Il fournit également une description de l’erreur dans le format d’erreur Hydra ou dans le format décrit dans le RFC 7807 , en fonction du format sélectionné lors de la négociation de contenu .

Conversion d’exceptions PHP en erreurs HTTP

https://api-platform.com/docs/core/errors#converting-php-exceptions-to-http-errors





Pagination




  • La pagination est activée par défaut pour toutes les collections.
  • Chaque collection contient 30 articles par page
  • L’activation de la pagination et le nombre d’éléments par page peuvent être configurés à partir de:
    • le côté serveur (globalement ou par ressource)
    • côté client, via un paramètre GET personnalisé (désactivé par défaut)

on configure de façon globale le nombre d’item par page à : 2

# api/config/packages/api_platform.yaml
api_platform:
    collection:
        pagination:
            items_per_page: 2

GET http://127.0.0.1:8000/api/products

{
  "@context": "/api/contexts/Product",
  "@id": "/api/products",
  "@type": "hydra:Collection",
  "hydra:member": [
    {
      "@id": "/api/products/99",
      "@type": "Product",
      "id": 99,
      "name": "string",
      "author": "/api/authors/83",
      "offers": [
        "/api/offers/171"
      ],
      "createdAt": "2018-01-01T00:00:00+00:00",
      "transportFees": 7.42,
      "properties": null,
      "isActive": true,
      "date": "2019-01-07T09:18:26+00:00"
    },
    {
      "@id": "/api/products/100",
      "@type": "Product",
      "id": 100,
      "name": "produit 2",
      "author": "/api/authors/81",
      "offers": [
        "/api/offers/172",
        "/api/offers/173"
      ],
      "createdAt": "2015-09-07T00:00:00+00:00",
      "transportFees": 8.35,
      "properties": null,
      "isActive": false,
      "date": "2019-01-07T09:18:26+00:00"
    }
  ],
  "hydra:totalItems": 16,
  "hydra:view": {
    "@id": "/api/products?page=1",
    "@type": "hydra:PartialCollectionView",
    "hydra:first": "/api/products?page=1",
    "hydra:last": "/api/products?page=8",
    "hydra:next": "/api/products?page=2"
  }
}

Hydra collection : C’est un document JSON (-LD) valide contenant les éléments de la page et des métadonnées demandées.

remarque :
en bas du JSON-LD, on constate plusieurs informations :

“hydra:totalItems”: 16, le nombre total d’item
“@id”: “/api/products?page=1”, la page courante
“hydra:first”: “/api/products?page=1”, la première page
hydra:last": "/api/products?page=8 la dernière page
hydra:next": "/api/products?page=2 la page suivante

Ainsi, dans le frontend, on peut utiliser ces liens pour naviguer dans la liste d’items


Désactiver la pagination

https://api-platform.com/docs/core/pagination#disabling-the-pagination

pour les petites collections, il peut être pratique de désactiver complètement la pagination.

  • Globalement

    La pagination peut être désactivée pour toutes les ressources à l’aide de cette configuration:

    # api/config/packages/api_platform.yaml
    api_platform:
        collection:
            pagination:
                enabled: false
    
  • Pour une ressource spécifique
    Il peut également être désactivé pour une ressource spécifique:

    <?php
    // src/Entity/Product.php
    
    use ApiPlatform\Core\Annotation\ApiResource;
    
    /**
    * @ApiResource(attributes={"pagination_enabled"=false})
    */
    class Product
    {
        // ...
    }
    

Côté client

https://api-platform.com/docs/core/pagination#client-side

Vous pouvez configurer API Platform Core pour laisser le client activer ou désactiver la pagination.

  • globalement

    # api/config/packages/api_platform.yaml
    api_platform:
        collection:
            pagination:
                client_enabled: true
                enabled_parameter_name: pagination # optional
    

    La pagination peut maintenant être activée ou désactivée en ajoutant un paramètre de requête nommé pagination:

    GET /products?pagination=false
    GET /products?pagination=true

    Toute valeur acceptée par le FILTER_VALIDATE_BOOLEAN filtre peut être utilisée comme valeur.


POUR UNE RESSOURCE SPÉCIFIQUE

https://api-platform.com/docs/core/pagination#for-a-specific-resource-1

La capacité du client à désactiver la pagination peut également être définie dans la configuration de la ressource:

<?php
// src/Entity/Product.php

use ApiPlatform\Core\Annotation\ApiResource;

/**
* @ApiResource(attributes={"pagination_client_enabled"=true})
*/
class Product
{
    // ...
}

Changer le nombre d’articles par page

https://api-platform.com/docs/core/pagination#changing-the-number-of-items-per-page



Changer le nombre maximum d’éléments par page

https://api-platform.com/docs/core/pagination#changing-maximum-items-per-page



Pagination partielle

https://api-platform.com/docs/core/pagination#partial-pagination

Lors de l’utilisation de la pagination par défaut, une COUNT Query sera émise pour la collection demandée actuelle.
Cela peut avoir un impact sur les performances des très grandes collections.
L’inconvénient est que les informations sur la dernière page sont perdues (ie:) hydra:last.



Eviter les doubles requêtes SQL sur Doctrine

https://api-platform.com/docs/core/pagination#avoiding-double-sql-requests-on-doctrine



Action du contrôleur personnalisé

https://api-platform.com/docs/core/pagination#custom-controller-action





Le système d’événement




  • API Platform Core implémente le modèle ADR (Action-Domain-Responder)
  • Fondamentalement, API Platform Core exécute une classe d’actions qui renverra une entité ou une collection d’entités.
  • Ensuite, une série d’écouteurs d’événements sont exécutés pour :
    • valider les données
    • les conserver dans la base de données
    • les sérialiser (généralement dans un document JSON-LD)
    • et créer une réponse HTTP à envoyer au client.
  • Pour ce faire, API Platform Core exploite les événements déclenchés par le noyau HTTP Symfony .
  • Vous pouvez également associer votre propre code à ces événements.
  • Ce sont des points d’extension pratiques et puissants disponibles à tous les stades du cycle de vie de la demande.

exemple:
nous enverrons un courrier chaque fois qu’un nouveau produit est créé à l’aide de l’API

<?php
// api/src/EventSubscriber/ProductMailSubscriber.php

namespace App\EventSubscriber;

use ApiPlatform\Core\EventListener\EventPriorities;
use App\Entity\Product;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class ProductMailSubscriber implements EventSubscriberInterface
{
    private $mailer;

    public function __construct(\Swift_Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::VIEW => ['sendMail', EventPriorities::POST_WRITE],
        ];
    }

    public function sendMail(GetResponseForControllerResultEvent $event)
    {
        $product = $event->getControllerResult();
        $method = $event->getRequest()->getMethod();

        if (!$product instanceof Product || Request::METHOD_POST !== $method) {
            return;
        }

        $message = (new \Swift_Message('A new product has been added'))
            ->setFrom('system@example.com')
            ->setTo('contact@les-tilleuls.coop')
            ->setBody(sprintf('The product #%d has been added.', $product->getId()));

        $this->mailer->send($message);
    }
}

remarque:

  • la méthode : getSubscribedEvents() …
    la fonction sendMail est appelé quand l’évenement suivant arrive : POST_WRITE
    (voir plus bas : ApiPlatform\Core\EventListener\EventPriorities)
  • la méthode : sendMail(…)
    c’est dans cette méthode que l’on envoit le mail.

les evenements:

  • Les événements Doctrine sont également disponibles (si vous les utilisez) si vous souhaitez vous connecter aux événements de cycle de vie d’objet.
  • Les écouteurs d’événements intégrés sont:
NOM ÉVÉNEMENT PRÉ ET POST PRIORITÉ DESCRIPTION
AddFormatListener kernel.request Aucun 7 deviner le meilleur format de réponse ( négociation de contenu )
ReadListener kernel.request PRE_READ , POST_READ 4 récupérer des données du système de persistance à l’ aide des fournisseurs de données ( GET , PUT , DELETE )
DeserializeListener kernel.request PRE_DESERIALIZE , POST_DESERIALIZE 2 désérialiser les données dans une entité de PHP ( GET , POST , DELETE ); Mettre à jour l’entité extraite à l’aide du fournisseur de données ( PUT )
ValidateListener kernel.view PRE_VALIDATE , POST_VALIDATE 64 valider les données ( POST , PUT )
WriteListener kernel.view PRE_WRITE , POST_WRITE 32 persister des changements dans le système de persistance à l’ aide des persisters de données ( POST , PUT , DELETE )
SerializeListener kernel.view PRE_SERIALIZE , POST_SERIALIZE 16 sérialiser l’entité PHP en chaîne selon le format de la requête
RespondListener kernel.view PRE_RESPOND , POST_RESPOND 8 transformer sérialisé en une instance Symfony\Component\HttpFoundation\Response
AddLinkHeaderListener kernel.response Aucun 0 ajouter un Link en-tête HTTP pointant vers la documentation Hydra
ValidationExceptionListener kernel.exception Aucun 0 sérialiser les exceptions de validation au format Hydra
ExceptionListener kernel.exception Aucun -96 sérialiser les exceptions PHP au format Hydra (y compris la trace de pile en mode débogage)

ApiPlatform\Core\EventListener\EventPriorities

  • kernel.request
    • 5 PRE_READ
    • 3 POST_READ
    • 3 PRE_DESERIALIZE
    • 1 POST_DESERIALIZE
  • kernel.view
    • 65 PRE_VALIDATE
    • 63 POST_VALIDATE
    • 33 PRE_WRITE
    • 31 POST_WRITE
    • 17 PRE_SERIALIZE
    • 15 POST_SERIALIZE
    • 9 PRE_RESPOND
  • kernel.response
    • 0 POST_RESPOND

Certains de ces écouteurs intégrés peuvent être activés / désactivés en définissant des attributs de requête ( par exemple dans l’attribut defaults d’une opération ):

Attribute Type Default Description
_api_receive bool true Enables or disables the ReadListener, DeserializeListener and ValidateListener
_api_respond bool true Enables or disables SerializeListener, RespondListener
_api_persist bool true Enables or disables WriteListener

Négociation de contenu

https://api-platform.com/docs/core/content-negotiation#content-negotiation

Le système API dispose de capacités de négociation de contenu intégrées.



Utiliser des vocabulaires externes

https://api-platform.com/docs/core/external-vocabularies#using-external-vocabularies

  • JSON-LD permet de définir les classes et les propriétés de votre API avec des vocabulaires ouverts tels que Schema.org et Good Relations .
  • API Platform Core fournit des annotations utilisables sur les classes PHP et les propriétés permettant de spécifier un IRI externe associé.



Extension du contexte JSON-LD

https://api-platform.com/docs/core/extending-jsonld-context#extending-json-ld-context

  • API Platform Core offre la possibilité d’étendre le contexte de propriétés JSON-LD



Fournisseurs de données

https://api-platform.com/docs/core/data-providers#data-providers

  • Pour récupérer les données exposées par l’API, API Platform utilise des classes appelées fournisseurs de données .
  • Un fournisseur de données utilisant Doctrine ORM pour extraire des données d’une base de données est inclus dans la bibliothèque et est activé par défaut.
  • Ce fournisseur de données prend en charge de manière native les collections et les filtres paginés.
  • Il peut être utilisé tel quel et s’adapte parfaitement aux usages courants.
  • Cependant, vous souhaitez parfois extraire des données d’autres sources telles qu’une autre couche de persistance, un service Web, ElasticSearch ou MongoDB.
  • Les fournisseurs de données personnalisés peuvent être utilisés à cette fin.
  • Un projet peut inclure autant de fournisseurs de données que nécessaire.
  • Le premier capable de récupérer des données pour une ressource donnée sera utilisé.
    -Pour une ressource donnée, vous pouvez implémenter deux types d’interfaces:
    • le CollectionDataProviderInterface est utilisé lors de la récupération d’une collection.
    • le ItemDataProviderInterface est utilisé lors de la récupération d’éléments.
    • Les deux implémentations peuvent également implémenter une troisième interface optionnelle appelée «RestrictedDataProviderInterface» si vous souhaitez limiter leurs effets à une seule ressource ou opération.



Données persistantes

https://api-platform.com/docs/core/data-persisters#data-persisters

Pour muter les états d’application pendant POST, PUT, PATCHou DELETE opérations , la plate - forme API utilise des classes appelées persisters de données .

-> lors d’un POST, PUT, DELETE
-> sur une entité
-> pour enregistrer/modifier/supprimer des données comme on le souhaite



Identifiants

https://api-platform.com/docs/core/identifiers#identifiers

  • cet identifiant soit généralement un nombre ID
  • il peut également s’agir d’une UUID

-> Je ne comprends pas les indications de la doc !

  • par contre, j’ai trouvé cette façon de faire et ça fonctionne :

    remplacer le champs id et ajouter les getter et setter sur id pour toutes les entités: author, product, offer, user

    ...
    ...
    
        /**
        * @ORM\Id()
        * @ORM\GeneratedValue(strategy="UUID")
        * @ORM\Column(type="guid", unique=true)
        */
        private $id;
    
    ...
    ...
        /**
         * Get the value of id
         */ 
        public function getId()
        {
            return $this->id;
        }
    
        /**
         * Set the value of id
         *
         * @return  self
        */ 
        public function setId($id)
        {
            $this->id = $id;
    
            return $this;
        }
    ...
    ...        
    
  • supprimer la base de donnée

  • php bin/console doctrine:schema:update --force

  • php bin/console doctrine:fixtures:load

  • GET http://localhost:8000/api/products

    ...
    "hydra:member": [
    {
        "@id": "/api/products/e3560aee-1651-11e9-8960-e0d55e2a8c3e",
    ...    
    

Identificateur personnalisé normalisateur

https://api-platform.com/docs/core/identifiers#custom-identifier-normalizer



Les extensions

https://api-platform.com/docs/core/extensions#extensions

  • API Platform Core fournit un système permettant d’étendre les requêtes sur les éléments et les collections.
  • Les extensions sont spécifiques à Doctrine et, par conséquent, le support Doctrine ORM doit être activé pour utiliser cette fonctionnalité.
  • Si vous utilisez des fournisseurs personnalisés, c’est à vous de mettre en œuvre votre propre système de poste ou non.



Sécurité

https://api-platform.com/docs/core/security#security



Déprécier les ressources et les propriétés (Alternative au contrôle de version)

https://api-platform.com/docs/core/deprecations#deprecating-resources-and-properties-alternative-to-versioning

Une bonne pratique en matière de développement d’API Web consiste à appliquer la stratégie d’évolution afin d’indiquer aux applications clientes quels types de ressources, opérations et zones sont obsolètes et ne devraient plus être utilisés.



Performance

https://api-platform.com/docs/core/performance/#performance



Activation du système d’invalidation de cache HTTP intégré

https://api-platform.com/docs/core/performance/#enabling-the-built-in-http-cache-invalidation-system


Activer le cache de métadonnées

https://api-platform.com/docs/core/performance/#enabling-the-metadata-cache


Utiliser PPM (PHP-PM)

https://api-platform.com/docs/core/performance/#using-ppm-php-pm



Doctrine Requêtes et Index

https://api-platform.com/docs/core/performance/#doctrine-queries-and-indexes


Search Filter


Eager Loading


MAX JOINS

https://api-platform.com/docs/core/performance/#max-joins


FORCE EAGER

https://api-platform.com/docs/core/performance/#force-eager


REMPLACEMENT AU NIVEAU DES RESSOURCES ET DES OPÉRATIONS

https://api-platform.com/docs/core/performance/#override-at-resource-and-operation-level


DÉSACTIVER LE CHARGEMENT RAPIDE

https://api-platform.com/docs/core/performance/#disable-eager-loading


Profiler avec Blackfire.io

https://api-platform.com/docs/core/performance/#profiling-with-blackfireio



Operation Path Naming

https://api-platform.com/docs/core/operation-path-naming#operation-path-naming



Accept application/x-www-form-urlencoded Form Data

https://api-platform.com/docs/core/form-data#accept-code-classlanguage-textapplicationx-www-form-urlencodedcode-form-data



FOSUserBundle Integration

https://api-platform.com/docs/core/fosuser-bundle#fosuserbundle-integration

  • API Platform Core est livré avec un pont pour FOSUserBundle .
  • Si l’ensemble FOSUser est activé, ce pont l’utilisera UserManagerpour créer, mettre à jour et supprimer des ressources utilisateur.

Remarque:
FOSUserBundle n’est pas bien adapté aux API. Nous vous encourageons vivement à utiliser le fournisseur d’utilisateur Doctrine fourni avec Symfony ou à créer un fournisseur d’utilisateur personnalisé au lieu d’utiliser cet ensemble.



Intégration NelmioApiDocBundle

https://api-platform.com/docs/core/nelmio-api-doc#nelmioapidocbundle-integration



AngularJS Integration

https://api-platform.com/docs/core/angularjs-integration#angularjs-integration



Swagger / Open API Support

https://api-platform.com/docs/core/swagger#swagger–open-api-support



GraphQL Support

https://api-platform.com/docs/core/graphql#graphql-support



Handling Data Transfer Objects (DTOs)

https://api-platform.com/docs/core/dto#handling-data-transfer-objects-dtos



#Handling File Upload

https://api-platform.com/docs/core/file-upload#handling-file-upload



================================================================



#Ajouter des données dans le token

  • de base, dans le token sont insérés:
    • username
    • les rôles de l’utilisateur
    • exp: la date d’expiration du token
    • iat: la date de création du token

services.yaml

    acme_api.event.jwt_created_listener:
        class: App\EventListener\JWTCreatedListener
        arguments: [ '@request_stack' ]
        tags:
            - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated }

/App/EventListener/JWTCreatedListener.php

<?php

// src/App/EventListener/JWTCreatedListener.php

namespace App\EventListener;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Symfony\Component\HttpFoundation\RequestStack;

class JWTCreatedListener
{

    /**
     * @var RequestStack
     */
    private $requestStack;

    /**
     * @param RequestStack $requestStack
     */
    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    /**
     * @param JWTCreatedEvent $event
     *
     * @return void
     */
    public function onJWTCreated(JWTCreatedEvent $event)
    {
        /** @var $user \AppBundle\Entity\User */
        $user = $event->getUser();

        // merge with existing event data
        $payload = array_merge(
            $event->getData(),
            [
                'password' => $user->getPassword()
            ]
        );

        $event->setData($payload);
    }
}

// ici, on ajoute le password dans le token


Quelques infos en vrac sur REST :

  • création :
    POST /articles

  • lecture :
    GET /articles
    GET /articles/{identifiant-unique}

  • mise à jour
    PUT /articles/{identifiant-unique}

  • suppression :
    delete /articles/{identifiant-unique}


Un identifiant unique peut être : id, uuid, slug


On laisse le ‘s’ de articles partout, pour simplifier (pour ne pas confondre, là un ‘s’ l’autre pas)


  • les codes d’erreurs :

  • 200 OK
    Tout s’est bien passé

  • 201 Created
    La création de la ressource s’est bien passée
    (en général le contenu de la nouvelle ressource est aussi renvoyée dans la réponse, mais ce n’est pas obligatoire - on ajoute aussi un header Locationavec l’URL de la nouvelle ressource)

  • 204 No content
    Même principe que pour la 201, sauf que cette fois-ci, le contenu de la ressource nouvellement créée ou modifiée n’est pas renvoyée en réponse

  • 304 Not modified
    Le contenu n’a pas été modifié depuis la dernière fois qu’elle a été mise en cache

  • 400 Bad request
    La demande n’a pas pu être traitée correctement

  • 401 Unauthorized
    L’authentification a échoué

  • 403 Forbidden
    L’accès à cette ressource n’est pas autorisé

  • 404 Not found
    La ressource n’existe pas

  • 405 Method not allowed
    La méthode HTTP utilisée n’est pas traitable par l’API

  • 406 Not acceptable
    Le serveur n’est pas en mesure de répondre aux attentes des entêtes Accept
    En clair, le client demande un format (XML par exemple) et l’API n’est pas prévue pour générer du XML

  • 500 Server error
    Le serveur a rencontré un problème.


Hypermedia controls : rendre votre API auto découvrable

{
    "id" : 1,
    "title" : "Le titre de l'article",
    "content" : "<p> Le contenu de l'article.</p>",
    "links" : {
        "update" : "http://domain.name/article/1",
        "associated" : "http://domain.name/article/16"
    }
}

si le client demande un article, non seulement il obtiendra les informations de l’article en question (titre, contenu…),
mais aussi la liste des liens (URL) pour effectuer d’autres actions sur cette ressource, comme la mettre à jour ou un
article associé par exemple.