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 
{ }