architecting ajax applications with zend framework

Download Architecting Ajax Applications with Zend Framework

If you can't read please download the document

Upload: matthew-weier-ophinney

Post on 16-Apr-2017

49.500 views

Category:

Technology


5 download

TRANSCRIPT

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.