Creating the API
It's time to create the API. The Symfony ecocsystem has a lot of tools that will help us to create the app and to host it on Heroku.
API Platform
An easy to use framework dedicated to API development.
Full disclaimer: I'm the author of this framework.
API Platform is a set of standalone components for Symfony dedicated to the APIs development.
It basically includes a bundle to expose APIs and a data model generator using the Schema.org vocabulary.
I will not present the generator nor all features of the API bundle today but come to the Symfony in Paris, I'll talk in depth about all API Platform and how it enables to use technologies of the semantic web with PHP.
API Platform is distributed as a Symfony edition with the tools I will present in the next slides already installed and configured.
But keep in mind that all tools I'll show can be installed in any existing Symfony project too.
API Platform can optionally leverage the Doctrine ORM but it can also work with any other data source.
Create an entity (Doctrine + Validator)
// src/AppBundle/Entity/Person.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
*/
class Person
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(nullable=true)
* @Assert\Type(type="string")
*/
private $name;
/**
* Yes it works with relations too.
*
* @ORM\ManyToOne(targetEntity="PostalAddress")
*/
private $address;
// Getter and setters
}
Create a standard entity with Doctrine and validation annotations.
Register a service
# app/config/services.yml
services:
resource.person:
parent: 'api.resource'
arguments: [ 'AppBundle\Entity\Person' ]
tags: [ { name: "api.resource" } ]
Register a service as explained in the documentation.
You have:
Full CRUD with support for relations and dates
Data validation
Collections pagination
Serialization and validation groups
Error serialization
Automatic routes registration
Filters on exposed properties
Sorting
Automatically generated API doc
Native hypermedia support (JSON-LD + Hydra)
Sorry for your eyes again.
With only this service registration, a fully working API is exposed.
This API has the following features:
Full CRUD with support for relations and dates
Data validation
Collections pagination
Serialization and validation groups
Error serialization
Automatic routes registration
Filters on exposed properties
Sorting
Automatically generated API doc
Native hypermedia support (JSON-LD + Hydra)
API Platform reuses metadata already existing in your class to expose and document the API including:
Doctrine
Validator constraints
Serializer groups
PHPDoc
GET /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": "Kevin",
"address": "/addresses/28"
}
]
}
A collection by API Platform (JSON-LD + Hydra output)
Here is a sample collection exposed by API Platform.
The at context, at id and at type keys are JSON-LD properties describing the content returned by the API.
They enable hypermedia features of the API. As you can see, you have nothing to do to get a level 3 REST API also know as the grail of REST.
The totalItems, firstPage and lastPage keys contain pagination information. It enables the client to iterate over the collection efficiently. Internally it uses the Doctrine paginator.
The member key contains all items of this page of the collection.
Another magic power of API Platform thanks to JSON-LD is its compliance with RDF. Any entity exposed by the API is a valid RDF document.
It is possible to request several APIs hosted on different servers with only one SPARQL query. It is possible to insert this document in a triple store and it is possible to transform this simple JSON in XML-RDF or turtle.
And many more features
Powerful event system (ex: send a mail after a POST
)
Embed relations (ex: GET
products and its related offers)
Custom operations and controllers
Custom filters and sorting options
Content Negotiation (experimental)
Alternatives: FOSRestBundle, LemonRestBundle
API Platform is also designed to be used on complex project and has advanced features including:
A powerful event system (ex: send a mail after a POST
)
Relations embedding
Custom operations and controllers
Custom filters and sorting options
and content Negotiation
I will present API Platform in depth at the Paris SymfonyCon in december.
CORS with NelmioCorsBundle
The webapp and the API have different domain names.
CORS headers must be set by the API to let the webapp querying it:
Because the webapp and the API have different domain names, CORS headers must be set. NelmioCorsBundle is here for that.
# src/app/config.yml
nelmio_cors:
defaults:
allow_origin: ["https://example.com"]
allow_methods: ["POST", "PUT", "GET", "DELETE", "OPTIONS"]
allow_headers: ["content-type", "authorization"]
max_age: 3600
paths:
'^/': ~
With a simple configuration like this one, valid CORS headers are added to all requests.
With the allow_origin key we authorize only the URL of our HTML5 client. We also whitelist supported methods and the Authorization header to make the JWT support working.
We also indicate to the web browser and proxies to cache the result of the OPTIONS request.
In the path section, we use a regex to enable CORS on all pages of the API.
Security with LexikJwtAuth[...]Bundle
Makes the Symfony form login returning a JWT token instead of setting a cookie (stateless)
Allows to use Symfony firewall rules to secure API endpoints
LexikJwtAuthenticationBundle adds JWT support to the Symfony Security Component. Once installed, a token can be requested by the client. This token is then used to authenticate the user.
# app/config/security.yml
security:
# ...
firewalls:
login:
pattern: ^/login$
stateless: true
anonymous: true
form_login:
check_path: /login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
api:
pattern: ^/
stateless: true
lexik_jwt: ~
access_control:
- { path: ^/, roles: ROLE_ANONYMOUS, methods: [GET] }
- { path: ^/special, roles: ROLE_USER }
- { path: ^/, roles: ROLE_ADMIN }
First we configure two firewalls. One for the login form using handlers provided by the bundles to return a JWT token or an error when applicable. Another to support JWT tokens on API endpoints.
Finally we set some access control rules to secure API endpoints. Following the REST pattern also helps to use config files. URLs are predictable and easy to match with regex.
The first rule allows anonymous access to all API endpoints in GET.
The second checks that the user is connected to access to the special URL.
The last one allows only admins to use HTTP methods different than GET.
Cache with FOSHttpCache
fos_http_cache:
cache_control:
rules:
-
match:
path: ^/content$
headers:
cache_control:
public: true
max_age: 64000
etag: true
The HTTP Cache bundle of Friends of Symfony makes it super easy to add a caching strategy to out API.
Here we add a cache HTTP header and an etag for the content URL.
I don't speak more about this bundle because there is a talk dedicated to it this afternoon.
Parameters using environment variable
PaaS (including Heroku) use environment variables for the conf
Symfony parameters can be set using them
Most platform as a service, including Heroku, provide the app configuration trough environment variables
Fortunately, Symfony container parameters can be set using environment variables
heroku config:set SYMFONY__MY_PARAMETER_NAME=abc # Heroku config
export SYMFONY__MY_PARAMETER_NAME=abc # local machine
my_bundle:
parameter: %my_parameter_name%
If an environment variable starts with SYMFONY and two underscores, a parameter of the same name without the suffix will be available in the container.
Here's an example of how to set a parameter using Heroku or locally with the export command.
A popular alternative is to use Incenteev Parameter Handler to map environment variables:
heroku config:set MY_PARAMETER_NAME=abc # Heroku config
export MY_PARAMETER_NAME=abc # local machine
// composer.json
{
"extra": {
"incenteev-parameters": {
"env-map": {
"my_param": "MY_PARAMETER_NAME"
}
}
}
}
The Symfony Standard edition is provided with a tool to manage parameters config files called Incenteev Parameter Handler.
This tool can be used to map maps environment variables as parameters. This method has the advantage of not breaking the usual parameters config file system.
Specs and tests with Behat
Behat and its Behatch extension make testing and API easy.
Behat is a popular Behavior Driven Development framework. It allows to specify the application and to verify that the feature is well implemented.
# 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
}
"""
The Behatch extension of Behat provides several useful sentences to test an API.
A spec like this one can be automatically executed by Behat.
Behat will manipulate the Symfony app using Mink and BrowserKit to check that the app behaves like described in the scenario.
If it's not the case, an error will be thrown.
Tools for the static webapp
Angular, React, Ember, Backbone or anything else
Restangular
Auth0 JWT libraries
Yeoman, Gulp or Grunt, Bower, Jasmine, Karma...
The JavaScript ecosystem is large and of high quality. An incredible number of tools will help you to build the webapp.
This is not the topic today but the displayed tools are very practical to create API clients.
A JavaScript client
'use strict';
angular.module('myApp')
.controller('MainCtrl', function ($scope, Restangular) {
var peopleApi = Restangular.all('people');
function loadPeople() {
peopleApi.getList().then(function (people) {
$scope.people = people;
});
}
loadPeople();
$scope.newPerson = {};
$scope.success = false;
$scope.errorTitle = false;
$scope.errorDescription = false;
$scope.createPerson = function (form) {
peopleApi.post($scope.newPerson).then(function () {
loadPeople();
$scope.success = true;
$scope.errorTitle = false;
$scope.errorDescription = false;
$scope.newPerson = {};
form.$setPristine();
}, function (response) {
$scope.success = false;
$scope.errorTitle = response.data['hydra:title'];
$scope.errorDescription = response.data['hydra:description'];
});
};
});
This is an AngularJS controller displaying the list of people returned by the API and handling a form to create a new person.
The Restangular library is used to retrieve data from the /people route. The base URL of the API should be configured in the application bootstrap file.
In the loadPeople function, Restangular is used to retrieve the first page of the collection. The getList function returns a promise. When it is resolved, the API response is automatically parsed and converted to an array of JS documents. This array is bind to the view using the Angular scope.
The view contains a form bind to the newPerson property of the Angular scope. When the createPerson function is called (for instance when a button is pressed) Restangular serialize the object to JSON and send it to the API.
If an error is returned, the client leverages the API Platform error serialization feature to display a user friendly message.