bgw-revised
RESTful Applications with Zend Framework
11 June 2010, RAI, Amsterdam
premise
How do you
detect and respond
to REST requests
in your Zend Framework application?
roadmap
Good Modelling
Introduction to Zend_Rest_Route
HTTP Fundamentals
Overview of context switching
in ZF
Getting content-type-specific parameters
Good Modelling
create entities that implement
__toString() and toArray()
Makes serializing simple
Makes passing to data access layer simple
Simplifies caching
(particularly when coupled with a fromArray() method)
serialization
class Foo{ public function __toString() { return 'foo'; }
public function toArray() { $array = array(); foreach ($this->_nodes as $node) { $array[] = $node->name; } return $array; }}
caching
if (!$data = $cache->load('foo_collection')) { $data = $this->fooObjects->toArray(); $cache->save($data, 'foo-collections');}
if (!$data = $cache->load('foo-item')) { $data = (string) $this->foo; $cache->save($data, 'foo-item');}
return collections as paginators
Consumers do not need to be aware of data format
Consumers can provide offset and limit
Consumers can decide how to castZend_Paginator implements IteratorAggregate, toJson()
Most paginator adapters will only operate once results are requested
Zend_Paginator basics
setCurrentPageNumber($page)
setItemCountPerPage($int)
setPageRange($int) (number of pages to show in paginator)
Constructor accepts an adapter capable of generating a count() and returning items based on an offset and item count
repository returning paginator
public function fetchAll(){ $select = $this->getDbTable()->select() ->where('disabled = ?', 0); $paginator = new Zend_Paginator( new Zend_Paginator_Adapter_DbSelect( $select ) ); return $paginator;}
paginator use in view script
$this->users->setItemCountPerPage(
$this->numItems) ->setCurrentPageNumber(
$this->page);echo $this->users->toJson();
going generic
Alternately, define a collection typee.g., to accept a MongoCursor
Implement Countable, and Iterator; optionally also some serialization interfaces
Define methods like skip() and limit() (or page()), and sort() and setItemClass()
mapper returning generic collection
public function find(
array $query, array $fields = array()
) { $query = $this->_formatQuery($query);
// mongo collection: $cursor = $this->getCollection()
->find($query, $fields); // paginator: $collection = new
Collection($cursor); $collection->setItemClass('Entry'); return
$collection;}
collection used in view script
$this->entries->start($this->offset)
->limit($this->numItems);echo $this->json(
$this->entries->toArray()
);
service layers
Define your
application's API
and implement
your application
in the service layer.
example
namespace Blog\Service;class Entries{ public function
create(array $data) {} public function fetch($permalink) {} public
function fetchCommentCount(
$permalink) {} public function fetchComments($permalink) {} public
function fetchTrackbacks($permalink) {} public function
addComment($permalink,
array $comment) {} public function addTrackback($permalink,
array $comment) {} public function fetchTagCloud() {}}
Been working on a new design for 6 months.
Host of new features that old did not have.
Look and feel have been reinvented
New concepts in place like member directory, online calendar, online map both using tech from Google
Info more easily updated
Special pags - Committee pages, ministry pages, youth pages, missions pages
Home page that gives quick access to current news, information, and links
Archives section to store video, audio, documents, images that can be searched
Members area for sensitive information
Possible Email Newsletter & Photo slideshows
what's found in Service Layers?
Resource marshalling and Dependency Injection
Application-specific logic:Authentication and Authorization (ACLs)
Input filtering/data validation
Search indexing
Caching
etc.
Zend_Rest_Route
basics
REST uses HTTP verbs
GET with no identifier: list
GET with identifier: resource
POST: create
PUT (with identifier): update
DELETE (with identifier): delete
Zend_Rest_Route
ResourceHTTP
MethodControllerAction
defining all routes RESTful
// Enable for all controllers$front =
Zend_Controller_Front::getInstance();$restRoute = new
Zend_Rest_Route($front);$front->getRouter()->addRoute(
'default', $restRoute
);
defining select modules as RESTful
// Enable for blog module only$front =
Zend_Controller_Front::getInstance();$restRoute = new
Zend_Rest_Route(
$front, array(), array('blog')
);$front->getRouter()->addRoute(
'rest', $restRoute
);
Defining select controllers
as RESTful
// Enable for specific controllers only$front =
Zend_Controller_Front::getInstance();$restRoute = new
Zend_Rest_Route(
$front, array(), array( 'blog' => array( 'comment', 'trackback',
), )
);$front->getRouter()->addRoute(
'rest', $restRoute
);
sample REST controller
class EntryController
extends Zend_Rest_Controller{ public function indexAction() { }
public function postAction() { } public function getAction() { }
public function putAction() { } public function deleteAction() {
}}
retrieving the identifier
if (!$id = $this->getUserParam('id', false)) { // redirect, error, etc.}
HTTP Fundamentals
request headers
Content-Type
(what it's providing)
Accept
(what it expects in response)
status codes
201 (Created)
400 (Bad Request)
(Failed validations)
401 (Unauthorized)
204 (No Content)
(useful with DELETE)
500 (Application Error)
response headers
Content-Type
(what you're returning)
Vary
(what and when to cache)
Context Switching
context switching is
Inspect HTTP request headers, and/or the request URI, and vary the response
In ZF, using the
ContextSwitch and/or AjaxContext action helper
They need some configuration
basics
Map actions to allowed contexts
A format request parameter indicates the detected context
When a context is detected, an additional suffix is added to the view script
Optionally, send some additional headersPush the response object into the view to facilitate
mapping contexts to actions
class Blog_CommentController
extends Zend_Controller_Action{ public function init() {
$contextSwitch = $this->_helper->getHelper( 'contextSwitch'
); $contextSwitch->addActionContext(
'post', 'xml') ->initContext(); }}
add view scripts per-context
blog|-- views| |-- scripts| | |-- comment| | | |-- post.phtml| | | `-- post.xml.phtml
injecting the response and request
into the view
use Zend_Controller_Action_HelperBroker
as HelperBroker;class InjectRequestResponse extends
Zend_Controller_Plugin_Abstract{ public function
dispatchLoopStartup( Zend_Controller_Request_Abstract $request ) {
$vr = HelperBroker::getStaticHelper(
'ViewRenderer'); $vr->view->assign(array( 'request' =>
$request, 'response' => $this->getResponse(), )); }}
html view
success
Getting the context
Two common options:Via the URI
(often, using a suffix;
e.g., .xml, .json)
Via HTTP headers
(the Accept header)
via a chained route
use Zend_Controller_Router_Route as StandardRoute, Zend_Controller_Router_Route_Regex as RegexRoute;$format = new RegexRoute('(?xml|json)');$entry = new StandardRoute('blog/entry/:id', array( 'module' => 'blog', 'controller' => 'entry', 'action' => 'view',));$entry->chain($format, '.');$router->addRoute('entry', $entry);
via Accept detection
class AcceptHandler extends Zend_Controller_Plugin_Abstract{ public function dispatchLoopStartup( Zend_Controller_Request_Abstract $request ) { $this->getResponse()->setHeader('Vary', 'Accept'); $header = $request->getHeader('Accept'); switch (true) { case (strstr($header, 'application/json')): $request->setParam('format', 'json'); break; case (strstr($header, 'application/xml') && (!strstr($header, 'html'))): $request->setParam('format', 'xml'); break; default: break; } }}
Content-Type specific params
the problem
Non form-encoded
Content-Types typically mean parameters are passed in the raw
request body
Additionally, they likely need to be decoded and serialized to a PHP array
Use an action helper to automate the process
Content-Type detection
class Params extends Zend_Controller_Action_Helper_Abstract{
public function init() { $request = $this->getRequest();
$contentType = $request
->getHeader('Content-Type'); $rawBody =
$request->getRawBody(); if (!$rawBody) { return; }
Content-Type detection (cont.)
switch (true) { case (strstr($contentType,
'application/json')): $this->setBodyParams(
Zend_Json::decode($rawBody)); break; case
(strstr($contentType,
'application/xml')): $config = new Zend_Config_Xml($rawBody);
$this->setBodyParams($config->toArray()); break; default: if
($request->isPut()) { parse_str($rawBody, $params);
$this->setBodyParams($params); } break;}
Content-Type detection (cont.)
public function getSubmitParams(){ if ($this->hasBodyParams()) { return $this->getBodyParams(); } return $this->getRequest()->getPost();}
public function direct(){ return $this->getSubmitParams();}
using the helper
public function postAction(){ $params = $this->_helper->params(); if (!$item = $this->service->create($params)) { // ... } // ...}
Summary
takeaways
Think about what behaviors you want to expose, and write models that do so.
Use HTTP wisely; examine request headers and send appropriate response headers.
Perform context switching based on the Accept header; check for the Content-Type when you examine the request.
most importantly
Keep it simple and predictable.
Thank You
Feedback: http://joind.in/1542http://twitter.com/weierophinney
Matthew Weier O'Phinney
Project Lead, Zend Framework
DPC