avril 2025

Comprendre le CORS et les requêtes Preflight

Qu’est ce que le CORS?

CORS veut dire Cross Origin Resource Sharing ou en français partage des ressources inter origines. bon dit comme ça ça veut pas dire grand chose. En gros, quand vous faites une requêtes AJAX, vous le faites depuis (en formation informatique) votre poste local localhost. Imaginons que vous vouliez faire une requête AJAX vers le site OpenWeatherMap.org, vous voyez bien que le nom de domaine est différent de localhost. Eh bien on a là un cas de requêtes AJAX Cross Origin.

Pourquoi parle t on de CORS?

Le web est un monde ouvert où on peut faire à peu près n’importe quoi. Il y a des considération de sécurité, concept très important dans un monde ouvert. Ici avec le CORS on fait la distinction entre les requêtes AJAX internes et les requêtes AJAX venants d’un autre site web. Je trouve personnellement que c’est fantastique qu’il soit possible de partager des informations entre sites web.

Les requêtes AJAX internes

Quand vous développez un front en en Javascript (Javascript pur ou ReactJS ou Angular peu importe) vous faites des requêtes AJAX locales pour tirer des informations à afficher. Vous requêtez votre serveur qui est dans le même nom de domaine que le code Javascript qui fait la requête. Dans ce cas il n’est pas de problème de CORS, tout est sur un même domaine.

Ce cas de figure, c’est la majorité des applications modernes, donc vous n’êtes pas confronté au problème de CORS.

Les requêtes externes

Mais imaginez que vous développiez une application IOS ou Android, dans ce cas vous ferez face à des problème de CORS. En effet la requête réseau ne provient pas du site web du serveur, donc ce sera considéré comme une requête CORS. Si vous ne faites rien côté serveur vous n’allez pas pouvoir servir le contenu à l’application mobile.

Le problème du CORS se règle côté serveur et non côté client.

Ayez le réflexe de regarder du côté du serveur pour résoudre le problème de CORS. Vous ne pouvez rien faire du côté du client. Nous allons regarder un exemple avec un code PHP.

Supposons que le code serveur ci-dessous se trouve sur le domaine localhost:

http://localhost/index.php
echo json_encode(['message' => 'Bonjour']);

Voici le code côté client en javascript : http://localhost/caller.html

fetch('http://localhost/index.php')
.then(response => response.json())
.then( data => console.log(data))

Le fichier dans lequel réside le Javascript est dans le domaine localhost, donc pas de problème de CORS. Mais regardons le cas suivant

http://localhost/index.php
echo json_encode(['message' => 'Bonjour']);

Voici le code côté client en javascript : http://127.0.0.1/caller.html

fetch('http://localhost/index.php')
.then(response => response.json())
.then( data => console.log(data))

Cette fois ci nous avons u problème de CORS en effet localhost n’est pas pareil que 127.0.0.1 bien que d’un point de vu résolution de chemin ce soit la même chose !

Mise en place du CORS en PHP

Comme la solution est du côté du serveur, nous allons ajouter à la réponse du serveur des en-têtes pour permettre au CORS de fonctionner.

header('Content-Type: application/json');
header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');    // cache for 1 day

echo json_encode(['message' => 'Bonjour']);

Ce qui est important c’est le Access-Control-Allow-Origin, qui va permettre de faire du CORS. Ici on a mis un astérisque qui désigne tous les domaines, mais on peut restreindre à un domaine en particulier.

Les requêtes PREFLIGHT

Maintenant que vous savez faire du CORS affinons le sujet avec les requêtes Preflight. Elles sont envoyées par le navigateur pour “sonder” le serveur dans le cas de requêtes CORS. Le verbe HTTP est OPTIONS. Cette requête est automatiquement envoyée par le serveur, le développeurs front end n’a pas à s’en soucier.

Le serveur quand il reçoit cette requêtes OPTIONS doit répondre avec Access-Control-Allow-Method

header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );

Installer une application API Platform (Symfony) React avec Symfony CLI

Si vous ne l’avez pas déjà fait téléchargez symfony CLI sur le site officiel. C’est un binaire donc en fonction de votre plateforme, ce n’est pas le même programme.

Un peit mot avnagt de commencer,ici on va installer API Platform en tant que bundle de Symfony, et ReactJS dans un répertoire indépendant. Symfony propose aussi d’installer ReactJS comme un greffon de Twig, personnellement je n’ai pas réussi à le faire marcher, les composants ne se renderisaient pas. Je préfère quand le front et le back sont séparés (pas de monorepo), car le webservice doit servir d’autres plateformes (mobile par exemple)

Démarrage du projet

Vérifiez que vous êtes en PHP 8.2 (Recommandé)

symfony new bookshop-api
cd bookshop-api

Etape importante installation du bundle:

symfony composer require api

Création de la base de donnée:

symfony console doctrine:database:create
symfony console doctrine:schema:create

// si vous avez des soucis sous MAMP pour créer la base de données, sachez que e port de MySQL n'est aps 3306 et 


Puis lancement du serveur avec Symfony CLI
symfony serve

Accéder à votre site

Symfony CLI va vous donner votre url pour le site. Il faut savoir qu’avec API Platform, vous n’avez pas de front end pour votre site. Le front end va être assuré par ReactJs qui sera complètement indépendant dans sa structure de fichiers d’API Platform.

http://localhost:8000/

Votre webservice est accessible à l’url http://localhost:8000/api ! cela intéresse ReactJS. Voici ce que vous verrez si vous accéder à cette url.

L’interface que vous voyez est Swagger, une librairie de documentation interactive de votre webservice. Il est important lorsque vous travaillez avec API Platform que vous devez oubliez ce que vous avez appris avec une application Symfony classique sauf pour les entités.

Création d’une entité Book

Copiez le code suivant (extrait de la documentation officielle)

<?php
// src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/** A book. */
#[ORM\Entity]
#[ApiResource]
class Book
{
    /** The ID of this book. */

    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    /** The ISBN of this book (or null if doesn't have one). */
    #[ORM\Column(nullable: true)]
    public ?string $isbn = null;

    /** The title of this book. */
    #[ORM\Column]
    public string $title = '';

    /** The description of this book. */
    #[ORM\Column(type: 'text')]
    public string $description = '';

    /** The author of this book. */
    #[ORM\Column]
    public string $author = '';

    /** The publication date of this book. */
    #[ORM\Column]
    public ?\DateTimeImmutable $publicationDate = null;

    /** @var Review[] Available reviews for this book. */
    #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])]
    public iterable $reviews;

    public function __construct()
    {
        $this->reviews = new ArrayCollection();
    }

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

Allez maintenant dans l’adresse de la documentation de votre API http://localhost:8000/api

Créez une seconde entité Review

<?php
// src/Entity/Review.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/** A review of a book. */
#[ORM\Entity]
#[ApiResource]
class Review
{
    /** The ID of this review. */
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    /** The rating of this review (between 0 and 5). */
    #[ORM\Column(type: 'smallint')]
    public int $rating = 0;

    /** The body of the review. */
    #[ORM\Column(type: 'text')]
    public string $body = '';

    /** The author of the review. */
    #[ORM\Column]
    public string $author = '';

    /** The date of publication of this review.*/
    #[ORM\Column]
    public ?\DateTimeImmutable $publicationDate = null;

    /** The book this review is about. */
    #[ORM\ManyToOne(inversedBy: 'reviews')]
    public ?Book $book = null;

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

Voici ce que vous devriez voir dans la documentation

Les annotation Doctrines sont déjà présentes dans le code car je les ai déjà ajoutées. Vous devez les ajouter pour toute nouvelle entité que vous créez. C’et nécessaire pour faire la migration et ainsi créer des tables dans la base de données.

php bin/console make:entity --api-resource.  // permet d'annoter

Migration vers la base de données

Tapes les lignes suivantes pour faire la migration

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

Si la migration des tables est faites sans problème nous allons pouvoir commencer !

Opération CRUD avec Swagger

Nous n’allons pas Postman pour faire nos requêtes AJAX mais directement dans Swagger, c’est l’intérêt de l’outil.

Allez dans Swagger, Book puis cliquez sur POST, une fenêtre se déroule, cliquez sur “Try it out”. Collez le json suivant dans le champs “Request body”

{
  "isbn": "9781782164104",
  "title": "Persistence in PHP with the Doctrine ORM",
  "description": "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": "2013-12-01"
}

Puis cliquez sur Execute.

Vérifiez que dans la table Book vous avez une entrée correspondante. Passez à l’insertion d’une review

{
  "book_id": "/books/1", // ou "/api/books/1
  "rating": 5,
  "body": "Interesting book!",
  "author": "Kévin",
  "publicationDate": "September 21, 2016"
}

Ici on a saisi directement l’id du Book, dnas la réalité, vous devez faire un GET des Book, puis lire l’id de celui que vous voulez faire une Review.