À la découverte de

API Platform

Kévin Dunglas

Les-Tilleuls.coop

1985

3615 ulla

2000

Le web
Twig, Assetic, Composant Form, CSRF...

L'édition standard de Symfony est toujours orientée pour développer ce type d'applications.

2015 ?

API-centric

Ils exposeront et consommeront vos données :

  • SPA et sites Responsive
  • applis mobiles natives
  • applis lourdes Linux / Windows / Mac
  • clients, fournisseurs, partenaires
  • moteurs de recherche, aggrégateurs, annuaires, comparateurs de prix, places de marché...

API : état de l'art

JSON

JavaScript Object Notation

  • Léger : efficace pour le mobile
  • Standard (RFC 7159, ECMA-404)
  • Fonctionne partout

REST

REpresentational State Transfer

  • Style d'architecture orienté ressources
  • Repose sur HTTP
  • stateless
  • Contrairement à RPC ou à SOAP, n'est ni un format, ni un protocole

HATEOAS / Linked Data

Hypermedia as the Engine of Application State

  • Hypermédia : IRI comme identifiants des ressources
  • Possibilité de référencer des données externes (similaire aux liens hypertextes)
  • Clients génériques

Vocabulaires

  • Sens commun aux données de différentes sources
  • Composés de classes, propriétés et relations
  • Schema.org, GoodRelations, FOAF, Dublin Core...

Formats

Un peu de ménage

JSON-LD

JSON for Linked Data

  • Standard : recommandation du W3C (depuis 2014)
  • Facile à prendre en main : ressemble à la manière "classique"
  • Adopté par Gmail, GitHub, la BBC, Microsoft, US gov...
  • Compatible avec les technologies du web sémantique (RDF, SPARQL, triple store...)

Schema.org

  • Définit un grand nombre d'éléments courants : personnes, publications, évènement, produits, avis, élèments chimiques et médicaux...
  • Créé et pris en charge par Google, Bing, Yahoo! et Yandex
  • Adoption massive, maintenant sous l'égide du W3C (Web schemas group)
  • Représentable sous forme de HTML (microdata), RDF (RDFa) et JSON-LD
  • Extensible

Hydra

  • Permet de décrire des API REST en JSON-LD
  • = support de l'écriture
  • = API auto-découvrable
  • = collections avec pagination
  • Draft W3C (Work In Progress)

Ce que ça donne


{
    "@context": "http://schema.org",
    "@type": "Event",
    "location": {
        "@type": "Place",
        "address": {
            "@type": "PostalAddress",
            "addressLocality": "Denver",
            "addressRegion": "CO",
            "postalCode": "80209",
            "streetAddress": "7 S. Broadway"
        },
        "name": "The Hi-Dive"
    },
    "name": "Typhoon with Radiation City",
    "offers": {
        "@type": "Offer",
        "price": "13.00",
        "priceCurrency": "USD",
        "url": "http://www.ticketfly.com/purchase/309433"
    },
    "startDate": "Sat Sep 14"
}
                        

Outillage Symfony

bundles de très bonne facture, mais :

  • Beaucoup de boilerplate code à écrire
  • Pas de support de JSON-LD ni de Hydra
  • Courbe d'apprentissage lente : beaucoup d'outils à maîtriser et intégrer
  • JMSSerializer = licence Apache 2 = GPL v2 (Drupal, phpBB...)

Dunglas's API Platform

Une solution facile à prendre en main, évolutive et intégrée pour réaliser des API REST.

github.com/dunglas/api-platform
bookstore-api.dunglas.fr (si t'as curl sur ton iPhone)

Une édition de Symfony avec

  • Un générateur de modèle utilisant Schema.org
  • Une API JSON-LD / Hydra qui just works)
  • Une belle documentation autogénérée (NelmioApiDoc inside)
  • Une plate-forme de test pour l'API (Behat + Behatch inside)
  • Authentification aisée avec JSON Web Token (LexikJWTAuthentication) ou OAuth (FOSOAuthServer)
  • En-têtes CORS (NelmioCors) et cache (FOSHttpCache)
  • Doctrine ORM, Monolog, Swiftmailer... : Symfony quoi

Getting started

composer create-project dunglas/api-platform --stability=dev my-api

Générer les classes d'entité avec

PHP Schema

php-schema.dunglas.com
github.com/dunglas/php-schema

Étape 1

Indiquer les types et propriétés Schema.org que l'on souhaite dans un fichier de config :

namespaces:
  entity: AppBundle\Entity
types:
  Thing:
    properties:
      name: ~
      url: ~
  Person:
    properties:
      birthDate: ~
      gender: ~
                        

Étape 2

Générer

bin/schema generate-types src/ app/config/schema.yml

Grâce aux types Schema.org, j'obtiens :

  • Des classes, propriétés, getters et setters (PSR compliant)
  • Un mapping Doctrine ORM fonctionnel (y compris pour les relations et l'héritage)
  • Les contraintes de validation (composant Validator)
  • Une PHPDoc complète
  • (optionnel) des interfaces
  • (optionnel) le mapping Doctrine ResolveTargetEntity
  • (optionnel) le support des vocabulaires pour l'API JSON-LD

Exemple de code généré


<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * A person (alive, dead, undead, or fictional).
 *
 * @see http://schema.org/Person Documentation on Schema.org
 *
 * @ORM\Entity
 */
class Person extends Thing
{
    /**
     * @var integer
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @var string An additional name for a Person, can be used for a middle name.
     * @Assert\Type(type="string")
     * @ORM\Column(nullable=true)
     */
    private $additionalName;
    /**
     * @var PostalAddress Physical address of the item.
     * @ORM\ManyToOne(targetEntity="PostalAddress")
     */
    private $address;
    /**
     * @var \DateTime Date of birth.
     * @Assert\Date
     * @ORM\Column(type="date", nullable=true)
     */
    private $birthDate;
    /**
     * @var string Email address.
     * @Assert\Email
     * @ORM\Column(nullable=true)
     */
    private $email;
    /**
     * @var string Family name. In the U.S., the last name of an Person. This can be used along with givenName instead of the name property.
     * @Assert\Type(type="string")
     * @ORM\Column(nullable=true)
     */
    private $familyName;
    /**
     * @var string Gender of the person.
     * @Assert\Type(type="string")
     * @ORM\Column(nullable=true)
     */
    private $gender;
    /**
     * @var string Given name. In the U.S., the first name of a Person. This can be used along with familyName instead of the name property.
     * @Assert\Type(type="string")
     * @ORM\Column(nullable=true)
     */
    private $givenName;
    /**
     * @var string The job title of the person (for example, Financial Manager).
     * @Assert\Type(type="string")
     * @ORM\Column(nullable=true)
     */
    private $jobTitle;
    /**
     * @var string The telephone number.
     * @Assert\Type(type="string")
     * @ORM\Column(nullable=true)
     */
    private $telephone;

    /**
     * Sets id.
     *
     * @param  integer $id
     * @return $this
     */
    public function setId($id)
    {
        $this->id = $id;

        return $this;
    }

    /**
     * Gets id.
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Sets additionalName.
     *
     * @param  string $additionalName
     * @return $this
     */
    public function setAdditionalName($additionalName)
    {
        $this->additionalName = $additionalName;

        return $this;
    }

    /**
     * Gets additionalName.
     *
     * @return string
     */
    public function getAdditionalName()
    {
        return $this->additionalName;
    }

    /**
     * Sets address.
     *
     * @param  PostalAddress $address
     * @return $this
     */
    public function setAddress(PostalAddress $address)
    {
        $this->address = $address;

        return $this;
    }

    /**
     * Gets address.
     *
     * @return PostalAddress
     */
    public function getAddress()
    {
        return $this->address;
    }

    /**
     * Sets birthDate.
     *
     * @param  \DateTime $birthDate
     * @return $this
     */
    public function setBirthDate(\DateTime $birthDate)
    {
        $this->birthDate = $birthDate;

        return $this;
    }

    /**
     * Gets birthDate.
     *
     * @return \DateTime
     */
    public function getBirthDate()
    {
        return $this->birthDate;
    }

    /**
     * Sets email.
     *
     * @param  string $email
     * @return $this
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

    /**
     * Gets email.
     *
     * @return string
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Sets familyName.
     *
     * @param  string $familyName
     * @return $this
     */
    public function setFamilyName($familyName)
    {
        $this->familyName = $familyName;

        return $this;
    }

    /**
     * Gets familyName.
     *
     * @return string
     */
    public function getFamilyName()
    {
        return $this->familyName;
    }

    /**
     * Sets gender.
     *
     * @param  string $gender
     * @return $this
     */
    public function setGender($gender)
    {
        $this->gender = $gender;

        return $this;
    }

    /**
     * Gets gender.
     *
     * @return string
     */
    public function getGender()
    {
        return $this->gender;
    }

    /**
     * Sets givenName.
     *
     * @param  string $givenName
     * @return $this
     */
    public function setGivenName($givenName)
    {
        $this->givenName = $givenName;

        return $this;
    }

    /**
     * Gets givenName.
     *
     * @return string
     */
    public function getGivenName()
    {
        return $this->givenName;
    }

    /**
     * Sets jobTitle.
     *
     * @param  string $jobTitle
     * @return $this
     */
    public function setJobTitle($jobTitle)
    {
        $this->jobTitle = $jobTitle;

        return $this;
    }

    /**
     * Gets jobTitle.
     *
     * @return string
     */
    public function getJobTitle()
    {
        return $this->jobTitle;
    }

    /**
     * Sets telephone.
     *
     * @param  string $telephone
     * @return $this
     */
    public function setTelephone($telephone)
    {
        $this->telephone = $telephone;

        return $this;
    }

    /**
     * Gets telephone.
     *
     * @return string
     */
    public function getTelephone()
    {
        return $this->telephone;
    }
}
                        

PHP Schema est un outil de scaffolding

Ajoutez vos propres classes, propriétés, index, contraintes de validation etc...

Créer votre API avec

JsonLdApiBundle

github.com/dunglas/DunglasJsonLdApiBundle

On définit la ressource à exposer


# app/config/services.yml

services:
    person_resource:
        parent:    "dunglas_json_ld_api.resource"
        arguments: [ "AppBundle\Entity\Person" ]
        tags:      [ { name: "json-ld.resource" } ]
                        

Et pour les cas simples...

OK... Mais ça fait quoi en vrai ?

Ça utilise ces métadonnées pour exposer et documenter une API REST JSON-LD / Hydra :

  • Doctrine ORM
  • Composant Validator
  • Composer Serializer (sf 2.7)
  • PHPDoc
  • Les vôtres (oui, on peut enregistrer des loaders perso)

On a donc, en quelques lignes :

  • CRUD complet (GET, POST, PUT, DELETE)
  • Validation des données et retour
  • Pagination des collections
  • serialization des erreurs (thx @sroze)
  • Enregistrement automatique des routes
  • Filtres sur les propriétés
  • Tris (très bientôt - thx @theofidry)
  • Un point d'entrée hypermédia
  • La gestion transparente des dates

Support complet JSON-LD / Hydra / Schema.org

Hydra Console Ça marche même dans Hydra Console !

Documentation utilisateur et sandbox

NelmioApiDoc Expérimental : NelmioApiDoc (dev-master) détecte automatiquement les resources exposées par le bundle.

Et c'est complétement extensible

  • Système d'évènement puissant (ex : mail après un POST)
  • Support des groupes de validation
  • Support des groupes de serialization (Symfony 2.7)
  • Support des structures imbriquées (ex : produits et offres avec 1 seule requête)
  • Enregistrement d'opérations personnalisées
  • Formats d'entrées / sorties personnalisés (via le composant Serializer)
  • Filtres et tris personnalisés
  • Classes Resource et contrôleurs personnalisés

Mais on n'a que 40 minutes...

La v0.1.0, c'est quand ?

En même temps que Symfony 2.7 :

dans 1 mois

Mais certains l'utilisent déjà :

Smile Les-Tilleuls.coop ExaqtWorld

Bientôt

  • Site dédié
  • Plus de doc
  • Support commercial
Thank you
PS : On recrute ! Contactez-nous : [email protected]

Bonus track

Les clients web

Quand le SEO n'a pas d'importance :

  • Web app
  • Back-office
  • Intranet

Faites-vous plaisir !

  • Testé et approuvé avec des SPA AngularJS (mais toutes les technos JS sont OK)
  • Doc d'intégration avec Restangular disponible
  • Le CORS sont supportés par défaut
  • API : un dépôt GIT, une URL propre
  • Web app : un dépôt GIT, une URL propre

Quand le SEO est important

Le saviez-vous ?

Depuis quelques mois, Google indexe les sites en full-JavaScript

D'ici peu les SPA seront la règle, en attendant :

  • App cliente séparée qui requête l'API : Symfony (sans Doctrine) + Guzzle + Twig
  • Données structurées de l'API dans le HTML (supporté par Google)

                            <script type="application/ld+json">
                            { "@context" : "http://schema.org",
                            "@type" : "Organization",
                            "url" : "http://www.your-company-site.com",
                            "contactPoint" : [
                            { "@type" : "ContactPoint",
                            "telephone" : "+1-401-555-1212",
                            "contactType" : "customer service"
                            } ] }
                            </script>