Architecting Ajax Applications with
Zend Framework
Matthew Weier O'PhinneyProject LeadZend Framework
Who I am
ZF Contributor since January 2006
Assigned to the ZF team in July 2007
Promoted to Software Architect in April 2008
Project Lead since April 2009
Photo 2009, Chris Shiflett
What we won't cover
Actual client-side codeWell, mostly none.
Why not the client side?
XmlHttpRequest is easy, and most good JS toolkits/frameworks abstract it
var req = new XMLHttpRequest();req.open('GET', '/foo', false);req.send(null);if (req.status == 200) { dump(req.responseText);}
Why not the client side?
XmlHttpRequest is easy, and most good JS toolkits/frameworks abstract it
$.get("/foo", function(data) { dump(data);});
Why not the client side?
XmlHttpRequest is easy, and most good JS toolkits/frameworks abstract it
dojo.xhrGet({ url: "/foo", load: function (data) { dump(data); }});
Why not the client side?
The real question is: how do you detect and respond to XHR requests in your Zend Framework application?
Topics we will cover
Write re-usable models that perform your application logic
Create standards-based services
Use HTTP wisely
Perform Context-Switching based on the requested Content-Type
Putting it all together: Tips and techniques
Writing Re-usable Models
Write your application logic once, but use it in many different ways
Why?
MVC is only one consumer
Can be called by job scripts, message systems, queues, etc.
Service endpoints can act as consumers
(Hint: JSON-RPC is a good way to feed your Ajax applications)
What?
Domain ModelsThe individual resources and units of work in your application
E.g. user, team, backlog, sprint, etc.
E.g., object relations, transactions, queries
Service LayersPublic API of the application; i.e., the behaviors you wish to expose
Logic for injecting dependencies
Service Layers
The guts of your application
Applications are like onions
they have layers
Photo 2008, Mike Chaput-Branson
Data Access Objects and Data store(s)
Data Mappers
Domain Models
Service Layer
Service Layer in Perspective
Benefits to Service Layers
Allows easy consumption of the application via your MVC layer
Allows easy re-use of your application via services
Write CLI scripts that consume the Service Layer
What kind of application logic?
Validation and filtering
Authentication and Authorization
Transactions and interactions between model entities
class PersonService{ public function create(array $data) { $person = new Person(); if (!$data = $this->getValidator() ->isValid($data) ) { throw new InvalidArgumentException(); } $person->username = $data['username']; $person->password = $data['password']; $person->email = $data['email']; $this->getMapper()->save($person); return $person; }}
Techniques
Return objects implementing __toString() and/or toArray()
Easily converted to a variety of formats for your XHR clientsJSON, XML
Easy to cacheStrings and arrays serialize and deserialize easily
class Foo{ public function __toString() { return 'foo'; }
public function toArray() { $array = array(); foreach ($this->_nodes as $node) { $array[] = $node->name; } return $array; }}
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 then provide offset and limit
Consumers can decide how to castPaginator implements IteratorAggregate, toJson()
Most Paginator adapters will only operate once results are requested
Return paginators from Domain Objects
class Foo_Model_User{ public function fetchAll() { $select = $this->getDbTable()->select() ->where('disabled = ?', 0); $paginator = new Zend_Paginator( new Zend_Paginator_Adapter_DbSelect( $select ) ); return $paginator; }}
$this->users->setItemCountPerPage($this->numItems) ->setCurrentPageNumber($this->page);echo $this->users->toJson();
Caching
Write-through caches are trivial to implement in service layers
Caching of result sets is trivial in service layersOr, built-in with Zend_Paginator!
class Foo_Model_User{ public function update(array $data) { //
... update ... $cache = $this->getCache(); $cache->clean(
Zend_Cache::CLEANING_MODE_MATCHING_TAG, array(
'users',
'user-' . $data['username'] ) );
$cache->save($data,
'user-' . $data['username']); }}
Zend_Paginator::setCache($cache);$users->setCacheEnabled(true);
Implement ACLs in Service Layers
Each service object is a resource
Once passed an ACL object, the service object defines the ACLs
Pass in role to the service object so it may do ACL checks
Throw a unique exception type for ACL failures; check for it in error handlers
Great way to make your RPC endpoints, such as JSON-RPC, ACL-aware
class Foo_Service_Team implements Zend_Acl_Resource_Interface{ // ...
public function getResourceId() { return 'team'; }
// ...}
class Foo_Service_Team implements Zend_Acl_Resource_Interface{ // ...
public function setAcl(Zend_Acl $acl) { $acl->add($this) //
... ->allow('admin', $this, array( 'register', 'update',
'list', 'delete')); $this->_acl = $acl; return $this; }
// ...}
class Foo_Service_Team implements Zend_Acl_Resource_Interface{ // ...
public function setRole(
Zend_Role_Interface $role
) { $this->_role = $role; return $this; }
// ...}
class Foo_Service_Team implements Zend_Acl_Resource_Interface{ // ...
public function fetchAll() { if (!$this->getAcl()->isAllowed( $this->getRole(), $this, 'list') ) { throw new UnauthorizedException(); } // ... }
// ...}
Writing Re-usable Models: Synopsis
MVC may be only one consumer of your application; you may want to create JSON-RPC or other RPC endpoints
Use return values that are not data-source dependent
Use Service Layers as your application's public API
Implement strategies such as caching, validation, and authorization in service layers
Create Standards-based Services
Standards are easier to consume
RPC and REST
RPC: Remote Procedure CallTypical when wanting to invoke methods outside of standard CRUD
Often namespaced: resource.method
Typically, request includes a method name and typed arguments; response will be typed based on the method signature
Usually uses HTTP POST for all operations
RPC and REST (cont.)
REST: Representational State TransferTypically manipulating resources; e.g. CRUD
Payloads vary based on requested Content-Type; XML and JSON are common
Uses HTTP verbs to indicate operations:GET: list all or retrieve individual resources
POST: create a new resource
PUT: update an existing resource
DELETE: delete a resource
RPC Services
Define application behaviors and expose them
RPC Services in Zend Framework
JSON-RPC: Zend_Json_Server
XML-RPC: Zend_XmlRpc_Server
SOAP: Zend_Soap_Server
AMF: Zend_Amf_Server
Common functionality
Mimic SoapServer API from PHPInstantiate server
Attach one or more objects/classes, optionally with a namespace
Handle the request
Return the response
JSON-RPC server
$server = new
Zend_Json_Server();$server->setClass('App_Service_Team', 'team')
->setClass('Scrum_Service_Backlog', 'backlog')
->setClass('Scrum_Service_Sprint', 'sprint');if ('GET' ==
$_SERVER['REQUEST_METHOD']) { $server->setTarget('/jsonrpc')
->setEnvelope(
Zend_Json_Server_Smd::ENV_JSONRPC_2
); header('Content-Type: application/json'); echo
$server->getServiceMap(); return;}$server->handle();
$server = new Zend_XmlRpc_Server();$server->setClass('App_Service_Team', 'team') ->setClass('Scrum_Service_Backlog', 'backlog') ->setClass('Scrum_Service_Sprint', 'sprint');$response = $server->handle();echo $response;
XML-RPC server
SOAP server
if ('GET' == $_SERVER['REQUEST_METHOD']) { $server = new Zend_Soap_AutoDiscover();} else { $server = new Zend_Soap_Server($soapUrl);}$server->setClass('App_Service_Team') ->setClass('Scrum_Service_Backlog') ->setClass('Scrum_Service_Sprint');$server->handle();
Common RPC questions
What about authentication?Determine method and authentication token from request object
Check for authentication prior to handling request
Authentication with RPC
$request = $server->getRequest();if (!in_array($method,
array('user.login'))) { $params = $request->getParams();
$authKey = array_shift($params); $adapter = new
My_Auth_Adapter_Api($authKey); $result =
Zend_Auth::getInstance()
->authenticate($adapter); if (Zend_Auth_Result::SUCCESS
!= $result->getCode()
) { $response = $server->fault(
'Unauthorized access', 401); echo $fault; return; }}
Common RPC questions
What about ACLs?Implement them in your service layer
Inject authentication identity when handling
Alternately, define ACLs based on request methods, and bail early
Injecting the auth identity
$server->setClass( 'Scrum_Service_Sprint', // class 'sprint', // namespace $auth->getIdentity() // ctor arg);
Checking ACLs based on method
list($resource, $action) = explode( '.', $method, 2);
if (!$acl->isAllowed( $auth->getIdentity(), $resource, $action)) { $response = $server->fault( 'Unauthorized', 401 ); echo $response; return;}
Common RPC questions
My Service Layer objects have getters and setters I don't want to expose.Create a proxy object that exposes only those methods you want exposed.
Pass in dependencies via constructor arguments, use a service locator, or use lazy-loading with sane defaults.
Creating a proxy object
class Foo_Service_Bar{ public function setBar(
Foo_Model_Bar $bar
) { } public function getBar() { } public function find($id) { }
public function fetchAll() { }}
class Foo_Service_BarProxy{ public function __construct($options = null){} protected function _setService( Foo_Service_Bar $service ) { } protected function _getService() { } public function find($id) { } public function fetchall() { }}
Injecting a service locator
class Foo_Service_BarProxy{ public function __construct(
$serviceLocator = null
) { }}
$server->setService( 'Foo_Service_BarProxy', 'bar', $serviceLocator);
Common RPC questions
How do I expose services via the MVCDon't.
Seriously, you want your services snappy; bypass the MVC when exposing RPC services.
Re-use Zend_Application to bootstrap dependencies
Alternately, extend your application Bootstrap, and override the run() method
Selectively bootstrapping resources
$application->bootstrap('auth');$application->bootstrap('db');$server->handle();
Overriding the application bootstrap
class XmlRpcService_Bootstrap extends Bootstrap{ public function run() { $server = new Zend_XmlRpc_Server(); // ... $server->handle(); }}
$application->bootstrap()->run();
Common RPC questions
How do I use methods other than POST?You don't.
Using the raw POST body is built into most RPC protocols
JSON Schema allows you to define a Service Mapping Description, which can allow you to selectively map HTTP methods to RPC methods; ZF does not yet support this in Zend_Json_Server.
RESTful Resources
Define resources and expose them
RESTful Resources in Zend Framework
Use Zend_Rest_Route to map controllers as RESTful resourcesMaps HTTP methods to actions
Allows for mimicing PUT and DELETE
Use Zend_Rest_Controller, or implement:indexAction()
postAction()
getAction()
putAction()
deleteAction()
What does REST have to do with Ajax?
It's predictable; if you have a RESTful resource, you know how to do CRUD
New client-side technologies such as
JSON-REST automate REST operations with XmlHttpRequest
Creating RESTful routes
// Entire application:$router->addRoute('default',
new Zend_Rest_Route($front)
);
// All controllers in a module:$router->addRoute('rest', new
Zend_Rest_Route(
$front, array(), array('scrum') ));
Creating RESTful routes (cont.)
// Adding select controllers for a
// given module$router->addRoute('rest', new
Zend_Rest_Route($front, array(), array( 'scrum' => array(
'backlog', 'sprint', ) )));
RESTful controllers
class Scrum_BacklogController
extends Zend_Rest_Controller{ public function indexAction() { } //
list public function postAction() { } // create public function
getAction() { } // view public function putAction() { } // update
public function deleteAction() { } // delete}
Standardized Services: Synopsis
Use standard server types.Wealth of existing clients that can consume them
Predictability == maintainability
Flexibility is typically built-in, if you exercise some creativity
Know when to use RPC and when to use REST and what the difference is.
Leverage existing application setup, authentication, and ACLs whenever possible.
Use HTTP Wisely
Get to know your local HTTP protocol
Use HTTP Wisely
Inspect incoming HTTP request headers
Set appropriate HTTP response headers
Why should I care?
Often you can evaluate the success of an XHR request based on just the HTTP response code.
You can tell the server what kind of content is being provided in the request, as well as what kind of content you want returned, via HTTP headers.This is a two-way street; the server needs to respond correctly!
Common HTTP Request Headers
Content-Type: the content type being sent to your application. E.g. application/json, application/xml, etc.
Accept: what content type the client is expecting in response. E.g., application/json, application/xml, etc.
Range: how many bytes or items should be returned in the response.
Common HTTP Response Codes
201 (Created)After a POST that creates a resource
400 (Bad Request)Failed validations
401 (Unauthorized)
204 (No Content)Often used after successful DELETE operations
500 (Application Error)
Common HTTP Response Headers
Content-Type: The content type of the response body.
Content-Range: the number of bytes returned and total number of bytes, OR the range of items returned, and total number of items.
Vary: hint to client-side caching what headers should be used to determine whether or not to cache.
Use HTTP Wisely: Synopsis
Inspect incoming HTTP request headers, and vary output and responses based on them.
Consider client-side caching when setting response headers.
Perform Context Switching based on the requested Content-Type
How to vary your responses based on the request
Context Switching
Inspect incoming HTTP request headers, and vary output and responses based on them.
Built-in support via the ContextSwitch and AjaxContext action helpers
but you need to give them some help.
Context Switching: Basics
Map controller actions to contexts
A format request parameter will indicate which context was detected
When a context is detected, an additional suffix is added to the view script
Optionally, additional headers might be sent.
Mapping contexts to actions
class FooController extends Zend_Controller_Action{ public function init() { $switch = $this->_helper ->getHelper('ContextSwitch');
// Add "xml" context to "bar" action: $switch->addActionContext('bar', 'xml');
// Add "json" context to "baz" action: $switch->addActionContext('baz', 'json');
// Detect context $switch->initContext(); }}
How AjaxContext differs
AjaxContext works just like ContextSwitch
but it only triggers on XMLHttpRequest
(i.e., when the X-Requested-With: XMLHttpRequest header is
present)
When detecting Accept and Content-Type headers, often you can simply use ContextSwitch
Request Header Detection
When to do it?Once per request
Front controller plugin: routeShutdown() or dispatchLoopStartup()
How to do it?Use the request object
getHeader($headerName) is your friend
What to do?Set request parameters accordingly
Sample Accept header detection
class AcceptPlugin
extends Zend_Controller_Plugin_Abstract{ public function
dispatchLoopStartup( Zend_Controller_Request_Abstract $request ) {
$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; } }}
Views per Context
When a context is detected, that context name is prepended to
the view script suffix:
.xml.phtml, .json.phtml, etc.
View scripts should set appropriate response headersControllers then do not need to be aware of current context
Headers are primarily the realm of the view
Create appropriate output for the context
Creating alternate views per context
application|-- controllers| `-- SprintController|-- views| |-- scripts| | |-- sprint| | | |-- index.phtml| | | |-- index.xml.phtml| | | |-- index.json.phtml
Sprints
Creating alternate views per context
application|-- controllers| `-- SprintController|-- views| |-- scripts| | |-- sprint| | | |-- index.phtml| | | |-- index.xml.phtml| | | |-- index.json.phtml
$this->response->setHeader(
'Content-Type', 'application/xml');$xmlWriter = new
Phly_Array2Xml();echo
$xmlWriter->toXml($this->sprints->toArray());
Creating alternate views per context
application|-- controllers| `-- SprintController|-- views| |-- scripts| | |-- sprint| | | |-- index.phtml| | | |-- index.xml.phtml| | | |-- index.json.phtml
$this->response->setHeader(
'Content-Type', 'application/json');echo
$this->json($this->sprints->toArray());
Putting it all Together
Content-Types, Ranges, and paginators oh, my!
Retrieving and decoding parameters from non-standard Content-Types
Parameters: 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
Retrieving parameters
class Scrummer_Controller_Helper_Params extends Zend_Controller_Action_Helper_Abstract{ protected $_bodyParams = array();
public function init() { $request = $this->getRequest(); $rawBody = $request->getRawBody(); if (empty($rawBody)) { return; } // ... }}
Retrieving parameters (cont.)
// ... $contentType = $request
->getHeader('Content-Type'); switch (true) { case (strstr(
$contentType, 'application/json')): $this->setBodyParams(
Zend_Json::decode($rawBody)); break; // ... }
Retrieving parameters (cont.)
// ... switch (true) { // ... case (strstr(
$contentType, 'application/xml')): $config = new
Zend_Config_Xml(
$rawBody); $this->setBodyParams(
$config->toArray()); break; // ... }
Retrieving parameters (cont.)
// ... switch (true) { // ... default: if ($request->isPut())
{ parse_str(
$rawBody, $params); $this->setBodyParams(
$params); } break; }
Retrieving parameters (cont.)
class Scrummer_Controller_Helper_Params extends Zend_Controller_Action_Helper_Abstract{ public function getBodyParam($name) { } public function hasBodyParam($name) { } public function hasBodyParams() { } public function getSubmitParams() { if ($this->hasBodyParams()) { return $this->getBodyParams(); } return $this->getRequest()->getPost(); } public function direct() { return $this->getSubmitParams(); }}
Retrieving parameters Usage
class FooController extends Zend_Rest_Controller{ public
function postAction() { // assuming Params helper
// is registered... $params = $this->_helper->params();
$model = $this->getModel(); if (!$model->create($params)) {
// ... } // ... }}
Detecting and Using Range Headers
Range: The Problem
Requesting all items from a RESTful resource is bad; so is returning all resultsMore results == worse performance
More results == more time to retrieve
More results often leads to exhausting memory resources
The Range header may be used to request a subset of the items
Use a plugin to detect the Range, and inject it into the request object
Detecting the Range header
public function dispatchLoopStartup( Zend_Controller_Request_Abstract $request) { if (!$range = $request->getHeader('Range')) { return; } // Format is: Range: items=0-9 $range = explode('=', $range); list($start, $end) = explode('-', $range[1]);
$request->setParams(array( 'range_start' => $start, 'range_end' => $end, ));}
Using range in a view script
// Assuming that sprints is a Paginator object:$request = $this->request;if ($request->range_start || $request->range_end) { $start = (int) $request->range_start; $end = (int) $request->range_end; $count = $this->sprints->getTotalItemCount(); $limit = (!$end) ? $count : $end - $start; $end = (!$end) ? $count : $end;
$items = $this->sprints->getAdapter()
->getItems($start, $limit);
$this->response->setHeader('Content-Range', 'items ' . $start
. '-' . $end
. '/' . $count);}
Leveraging REST to create
CRUD applications using Dojo
CRUD: The Problem
CRUD == Create Read Update Delete
bo-ring!
REST solves CRUD on the server-side and makes it trivial
Leverage client-side UI toolkits to solve the problem of CRUD screensdojox.data.JsonRestStore interfaces with REST endpoints
dojox.grid.DataGrid can consume JsonRestStore
Defining a Simple Mapping Description
scrum.sprint.schema = {properties: { id: { type: "integer" }, backlog_id: { type: "integer" }, startDate: { type: "string", "default": new Date() }, endDate: { type: "string", "default": new Date() }, scrumTime: { type: "string", "default": "T10:00:00" }}};
Defining a JsonRestStore
dojo.require("dojox.data.JsonRestStore");var store = new dojox.data.JsonRestStore({ target: "/sprint", idAttribute: "id", schema: scrum.sprint.schema});
Defining a DataGrid layout
var layout = [ { field: 'backlog_id', name: 'Backlog',
editable: true }, { field: 'startDate', name: 'Start Date',
editable: true, cellType: "dojox.grid.cells.DateTextBox" }, // ...
{ field: 'scrumTime', name: 'Scrum Time',
editable: true, cellType: "scrum.grid.TimeTextBox" }];
Defining a DataGrid grid
var grid = new dojox.grid.DataGrid({ store: store, rowsPerPage: 10, autoheight: 5, structure: layout}, document.createElement('div'));
Defining a DataGrid (declarative)
Backlog Start Date Scrum Time
DataGrid in Action
In summary
Listen to the fat guy sing
Conclusions
Think about what behaviors you want to expose, and write models that do so.
Use predictable, standards-based services, and use them throughout your application.
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.
Conclusions (cont.)
Leverage headers such as Range to reduce the size of the response, and use Vary to aid in client-side caching.
Leverage client-size UI widgets to simplify common CRUD tasks.
Most importantly
Keep it simple and predictable.
Thank you!
Feedback: http://joind.in/talk/view/883
ZF site: http://framework.zend.com/
Twitter: http://twitter.com/weierophinney
Blog: http://weierophinney.net/matthew/
Click to edit the title text format
Click to edit the outline text formatSecond Outline LevelThird Outline LevelFourth Outline LevelFifth Outline LevelSixth Outline LevelSeventh Outline LevelEighth Outline LevelNinth Outline Level
All rights reserved. Zend Technologies, Inc.
Click to edit the title text format
Click to edit the outline text formatSecond Outline LevelThird Outline LevelFourth Outline LevelFifth Outline LevelSixth Outline LevelSeventh Outline LevelEighth Outline LevelNinth Outline Level
All rights reserved. Zend Technologies, Inc.
Click to edit the title text format
Click to edit the outline text formatSecond Outline LevelThird Outline LevelFourth Outline LevelFifth Outline LevelSixth Outline LevelSeventh Outline LevelEighth Outline LevelNinth Outline Level
All rights reserved. Zend Technologies, Inc.