Apigility Workshop


Julien Guittard
Matthew Weier O'Phinney


19 October 2015 — Las Vegas

About Us

Julien is a Zend/PHP Architect and Trainer.

Matthew is a Prinicipal Engineer at Zend Technologies, and Project Lead for Zend Framework and Apigility.

Workshop Summary

  • Web API and RESTful architectures
  • Introduction to Apigility
  • Hands-on Apigility:
    • Build an RPC service
    • Build a RESTful service
    • Validate input for REST services
    • Manage HAL-JSON responses

API

Application Programming Interface

How software should interact.

Web API

APIs delivered over HyperText Transfer Protocol (HTTP)

REST

REpresentational State Transfer

An architecture designed around the HTTP specification.

RESTful APIs

REST leverages HTTP's strengths, and builds on:

  • URIs as unique identifiers for resources
  • Rich set of HTTP verbs for operations on resources
  • Specifying the representation format for payloads
  • Linking between resources (hypermedia controls)

Glory of REST

The Richardson Maturity Model:

REST: Level 0

  • The usage of HTTP as communication layer
  • A single format for data representation (e.g. JSON)
  • Basically a Remote Procedure Call (RPC)

REST: Level 1

  • URIs as unique identifiers for resources; e.g. the resource User might be identified as:
        http://domain/api/user[/:user_id]
    where user_id is an optional parameter.

REST: Level 2

  • Usage of HTTP verbs for operations on resources:
    • POST => create a new entity
    • GET => retrieve a collection or entity
    • PATCH => update an entity
    • etc.

REST: Level 3

Linking between resources to indicate relationships (hypermedia controls)


GET /api/user/jguittard

{
	"_links": {
		"self": {
			"href": "http://domain/api/user/jguittard"
		},
		"contacts": [
			{ "href": "http://domain/api/user/mwop" },
			{ "href": "http://domain/api/user/zeevs" }
		]
	},
	"id": "jguittard",
	"name": "Julien Guittard"
}
            

JSON-HAL format

Advantages of REST

  • Scalable architecture
  • Ease of consumption
  • Reduced client/server coupling
  • Discoverability

REST in PHP


header('Content-Type: application/json');
echo json_encode([
	'id'   => 'jguittard',
	'name' => 'Julien Guittard'
]);
            

Quite simple, right?

Wrong!

What about:

  • content negotiation?
  • hypermedia?
  • data validation?
  • error handling?
  • versioning?

Apigility

  • API builder and engine.
  • Open source project by Zend Technologies
  • Built on top of ZF2; use it standalone, add it to an existing ZF2 application, or incorporate it as part of another PHP application!
  • Admin UI is built using AngularJS.
  • Latest stable version is 1.3.1
  • Official website: apigility.org

Main features

  • Offers RPC and REST services.
  • JSON (HAL) as default format.
  • Error handling (Problem Details for HTTP APIs).
  • Content negotiation.
  • Versioning (via URI and Accept header, and for backing code).
  • Data validation.
  • Authentication (HTTP Basic/Digest, OAuth2).
  • Interactive documentation (HTML, Swagger, Blueprint).
  • Deployment; build production package files.

JSON HAL

  • JSON Hypertext Application Language (internet draft)
  • Example:
    
    GET /api/user/jguittard
    
    {
      "_links": {
        "self": {
          "href": "http://domain/api/user/jguittard"
        }
      }
      "id": "jguittard",
      "name": "Julien Guittard"
    }
                  

Embedded Entities


{
  "_embedded": {
    "contacts": [
      {
        "_links": {
          "self": {
            "href": "http://domain/api/user/mwop"
          }
        },
        "id": "mwop",
        "name": "Matthew Weier O'Phinney"
      }
    ]
  }
}
          

Collections


{
    "_links": {
        "self": {
            "href": "http://domain/api/user?page=3"
        },
        "first": {
            "href": "http://domain/api/user"
        },
        "prev": {
            "href": "http://domain/api/user?page=2"
        },
        "next": {
            "href": "http://domain/api/user?page=4"
        },
        "last": {
            "href": "http://domain/api/user?page=133"
        }
    }
    "count": 3,
    "total": 498,
    "_embedded": {
        "users": [ /* ... */ ]
    }
}
          

Problem Details for HTTP APIs

AKA API Problem

  • ietf-appsawg-http-problem (internet draft)
  • Example:
    
    HTTP/1.1 405 Method Not Allowed
    Content-Type: application/problem+json
    
    {
        "detail": "The GET method has not been defined for entities",
        "status": 405,
        "title": "Method Not Allowed",
        "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html"
    }
                

Content negotiation

Request and provide different mediatypes for the same resource.
  • Clients indicate what mediatype they want:
    
    Accept: application/hal+json, application/json
                  
  • or submit data using a mediatype:
    
    Content-Type: application/vnd.conference+json
                  
  • and servers respond with a mediatype:
    
    Content-Type: application/hal+json
                  
    (hopefully one the client accepts!)

API Versioning

Agility uses three approaches:

  • In the URL, as the first segment:
    
    /v1/api/user
                  
  • Via Accept header:
    
    Accept: application/vnd.example.v1+json
                  
  • In PHP namespaces:
    
    namespace Conference\V1\Rest\Speaker
                  

Authentication

Apigility officially supports three authentication systems:

Authorization

  • Public API by default (configurable!)
  • Enable/disable authentication gateway per-method of any service.
  • Allows providing your own ACLs (zend-permissions-acl).
  • Listen to the authorization event for custom assertions.

Hands-on!

https://github.com/weierophinney/apigility-zendcon-tutorial

Start and initializer the environment:


$ vagrant up
$ vagrant ssh
$ cd /vagrant
$ phing init
          

Hopefully you did this before arriving today!

In your laptop:

Open the browser to localhost:8888

Exercises on github

All the exercise are on github:

https://github.com/weierophinney/apigility-zendcon-tutorial

Four exercises with solutions in branches: exercise/1, exercise/2, exercise/3, and exercise/4.

Switch between them within your vagrant box:


$ vagrant ssh
$ cd /vagrant && git checkout -b exercise/1 origin/exercise/1
          

Exercise 1: RPC service

  1. Create an API named "Conference"
  2. Create an RPC service named "Timezone" that returns the timezone of the server via GET /timezone
  3. Enable POST and check if the timezone parameter is a valid timezone.

Check the solution

In your environment:


$ cd /vagrant
$ git checkout -b exercise/1 origin/excercise/1
          

Build a Conference API

  • We want to build a simple API for the ZendCon 2015 conference
  • We want to publish the following URIs:
    
    /speaker[/:speaker_id]
    /talk[/:talk_id]
                  

The SQLite database


CREATE TABLE speakers (
  id INTEGER PRIMARY KEY,
  name VARCHAR(80) NOT NULL,
  url VARCHAR(255),
  twitter VARCHAR(80)
);

CREATE TABLE talks (
  id INTEGER PRIMARY KEY,
  title TEXT,
  abstract TEXT,
  day TEXT,
  start_time TEXT
);

CREATE TABLE talks_speakers (
  talk_id INTEGER NOT NULL,
  speaker_id INTEGER NOT NULL
);

Data Mapper

  • The Data Mapper is a class that interacts with the data model.
  • Consume it in the REST Resource to return Entities and Collections.
  • Inject it in the the Resource using the ResourceFactory

Database Adapters

  • Apigility offers a database abstraction layer based around adapters via the zend-db component
  • Supports PDO, MySQL, Oci8, IBM DB2, PgSQL, and SqlSrv

TableGateway

  • Data TableGateway Pattern
  • An object that acts as a Gateway to a database table.
  • One instance handles all the rows in the table.
  • != ActiveRecord

						class TableGateway extends AbstractTableGateway
{
	public function __construct($table,
			AdapterInterface $adapter,
			$features = null,
			ResultSetInterface $resultSetPrototype = null,
			Sql $sql = null)
	{

	}
}
					

ResultSet

  • You can define a way to map the resulting data from your queries with Zend\Db\ResultSet
  • This mapping feature is called hydrating

						class HydratingResultSet extends AbstractResultSet
{
	public function __construct(HydratorInterface $hydrator = null,
			$objectPrototype = null)
    {
        $this->setHydrator(($hydrator) ?: new ArraySerializable);
        $this->setObjectPrototype(($objectPrototype) ?: new ArrayObject);
    }
}
//...

$resultSet = new HydratingResultSet();
$resultSet->setObjectPrototype(new TalkEntity());
					

Exercise 2: REST service

  1. Create the DB adapter, naming it "conference".
  2. Create two REST services for speakers and talks:
    • Service "speaker" to listen at /speaker[/:speaker_id]
    • Service "talk" to listen at /talk[/:talk_id]
  3. Create the Mapper classes for speakers and talks.
  4. Use the SQLite adapter inside the Mapper.
  5. Consume the Mapper class in the REST Resources.
  6. Implement GET, PATCH and DELETE for Entities.
  7. Implement GET and POST for Collections.

Check the solution

In your environment:


cd /vagrant
git checkout -b exercise/2 origin/exercise/2
            

Data Validation

  • Supports per-field validations.
  • Allows marking fields required/optional, specifying normalization filters and validators, etc.

Exercise 3: Fields and Validators

  1. Add required fields name and url to the Speaker resource.
  2. Add fields title, and day to the Talk resource.
  3. Add a validator for day to accept dates only in the format YYYY-MM-DD*
  4. Add a custom error message for the validator day.
  5. Try to POST against /speaker and /talk without data.

* use Zend\Validator\Date with the options "format" to "Y-m-d"

Check the solution

In your environment:


cd /vagrant
git checkout -b exercise/3 origin/exercise/3
          

Embedding entities

  • If Apigility knows about an Entity or Collection class, it will return the item as an embedded resource!

ZF2 SQL Abstraction


$sql = $table->getSql();
$select = $sql->select();
$select->join('talks_speakers', 'talks_speakers.speaker_id = speakers.id')
  ->where(['talks_speakers.talk_id' => $talkId]);
          

Pagination

You'll want the DbSelect paginator:


use Zend\Paginator\Adapter\DbSelect;
$paginator = new DbSelect(
    $select,
    $table->adapter,
    $table->getResultSetPrototype() // <-- THIS MAKES IT WORK!
);
          

Exercise 4: Embedded Collections

Using the _embedded field format, add the following:

  1. Add "speakers" to entities returned via GET /talk/:talk_id *
  2. Add "talks" to entities returned via GET /speaker/:speaker_id **

* Add a speakers field in TalkEntity to return a SpeakerCollection

** Add a talks field in SpeakerEntity to return a TalkCollection

SQL joins: http://framework.zend.com/manual/current/en/modules/zend.db.sql.html

DbSelect Paginator: http://framework.zend.com/manual/current/en/modules/zend.paginator.usage.html

Hint: you'll want factories for your mappers...

Check the solution

In your environment:


cd /vagrant
git checkout -b exercise/4 origin/exercise/4
          

Thanks!


Creative Commons License
This work is licensed under a
Creative Commons Attribution-ShareAlike 3.0 Unported License.
we used reveal.js to make this presentation.