Building RESTful Zend Framework Applications

Embed Size (px)

Citation preview

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