Zend Framework Application Patterns at DPC10

I’m currently in the fine city of Amsterdam enjoying what is incredibly my first PHP conference in ten years of developing with the language! Yesterday was tutorial day, with the full conference starting today, and I sat in Zend Framework Application Patterns by the informative and engaging Matthew Weier O’Phinney and Rob Allen.

The session was excellent, well worth attending, and dipped into many areas of ZF. Some of which I knew already, but there was certainly enough good tips on how to organise applications efficiently in ZF which I’ll be telling my team all about when I get back to the UK.

My notes from the tutorial day appear below, be warned they are rather long! You can also review the Zend Framework Workshop slides over at Slideshare.

Service layers

Think of the application as an onion layer. From the inside out:

  • Data Access objects (inner layer)
  • Data Mappers, Repositories, Transaction scripts
  • Domain Models and Entities (plain old PHP objects)
  • Service layer (outer layer: how will I interact with all this, the public API)

It’s better to have a rich domain model, validation in domain model.

  • Start with a plain old PHP object
  • Define schema based on objects you use
  • Create objects first
  • Create schema once models have been created and tested

Use mappers or transaction scripts to translate objects to data and back again.

Use Service Object to manipulate entities. Ie.

  • fetchEntry()
  • fetchComments()
  • addComment(), etc

What’s in a Service Layer?

  • Application specific logic
  • Forms, validation, caching

All the bits and pieces of the app I don’t want to deal with when writing the front-end.

Q: Should we use services for everything?
A: Use for all parts of an app, easy to test

Rob added:

  • he uses the form as the service layer
  • uses a mapper class based on ZendDbTable
  • compose the form in the service layer
  • tend to put search indexing on mapper layer (postSave method)

ACL

ACL is roles, resources and rights (resource privileges). By default ACL operates as a whitelist (i.e. open up privileges to those you trust). Blacklisting is less secure, not recommended.

One approach is to extend Zend_Acl and set up roles and resources in constructor. Roles are simply text strings. Resources are much the same but have an array of privileges that can also be passed (i.e. read, write).

Suggested logic:

  • Check ACL
  • Check the role acting on a resource has a right to perform the action
  • isAllowed() – checks role, resource, privilege

Matthew added:

  • I like to throw a specific exception for failed ACLs so I can detect it
  • I can then check for another exception case in the ErrorController to display a Not Authorised page (nice approach)

Paginators

  • Return paginators from your domain models
  • Consumers can specify offset and limit
  • Zend_Paginator implements IteratorAggregate and toJson()
  • Paginators have efficient, lazy loading

I.e. fetchAll() in domain model returns paginator

Zend_Application

  • For common configuration & per-environment configuration
  • Resource injection so you can use bootstrap as resource repository
  • Resources: in bootstrap class or as a re-usable plugin resource
  • Reusing for a service endpoint (i.e. set environment = jsonrpc)
  • Dependency tracking
  • Accessing resources via controller
  • Module bootstrapping. Not particularly good at present. Runs all module bootstraps if exists. Order of running module/main app boostraps ambigious. Don’t mix resource names/application.ini key names in main app + module. Kathryn Reeve wrote a nice module bootstrapping solution.

Routing clinic

As an aside throughout the day Matthew and Rob have been using the syntax use Zend_Long_Class_Name as ShortName – nice way to alias class names in PHP 5.3

  • Basics: Name the route, specify the path, specify default values and optional validation
  • RegexRoute can support: 'blog/(?<id>[^.]*\+)\.(?<format>xml|json)'
  • That regex route stores first param as ‘id’, 2nd as ‘format’ (that’s the handy ?<name> format)
  • Regex routes are very fast, even more so in PHP 5.3
  • Touched on hostname routing which you then need to chain with a normal route
  • RESTful routes Zend via Zend_Rest_Route
  • Can specify as default route, or add it for all controllers in a module
  • Or set REST routes for specific module / controller / actions
  • Controller then needs to extend ZendRestController (listAction, postAction, etc)
  • ID is accessed via getParam(‘id’)

Layout View Helpers

  • You should always run output via the $this->escape() view helper! In ZF2 it will be default when you echo content
  • Layout helpers: Set metadata, aggregate content, rendered in layout
  • If you have CSS/JS related to a particular layout put it in your view script. Really important for maintenance.
  • doctype() affects rendering of other view helpers, i.e. forms. Ideally needs to be set in bootstrap: resources.view.doctype = XHTML1_STRICT
  • Head view helpers – can append and prepend content.
    • headMeta(), headTitle(), headScript(), headStyle(), headLink()
    • inlineStyle() – usually at top of body
    • inlineScript() – usually at bottom of body
  • Use a layout helper if your functionality is shared across multiple view scripts to set all this common stuff
  • Append and prepend helps give an order to things like how CSS files are loaded
  • echo $this->baseUrl('path/to/file') will return BASE_URL . ‘path/to/file’ which is handy

Zend_Navigation

  • Helps manage trees of pointers to web pages
  • Supports HTML header links (i.e. next, previous, alt formats)
  • Can generate XML Sitemaps (used by Google)
  • Translate link text
  • Conditionally display links based on ACL
  • Two main concepts are pages and containers
  • Page types: MVC (router based URLs), URI (static URLs)
  • $page = Zend_Navigation_Page::factory(array('uri' => '/uri'))
  • Containers implements RecursiveInterator (iterate entire tree) & Countable
  • Pages are also a container, you can have trees of pages
  • Add pages with $container->addPage()
  • Can add multiple pages via addPages()
  • You can remove pages based on index or order number
  • Visibility and active page properties affect whether a page is rendered to the page
  • Can find pages via methods like findOneBy(), findAllBy(), findOneByLabel()
  • You can define your own metadata for pages. For example if you have a ‘tag’ property, find method for nav is findAllBy(‘tag’, ‘value’) or findAllByTag(‘value’)
  • Rendering on the page:

    foreach ($container as $page) {
        echo $page->label;
    }

Cache your definitions

  • On first request build programmatically and cache via toArray()
  • On subsequent requests load from cached array
  • Only use when you need it (i.e. not when doing JSON requests, service calls, etc)

Navigation view helpers

  • $view->navigation()->setAcl($acl);
  • $view->navigation()->setRole($role);
  • Can set default ACL and default roles in your bootstrap. I’d suggest passing ACL from your controller into your view.
  • Check for ACLs with hasAcl() & hasRole()
  • echo $this->navigation()->htmlify($page);
  • Renders link, uses label as title attribute
  • Conditionally echo link

    if ($this->navigation()->accept($page)) {
        $this->navigation()->htmlify($page);
    }
  • Note that other Nav view helpers are ACL-aware. I.e. echo $this->navigation()->sitemap()

  • Sitemap specific metadata: lastmod, changefreq, priority
  • Set priority of news list pages lower than news pages themselves, so Google deeplinks to article pages rather than news list pages
  • Render breadcrumbs via $this->navigation()->breadcrumb()
  • Useful configuration is setMaxDepth($depth) to avoid very long breadcrumbs
  • echo $view->navigation()->menu()->renderSubMenu() – current active tree only
  • Gotcha with breadcrumbs. If current page is hidden it won’t render breadcrumb

Caching

  • Principle: Request from browser, is it cached? If so serve from cache, if not generate and save in cache
  • Adapter based system. Front-end = what to cache, Back-end = where to cache
  • Front-end options: set lifetime = null to cache forever (you then control when you empty the cache)
  • Manually initialise cache in bootstrap. I.e. _initCache()
  • Don’t do it that way! There is a better way! Use Zend_Cache_Manager (manage multiple caches)

    • Lazy loads on demand
    • Contains preconfigured caches
    • Application resource
    • Can use action helper to access it
resources.cachemanager.default.frontend.options.lifetime = "..."
backend.options.cachedir = APPLICATIONPATH ".."

‘default’ is name of cache. Make sure you use different keys for multiple caches.

Controller:

protected function _getCache()
{
   $bootstrap = $this->getInvokeArg('boostrap');
   return $bootstrap->getResource('cachemanager')->getCache('default');
}

// Helper
$cache = $this->_helper->getHelper('Cache')->getManager()->getCache('default');
  • Zend_Cache_Page = full page caching – skip entire MVC stack
  • Implement in index.php before config loading
  • frontend options: ‘regexps’states which pages to enable page caching for. Not usually what you want for all pages on a site (i.e. dynamic content, forms for error validation, shopping baskets, etc)
  • You can state cache everything apart from exceptions with regex rules

Some numbers to think about

  • No caching: 12 trans/sec, cache DB: 29 trans/sec, cache page: 359 trans/sec
  • With APC enabled: no caching: 19 trans/secs, cache db: 289 trans/sec, cache page: 3251 trans/sec

  • Static page cache exists to store cache of HTML files to local document root. Amazing performance.

Context Switching

  • Context switching is the act of providing different output based on criteria from the request
  • I.e. XMLHttpRequest, REST, Mobile device
  • WRFL – list of all mobile platforms and browser detection strings & capabilities
  • Works via a ‘format’ parameter request. This can be set via:
    – Query param
    – Special route
    – Chained routes (‘xml’, chain with a ‘.’ – i.e. controller/action.xml)
    – HTTP headers (Accept header)
  • ContextSwitch action helper determines whether action has context matching the format
  • Additional view suffix is added (i.e. viewscript.json.phtml, viewscript.xml.phtml)
  • Keeps things explicit so you know where your code/views are

Add context switching to controller:

$contextSwitch = $this->_helper->getHelper('contextSwitch');
$contextSwitch->addActionContext('action', 'format')
              ->initContext();

Sitemaps and robots.txt

A neat example of context switching from Rob.

  • Two static routes sitemap.xml & robots.txt
  • Format values of ‘xml’ and ‘txt’
  • Have to manually add context for txt (sets the correct Content-type header)
  • View = sitemap.xml.phtml & robots.txt.phtml
  • Can support HTML version of sitemap in sitemap.phtml
  • So context switching changes Content-type and view script and uses same controller action
  • Can do other stuff by getting param ‘format’

Header detection

  • setHeader(‘Vary’, ‘Accept’) – tells browser not to cache Accept header. Important!
  • Set format based on accept header
  • getHeader(‘Accept’): application/json or application/xml
  • To be honest I got lost around here. Will check out slides & Matthew’s blog later :)

Form Decorators

  • Renders elements & forms
  • Decorators stack from inside to outside
  • The order of your decorators is the most important thing

Standard element decorators:

  1. Zend_Form_Decorator_ViewHelper – renders input field
  2. Zend_Form_Decorator_Errors – UL list of errors
  3. Zend_Form_Decorator_Description – P description
  4. Zend_Form_Decorator_HtmlTag – DL/DL tags
  5. Zend_Form_Decorator_Label – field label

  • The above happens in Zend_Form_Element::loadDefaultDecorators()
  • Decorator itself states whether it wraps other elements or not
  • Change this with: element->placement->prepend() – puts in front

Standard form decorators:

  1. Zend_Form_Decorator_FormElements – form elements
  2. Zend_Form_Decorator_HtmlTag – DL tag
  3. Zend_Form_Decorator_Form – FORM tag

Common use cases to customise decorators:

  • Change form element HTML (i.e. ul instead of dl)
    • Clear decorators and re-add them as you want them
  • Change HTML outputted, i.e. parse form description using markdown
    • Extend Description decorator and alter output of description HTML

If you use your own decorator classes:

$form->addPrefixPath() – for Form class
$form->addElementPrefixPath() – for Form element & Validate classes

  • Write your own decorators. Implement a render() method to return HTML. Extends Zend_Form_Decorator_Abstract
  • render($content) accepts previously rendered content. You need to return the content you get in so you can wrap / append / prepend content – (that bit never made sense to me before!)
  • Good idea to check the user defined separator & placement and alter output accordingly (otherwise you may break expected functionality)
  • Sidenote: Rob uses simple App_ namespace for client website specific library files. Nice and straightforward
  • Rob displays errors on form, not element. He removes errors on each element and puts a list at the top. This is achieved via a custom decorator called Form_Errors

Testing

  • What do I test?
    • Domain models
    • Service layer objects
    • Integration testing (does your MVC generate the content you’re expecting?)

Testing MVC pages

  • Extend Zend_Test_PHPUnit_ControllerTestCase
  • setup() creates instance of Zend_Application
  • assertions to match content. Uses CSS selectors aka jQuery (nice)
  • also supports XPath expressions to match content
  • redirect assertions
  • response assertions (response codes, matching header content)
  • request assertions (matching module, controller, action, route)

I.e. Assert a H1 heading exists: $this->assertQuery('#content h1')

Q&A

I quizzed Rob in the break on CMS style routing where all page URLs are stored in a database. He expanded on this in the Q&A session.

  • MS_Controller_RouterRoutePostSlug (a slug = a URL fragment in good old WordPress terminology)
  • implements Zend_Controller_Router_Route_Interface
  • $router->addRoute(‘postSlug’, new MSControllerRouterRoutePostSlug)
  • Must implement 3 methods:
    • getInstance() – not used in Rob’s example, return new self(); neatly disables it
    • match() – must return array of module, controller, action. (This is totally undocumented in code or manual! Nice to see an example)
    • asemble() – creates a URL via url view helper. Usage may pass menu ID to create URL

match($path)

  • method argument $path = URL path
  • if return false, router falls through to next route
  • Get slugs/URL fragments from DB in bootstrap & cache it for speed
  • $slugs = array(URL => array(‘module’, ‘controller’, ‘action’, ‘navigaton object’, ‘menu_id’) )
  • custom keys accessed via $request->getParam(‘menu_id’)

assemble($data = array(), etc)

  • For normal routes you pass the module, controller, action, etc in via the $data array
  • In this example you can put the following into the $data array: full url, page, menuId
  • Returns URL string: output URL string . $this->_assembleRemainingParameters($data)
  • That last bit is important if any custom params have been passed to assemble by the user

We all finished off with a stirring debate on how awful WSYIWYG editors are when someone asked how Rob convinces clients to use markdown.

  • Rob just doesn’t give them a choice, doesn’t get many problems most clients OK (he is tech director so he can also decide within his company)
  • Rob uses Markdown for clients (exactly what I used to markup all these notes!)
  • If you allow HTML input from the user then don’t try to sanitize with ZendFilterStriptags. Use HTML Purifier instead which can be used to create whitelists. Pádraic Brady has blogged a lot about HTML Purifier

3 thoughts on “Zend Framework Application Patterns at DPC10

Comments are closed.