Ornament

PHP7 ORM toolkit, core package

ORM is a fickle beast. Many libraries (e.g. Propel, Doctrine, Eloquent etc) assume your database should correspond to your models. This is simply not the case; models contain business logic and may, may not or may in part refer to database tables, NoSQL databases, flat files, an external API or whatever (the "R" in ORM should really stand for "resource", not "relational"). The point is: the models shouldn't care, and there should be no "conventional" mapping through their names. (A common example would be a model of pages in multiple languages, where the data might be stored in a page table and a page_i18n table for the language-specific data.)

Also, the use of extensive and/or complicated config files sucks. (XML? This is 2017, people!)

Ornament's design goals are:

Installation

$ composer require ornament/core

You'll likely also want auxiliary packages from the ornament/* family.

Basic usage

Ornament models (or "entities" if you're used to Doctrine-speak) are really nothing more than vanilla PHP classes; there is no need to extend any base object of sorts (since you might want to do that in your own framework).

Ornament is a toolkit, so it supplies a number of Traits one can use to extend your models' behaviour beyond the ordinary.

The most basic implementation would look as follows:

<?php

use Ornament\Core\Model;

class MyModel
{
    // The generic Model trait that bootstraps this class as an Ornament model;
    // it contains core functionality.
    use Model;

    // All protected properties on a model are considered "handleable" by
    // Ornament. They can be decorated (see below) and are exposed for getting
    // and setting via the Model API:
    protected $id;
    // Public properties are also potentially handleable, but they cannot
    // be decorated and can only be used verbatim:
    public $name;
    // Private properties are just that: private. They're left alone:
    private $password;
}

// Assuming $source is a handle to a data source (in this case, a PDO
// statement):
$model = $source->fetchObject(MyModel::class);
echo $model->id; // 1
echo $model->name; // Marijn
echo $model->password; // Error: private property.
$model->name = 'Linus'; // Ok; public property.
$model->id = 2; // Error: read-only property.

The above example didn't do much yet except exposing the protected id property as read-only. Note however that Ornament models also prevent mutating undefined properties; trying to set anything not explicitly set in the class definition will throw an Error mimicking PHP's internal error when accessing protected or private properties.

Annotating and decorating models

Ornament doesn't get really useful until you start decorating your models. This is done (mostly) by specifying annotations on your properties and methods.

Let's look at the simplest annotation possible: type coercion. Let's say we want to make sure that the id property from the previousl example is an integer:

<?php

class MyModel
{
    //...
    /** @var int */
    public $id;
}

//...
$model->id = 'foo';
echo $model->id; // (int)0

This works for all types supported by PHP's settype function, and works for getting as well as setting.

Getters and setters

Sometimes you'll want to specify your own getters and setters. No problem; define a method and annotate it with @get PROPERTY or @set PROPERTY:

<?php

class MyModel
{
    // ...

    /** @get id */
    public function exampleGetter()
    {
        // A random ID between 1 and 100.
        return rand(1, 100);
    }

    /** @set id */
    public function exampleSetter(int $id) : int
    {
        // When setting, we multiply the id by 2.
        return $id * 2;
    }
}

Note that a setter accepts a single parameter (the thing you want to set) and returns what you actually want to set. The internal storage is further handled by the Ornament model, so no need to worry about the details.

Getters work for public and protected properties; setters obviously only for public properties (since protected properties are read-only).

Decorator classes

For more complex types you can also annotate a property with a decorator class. An example where this could be useful is e.g. to automatically wrap a property containing a timezone in an instance of Carbon\Carbon.

Specifying a decorator class is as simple as annotating the property with @var CLASSNAME:

<?php

class MyModel
{
    // ...

    /**
     * @var CarbonDecorator
     */
    public $date;
}

Note that you must use the fully qualified classname; PHP cannot know (well, at least not without doing scary voodoo on your sourcecode) which namespaces were imported.

Each Decorator class must implement the Ornament\Core\DecoratorInterface interface. Usually this is done by extending Ornament\Core\Decorator, but it is allowed to write your own implementation. Decorator classes are instantied with the internal "status model" (a StdClass) and the name of the property to be decorated. This allows you to access the rest of the model too, if needed (example: a fullname decorated field which consists of firstname and lastname properties). To access the underlying value, use the getSource() method. Decorators also must implement a __toString() method to ensure decorated properties can be safely used (e.g. in an echo statement). For the abstract base decorator, this is implemented as (string)$this->getSource() which is usually what you want.

It is also possible to specify extra constructor arguments for the decorator using the @construct annotation. Multiple @construct arguments can be set; they will be passed as the second, third etc. arguments to the decorator's constructor. An exmaple:

<?php

class MyModel
{
    // ...

    /**
     * @var SomeDecorator
     * @construct 1
     * @construct 2
     */
    public $foo;

}

class SomeDecorator extends Ornament\Core\Decorator
{
    public function __construct(StdClass $model, string $property, int $arg1, int $arg2)
    {
        // ...
    }
}

If your decorator gets really complex and cannot be instantiated using static arguments, one should use an @getter.

Caution: annotations are returned as either "the actual value" or, if multiple annotations of the same name were specific, an array. There is no way for Ornament to differentiate between "multiple constructor arguments" and "a single argument with a simple array". So internally Ornament assumes that if the @construct annotation is already an array, with an index 0 set, and a count() larger than one, you are specifying multiple constructor arguments. This check will fail if you meant to specify just a single argument, which happens to be a simple array with multiple elements (e.g. [1, 2, 3]).

In these corner cases, just supply a second (dummy) constructor argument so the annotations will already be an array by the time Ornament inspects them.

Loading and persisting models

This is your job. Wait, what? Yes, Ornament is storage engine agnostic. You may use an RDBMS, interface with a JSON API or store your stuff in Excel files for all we care. We believe that you shouldn't tie your models to your storage engine.

Our personal preference is to use "repositories" that handle this. Of course, you're free to make a base class model for yourself which implements save() or delete() methods or whatever.

Stateful models

Having said that, you're not completely on your own. Models may use the Ornament\Core\State trait to expose some convenience methods:

All these methods are public. You can use them in your storage logic to determine how to proceed (e.g. skip an expensive UPDATE operation if the model isPristine() anyway).