ZF Meetup - Praha, 31.10.2013
Ivan Novakov
ivan.novakov@debug.cz
@ivannovakov
/users
/users/123
/users/123/address
/users/123/emails
/users/123/rooms/456
Interface based on the HTTP protocol:
GET /users
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
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"
}
{
"id": 123,
"name": "Ivan Novakov",
"_links": {
"self": {
"href": "https://server.example.org/api/users/123"
}
}
}
{
"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"
}
}
}
class User {
protected $id;
protected $name;
//...
}
class UserPersistence {
public function fetchAll()
{
//...
}
public function fetch($id)
{
//...
}
public function save(User $user)
{
//...
}
//...
}
class PersistenceService
{
public function saveUser($id, $name)
{
$user = $this->getUserFactory()->createUser();
$user->setId($id);
$user->setName($name);
return $this->getUserPersistence()->save($user);
}
//...
}
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
}
}
For example:
20 resources = 20 controllers ~ 100 actions
'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'
)
)
)
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; } }
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;
}
)
);
}
For example, if the action is create(),
the "pre.create" event is triggered before execution
and the "post.create" event is triggered after it.
Example configuration:
'renderer' => array(
'default_hydrator' => 'ArraySerializable',
'hydrators' => array(
'My\Resources\Foo' => 'My\Hydrators\FooHydrator',
'My\Resources\Bar' => 'My\Hydrators\BarHydrator',
),
),
'metadata_map' => array(
'Album' => array(
'hydrator' => 'Album\Hydrator\AlbumHydrator',
'identifier_name' => 'id',
'route' => 'rest-album',
)
),
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"
}
Ivan Novakov
ivan.novakov@debug.cz
@ivannovakov