Skip to content
This repository has been archived by the owner on Jul 5, 2022. It is now read-only.

RFC: Ensemble Kernel

juriansluiman edited this page Sep 15, 2012 · 4 revisions

Ensemble is a system based on Zend Framework 2. With a set of modules it provides a DRY way to create "pages" in a website. A Page in this context is a web address (URI) where, in Zend Framework 2 terms, a route is provided and some navigation configured. Ensemble concentrates around pages with content management involved. For example, static text pages, contact forms, blogs and portfolios are common scenarios for pages. In all examples, content management is required, pages have routing & navigation and modules might be designed DRY to serve content for multiple pages from a single module.

The goal for ensemble is to supply a Content Management Framework: it must be made easy to create content managed application with the option for application developers to create their own, custom Content Management System. Ensemble has the goal to make this setup flexible and powerful, without compromising 3rd party modules which already provide other kinds of MVC structures (example: the routing of ZfcUser can reside next to the routing of pages in Ensemble).

Ensemble's kernel

Ensemble centralizes all features around a kernel. The kernel is an opinionated ZF2 module where a page entity is used to build a site. Multiple pages build the so-called sitemap in a tree structure. This structure is used for both routing and navigation. The kernel uses events to direct the application flow and leverage ZF2 components as much as possible. Two important events are used throughout the application life cycle:

Flow

There are similarities in bootstrap vs dispatch and parse vs load: bootstrap is unaware of any specific route, in dispatch the specific controller/action is executed. In parse, all page entities are parsed into a route and navigation structure (unaware of what page later needs to be loaded) and load is specifically meant to trigger when an ensemble page is executed.

Some example listeners for the parse event:

  1. Load all routes (from cache, if present) and inject those into the router
  2. Load all navigation pages (from cache, if present) and set the container as default in the view helper

Some example listeners for the load event:

  1. Load the page entity from the database
  2. Find the page in the navigation tree and mark it active
  3. Inject the title of the page into the headTitle view helper

Link between page and module

A page is attached to a single module. One module can provide content for multiple pages. A page is aware of its module because of a module property. The module property is a string value. The module can distinguish pages because the module can put a module identifier in the page.

<?php

interface PageInterface
{
  // more methods here

  public function getModule();
  public function getModuleId();
}

For the kernel, several processes happen where the module provides configuration based on this module name. The page is also injected as parameter into the MvcEvent, so when a controller is dispatched, a module using ensemble's kernel can check the page id.

A simple example for the latter:

<?php

namespace MyModule\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $id   = $this->getEvent()->getParam('page')->getModuleId();
        $text = $this->getRepository()->findById($id);

        return new ViewModel(array('text' => $text));
    }
}

In above example, it is already clear modules using ensemble's kernel do not change much. The Module class is the same, they provide normal controllers and they are dispatched because of a route match just like other controllers.

The only difference is that the ensemble kernel parses a list of routes, based on a collection of pages, and inject those into the router. All these parsed routes simply point to a controller/action just like normal 3rd party controllers. This makes the system very powerful.

The Parse event

Parsing now supplies two default events: one for routing and one for navigation. For the sake of clarity, this RFC explains the navigation first.

Navigation

The Page interface has a method to fetch a related meta data object:

<?php

interface PageInterface
{
    public function getParent();
    public function getChildren();
    public function hasChildren();

    public function getRoute();
    public function getMetaData();

    // more methods here
}

interface MetaDataInterface
{
    public function getTitle();
    public function getNavigationTitle();
    public function getDescriptiveTitle();

    public function getDescription();
    public function getKeywords();
}

The navigation walks through all pages (including children) and creates a Zend\Navigation\Navigation object with all the pages and their child pages. The navigation factory instantiates MVC pages and it uses the navigation title as label for the navigation object and the route.

Navigation and descriptive titles

The current default implementation checks if the navigation property is set. If so, it returns that one, otherwise, it returns the title. The same holds for the descriptive title. The reasoning behind this is the idea to have optionally shorter titles for some pages. You might show the page title as "Frequently Asked Question", but obviously you want to abbreviate this to "FAQ" in your menu. Also, for example home pages usually have larger titles in the markup's <title> tag. For example, www.zend.com doesn't have "Zend" or "Home" as a title, but "PHP Web Application Server - PHP Development tools - PHP Training - Zend.com". In this case, you might want to call the page "Home", but have a much longer descriptive title.

Routing

The implementation of routing is made because of two requirements: route setup for pages and flexible routes for modules.

A page has a route part as one of its properties. This means that the page "About" for example could have a route part /about. The About page has a sub-page named "Team", which has a route part /team. This results in a compiled route of the team page /about/team. The kernel recursively walks through all pages and parses routes for every page.

Modules can provide parts of routes too. For example, you have a photo blog where you post a new photo every day. In a normal zf2 application, such route might look like /photoblog/:day. In ensemble, a page can provide the /photoblog part of the route, where the module provides a configuration value telling the kernel to append /:day as a parameter. With this setup, it is also possible to create pages where the module provides child routes. The parser merges the route config from the page with the route supplied by the module.

As an example, take a look at this simple blog. It has a route that will list the articles and a child route that will display a specific article:

<?php

return array(
    'cmf_routes' => array(
        'blog' => array(  // This is the module identifier
            'options' => array(
                'defaults' => array(
                    'controller' => 'Blog\Controller\ArticleController',
                    'action'     => 'index'
                ),
            ),
            'child_routes' => array(
                'view' => array(
                    'type'    => 'segment',
                    'options' => array(
                        'route'    => '/article/:id',
                        'defaults' => array(
                            'action' => 'view'
                        ),
                        'constraints' => array(
                            'id' => '[0-9]+'
                        ),
                    ),
                ),
            ),
        ),
    ),
);

Conclusion

Routes and navigation play an important role in ensemble. It tries to make both as transparent as possible, without interfering with other 1st or 3rd party modules which do not use ensemble. Modules can plug into ensemble very easily, with great benefits. In a DRY way one module could provide content for multiple pages. With the right admin interface, end users can create websites in a few clicks. A last benefit is ensemble keeps the implementation of the content to the modules. It means a module could fetch content from a SQL database, external service, MongoDB, XML file on the file system or whatever format.

Adapters

The kernel is persistency-layer agnostic. As long as an adapter returns a PageCollection where a tree of PageInterface entities is stored, the kernel can work. There are two adapters planned: a Zend\Db adapter and a Doctrine adapter. Both provide 100% feature coverage of the ensemble kernel.

In both modules an implementation of the Ensemble\Kernel\Service\PageInterface interface. A factory in the kernel is present to load this implementation under the service name Ensemble\Kernel\Service\Page. In the module configuration, the module must provide a FQCN under this key:

<?php
return array(
    'ensemble_kernel' => array(
        'page_service_class' => 'Ensemble\KernelDoctrineOrm\Service\Page',
    ),
);

The module provides a factory for the Ensemble\KernelDoctrineOrm\Service\Page service too. The kernel fetches this service from the page_service_class, checks if the service implements the interface and returns it. This way, the call to a page service implementation keeps the same and the adapter modules can create their own factories for the service.