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.