API Platform (beta) : le framework web PHP nouvelle génération (partie 2)

Cette semaine, nous vous dévoilons la deuxième partie du tutoriel consacré à API Platform, un framework développé par notre gérant Kévin Dunglas et auquel nous contribuons activement. Cet article est également disponible en version anglaise.

Exposer l’API

Nous avons un modèle de données fonctionnel appuyé par une base de données. A présent, nous allons créer une API REST hypermédia grâce à un autre composant de Dunglas’ API Platform : DunglasApiBundle.

Comme PHP Schema, il est déjà préinstallé et configuré correctement. Nous avons juste besoin de déclarer les ressources que nous souhaitons exposer.

Exposer une collection de ressources consiste à enregistrer un nouveau service Symfony. Pour notre application de blog, nous allons exposer, à travers l’API, les deux classes d’entités générées par PHP Schema :

BlogPosting (article du blog) et Person  (auteur de l’article) :

# app/config/services.yml

services:
    resource.blog_posting:
        parent:    "api.resource"
        arguments: [ "AppBundle\\Entity\\BlogPosting" ]
        tags:      [ { name: "api.resource" } ]

    resource.person:
        parent:    "api.resource"
        arguments: [ "AppBundle\\Entity\\Person" ]
        tags:      [ { name: "api.resource" } ]

Et voilà, notre API est déjà terminée ! Il n’y a pas plus simple !

Démarrez le serveur web de développement intégré : app/console server:start

Puis ouvrez http://localhost:8000/doc avec un navigateur web.

Grâce au support de DunglasApiBundle par NelmioApiDocBundle et à son intégration avec API Platform, vous obtenez sans plus d’efforts une documentation de l’API lisible par les humains et automatiquement générée. Celle-ci comprend également un bac à sable pour tester l’API.

Vous pouvez également utiliser votre client HTTP préféré pour interroger l’API. Je vous recommande vivement d’utiliser Postman. Il est plus bas niveau que la sandbox et il vous permettra d’inspecter facilement les requêtes et réponses JSON.

Ouvrez http://localhost:8000 avec Postman. Cette URL est le point d’entrée de l’API. Il vous donne accès à toutes les ressources exposées. Comme vous pouvez le voir, l’API retourne du JSON-LD compressé. Pour une meilleure lisibilité, les snippets JSON ont été indentés dans ce document.

Essayer l’API

Ajoutez une personne appelée Kévin en lançant une requête POST sur http://localhost:8000/people avec le document JSON suivant comme corps brut :

{"name": "Kévin"}

Les données sont insérées dans la base de données. Le serveur répond avec une représentation en JSON-LD de la ressource nouvellement créée. Grâce à PHP Schema, la propriété @type du document JSON-LD fait référence à un type Schema.org :

{
    "@context": "/contexts/Person",
    "@id": "/people/1",
    "@type": "http://schema.org/Person",
    "name": "Kévin"
}

La spécification JSON-LD est pleinement supportée par le bundle. Vous voulez une preuve ? Parcourez http://localhost:8000/contexts/Person.

Par défaut, l’API autorise les méthodes HTTP GET (récupérer, pour les collections et les items), POST (créer), PUT (mettre à jour) et DELETE. Vous pouvez ajouter et supprimer n’importe quelle autre opération si vous le souhaitez. Essayez !

À présent, parcourez http://localhost:8000/people :

{
    "@context": "/contexts/Person",
    "@id": "/people",
    "@type": "hydra:PagedCollection",
    "hydra:totalItems": 1,
    "hydra:itemsPerPage": 30,
    "hydra:firstPage": "/people",
    "hydra:lastPage": "/people",
    "hydra:member": [
        {
            "@id": "/people/1",
            "@type": "http://schema.org/Person",
            "name": "Kévin"
        }
    ]
}

La pagination est également supportée.

Il est temps de publier notre premier article. Exécutez une requête POST sur http://locahost:8000/blog_posting avec le document JSON suivant dans son corps :

{
    "name": "Dunglas's API Platform is great",
    "headline": "You'll love that framework!",
    "articleBody": "The body of my article.",
    "articleSection": "technology",
    "author": "/people/1",
    "isFamilyFriendly": "maybe",
    "datePublished": "2015-05-11"
}

Oups… La propriété isFamilyFriendly est booléenne. Notre JSON contient une chaîne incorrecte. Fort heureusement, le bundle est assez intelligent pour détecter l’erreur : il utilise les annotations de validation Symfony générées précédemment par PHP Schema. Il retourne un message d’erreur détaillé au format de sérialisation des erreurs Hydra :

{
    "@context": "/contexts/ConstraintViolationList",
    "@type": "ConstraintViolationList",
    "hydra:title": "An error occurred",
    "hydra:description": "isFamilyFriendly: This value should be of type boolean.\n",
    "violations": [
        {
            "propertyPath": "isFamilyFriendly",
            "message": "This value should be of type boolean."
        }
    ]
}

Corrigez le corps et envoyez la requête de nouveau :

{
    "name": "Dunglas's API Platform is great",
    "headline": "You'll love that framework!",
    "articleBody": "The body of my article.",
    "articleSection": "technology",
    "author": "/people/1",
    "isFamilyFriendly": true,
    "datePublished": "2015-05-11"
}

Nous avons réglé le problème ! En passant, vous avez appris comment travailler avec des relations. Dans une API hypermédia, chaque ressource est identifiée par un IRI unique (une URL est un IRI). Les IRI se trouvent dans la propriété @id de chaque document JSON-LD généré par l’API et vous pouvez les utiliser comme références pour créer des relations comme nous l’avons fait dans le précédent extrait de code pour la propriété auteur.

Le framework API Platform est assez intelligent pour comprendre n’importe quel format de date supporté par les fonctions date PHP. En production, nous vous recommandons le format spécifié par le RFC 3339.

Nous avons déjà une API REST hypermédia puissante (toujours sans écrire la moindre ligne de PHP), mais ce n’est pas fini.

Notre API est auto-découvrable. Ouvrez http://localhost:8000/vocab et jetez un oeil au contenu. Les capacités de l’API sont entièrement décrites dans un format lisible par les machines : ressources disponibles, propriétés et opérations, description des éléments, propriétés lisibles et enregistrables, types retournés et attendus…

En ce qui concerne les erreurs, l’API toute entière est décrite avec le vocabulaire Hydra Core, un standard web ouvert pour décrire des APIs REST hypermédia en JSON-LD. Un client ou une librairie compatible avec Hydra est capable d’interagir avec l’API sans rien connaître d’elle ! Le client Hydra le plus populaire est Hydra Console. Utilisez-le pour ouvrir une URL de l’API et vous obtiendrez une interface de gestion plutôt jolie.

Vous pouvez aussi essayer la toute nouvelle bibliothèque Hydra Core Javascript.

DunglasApiBundle offre de nombreuses autres fonctionnalités incluant :

Lisez la documentation qui lui est dédiée pour voir comment les exploiter et comment y insérer votre code.

Spécifier et tester l’API

Behat (un framework de développement piloté par le comportement) est préconfiguré avec des contextes utiles pour spécifier et tester les documents REST API et JSON.

Avec Behat, vous pouvez écrire la spécification de l’API (comme les scénarios utilisateurs) dans un langage naturel puis exécuter des scénarios contre l’application pour valider son comportement.

Créez un fichier Gherkin contenant les scénarios que nous avons exécutés manuellement au chapitre précédent :

Feature: Blog
  In order to post news
  As a client software developer
  I need to be able to retrieve, create, update and delete authors and posts trough the API.

  # "@createSchema" creates a temporary SQLite database for testing the API
  @createSchema
  Scenario: Create a person
    When I send a "POST" request to "/people" with body:
    """
    {"name": "Kévin"}
    """
    Then the response status code should be 201
    And the response should be in JSON
    And the header "Content-Type" should be equal to "application/ld+json"
    And the JSON should be equal to:
    """
    {
      "@context": "/contexts/Person",
      "@id": "/people/1",
      "@type": "http://schema.org/Person",
      "name": "Kévin"
    }
    """

  Scenario: Retrieve the user list
    When I send a "GET" request to "/people"
    Then the response status code should be 200
    And the response should be in JSON
    And the header "Content-Type" should be equal to "application/ld+json"
    And the JSON should be equal to:
    """
    {
      "@context": "/contexts/Person",
      "@id": "/people",
      "@type": "hydra:PagedCollection",
      "hydra:totalItems": 1,
      "hydra:itemsPerPage": 30,
      "hydra:firstPage": "/people",
      "hydra:lastPage": "/people",
      "hydra:member": [
          {
              "@id": "/people/1",
              "@type": "http://schema.org/Person",
              "name": "Kévin"
          }
      ]
    }
    """

  Scenario: Throw errors when a post is invalid
    When I send a "POST" request to "/blog_postings" with body:
    """
    {
        "name": "Dunglas's API Platform is great",
        "headline": "You'll that framework!",
        "articleBody": "The body of my article.",
        "articleSection": "technology",
        "author": "/people/1",
        "isFamilyFriendly": "maybe",
        "datePublished": "2015-05-11"
    }
    """
    Then the response status code should be 400
    And the response should be in JSON
    And the header "Content-Type" should be equal to "application/ld+json"
    And the JSON should be equal to:
    """
    {
        "@context": "/contexts/ConstraintViolationList",
        "@type": "ConstraintViolationList",
        "hydra:title": "An error occurred",
        "hydra:description": "isFamilyFriendly: This value should be of type boolean.\n",
        "violations": [
            {
                "propertyPath": "isFamilyFriendly",
                "message": "This value should be of type boolean."
            }
        ]
    }
    """

  # "@dropSchema" is mandatory to cleanup the temporary database on the last scenario
  @dropSchema
  Scenario: Post a new blog post
    When I send a "POST" request to "/blog_postings" with body:
    """
    {
        "name": "Dunglas's API Platform is great",
        "headline": "You'll that framework!",
        "articleBody": "The body of my article.",
        "articleSection": "technology",
        "author": "/people/1",
        "isFamilyFriendly": true,
        "datePublished": "2015-05-11"
    }
    """
    Then the response status code should be 201
    And the response should be in JSON
    And the header "Content-Type" should be equal to "application/ld+json"
    And print last JSON response
    And the JSON should be equal to:
    """
    {
      "@context": "/contexts/BlogPosting",
      "@id": "/blog_postings/1",
      "@type": "http://schema.org/BlogPosting",
      "articleBody": "The body of my article.",
      "articleSection": "technology",
      "author": "/people/1",
      "datePublished": "2015-05-11T00:00:00+02:00",
      "headline": "You'll that framework!",
      "isFamilyFriendly": true,
      "name": "Dunglas's API Platform is great"
    }
    """

L’intégration de Behat fournie par API Platform comporte également une base de données SQLite temporaire dédiée aux tests. Elle est prête à l’emploi.

Exécutez simplement bin/behat. Tout devrait être vert :

4 scenarios (4 passed)
21 steps (21 passed)

Vous obtenez alors une puissante API hypermédia exposant des données structurées, spécifiée et testée grâce à Behat. Et toujours sans aucune ligne de PHP !

C’est très utile pour le prototypage et le développement rapide d’applications (RAD). Mais le framework est conçu pour fonctionner dans un environnement de production. Il bénéficie de solides points d’extension et a été optimisé pour les sites à très haut trafic (API Platform propulse la nouvelle version du site web d’un grand média international).

Autres fonctionnalités

API Platform dispose d’un grand nombre d’autres fonctionnalités et peut être étendu avec les bibliothèques PHP et les bundles Symfony. Restez connectés, une documentation plus fournie et d’autres livres de recettes sont en préparation !

Voici une liste non exhaustive de ce que vous pouvez faire avec API Platform :