RESTfully

with
Zend Framework 2


ZF Meetup - Praha, 31.10.2013




Ivan Novakov

ivan.novakov@debug.cz

@ivannovakov

Presentation


  • REST principles
  • Building our RESTfull service
  • The power of PhlyRestfully

REST principles

  1. addressability
  2. uniform interface
  3. communication through representation
  4. stateless

Addressability

  • everything is a resource
  • every resource has a unique identifier

/users/users/123/users/123/address/users/123/emails/users/123/rooms/456

Uniform interface

Interface based on the HTTP protocol:

  • methods - GET, POST, PUT, DELETE
  • headers - Accept, Content-Type, ...
  • status codes - 200, 201, 400, 403, 404, ...

GET /usersGET /users/123POST /usersPUT /users/123DELETE /users/123

Representation


POST /users
Content-Type: application/json
Accept: application/json

{
    "firstname": "Ivan",
    "surname": "Novakov"
}

201 Created
Content-Type: application/json

{
    "id": 123,
    "firstname": "Ivan",
    "surname": "Novakov"
}

Hypermedia


  • resources contain relevant links to self or other resources
  • the client side does not need to construct URLs


 {
    "id": 123,
    "name": "Ivan Novakov",
    "_links": {
        "self": {
            "href": "https://server.example.org/api/users/123"
        }
    }
}

Hypermedia


 {
    "total": 124,
    "users": [{
        "id": 123,
        "name": "Ivan Novakov",
        "_links": {
            "self": {
                "href": "https://server.example.org/users/123"
            }
        }
    }, { ... }],
    "_links": {
        "self": {
            "href": "https://server.example.org/users"
        },
        "next": {
            "href": "https://server.example.org/users?page=1"
        }
    }
}

Building our RESTfull service


  • Model
  • Persistence layer
  • (Service layer)
  • MVC application

Model


class User {
    protected $id;
    protected $name;

    //...
}

Persistence layer


class UserPersistence {
    
    public function fetchAll()
    {
        //...
    }
    
    public function fetch($id)
    {
        //...
    }

    public function save(User $user)
    {
        //...
    }
    
    //...
}

Persistence service


class PersistenceService
{

    public function saveUser($id, $name)
    {
        $user = $this->getUserFactory()->createUser();
        $user->setId($id);
        $user->setName($name);

        return $this->getUserPersistence()->save($user);
    }
    
    //...
}

MVC application


class UserController {

    public function saveAction()
    {
        $id = $this->getParam('id');
        $name = $this->getParam('name');
        // input validation ...

        $createdUser = $this->getPersistenceService()->save($id, $name);

        // check for errors
        // set status code
        // generate hypermedia links
        // JSON encode data
        // set headers
    }
}

Scalability problems


  • one controller per resource
  • several actions per controller
  • most of the code is the same


For example:

20 resources = 20 controllers ~ 100 actions

PhlyRestfully

by Mattew Weier O'Phinney

  • ZF2 module
  • handles the "I/O" needed in a RESTful application
  • focus on JSON
  • standards - HAL, Problem API
  • uses events to handle resource actions

Basics


  • ResourceController - internal
  • Resource - internal
  • ResourceListener - needs to be provided
  • Routing - needs to be configured
  • Advanced configuration - metadata, hydrators, links, ...

ResourceController


  • based on the AbstractRestfulController
  • maps HTTP requests to actions - create, update, fetch, ...
  • uses the injected Resource to execute the actions
  • triggers controller events
  • handles the output


Method to action mapping:

  • GET /users - fetchAll() - return a collection of users
  • GET /users/123 - fetch() - return a specific user
  • POST /users - create() - create a new user
  • PUT /users/123 - update() - update a user
  • DELETE /users/123 - delete() - delete a user

Resource


  • Handles the actions - create, update, fetch, ...
  • Triggers resource events
  • Returns the result of the last listener callback or the first error

Resource listener


  • Links the Model layer with the View-Controller layer
  • Listens to resource events
  • Uses the persistence layer to execute actions

Basic example

based on the official ZF2 tutorial -
the Album application

Configuration


     'rest-album' => array(
        'type' => 'segment',
        'options' => array(
            'route' => '/rest/albums[/:id]',
            'constraints' => array(
                'id' => '[0-9]+'
            ),
            'defaults' => array(
                'controller' => 'Album\Controller\RestAlbumController'
            )
        )
    ),

    'phlyrestfully' => array(     
        'resources' => array(
            'Album\Controller\RestAlbumController' => array(
                'listener' => 'Album\Listener\AlbumResourceListener',
                'route_name' => 'rest-album'
            )
        )
    )  

Resource listener


class AlbumResourceListener extends AbstractListenerAggregate
{
    public function attach(EventManagerInterface $events)
    {
        $this->listeners[] = $events->attach('create', array($this, 'onCreate'));
        //..
    }

    public function onCreate(ResourceEvent $e)
    {
        $data = $e->getParam('data');
        $album = new Album();        //..
        $album = $this->persistence->saveAlbum($album);
        if (! $album) {
            throw new CreationException();
        }
        return $album;
    }
}

Module.php


    public function getServiceConfig()
    {
        return array(
            'factories' => array(
                'Album\Model\AlbumTable' => function ($sm)
                {
                    $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
                    $table = new AlbumTable($dbAdapter);
                    return $table;
                },
                'Album\Listener\AlbumResourceListener' => function ($sm)
                {
                    $persistence = $sm->get('Album\Model\AlbumTable');
                    $listener = new AlbumResourceListener($persistence);
                    return $listener;
                }
            )
        );
    }

Why events?


  • loose coupling
  • possibility to attach multiple callbacks to an action

ResourceEvent


  • contains routing information
  • contains query params
  • can pass custom params

Controller events


  • triggered before and after the action
  • used for advanced customization


For example, if the action is create()

the "pre.create" event is triggered before execution 

and the "post.create" event is triggered after it.

Hydrators


  • used for extracting objects into arrays
  • default hydrator


Example configuration:

        'renderer' => array(
            'default_hydrator' => 'ArraySerializable',
            'hydrators' => array(
                'My\Resources\Foo' => 'My\Hydrators\FooHydrator',
                'My\Resources\Bar' => 'My\Hydrators\BarHydrator',
            ),
        ),

Metadata mapping


  • maps a model class to a set of rules
  • used for:
    • proper link generation
    • proper object extraction (hydrators)
    • proper handling of embedded resources

        'metadata_map' => array(
            'Album' => array(
                'hydrator'        => 'Album\Hydrator\AlbumHydrator',
                'identifier_name' => 'id',
                'route'           => 'rest-album',
            )
        ),

Error reporting


  • API Problem specification (draft)
  • status code is set apropriately
  • certain conditions automatically turned into errors

API Problem example


HTTP/1.1 500 Internal Error
Content-Type: application/api-problem+json

{
    "describedBy": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
    "detail": "Status failed validation",
    "httpStatus": 500,
    "title": "Internal Server Error"
}

Triggering errors


  • by triggering special exceptions - CreateException, UpdateException, ...
  • by triggering custom exceptions based on the ProblemExceptionInterface
  • directly by returning an instance of ApiProblem

Advanced features


  • advanced routing
  • HTTP method whitelisting
  • advanced rendering
  • collections and pagination
  • embedded resource

Reference


  1. Official documentation - https://phlyrestfully.readthedocs.org/
  2. Github repo - https://github.com/phly/PhlyRestfully
  3. Example - https://github.com/ivan-novakov/zf2-tutorial

Questions?




Ivan Novakov

ivan.novakov@debug.cz

@ivannovakov

Made with Slides.com