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
- addressability
- uniform interface
- communication through representation
- 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 /users
GET /users/123
POST /users
PUT /users/123
DELETE /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
- Official documentation - https://phlyrestfully.readthedocs.org/
- Github repo - https://github.com/phly/PhlyRestfully
- Example - https://github.com/ivan-novakov/zf2-tutorial
Questions?
Ivan Novakov
ivan.novakov@debug.cz
@ivannovakov
RESTfully with Zend Framework 2
By Ivan Novakov
RESTfully with Zend Framework 2
- 1,955