Fully documented, step-by-step example of declarative meta-programming

File: demo/index.php

namespace mindplay\demo;

use Composer\Autoload\ClassLoader;
use mindplay\annotations\AnnotationCache;
use mindplay\annotations\Annotations;
use mindplay\demo\annotations\Package;

Configure a simple auto-loader

$vendor_path = dirname(__DIR__) . '/vendor';

if (!is_dir($vendor_path)) {
    echo 'Install dependencies first' . PHP_EOL;
    exit(1);
}

require_once($vendor_path . '/autoload.php');

$auto_loader = new ClassLoader();
$auto_loader->addPsr4("mindplay\\demo\\", __DIR__);
$auto_loader->register();

Configure the cache-path. The static Annotations class will configure any public properties of AnnotationManager when it creates it. The AnnotationManager::$cachePath property is a path to a writable folder, where the AnnotationManager caches parsed annotations from individual source code files.

Annotations::$config['cache'] = new AnnotationCache(__DIR__ . '/runtime');

Register demo annotations.

Package::register(Annotations::getManager());

For this example, we’re going to generate a simple form that allows us to edit a Person object. We’ll define a few public properties and annotate them with some useful metadata, which will enable us to make decisions (at run-time) about how to display each field, how to parse the values posted back from the form, and how to validate the input.

Note the use of standard PHP-DOC annotations, such as @var string - this metadata is traditionally useful both as documentation to developers, and as hints for an IDE. In this example, we’re going to use that same information as advice to our components, at run-time, to help them establish defaults and make sensible decisions about how to handle the value of each property.

class Person
{
    /**
     * @var string
     * @required
     * @length(50)
     * @text('label' => 'Full Name')
     */
    public $name;

    /**
     * @var string
     * @length(50)
     * @text('label' => 'Street Address')
     */
    public $address;

    /**
     * @var int
     * @range(0, 100)
     */
    public $age;
}

To build a simple form abstraction that can manage the state of an object being edited, we start with a simple, abstract base class for input widgets.

abstract class Widget
{
    protected $object;
    protected $property;

    public $value;

Each widget will maintain a list of error messages.

    public $errors = array();

A widget needs to know which property of what object is being edited.

    public function __construct($object, $property)
    {
        $this->object = $object;
        $this->property = $property;
        $this->value = $object->$property;
    }

Widget classes will use this method to add an error-message.

    public function addError($message)
    {
        $this->errors[] = $message;
    }

This helper function provides a shortcut to get a named property from a particular type of annotation - if no annotation is found, the $default value is returned instead.

    protected function getMetadata($type, $name, $default = null)
    {
        $a = Annotations::ofProperty($this->object, $this->property, $type);

        if (!count($a)) {
            return $default;
        }

        return $a[0]->$name;
    }

Each type of widget will need to implement this interface, which takes a raw POST value from the form, and attempts to bind it to the object’s property.

    abstract public function update($input);

After a widget successfully updates a property, we may need to perform additional validation - this method will perform some basic validations, and if errors are found, it will add them to the $errors collection.

    public function validate()
    {
        if (empty($this->value)) {
            if ($this->isRequired()) {
                $this->addError("Please complete this field");
            } else {
                return;
            }
        }

        if (is_string($this->value)) {
            $min = $this->getMetadata('@length', 'min');
            $max = $this->getMetadata('@length', 'max');

            if ($min !== null && strlen($this->value) < $min) {
                $this->addError("Minimum length is {$min} characters");
            } else {
                if ($max !== null && strlen($this->value) > $max) {
                    $this->addError("Maximum length is {$max} characters");
                }
            }
        }

        if (is_int($this->value)) {
            $min = $this->getMetadata('@range', 'min');
            $max = $this->getMetadata('@range', 'max');

            if (($min !== null && $this->value < $min) || ($max !== null && $this->value > $max)) {
                $this->addError("Please enter a value in the range {$min} to {$max}");
            }
        }
    }

Each type of widget will need to implement this interface, which renders an HTML input representing the widget’s current value.

    abstract public function display();

This helper function returns a descriptive label for the input.

    public function getLabel()
    {
        return $this->getMetadata('@text', 'label', ucfirst($this->property));
    }

Finally, this little helper function will tell us if the field is required - if a property is annotated with @required, the field must be filled in.

    public function isRequired()
    {
        return count(Annotations::ofProperty($this->object, $this->property, '@required')) > 0;
    }
}

The first and most basic kind of widget, is this simple string widget.

class StringWidget extends Widget
{

On update, take into account the min/max string length, and provide error messages if the constraints are violated.

    public function update($input)
    {
        $this->value = $input;

        $this->validate();
    }

On display, render out a simple <input type="text"/> field, taking into account the maximum string-length.

    public function display()
    {
        $length = $this->getMetadata('@length', 'max', 255);

        echo '<input type="text" name="' . get_class($this->object) . '[' . $this->property . ']"'
            . ' maxlength="' . $length . '" value="' . htmlspecialchars($this->value) . '"/>';
    }
}

For the age input, we’ll need a specialized StringWidget that also checks the input type.

class IntWidget extends StringWidget
{

On update, take into account the min/max numerical range, and provide error messages if the constraints are violated.

    public function update($input)
    {
        if (strval(intval($input)) === $input) {
            $this->value = intval($input);
            $this->validate();
        } else {
            $this->value = $input;

            if (!empty($input)) {
                $this->addError("Please enter a whole number value");
            }
        }
    }
}

Next, we can build a simple form abstraction - this will hold and object and manage the widgets required to edit the object.

class Form
{
    private $object;

    /**
     * Widget list.
     *
     * @var Widget[]
     */
    private $widgets = array();

The constructor just needs to know which object we’re editing.

Using reflection, we enumerate the properties of the object’s type, and using the @var annotation, we decide which type of widget we’re going to use.

    public function __construct($object)
    {
        $this->object = $object;

        $class = new \ReflectionClass($this->object);

        foreach ($class->getProperties() as $property) {
            $type = $this->getMetadata($property->name, '@var', 'type', 'string');

            $wtype = 'mindplay\\demo\\' . ucfirst($type) . 'Widget';

            $this->widgets[$property->name] = new $wtype($this->object, $property->name);
        }
    }

This helper-method is similar to the one we defined for the widget base class, but fetches annotations for the specified property.

    private function getMetadata($property, $type, $name, $default = null)
    {
        $a = Annotations::ofProperty(get_class($this->object), $property, $type);

        if (!count($a)) {
            return $default;
        }

        return $a[0]->$name;
    }

When you post information back to the form, we’ll need to update it’s state, validate each of the fields, and return a value indicating whether the form update was successful.

    public function update($post)
    {
        $data = $post[get_class($this->object)];

        foreach ($this->widgets as $property => $widget) {
            if (array_key_exists($property, $data)) {
                $this->widgets[$property]->update($data[$property]);
            }
        }

        $valid = true;

        foreach ($this->widgets as $widget) {
            $valid = $valid && (count($widget->errors) === 0);
        }

        if ($valid) {
            foreach ($this->widgets as $property => $widget) {
                $this->object->$property = $widget->value;
            }
        }

        return $valid;
    }

Finally, this method renders out the form, and each of the widgets inside, with a <label> tag surrounding each input.

    public function display()
    {
        foreach ($this->widgets as $widget) {
            $star = $widget->isRequired() ? ' <span style="color:red">*</span>' : '';
            echo '<label>' . htmlspecialchars($widget->getLabel()) . $star . '<br/>';
            $widget->display();
            echo '</label><br/>';

            if (count($widget->errors)) {
                echo '<ul>';
                foreach ($widget->errors as $error) {
                    echo '<li>' . htmlspecialchars($error) . '</li>';
                }
                echo '</ul>';
            }
        }
    }
}

Now let’s put the whole thing to work…

We’ll create a Person object, create a Form for the object, and render it!

Try leaving the name field empty, or try to tell the form you’re 120 years old - it won’t pass validation.

You can see the state of the object being displayed below the form - as you can see, unless all updates and validations succeed, the state of your object is left untouched.

echo <<<HTML
<html>
  <head>
    <title>Metaprogramming With Annotations!</title>
  </head>
  <body>
    <h1>Edit a Person!</h1>
    <h4>Declarative Metaprogramming in action!</h4>
    <form method="post">
HTML;

$person = new Person;

$form = new Form($person);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if ($form->update($_POST)) {
        echo '<h2 style="color:green">Person Accepted!</h2>';
    } else {
        echo '<h2 style="color:red">Oops! Try again.</h2>';
    }
}

$form->display();

echo <<<HTML
    <br/>
    <input type="submit" value="Go!"/>
    </form>
HTML;

echo "<pre>\n\nHere's what your Person instance currently looks like:\n\n";
var_dump($person);
echo '</pre>';

echo <<<HTML
  </body>
</html>
HTML;