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.
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
composer req api
Plusieurs façons, au choix :
php -S 127.0.0.1:8000 -t public
composer req server --dev
php bin/console server:run
…
…
Vous pouvez dès à present tester que tout va bien :
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.
Il faut sécuriser notre API afin de controler les accès.
composer require symfony/security-bundle
composer require symfony/orm-pack
copier .env en .env.local
.env.local
...
DATABASE_URL=mysql://root:@127.0.0.1:3306/db_my_product_api
...
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
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
on ajoute 4 nouvelles entités : (Product, Offer, Author, User)
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
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
...
php bin/console doctrine:fixtures:load
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.
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
#/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 :
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:
api :
access_control
#/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()));
}
}
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
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 -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 :
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 !
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
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 + 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.
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
}
les dates :
@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.
https://api-platform.com/docs/core/pagination/
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)
https://api-platform.com/docs/core/operations#enabling-and-disabling-operations
remarque : PATCH est pris en charge lors de l’ utilisation du format API JSON
* @ApiResource(
* collectionOperations={"get"}, // 1..
* itemOperations={"get"} // 3..
* )
*/
class Product
{
pour Product, on peut faire que les opérations suivantes :
* @ApiResource(
* collectionOperations={"get"}, // 1...
* itemOperations={} // aucun
* )
*/
class Product
{
pour Product, on peut faire que l’opération suivante :
* @ApiResource(
* collectionOperations={"get"}, // 1..
* )
*/
class Product
{
pour Product, on peut faire que les opérations suivantes :
Remarque :
le fait de ne pas mentionner itemOperations, cela intègre toutes les opérations lui concernant soit : 3… 4… 5… (6…)
* @ApiResource(
* collectionOperations={"post"}, // 2...
* itemOperations={"put"} // 4...
* )
*/
class Product
{
pour Product, on peut faire que les opérations suivantes :
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
{
remarque :
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
{
https://api-platform.com/docs/core/operations#subresources
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
}
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 :
https://api-platform.com/docs/core/operations#subresources
…
…
https://api-platform.com/docs/core/operations#control-the-path-of-subresources
…
…
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
/**
* @ApiResource(
* attributes={"access_control"="is_granted('ROLE_ADMIN')"},
* )
* @ORM\Entity
*/
class Product {
...
https://api-platform.com/docs/core/operations#control-the-depth-of-subresources
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;
// ...
}
https://api-platform.com/docs/core/operations#creating-custom-operations-and-controllers
Remarque :
le système d’événements doit être préféré aux contrôleurs personnalisés, le cas échéant.
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
{
//...
conclusion:
POST /products/{id}/publication
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 :
/**
* @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
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 |
https://api-platform.com/docs/core/operations#alternative-method
…
…
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"}},
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é:
https://api-platform.com/docs/core/filters#basic-knowledge
à 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
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 |
Ajoutez la lettre i au filtre si vous souhaitez qu’elle ne soit pas sensible à la casse :
iexact
ipartial
istart
iend
iword_start
remarque :
<?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:
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
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 ) |
<?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
{
// ...
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
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
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
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 |
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
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 ) |
<?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
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"
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
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 |
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
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
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
{
http://localhost:8000/api/products?properties[]=name
http://localhost:8000/api/products?properties[]=name,createdAt
http://127.0.0.1:8000/api/products?properties=name&properties=createdAt
http://localhost:8000/api/products?properties[offer][]=description
implementer : ApiPlatform\Core\Api\FilterInterface
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))
...
# api/config/services.yaml
services:
# ...
'App\Filter\RegexpFilter':
# Uncomment only if autoconfiguration isn't enabled
#tags: [ 'api_platform.filter' ]
# 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
{
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é
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 :
public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
...
arguments: [ '@doctrine.orm.default_entity_manager', '@security.token_storage', '@annotations.cached_reader']
liste des services disponible pour l’autowire :utlisation :
GET http://localhost:8000/api/products
-> on récupère que les produits qui appartiennent à l’utilisateur
https://api-platform.com/docs/core/filters#overriding-extraction-of-properties-from-the-request
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
{
// ...
}
https://api-platform.com/docs/core/serialization
https://api-platform.com/docs/core/serialization#using-serialization-groups
il y a 2 contexts : la normalisation et la denormalisation
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 :
<?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”))
https://api-platform.com/docs/core/serialization#using-serialization-groups-per-operation
exemple 1 :
* "get",
* "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:
remarque:
Donc au final, on peut faire :
https://api-platform.com/docs/core/serialization#embedding-relations
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”
https://api-platform.com/docs/core/serialization#denormalization
à savoir:
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
}
{
"name": "pdouit 1",
"isActive": true,
"createdAt": "2018-01-01"
}
la dénormalization concerne que les champs : “name” et “isActive”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 :
à savoir:
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 :
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
avec un PUT : (et rôle ADMIN)
PUT http://127.0.0.1:8000/api/products/1
{
"author": "string",
"offers": [
"string"
],
"isActive": "true"
}
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
/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;
...
...
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
...
...
"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'
...
...
"created_at": "2018-01-01T00:00:00+00:00",
"transport_fees": 7.42,
"is_active": true
},
-> created_at, transport_fees, is_activehttps://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”
https://api-platform.com/docs/core/serialization#entity-identifier-case
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
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é !
https://api-platform.com/docs/core/validation#validation
https://api-platform.com/docs/core/validation#validating-submitted-data
php bin/console doctrine:schema:update --force
https://api-platform.com/docs/core/validation#using-validation-groups
…
…
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:
* itemOperations={
* "delete",
* "get",
* "put"={"validation_groups"={"Default", "putValidation"}}
* }
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"
}
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 ]
https://api-platform.com/docs/core/validation#error-levels-and-payload-serialization
# 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.
# api/config/packages/api_platform.yaml
api_platform:
validator:
serialize_payload_fields: [ severity, anotherPayloadField ]
Dans cet exemple, seul severity et anotherPayloadField sera sérialisé.
https://api-platform.com/docs/core/errors#converting-php-exceptions-to-http-errors
…
…
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
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
{
// ...
}
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.
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
{
// ...
}
https://api-platform.com/docs/core/pagination#changing-the-number-of-items-per-page
…
…
https://api-platform.com/docs/core/pagination#changing-maximum-items-per-page
…
…
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.
…
…
https://api-platform.com/docs/core/pagination#avoiding-double-sql-requests-on-doctrine
…
…
https://api-platform.com/docs/core/pagination#custom-controller-action
…
…
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:
les evenements:
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
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 |
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.
…
…
https://api-platform.com/docs/core/external-vocabularies#using-external-vocabularies
…
…
https://api-platform.com/docs/core/extending-jsonld-context#extending-json-ld-context
…
…
https://api-platform.com/docs/core/data-providers#data-providers
…
…
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
…
…
https://api-platform.com/docs/core/identifiers#identifiers
-> 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",
...
https://api-platform.com/docs/core/identifiers#custom-identifier-normalizer
…
…
https://api-platform.com/docs/core/extensions#extensions
…
…
https://api-platform.com/docs/core/security#security
…
…
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.
…
…
https://api-platform.com/docs/core/performance/#performance
…
…
https://api-platform.com/docs/core/performance/#enabling-the-built-in-http-cache-invalidation-system
…
…
https://api-platform.com/docs/core/performance/#enabling-the-metadata-cache
https://api-platform.com/docs/core/performance/#using-ppm-php-pm
…
…
https://api-platform.com/docs/core/performance/#doctrine-queries-and-indexes
…
…
…
…
…
…
https://api-platform.com/docs/core/performance/#max-joins
…
…
https://api-platform.com/docs/core/performance/#force-eager
…
…
https://api-platform.com/docs/core/performance/#override-at-resource-and-operation-level
…
…
https://api-platform.com/docs/core/performance/#disable-eager-loading
…
…
https://api-platform.com/docs/core/performance/#profiling-with-blackfireio
…
…
https://api-platform.com/docs/core/operation-path-naming#operation-path-naming
…
…
…
…
https://api-platform.com/docs/core/fosuser-bundle#fosuserbundle-integration
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.
…
…
https://api-platform.com/docs/core/nelmio-api-doc#nelmioapidocbundle-integration
…
…
https://api-platform.com/docs/core/angularjs-integration#angularjs-integration
…
…
https://api-platform.com/docs/core/swagger#swagger–open-api-support
…
…
https://api-platform.com/docs/core/graphql#graphql-support
…
…
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
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
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.