Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building APIs in an easy way using API Platform

Building APIs in an easy way using API Platform

The web has changed! Users spend more time on mobile than on desktops and expect to have an amazing user experience on both. APIs are the heart of the new web as the central point of access data, encapsulating logic and providing the same data and same features for desktops and mobiles.

In this workshop, Paula and Antonio will show you how to create complex APIs in an easy and quick way using API Platform built on Symfony.

Repository: https://github.com/locastic/wscAPI2017

Antonio Peric-Mazar

September 11, 2017
Tweet

More Decks by Antonio Peric-Mazar

Other Decks in Programming

Transcript

  1. Building APIs in an easy
    way using API Platform
    Paula Čučuk, Antonio Perić-Mažar

    30.08.2017., #websc

    View Slide

  2. Paula Čučuk
    Software Developer
    [email protected]
    @paulala_14

    View Slide

  3. Antonio Perić-Mažar
    CEO, Software Developer
    [email protected]
    @antonioperic

    View Slide

  4. Locastic
    • We help clients create amazing web and mobile apps (since 2011)
    • mobile development
    • web development
    • UX/UI
    • Training and Consulting
    • Shift Conference, Symfony Croatia
    • www.locastic.com @locastic

    View Slide

  5. View Slide

  6. Why API?

    View Slide

  7. View Slide

  8. The web has changed
    • Javascript webapps are standard (SPA)
    • Users spend more time on using mobile devices than desktop or TV.
    • Linked Data and the semantic web are a reality

    View Slide

  9. APIs are the heart of this new web
    • Central point to access data (R/W data)
    • Encapsulate business logic
    • Same data and same features for desktops, mobiles, TVs and etc
    • It is stateless (PHP Sessions make horizontal scaling harder)

    View Slide

  10. Client Apps
    • HTML5 (SPA), mobile apps, TVs, Cars etc.
    • Holds all the presentation logic
    • Is downloaded first (SPA, shell model)
    • Queries the API to retrieve and modify data using AJAX
    • Is 100% composed of HTML, JavaScript and assets (CSS and etc)
    • Can be hosted on a CDN

    View Slide

  11. View Slide

  12. Immediate benefits
    • Speed (even on mobile)
    • Scalability and robustness
    • Development comfort
    • Long term benefits

    View Slide

  13. Immediate benefits
    • Speed (even on mobile)
    • Scalability and robustness
    • Development comfort
    • Long term benefits
    • … and no, SEO and SMO are no more drawbacks

    View Slide

  14. Formats, standards, patterns

    View Slide

  15. HTTP + REST + JSON
    • Work everywhere
    • Lightweight
    • Stateless
    • HTTP has a powerful caching model
    • Extensible (JSON-LD, Hydra, Swagger, HAL…)
    • High quality tooling

    View Slide

  16. HATEOAS / Linked Data
    • Hypermedia as the Engine of Application State
    • Hypermedia: IRI as identifier
    • Ability to reference external data (like hypertext links)
    • Auto discoverable <=> Generic clients

    View Slide

  17. View Slide

  18. JSON-LD (JSON for Linked Data)
    • Standard: W3C recommandation (since 2014)
    • Machine readable data
    • Easy to use: looks like a typical JSON document
    • Already used by Gmail, GitHub, BBC, Microsoft, US gov…
    • Compliant with technologies of the semantic web: RDF, SPARQL, triple store…
    • Good for SEO

    View Slide

  19. Hydra
    • Describe REST APIs in JSON-LD
    • = write support
    • = auto-discoverable APIs
    • = standard for collections, paginations, errors, filters
    • Draft W3C (Work In Progress)

    View Slide

  20. {
    "@context": "/contexts/Book",
    "@id": "/books/2",
    "@type": "http://schema.org/Book",
    "id": 2,
    "isbn": "9790070863971",
    "description": "A long but very interesting story about REST and asyncio.",
    "author": "The life!",
    "title": "X",
    "publicationDate": "2002-01-29T00:00:00+00:00"
    }

    View Slide

  21. {
    "@context": "contexts/Errors",
    "@type": “hydra:Error”,
    “hydra:title”: “An error occurred”,
    “hydra:description”: “Not found”
    }

    View Slide

  22. {
    "@context": "/contexts/Book",
    "@id": "/books",
    "@type": "hydra:Collection",
    "hydra:member": [
    {
    "@id": "/books/2",
    "@type": "http://schema.org/Book",
    "id": 2,
    "isbn": "9790070863971",
    "description": "A long but very interesting story about REST and asyncio.",
    "author": "The life!",
    "title": "X",
    "publicationDate": "2002-01-29T00:00:00+00:00"
    },

    {
    "@id": "/books/31",
    "@type": "http://schema.org/Book",
    "id": 31,
    "isbn": "9791943452827",
    "description": "Tempora voluptas ut dolorem voluptates. Provident natus ipsam fugiat est ipsam quia. Sint mollitia sed facere qui
    sit. Ad iusto molestias iusto autem laboriosam nulla earum eius.",
    "author": "Miss Gladyce Nader I",
    "title": "Voluptas doloremque esse dolor qui illo placeat harum voluptatem.",
    "publicationDate": "1970-10-11T00:00:00+00:00"
    }
    ],
    "hydra:totalItems": 125,
    "hydra:view": {
    "@id": "/books?page=1",
    "@type": "hydra:PartialCollectionView",
    "hydra:first": "/books?page=1",
    "hydra:last": "/books?page=5",
    "hydra:next": "/books?page=2"
    }
    }

    View Slide

  23. View Slide

  24. View Slide

  25. API Platform: the promise
    • Fully featured API supporting Swagger + JSON-LD + Hydra + HAL in minutes
    • An auto generated doc
    • Convenient API spec and test tools using Behat
    • Easy authentication management with JWT or OAuth
    • CORS and HTTP cache
    • All the tools you love: Doctrine ORM, Monolog, Swiftmailer...

    View Slide

  26. API Platform <3 Symfony
    • Built on top of Symfony full-stack
    • Install any existing SF bundles
    • Follow SF Best Practices
    • Use your Symfony skills
    • Can be used in your existing SF app
    • (Optional) tightly integrated with Doctrine

    View Slide

  27. Features
    • CRUD
    • Filters
    • Serializations groups and relations
    • Validation
    • Pagination
    • The event system
    • Content Negotion
    • Extensions
    • HTTP and reverse proxy caching
    • JS Admin apps
    • etc

    View Slide

  28. Coding session
    https://github.com/locastic/wscAPI2017

    View Slide

  29. Ping Pong Score
    keeping API

    View Slide

  30. Setup
    $ git fetch --all
    $ git pull
    $ php bin/console doctrine:schema:update —force

    View Slide

  31. #1 Task
    Player CRUD

    View Slide

  32. CRUD
    namespace AppBundle\Entity;
    use FOS\UserBundle\Model\User as BaseUser;
    class Player extends BaseUser
    {
    protected $id;
    private $firstName;
    private $lastName;
    protected $email;
    // ...
    }
    # UserBundle/Resources/config/
    api_resources/resources.yml
    resources:
    AppBundle\Entity\Player:~

    View Slide

  33. View Slide

  34. schema.org

    View Slide

  35. View Slide

  36. View Slide

  37. View Slide

  38. {
    "@context": "http://schema.org",
    "@type": "FlightReservation",
    "reservationNumber": "RXJ34P",
    "reservationStatus": "http://schema.org/Confirmed",
    "underName": {
    "@type": "Person",
    "name": "Eva Green"
    },
    "reservationFor": {
    "@type": "Flight",
    "flightNumber": "110",
    "airline": {
    "@type": "Airline",
    "name": "United",
    "iataCode": "UA"
    },
    "departureAirport": {
    "@type": "Airport",
    "name": "San Francisco Airport",
    "iataCode": "SFO"
    },
    "departureTime": "2017-03-04T20:15:00-08:00",
    "arrivalAirport": {
    "@type": "Airport",
    "name": "John F. Kennedy International Airport",
    "iataCode": "JFK"
    },
    "arrivalTime": "2017-03-05T06:30:00-05:00"
    }
    }

    View Slide

  39. Using schema.org in Api Platform
    resources:
    AppBundle\Entity\FlightReservation:
    iri: 'http://schema.org/FlightReservation'

    View Slide

  40. Using schema.org in Api Platform
    resources:
    AppBundle\Entity\FlightReservation:
    iri: 'http://schema.org/FlightReservation'
    properties:
    status:
    iri: 'http://schema.org/reservationStatus'

    View Slide

  41. Your turn!
    #1 Task

    View Slide

  42. #2 Task
    Add serialization & deserialization
    groups to Player

    View Slide

  43. Serialization Groups
    • API Platform Core allows to choose which attributes of the resource are
    exposed during the normalization (read) and denormalization (write) process. It
    relies on the serialization (and deserialization) groups feature of the Symfony
    Serializer component.
    • allows to specify the definition of serialization using XML, YAML, or annotations.

    View Slide

  44. Serialization Groups

    View Slide

  45. namespace AppBundle\Entity;
    use FOS\UserBundle\Model\User as BaseUser;
    use Symfony\Component\Serializer\Annotation\Groups;
    class Player extends BaseUser
    {
    /**
    * @var int
    */
    protected $id;
    /**
    * @Groups({"player_read", "player_write"})
    */
    private $firstName;
    /**
    * @Groups({"player_read", "player_write"})
    */
    private $lastName;
    /**
    * @Groups({"player_read", "player_write"})
    */
    protected $email;
    // ...
    }
    # UserBundle/Resources/api_resources/resources.yml
    resources:
    AppBundle\Entity\Player:
    attributes:
    normalization_context:
    groups: ['player_read']
    denormalization_context:
    groups: ['player_write']

    View Slide

  46. Using Different Serialization Groups
    per Operation
    # UserBundle/Resources/api_resources/resources.yml
    resources:
    AppBundle\Entity\Player:
    itemOperations:
    get:
    method: 'GET'
    normalization_context:
    groups: ['player_read', 'player_extra']
    put:
    method: 'PUT'
    delete:
    method: 'DELETE'
    attributes:
    normalization_context:
    groups: ['player_read']
    denormalization_context:
    groups: ['player_write']

    View Slide

  47. Your turn!
    #2 Task

    View Slide

  48. #3 Task
    Implement Authentication using JSON
    Web Token (JWT)

    View Slide

  49. JSON Web Token (JWT)
    • Lightweight and simple authentication system
    • Stateless: token signed and verified server-side then stored client-side and sent
    with each request in an Authorization header
    • Store the token in the browser local storage

    View Slide

  50. View Slide

  51. View Slide

  52. API and JWT Integration
    • We need to install and configure
    • LexikJWTAuthenticationBundle
    • JWTRefreshTokenBundle

    View Slide

  53. View Slide

  54. View Slide

  55. Your turn!
    #3 Task

    View Slide

  56. #4 Task
    Create event subscriber for posting new
    Player

    View Slide

  57. Api platform events

    View Slide

  58. // src/AppBundle/EventSubscriber/MatchEventSubscriber.php
    class MatchEventSubscriber implements EventSubscriberInterface
    {
    private $matchHelper;
    public function __construct(MatchHelper $matchHelper)
    {
    $this->matchHelper = $matchHelper;
    }
    public static function getSubscribedEvents()
    {
    return [
    KernelEvents::VIEW => [['addWinner', EventPriorities::POST_VALIDATE]],
    ];
    }
    public function addWinner(GetResponseForControllerResultEvent $event)
    {
    $match = $event->getControllerResult();
    $method = $event->getRequest()->getMethod();
    if(!$match instanceof Match || $method !== 'POST') {
    return;
    }
    $winner = $this->matchHelper->getWinner($match);
    $match->setWinner($winner);
    }
    }

    View Slide

  59. Your turn!
    #4 Task

    View Slide

  60. #5 Task
    Match CRUD and pagination config

    View Slide

  61. Pagination
    # app/config/config.yml
    api_platform:
    # ...
    collection:
    pagination:
    items_per_page: 30 # Default value
    client_items_per_page: true # Disabled by default
    items_per_page_parameter_name: itemsPerPage # Default value
    client_enabled: true # optional
    enabled_parameter_name: pagination # optional
    page_parameter_name: _page # optional

    View Slide

  62. Your turn!
    #5 Task

    View Slide

  63. #6 Task
    Add matchesWon number to GET
    single Player endpoint

    View Slide

  64. Operations
    • API Platform Core relies on the concept of operations. Operations can be
    applied to a resource exposed by the API. From an implementation point of
    view, an operation is a link between a resource, a route and its related
    controller.
    • There are two types of operations:
    • Collection operations act on a collection of resources. By default two routes
    are implemented: POST and GET.
    • Item operations act on an individual resource. 3 default routes are
    defined GET, PUT and DELETE.

    View Slide

  65. Custom operation
    class Match
    {
    private $id;
    /**
    * @Groups({"match_read"})
    */
    private $datetime;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerOnePoints;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerTwoPoints;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerOne;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerTwo;
    /**
    * @Groups({"match_read"})
    */
    private $winner;
    /**
    * @Groups({"match_read"})
    */
    private $result;
    }

    View Slide

  66. Custom operation
    class Match
    {
    private $id;
    /**
    * @Groups({"match_read"})
    */
    private $datetime;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerOnePoints;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerTwoPoints;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerOne;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerTwo;
    /**
    * @Groups({"match_read"})
    */
    private $winner;
    /**
    * @Groups({"match_read"})
    */
    private $result;
    }
    class MatchController extends Controller
    {
    /**
    * @param Match $data
    *
    * @return Match
    */
    public function getMatchAction($data)
    {
    $result = $data->getPlayerOne() . ' ' . $data->getPlayerOnePoints().':'
    .$data->getPlayerTwoPoints() . ' ' . $data->getPlayerTwo();
    $data->setResult($result);
    return $data;
    }
    }

    View Slide

  67. Custom operation
    class Match
    {
    private $id;
    /**
    * @Groups({"match_read"})
    */
    private $datetime;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerOnePoints;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerTwoPoints;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerOne;
    /**
    * @Groups({"match_read", "match_write"})
    */
    private $playerTwo;
    /**
    * @Groups({"match_read"})
    */
    private $winner;
    /**
    * @Groups({"match_read"})
    */
    private $result;
    }
    class MatchController extends Controller
    {
    /**
    * @param Match $data
    *
    * @return Match
    */
    public function getMatchAction($data)
    {
    $result = $data->getPlayerOne() . ' ' . $data->getPlayerOnePoints().':'
    .$data->getPlayerTwoPoints() . ' ' . $data->getPlayerTwo();
    $data->setResult($result);
    return $data;
    }
    }
    # app/config/routing.yml
    get_match:
    path: /api/v1/matches/{id}.{_format}
    methods: ['GET']
    defaults:
    _controller: AppBundle:Match:getMatch
    _api_resource_class: AppBundle\Entity\Match
    _api_item_operation_name: get

    View Slide

  68. Your turn!
    #6 Task

    View Slide

  69. #7 Task
    Create /me/matches endpoint which
    returns all matches current user played.

    View Slide

  70. Extensions
    • API Platform Core provides a system to extend queries on items and collections.
    • Custom extensions must implement
    the ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\Query
    CollectionExtensionInterface and / or
    the ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\Query
    ItemExtensionInterface interfaces, to be run when querying for a
    collection of items and when querying for an item respectively.

    View Slide

  71. class GetPlayersExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
    {
    public function applyToItem(
    QueryBuilder $queryBuilder,
    QueryNameGeneratorInterface $queryNameGenerator,
    string $resourceClass,
    array $identifiers,
    string $operationName = null,
    array $context = []
    ) {
    $this->addWhere($queryBuilder, $resourceClass, $operationName);
    }
    public function applyToCollection(
    QueryBuilder $queryBuilder,
    QueryNameGeneratorInterface $queryNameGenerator,
    string $resourceClass,
    string $operationName = null
    ) {
    $this->addWhere($queryBuilder, $resourceClass, $operationName);
    }
    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName = null)
    {
    if ($resourceClass != Player::class || $operationName != 'get') {
    return;
    }
    $rootAlias = $queryBuilder->getRootAliases()[0];
    $queryBuilder->andWhere(
    $queryBuilder->expr()->eq($rootAlias.'.enabled', ':enabled')
    )->setParameter('enabled', true);
    }
    }

    View Slide

  72. services:
    app.extension.get_players:
    class: AppBundle\Doctrine\ORM\Extension\GetPlayersExtension
    public: false
    tags:
    - { name: api_platform.doctrine.orm.query_extension.collection, priority: 9 }
    - { name: api_platform.doctrine.orm.query_extension.item }

    View Slide

  73. Your turn!
    #7 Task

    View Slide

  74. #8 Task
    Add some filters

    View Slide

  75. Filters
    • If Doctrine ORM support is enabled, adding filters is as easy as registering a filter
    service in your app/config/services.yml file and adding an attribute to
    your resource configuration.
    • Filters add extra conditions to base database query
    • Useful filters for the Doctrine ORM are provided with the library. You can also
    create custom filters that would fit your specific needs.

    View Slide

  76. Filters
    • Search filter (partial, start, end, exact, ipartial, iexact)
    • Date filter (?property[]=value )
    • Boolean filter (?property=[true|false|1|0])
    • Numeric filter (?property=int|bigint|decimal)
    • Range filter (?property[lt]|[gt]|[lte]|[gte]|[between]=value)
    • Order filter (?order[property]=)
    • Custom filters

    View Slide

  77. Filters examples
    # AppBundle/Resources/config/api_resources/resources.yml
    resources:
    AppBundle\Entity\Player:
    # ...
    attributes:
    filters: ['player.search', 'player.order']
    AppBundle\Entity\Match:
    # ...
    attributes:
    filters: ['match.date']
    services:
    player.search_filter:
    parent: 'api_platform.doctrine.orm.search_filter'
    arguments: [ { id: 'exact', email: 'exact', firstName: 'partial' } ]
    tags: [ { name: 'api_platform.filter', id: 'player.search' } ]
    match.date_filter:
    parent: 'api_platform.doctrine.orm.date_filter'
    arguments: [ { datetime: ~ } ]
    tags: [ { name: 'api_platform.filter', id: 'match.date' } ]
    player.order_filter:
    parent: 'api_platform.doctrine.orm.order_filter'
    arguments: [{ firstName: 'ASC', lastName: 'ACS', email: ~ }]
    tags: [{ name: 'api_platform.filter', id: 'player.order' }]

    View Slide

  78. Your turn!
    #8 Task

    View Slide

  79. More features

    View Slide

  80. Better documentation

    View Slide

  81. Per Resource Authorization Mechanism
    namespace AppBundle\Entity;
    use ApiPlatform\Core\Annotation\ApiResource;
    use Doctrine\ORM\Mapping as ORM;
    /**
    * @ApiResource(
    * attributes={"is_granted"="has_role('ROLE_ADMIN')"},
    * itemOperations={
    * "get"={"method"="GET", "is_granted"="object.getOwner() == user"}
    * }
    * )
    * @ORM\Entity
    */
    class Secured
    {
    /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    public $id;
    /**
    * @ORM\Column(type="text")
    */
    public $owner;

    View Slide

  82. Subresources Support
    /**
    * @ApiResource
    */
    class Product {
    /**
    * @ApiProperty(subcollection=true)
    */
    public $reviews;
    }
    /**
    * http://example.com/products/1/reviews
    */

    View Slide

  83. Cache invalidation is builtin

    View Slide

  84. Specs and tests with Behat
    Behat and its Behatch extension make testing and API easy.
    # features/put.feature
    Scenario: Update a resource
    When I send a "PUT" request to "/people/1" with body:
    """
    {
    "name": "Kevin"
    }
    """
    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/1",
    "@type": "Person",
    "name": "Kevin",
    "address": null
    }
    """

    View Slide

  85. More features
    • ReactJS Based Admin generator
    • A React/Redux Webapp Generator
    • AngularJS app bootstrap
    • Symfony Flex support
    • Brand new docker setup (with varnish)

    View Slide

  86. Planned features for 2.2
    • JSONAPI support
    • MongoDB native
    • GraphQL support

    View Slide

  87. Thank you!

    View Slide

  88. QA?

    View Slide