PHP

All things PHP

Appendix: Build Your Own

Here are the steps required to work from the Zend Skeleton Application to the Starter Application (as at 29 July 2015).

  1. Download and install the Zend Skeleton Application according to its instructions
  2. Browse to the virtual host to check that it works
  3. Remove or un-configure any features that you do not require, for example caching or translation
  4. Add or configure any additional features that you require, for example configure logging
  5. Rename the Application module to Core (or another name), and update configuration so that it works
  6. Add a Mapper module including a minimum configuration (Module.php)
  7. Create the following directory structure in the Core/src directory (to mirror the Core namespace):
    Core     
        /Controller         
            /Plugin
        /Domain
            /iDomain
            /Proxy
            /Factory           
            /Collection             
                /iCollection             
                /Proxy
        /Service         
            /Event         
            /InputFilters     
        /View         
            /Helper      
        /Exception     
        /iMapper
    
  8. In the root of the Mapper module, create the following directory: src/Mapper/Exception
  9. Create the following files, or copy them from the Starter Application:
    Core/HelperTrait.php
    Core/Controller/AbstractController.php
    Core/iMapper/Mapper.php
    
    Core/Exception/Exception.php
    Core/Exception/Collection.php
    Core/Exception/Factory.php
    Core/Exception/InvalidInput.php
    Core/Exception/Proxy.php
    
    Core/Domain/AbstractDomain.php
    Core/Domain/DomainWatcher.php
    Core/Domain/Collection/AbstractCollection.php
    Core/Domain/Collection/iCollection/Collection.php
    Core/Domain/Collection/Proxy/AbstractCollectionProxy.php
    Core/Domain/Factory/AbstractFactory.php
    Core/Domain/Factory/FactoryLoader.php
    Core/Domain/iDomain/Domain.php
    Core/Domain/Proxy/AbstractProxy.php
    
    Core/Service/AbstractService.php
    Core/Service/ServiceLoader.php
    Core/Service/Event/AbstractListener
    
    Mapper/AbstractMapper.php
    Mapper/MapperLoader.php
    Mapper/Exception/Exception.php
    
  10. Modify the IndexController.php to extend from AbstractController.php. Browse the virtual host to check that it still works
  11. Configure the Core module to use abstract factories that create Domain object factories and Service Layer services:
    'service_manager' => array(     
        'abstract_factories' => array(
            'Core\Domain\Factory\FactoryLoader',         
            'Core\Service\ServiceLoader',
        ),
     ),
  12. Configure the Mapper module to use an abstract factory to create Mapper objects:
    'service_manager' => array(
        'abstract_factories' => array(
            'Mapper\MapperLoader',
        ),
    ),
  13. Browse to the virtual host again to check that it still works
Tags: 

The N+1 Selects Problem

The N+1 selects problem can occur wherever a Domain object has a multi-valued property. Client code triggers a select query on the database to get hold of a first object, then a further 'N' selects to get hold of each member of the multi-valued property.

In the case of the Starter Application, most of the problem is taken care of in the object factories. The factory that creates the first object sets its multi-valued properties to collection proxies (in doAddRelations). In the diagram below for example, the Foo factory sets a Foo::Bars property to a Bar collection proxy. If/when the multi-valued property is accessed, data for the entire collection is retrieved in a single query.



Potential for N+1 Select Problems

This works fine for simple two-level, 'parent/child' relationships, however the situation becomes more complex when additional levels are introduced. Where three levels of objects are accessed, the factory that creates the first object must use special collection proxies for multi-valued properties, proxies which not only load child data, but also 'eagerly load' data for objects related to those children.

Eager loading can become unwieldy in complex object graphs. If you are regularly working more than three levels deep it may be time to consider an ORM or alternative database access patterns.

Eager Loading

In the case of the diagram above, if Foo is loaded in order to access Baz or the Bats, the N+1 problem will occur. To avoid this, client code must forewarn the Foo factory that it intends to access Baz and Bat, and the Foo factory must then attach a Bar collection proxy that eagerly loads Baz and Bat data. I use 'flavours' to let the factory know when it should eagerly load data.

The possible flavours of a factory are stored as class constants in the factory itself, where the value of each constant is a bitmask. The actual flavour of a factory instance is stored as a bit field in $factory->flavour, and can be modified via the fluent methods setFlavour and addFlavour. (Note: if, for some reason, your service locator returns a new instance of a factory each time it is called, you will need to use a static factory::flavour.)

Suitable flavours for the Foo factory would be:

const BARS_WITH_BAZ  = 1;
const BARS_WITH_BATS = 2;

Client code, such as the Service Layer, sets the flavour as follows.

$this->serviceLocator->get('Core\Domain\Factory\Foo')
    ->setFlavour(Factory\Foo::BAR_WITH_BAZ)
    ->addFlavour(Factory\Foo::BAR_WITH_BATS);

The Foo factory needs to be updated with conditional code that sets the correct proxies based on the current factory flavour. The changes are limited to doAddRelations, and the updated method is as follows:

protected function doAddRelations(array &$data)
{
    if (empty($data['bars'])) {

        if (($this->flavour & self::BAR_WITH_BAZ) &&
            ($this->flavour & self::BAR_WITH_BATS)) {

            $data['bars'] = $this->getCollectionProxy(
                'Bar', 'findByFooWithBazAndBats', array($data['id']));

        } elseif ($this->flavour & self::BAR_WITH_BAZ) {

            $data['bars'] = $this->getCollectionProxy(
                'Bar', 'findByFooWithBaz', array($data['id']));

        } elseif ($this->flavour & self::BAR_WITH_BATS) {

            $data['bars'] = $this->getCollectionProxy(
                'Bar', 'findByFooWithBats', array($data['id']));

        } else {

            $data['bars'] = $this->getCollectionProxy(
                'Bar', 'findByFoo', array($data['id']));
        }
    }
}

Of course this will have a knock-on effect on the Bar mapper which must be updated with three new finder methods: findByFooWithBazAndBats, findByFooWithBaz, findByFooWithBats.

These new methods are more complex than other mapper methods. You will need to decide whether to join tables and bring data back in one query, or run multiple queries, and you will need to manipulate arrays and sub-arrays. It will help if you have mastered PHP array functions. Closures are often useful here too.

Note that BarFactory::doTypeConversion is already set up to handle arrays as inputs for the Baz and Bats properties, and is therefore ready to work with the multi-dimensional arrays produced by the new Bar mapper methods. I have found that it is a good policy to build factories that handle arrays as inputs for all properties that are objects or collections. In most cases they will eventually come in handy.

If you are comfortable with the eager loading approach, but not the proliferation of Mapper methods, you could consider a solution that uses a single mapper method to load the Bar collection with all direct associates, something like BarMapper::findByFooWithAssociates.

A Note on Collections

I started this section by saying that the N+1 problem can occur wherever a Domain object has a multi-valued property, but there is a second form of the problem too. The second form occurs when client code, usually the Service Layer, calls a mapper method which returns a collection, and each member of the collection has a proxied property. If the client iterates the collection, accessing the proxied property along the way, the result is N trips to the database.

The obvious solution to this is to add mapper methods that eagerly load data for properties which were proxied, and let the Service Layer call these instead of the more simple finders. In my experience, these additional mapper methods are often one-and-the-same as the methods required for eager loading in the first form of the problem, which is convenient.

Tags: 

The Data Mapper

Data Mappers add to the development effort but they give the model the "freedom to be whatever it needs to be". A typical mapper includes:

  • A find($id) method that returns a single Domain object. The method checks the identity map to ensure the object is not already in memory before taking a trip to the database.
  • Other single-object finders, such as findByName($name).
  • Numerous collection finders, such as findAll($status = null) and findByFoo($id).
  • An insert($object) method which constructs an array from the object’s properties then constructs and executes an SQL insert statement.
  • An update($object) method which constructs an array from the object’s updatable properties then constructs and executes an SQL update statement.

In a simple case there may be one Data Mapper for each Domain object however this won’t always be the case. Martin Fowler’s POEAA has a good discussion on the variations, (Chapter 12, Object-Relational Structural Patterns). Patterns that don’t necessarily conform to the 1-to-1 norm include dependent mapping, embedded value, and each of the table inheritance mappers.

As a general rule I start by assuming a 1-to-1 relationship, and when this becomes strained I start looking at the alternatives. Even in cases where it seems obvious to break the 1-to-1 norm, such as class table inheritance, it may transpire that the 1-to-1 works better.

Here is a Bar mapper example.

namespace Mapper;

use Core;

class Bar extends AbstractMapper implements Core\iMapper\Bar
{
    public function find($id)
    {
        if ($exists = $this->getWatcher()->exists($this->getShortType($this), $id)) {
            return $exists;
        }

        $sql    = "SELECT * FROM bar WHERE id = ?";
        $stmt   = $this->getAdapter()->createStatement($sql);
        $result = $stmt->execute(array($id));

        if (!count($result)) {
            return null;
        }

        return $this->create($result->current());
    }

    public function findByFoo($id)
    {
        $sql    = 'SELECT * FROM bar WHERE foo = ?';
        $stmt   = $this->getAdapter()->createStatement($sql);
        $result = $stmt->execute(array($id));
        $rows   = $result->getResource()->fetchAll();

        return new Core\Domain\Collection\Bar(
            $this->getFactory(), $this->convertKeysToCamel($rows));
    }

    public function insert(Core\Domain\iDomain\Bar $bar)
    {
        $data = array(
            ':startDate' => $bar->startDate->format('U'),
            ':condition' => $bar->condition,
            ':foo'       => $bar->foo->id,
            ':baz'       => $bar->baz->id,
        );

        $sql    = 'INSERT INTO bar (startDate, condition, foo, baz) ';
        $sql   .= 'VALUES (:startDate, :condition, :foo, :baz)';
        $stmt   = $this->getAdapter()->createStatement($sql);

        $stmt->execute($data);
        $bar->id = $this->getAdapter()->driver->getLastGeneratedValue();
        $this->getWatcher()->add($bar);

        return $bar->id;
    }

    public function update(Core\Domain\iDomain\Bar $bar)
    {
        $data = array(
            ':id'        => $bar->id,
            ':condition' => $bar->condition,
            ':baz'       => $bar->baz->id,
        );

        $sql    = 'UPDATE bar SET condition = :condition, baz = :baz WHERE id = :id';
        $stmt   = $this->getAdapter()->createStatement($sql);
        $stmt->execute($data);
    }

}

Data Mappers are loaded via an abstract factory, the MapperLoader. Note that the mapper converts field names between under_score format in the RDBMS and camelCase for objects.

The Service Layer

My Service Layer provides whatever public methods are required by the controllers and occasionally views. To maximise the utility of the Service Layer, each of its classes is service-locator-aware, and each has its own event listener.

Core\Service\ServiceLoader is the abstract factory responsible for creating Service Layer services and you can see there how it creates and injects the event listener.

Here is the code for a sample Bar Service.

namespace Core\Service;

class Bar extends AbstractService
{
    public function updateBaz($bar, $baz)
    {
        $bar = $this->serviceLocator->get('Mapper\Bar')->find($bar);
        $baz = $this->serviceLocator->get('Mapper\Baz')->find($baz);

        $bar->baz = $baz;
        $this->serviceLocator->get('Mapper\Bar')->update($bar);

        // optional: trigger an event
        $this->eventManager->trigger(__FUNCTION__ . '.post', $this, array(
            'bar' => $bar,
            'baz' => $baz
        ));

        return $baz;
    }
}

As a general rule, all calls to Mappers to persist changes are made from the Service Layer. If this becomes difficult to manage, you might consider extending the Domain Watcher, and using the Unit of Work patten for persistence.

Service Layer Events

Service Layer Events can be used to separate out entire aspects of the application, such as logging or authentication, from the main workflow. Alternatively they may be used for one-off tasks such as triggering an email.

Events which are triggered by a range of workflows are best placed in the AbstractListener base class. Those that apply only to a particular Service Layer service may be placed in the concrete listener that belongs to that particular service. All listeners are stored in the Core\Service\Event namespace.

Here's an example of a listener that should be activated when Bar::updateBaz has executed.

namespace Core\Service\Event;

use Zend\EventManager\EventInterface;
use Zend\EventManager\EventManagerInterface;

class BarService extends AbstractListener 
{
    public function attach(EventManagerInterface $em)
    {
        $em->attach('updateBaz.post', array($this, 'emailAdmin'));
    }

    public function emailAdmin(EventInterface $e)
    {
        // do some stuff here
    }
}

Collections

Collection classes are iterable and countable. The raw data for each member is stored in an array, and members are instantiated on request only. There is generally a collection class for each Domain class and it may include custom logic that applies only to a collection of the given objects.

Custom logic may relate to sorting, filtering, adding members, removing members, comparing collections, and more. For methods that filter or sort the collection, I find it easiest and safest to pass out a read-only clone, rather than reorder the underlying data.

Collections are commonly returned by a mapper. On construction a collection takes two parameters: a factory for creating objects of the given type, and an array such as a database record set, where each element is a sub-array with the raw data for a single object. The code below is for a Foo collection. Notice that most of the functionality is implemented in the AbstractCollection.

namespace Core\Domain\Collection;

use Core\Domain\Collection\iCollection;
use Core\Domain\iDomain;

class Foo extends AbstractCollection implements iCollection\Foo 
{
    public function add(iDomain\Foo $foo)
    {
        $this->doAdd($foo);
    }
} 

The role of the iCollection\Foo interface is to ensure that only iDomain\Foo objects are added to the collection. In addition, it makes way for collection substitutes such as collection proxies. Following is an interface for a Foo collection.

namespace Core\Domain\Collection\iCollection;

use Core\Domain\iDomain;

interface Foo extends Collection
{
    public function add(iDomain\Foo $foo);
}

Collection Proxies

In the same way that there are proxies for Domain objects, there are proxies for collections of Domain objects too. The following class diagram shows how concrete collections (e.g. Core\Domain\Collection\Foo) relate to their proxies (Core\Domain\Collection\Proxy\Foo).


Class Diagram: Collections and their Proxies

Both a collection and its proxy inherit most of their characteristics through a common hierarchy of interfaces. The key difference is that when the collection extends its parent it becomes an \Iterator, and when the proxy extends its parent it becomes an \IteratorAggregate. The 'iterator aggregate' is a proxy for a real iterator, the actual collection.

From the constructor of the AbstractCollectionProxy you can see that proxies are injected with:

  • The Mapper that must be used to retrieve the collection,
  • The name of the method to call on the Mapper, and
  • An array of arguments that must be provided to the method.

When client code attempts to iterate the proxy or access collection methods, the proxy is realised and calls are passed to the true collection. Any attempts to access custom logic methods are passed from the proxy to the real collection using method call overloading.

Here is the code for a Foo collection proxy:

namespace Core\Domain\Collection\Proxy;

use Core\Domain\Collection\iCollection;
use Core\Domain\iDomain;

class Foo extends AbstractCollectionProxy implements iCollection\Foo
{
    public function add(iDomain\Foo $foo)
    {
        $this->getIterator()->add($foo);
    }
}

Collection proxies are used extensively by factories as a proxy for multi-valued properties. Under normal operation, it is not possible for the factory to know which multi-valued properties will be accessed on the object under construction. Instead of retrieving and attaching a large number of properties which may not be used, the factory provides a collection proxy which can efficiently retrieve the collection if/when required.

Factories

Domain object factories are some of the longest and most complex classes. They are loaded via an abstract factory, Core\Domain\Factory\FactoryLoader, and use the factory method pattern to execute the same series of steps irrespective of which particular factory is in use.

The steps outlined in the table below work well with my way of thinking. You should add/remove methods to come up with an implementation that works best for you and incorporate it in to the abstract base factory. Concrete objects should override base methods as required.

Note that when creating an object the code path differs depending on whether the object is an existing entity or entirely new. To differentiate between existing and new entities prior to instantiation, factories test the input data for the existence of an identifier. To differentiate after instantiation, factories test the object for an identifier.

Step Actions
New Object Defaults

Default property values for new entities are set in doNewObjectDefaults. Defaults may override a passed in value, or may apply only when no value is passed. In the case of multi-valued properties, if no defaults apply I usually add an empty collection of the given type.

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    protected function doNewObjectDefaults(array &$data)
    {
        $data['startDate'] = empty($data['startDate']) ? new \DateTime() : 
                $data['startDate'];
        $data['condition'] = 'Good';
        $data['bats']      = $this->getCollection('Bat');
    }
}
Type Conversion

Property values passed to the factory are either scalars, objects or arrays. Exactly how they are handled by the factory depends entirely on the context. Here are some common conversions:

  • A string that represents a date may be converted to a unix timestamp or a \DateTime object
  • An array may be converted to a new Domain object
  • An array of arrays may be converted to a new Domain object collection
  • An integer or other identifier may be converted to a Domain object proxy

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    // ... //

    protected function doTypeConversion(array &$data)
    {
        if (is_int($data['startDate']) || ctype_digit($data['startDate'])) {
            $data['startDate'] = new \DateTime('@'.$data['startDate']);
        }

        if (is_int($data['foo']) || ctype_digit($data['foo'])) {
            $data['foo'] = $this->getProxy('Foo', $data['foo']);
        } elseif (is_array($data['foo'])) {
            $data['foo'] = $this->serviceLocator->get('Core\Domain\Factory\Foo')
                ->create($data['foo']);
        }

        if (is_int($data['baz']) || ctype_digit($data['baz'])) {
            $data['baz'] = $this->getProxy('Baz', $data['baz']);
        } elseif (is_array($data['baz'])) {
            $data['baz'] = $this->serviceLocator->get('Core\Model\Factory\Baz')
                ->create($data['baz']);
        }

        if (isset($data['bats']) && is_array($data['bats'])) {
            $data['bats'] = $this->getCollection('Bat', $data['bats']);
        }
    }

    // ... ..
}
Add Relations

In the case of multi-valued properties it is common for no value to be present in the factory input data. In such cases the doAddRelations method adds a collection proxy, which is ready to retrieve the related objects on demand.

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    // ... //

    protected function doAddRelations(array &$data)
    {
        if (empty($data['bats'])) {
            $data['bats'] = $this->getCollectionProxy(
                'Bat', 'findByBar', array($data['id']));
        }
    }

    // ... //
}
Instantiation

Domain objects are instantiated with an array of property values and optional arrays of finders and factories. doInstantiation uses the service manager to retrieve any required mappers and factories, then instantiates the Domain object.

Example:

namespace Core\Domain\Factory;

use Core\Domain;

class Bar extends AbstractFactory
{
    // ... //

    protected function doInstantiation(array $data)
    {
        return new Domain\Bar(
            $data,
            array('Bat' => $this->serviceLocator->get('Core\Domain\Factory\Bat')),
            array('Bat' => $this->serviceLocator->get('Mapper\Bat')));
    }

    // ... //
}
Post Initialisation

This is a good place to check preconditions for objects entering the active state. The tests often involve high-level business logic and may need to crawl the object’s dependencies. Code paths often differ between new and existing entities. Illegal conditions result in an exception.

Example:

namespace Core\Domain\Factory;

use Core\Domain;
use Core\Exception;

class Bar extends AbstractFactory
{
    // ... //

    protected function doPostInit(Domain\AbstractDomain $bar)
    {
        if ($bar->baz->status !== 'Active') {
            throw new Exception\Factory('$bar->baz must be Active');
        }
    }
}
Domain Watcher

doDomainWatcher checks that an object of the given type and identity is not already in play. If it is, the existing object is returned. Otherwise the new object is added to the identity map and returned.

This method is final and can not be overwritten.

Domain object factories are service-locator-aware and are therefore capable of many things. In the past I have added methods to configure a Domain event manager, for example. Care is required however to avoid introducing unnecessary dependencies. It would not make sense to use Service Layer services within a factory.

Note that the AbstractFactory includes three methods and a property which are related to the 'flavour' of the factory. Flavour is discussed as part of the N+1 Selects Problem.

Domain Objects

Domain objects inherit from an abstract domain class which provides encapsulation and commonly required features. Each implements its own interface which is declared in the Core\Domain\iDomain namespace. The interfaces are used in type tests only, to allow substitutes such as proxies and mocks. They inherit from a parent domain interface and there is no need to declare any class features.

Here's an example of a Foo interface.

namespace Core\Domain\iDomain;

interface Foo extends Domain
{ }

Each concrete class has an $allowed array where each key is the name of an allowed property, and each value is a Boolean that declares whether the property is required (true), or optional (false). Actual property values are stored in a protected $data array.

A Foo object with some optional and some required properties may look as follows:

namespace Core\Domain;

class Foo extends AbstractDomain implements iDomain\Foo
{
    protected $allowed = array(
        'id'      => false,
        'name'    => true,
        'address' => false,
        'country' => true
    );
}

Construction

The Domain objects are designed for use with Domain object factories. The factories do most of the heavy lifting and it is rarely necessary for a Domain object to override the constructor of its parent.

The constructor expects an array of values. It cycles through each value, checking first that it is an allowable property, then setting the value of that property. Note that it sets the properties directly rather than using any setter methods that may be available. This works because I have conducted any filtering, validation and transformation prior to construction. (More on filtering and validation later.)

Once the property values are set the constructor checks that all mandatory values have been supplied then stores any factories or finders that have been injected. Factories and finders are sometimes required by behaviour methods.

‘Finder’ is a pseudonym for ‘Data Mapper’. Introducing a dependency from a Domain object to a Data Mapper is breaking the rules, so I do it only with a good reason. I refer to the Mappers here as ‘finders’, since to use them in any other capacity, such as to persist changes, would be a highly questionable violation of the rules.

Access

The property values stored in the protected $data array may be accessed via public methods or overloading only. Overloading is the default case and the magic access methods have different biases. By default:

  • the __get implementation assumes client code can read properties, and
  • the __set implementation assumes client code can not write to properties.

Another way to think of this is that by default, Domain objects are 'open for reading, closed for writing'. To make a property writable an explicit public setter is required and must take the form of setPropertyName($value). Similarly, an explicit getter may be used to hide a property, however in my experience this is rarely required.

In the following example, the country setter would be called by $bar->baz = $value.

namespace Core\Domain;

class Bar extends AbstractDomain implements iDomain\Bar
{
    // ... //

    public function setBaz(iDomain\Baz $baz)
    {
        if ($baz->status !== 'Active') {
            throw new Exception\Exception('Bar->baz must be Active');
        }

        $this-> data['baz'] = $baz;
    }
}

Identity

The identity map, or Domain Watcher, avoids loading the same Domain object in to memory more than once. This protects the integrity of in-memory objects and improves performance. Before attempting to create an object for an existing entity, a factory checks the domain watcher using the entity's identifier. If a matching object exists, it is returned. Data Mappers check the domain watcher too before executing a ‘find’ for a single object.

In the Domain Watcher you will see that my globally unique object identifiers are a combination of the object’s 'short type', and an identifier which must be unique amongst members of the same type. In most cases the identifier is simply an id property on the object, which usually doubles as the primary key in the database. In some cases however it may be another property, such as email, or the identifier of a ‘parent’ object (in the case of object composition).

The abstract Domain object has setId and getId methods for the base case. Note that in these cases the id is marked as optional (in $allowed) since newly created objects are unlikely to have an identity until they have been added to the database. Where objects do not conform to the base case the setId and getId methods should be overridden.

'Value' objects are objects that do not have identities. If your project has only a small number of these you may be able to handle them effectively by avoiding attempts to add them to the domain watcher, and by raising exceptions in their setId and getId methods. If you have a large number however it may be necessary to distinguish them via the object hierarchy. In this case you can declare separate entity and value interfaces, and return early from DomainWatcher::add($object) when $object is not an entity.

Proxies

Most Domain objects have a corresponding proxy class. When a factory constructs a Domain object which composes (or aggregates) other Domain objects, it may not have all the information required to construct these other objects. In such cases the factory may substitute a proxy instead.

Upon construction a Domain object proxy has two real properties: the identifier of the real object, and the Mapper by which the real object may be found. A proxy is realised if/when it is first accessed, and subsequent method calls and property accesses are passed to the real object. This is not the case however when an object is accessed to read its identity only.

When an object’s identity is accessed the proxy returns its own copy of the identifier. This avoids unnecessary database calls for common functions such as testing for object equality, and printing object identifiers in URL lists.

By way of example, here is the code for a Foo proxy.

namespace Core\Domain\Proxy;

use Core\Domain\iDomain;

class Foo extends AbstractProxy implements iDomain\Foo 
{ }

MVC: Presentation Layer

As mentioned, there is nothing particularly special about the Starter Application views and controllers beyond what is provided out of the ZF2 box. Controllers pass variables to views. View scripts render them.

Views

It is worth noting that, with some extra coding in the Domain factories, view scripts can access an object’s properties, including multi-valued properties, without causing performance issues such as the N+1 selects problem. More on this later.

View Helpers

I use view helpers extensively to isolate complex logic and avoid duplication. They live in the Core\View\Helper namespace, with sub-namespaces for further organisation. They are registered in the module's configuration, usually as an invokable or a factory.

Forms

Zend\Form makes easy work of creating and rendering forms. In most cases however I revert to hand-crafted forms for complete control over the layout. The inlineScript view helper makes easy work of managing related javascript. 

Controllers

The Starter Application's controllers extend from Zend's abstract controller and are therefore service-locator-aware. Controllers use the service locator to access other services including Service Layer services and Data Mappers. In addition, the Starter Application’s base controller inserts a helper trait.

All code examples in this discussion are based on the Foo-Bar application shown in the class diagram on the N+1 Selects Problem page. Here is an example of a Bar controller.

namespace Core\Controller;

class BarController extends AbstractController
{
    public function updateBazAction()
    {
        $this->serviceLocator->get('Core\Service\Bar')->updateBaz(
            $this->params()->fromQuery('bar'),
            $this->params()->fromQuery('baz')
        );
    }
}

Helper Trait

The helper trait is inserted into most supertypes within the Starter Application, not just the abstract controller. It is a good place for any utilities that are used widely throughout an application. One such utility that the Starter Application uses extensively is getShortType, and this brings us to an important naming convention.

In the discussion on Domain objects you will see that there is often a 1-to-1 relationship between a Domain object and its associates. For example a Foo object may have an associated Foo proxy, Foo factory, and more. The getShortType function accepts an object or class name, and merely returns the unqualified name of the class. Client code may use this function to find out exactly which Domain object, proxy or factory it is working with.

Tags: 

PHP Starter Application

This is a starting point for applications with complex business logic. It is a PHP MVC skeleton with a Domain (Domain Model), Data Mapper and Service Layer. It uses Zend Framework 2 however may be ported to other frameworks quite easily. You are welcome to use it as the foundation for your next application.

The Starter Application includes:

  • A means of organising business logic (the Domain),
  • A flexible mechanism for database interaction (the Data Mapper),
  • A clean, clear API for client code (the Service Layer),
  • Factories for creating complex Domain objects,
  • An identity map to avoid duplicating Domain objects in memory,
  • Custom collections for Domain objects, and
  • Proxies for efficient loading of individual Domain objects and collections.

The Starter Application is aimed at intermediate-level developers who have ZF2 experience and are addressing problems which are too complex for transaction scripts or table modules. I am planning to maintain and improve both the code and this discussion, so please contribute any improvements by forking the code on github.

Overview

The Starter Application consists of two modules: Core and Mapper. All codes lives in Core, with the exception of the Data Mapper which is declared in Core and implemented in Mapper.

The presentation layer of the Starter Application (i.e. views and controllers), is straightforward. All the interesting work takes place in the model which includes the Domain, Data Mapper and Service Layer.

The MVC Model

Getting Started

To start building your own application you can:

  1. Install the Starter Application using the method described here
  2. Read each of the topics that follow, exploring the installed code as you go
  3. Follow the steps in Using the Starter Application