Sections

Annotations


Overview

Phalcon introduced the first annotations parser component written in C for PHP. The Phalcon\Annotations namespace contains general purpose components that offer an easy way to parse and cache annotations in PHP applications.

Usage

Annotations are read from docblocks in classes, methods and properties. An annotation can be placed at any position in the docblock:

<?php

/**
 * This is the class description
 *
 * @AmazingClass(true)
 */
class Example
{
    /**
     * This a property with a special feature
     *
     * @SpecialFeature
     */
    protected $someProperty;

    /**
     * This is a method
     *
     * @SpecialFeature
     */
    public function someMethod()
    {
        // ...
    }
}

An annotation has the following syntax:

/**
 * @Annotation-Name
 * @Annotation-Name(param1, param2, ...)
 */

Also, an annotation can be placed at any part of a docblock:

<?php

/**
 * This a property with a special feature
 *
 * @SpecialFeature
 *
 * More comments
 *
 * @AnotherSpecialFeature(true)
 */

The parser is highly flexible, the following docblock is valid:

<?php

/**
 * This a property with a special feature @SpecialFeature({
someParameter='the value', false

 })  More comments @AnotherSpecialFeature(true) @MoreAnnotations
 **/

However, to make the code more maintainable and understandable it is recommended to place annotations at the end of the docblock:

<?php

/**
 * This a property with a special feature
 * More comments
 *
 * @SpecialFeature({someParameter='the value', false})
 * @AnotherSpecialFeature(true)
 */

An example for a model is:

<?php

use Phalcon\Mvc\Model;

/**
 * Customers
 *
 * Represents a customer record
 *
 * @Source('co_customers');
 * @HasMany("cst_id", "Invoices", "inv_cst_id")
 */
class Customers extends Model
{
    /**
     * @Primary
     * @Identity
     * @Column(type="integer", nullable=false, column="cst_id")
     */
    public $id;

    /**
     * @Column(type="string", nullable=false, column="cst_name_first")
     */
    public $nameFirst;

    /**
     * @Column(type="string", nullable=false, column="cst_name_last")
     */
    public $nameLast;
}

Types

Annotations may have parameters or not. A parameter could be a simple literal (strings, number, boolean, null), an array, a hashed list or other annotation:

/**
 * @SomeAnnotation
 */

Simple Annotation

/**
 * @SomeAnnotation('hello', 'world', 1, 2, 3, false, true)
 */

Annotation with parameters

/**
 * @SomeAnnotation(first='hello', second='world', third=1)
 * @SomeAnnotation(first: 'hello', second: 'world', third: 1)
 */

Annotation with named parameters

/**
 * @SomeAnnotation([1, 2, 3, 4])
 * @SomeAnnotation({1, 2, 3, 4})
 */

Passing an array

/**
 * @SomeAnnotation({first=1, second=2, third=3})
 * @SomeAnnotation({'first'=1, 'second'=2, 'third'=3})
 * @SomeAnnotation({'first': 1, 'second': 2, 'third': 3})
 * @SomeAnnotation(['first': 1, 'second': 2, 'third': 3])
 */

Passing a hash as parameter

/**
 * @SomeAnnotation({'name'='SomeName', 'other'={
 *     'foo1': 'bar1', 'foo2': 'bar2', {1, 2, 3},
 * }})
 */

Nested arrays/hashes

/**
 * @SomeAnnotation([email protected](1, 2, 3))
 */

Nested Annotations

Adapters

This component makes use of adapters to cache or no cache the parsed and processed annotations improving performance:

アダプター Description
Phalcon\Annotations\Adapter\Apcu Use APCu to store parsed and processed annotations (production)
Phalcon\Annotations\Adapter\Memory Use memory fo store annotations (development)
Phalcon\Annotations\Adapter\Stream Use a file stream to store annotations. Must be used with a bytecode cache.

Apcu

Phalcon\Annotations\Adapter\Apcu stores the parsed and processed annotations using the APCu cache. This adapter is suitable for production systems. However, once the web server restarts, the cache will be cleared and will have to be rebuilt. The adapter accepts two parameters in the constructor’s options array: - prefix - the prefix for the key stored - lifetime - the cache lifetime

<?php

use Phalcon\Annotations\Adapter\Apcu;

$adapter = new Apcu(
    [
        'prefix'   => 'my-prefix',
        'lifetime' => 3600,
    ]
);

Internally, the adapter stores data prefixing every key with _PHAN. This setting cannot be changed. It however gives you the option to scan APCu for keys that are prefixed with _PHAN and clear them if needed.

<?php

use APCuIterator;

$result   = true;
$pattern  = "/^_PHAN/";
$iterator = new APCuIterator($pattern);

if (true === is_object($iterator)) {
    return false;
}

foreach ($iterator as $item) {
    if (true !== apcu_delete($item["key"])) {
        $result = false;
    }
}

return $result;

Memory

Phalcon\Annotations\Adapter\Memory stores the parsed and processed annotations in memory. This adapter is suitable for development systems. The cache is rebuilt on every request, and therefore can immediately reflect changes while developing your application.

<?php

use Phalcon\Annotations\Adapter\Memory;

$adapter = new Memory();

Stream

Phalcon\Annotations\Adapter\Stream stores the parsed and processed annotations in a file on the server. This adapter can be used in production systems but it will increase the I/O since for every request the annotations cache files will need to be read from the file system. The adapter accepts one parameter in the constructor’s options array: - annotationsDir - the directory to store the annotations cache

<?php

use Phalcon\Annotations\Adapter\Stream;

$adapter = new Stream(
    [
        'annotationsDir' => '/app/storage/cache/annotations',
    ]
);

If there is a problem with storing the data in the folder due to permissions or any other reason, a Phalcon\Annotations\Exception will be thrown.

Custom

Phalcon\Annotations\Adapter\AdapterInterface is available. Extending this interface will allow you to create custom adapters.

Factory

newInstance

We can easily create an annotations adapter class using the new keyword. However Phalcon offers the Phalcon\Annotations\AnnotationsFactory class, so that developers can easily instantiate annotations adapters. The factory will accept an array of options which will in turn be used to instantiate the necessary adapter class. The factory always returns a new instance that implements the Phalcon\Annotations\Adapter\AdapterInterface. The names of the preconfigured adapters are:

Name アダプター
apcu Phalcon\Annotations\Adapter\Apcu
memory Phalcon\Annotations\Adapter\Memory
stream Phalcon\Annotations\Adapter\Stream

The example below shows how you can create an Apcu annotations adapter:

<?php

use Phalcon\Annotations\AnnotationsFactory;

$options = [
    'prefix'   => 'my-prefix',
    'lifetime' => 3600,
];

$factory = new AdapterFactory();
$apcu    = $factory->newInstance('apcu', $options);

load

The Phalcon\Annotations\AnnotationsFactory also offers the load method, which accepts a configuration object. This object can be an array or a Phalcon\Config object, with directives that are used to set up the adapter. The object requires the adapter element, as well as the options element with the necessary directives.

<?php

use Phalcon\Annotations\AnnotationsFactory;

$options = [
    'adapter' => 'apcu',
    'options' => [
        'prefix'   => 'my-prefix',
        'lifetime' => 3600,
    ]
];

$factory = new AdapterFactory();
$apcu    = $factory->load($options);

Reading Annotations

A reflector is implemented to easily get the annotations defined on a class using an object-oriented interface. Phalcon\Annotations\Reader is used along with Phalcon\Annotations\Reflection. They also utilize the collection Phalcon\Annotations\Collection that contains Phalcon\Annotations\Annotation objects once the annotations are parsed.

<?php

use Phalcon\Annotations\Adapter\Memory;

$adapter = new Memory();

$reflector   = $adapter->get('Invoices');
$annotations = $reflector->getClassAnnotations();

foreach ($annotations as $annotation) {
    echo $annotation->getName(), PHP_EOL;
    echo $annotation->numberArguments(), PHP_EOL;

    print_r($annotation->getArguments());
}

In the above example we first create the memory annotations adapter. We then call get on it to load the annotations from the Invoices class. The getClassAnnotations will return a Phalcon\Annotations\Collection class. We iterate through the collection and print out the name (getName), the number arguments (numberArguments) and then we print all the arguments (getArguments) on screen.

The annotation reading process is very fast, however, for performance reasons it is recommended to store the parsed annotations using an adapter so as to reduce unnecessary CPU cycles for parsing.

Exceptions

Any exceptions thrown in the Phalcon\Annotations namespace will be of type Phalcon\Annotations\Exception. You can use these exceptions to selectively catch exceptions thrown only from this component.

<?php

use Phalcon\Annotations\Adapter\Memory;
use Phalcon\Annotations\Exception;
use Phalcon\Mvc\Controller;

class IndexController extends Controller
{
    public function index()
    {
        try {
            $adapter = new Memory();

            $reflector   = $adapter->get('Invoices');
            $annotations = $reflector->getClassAnnotations();

            foreach ($annotations as $annotation) {
                echo $annotation->getExpression('unknown-expression');
            }
        } catch (Exception $ex) {
            echo $ex->getMessage();
        }
    }
}

Examples

Controller based access

You can use annotations to define which areas are controlled by the ACL. We can do this by registering a plugin in the events manager listening to the beforeExceuteRoute event, or simply implement the method in our base controller.

First we need to set the annotations manager in our DI container:

<?php

use Phalcon\Di\FactoryDefault;
use Phalcon\Annotations\Adapter\Apcu;

$container = new FactoryDefault();

$container->set(
    'annotations',
    function () {
        return new Apcu(
            [
                'lifetime' => 86400
            ]
        );
    }
);

and now in the base controller we implement the beforeExceuteRoute method:

<?php

namespace MyApp\Controllers;

use Phalcon\Annotations\Adapter\Apcu;
use Phalcon\Events\Event;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Mvc\Controller
use MyApp\Components\Auth;

/**
 * @property Apcu $annotations
 * @property Auth $auth 
 */
class BaseController extends Controller
{
    /**
     * @param Event $event
     * @param Dispatcher $dispatcher
     *
     * @return bool
     */
    public function beforeExceuteRoute(
        Dispatcher $dispatcher
    ) {
        $controllerName = $dispatcher->getControllerClass();

        $annotations = $this
            ->annotations
            ->get($controllerName)
        ;

        $exists = $annotations
            ->getClassAnnotations()
            ->has('Private')
        ;

        if (true !== $exists) {
            return true;
        }

        if (true === $this->auth->isLoggedIn()) {
            return true;
        }

        $dispatcher->forward(
            [
                'controller' => 'session',
                'action'     => 'login',
            ]
        );

        return false;
    }
}

NOTE You can also implement the above to a listener and use the beforeDispatch event if you wish.

and in our controllers we can specify:

<?php

namespace MyApp\Controllers;

use MyApp\Controllers\BaseController;

/**
 * @Private(true) 
 */
class Invoices extends BaseController
{
    public function indexAction()
    {
    }
}

Group based access

You might want to expand on the above and offer a more granular access control for your application. For this, we will also use the beforeExceuteRoute in the controller but will add the access metadata on each action. If we need a specific controller to be locked we can also use the initialize method.

First we need to set the annotations manager in our DI container:

<?php

use Phalcon\Di\FactoryDefault;
use Phalcon\Annotations\Adapter\Apcu;

$container = new FactoryDefault();

$container->set(
    'annotations',
    function () {
        return new Apcu(
            [
                'lifetime' => 86400
            ]
        );
    }
);

and now in the base controller we implement the beforeExceuteRoute method:

<?php

namespace MyApp\Controllers;

use Phalcon\Annotations\Adapter\Apcu;
use Phalcon\Events\Event;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Mvc\Controller;
use MyApp\Components\Auth;

/**
 * @property Apcu $annotations
 * @property Auth $auth 
 */
class BaseController extends Controller
{
    /**
     * @param Event $event
     * @param Dispatcher $dispatcher
     *
     * @return bool
     */
    public function beforeExceuteRoute(
        Dispatcher $dispatcher
    ) {
        $controllerName = $dispatcher->getControllerClass();
        $actionName     = $dispatcher->getActionName()
                        . 'Action';

        $data = $this
            ->annotations
            ->getMethod($controllerName, $actionName)
        ;
        $access    = $data->get('Access');
        $aclGroups = $access->getArguments();

        $user   = $this->acl->getUser();
        $groups = $user->getRelated('groups');

        $userGroups = [];
        foreach ($groups as $group) {
            $userGroups[] = $group->grp_name;
        }

        $allowed = array_intersect($userGroups, $aclGroups);
        $allowed = (count($allowed) > 0);

        if (true === $allowed) {
            return true;
        }

        $dispatcher->forward(
            [
                'controller' => 'session',
                'action'     => 'login',
            ]
        );

        return false;
    }
}

and in our controllers:

<?php

namespace MyApp\Controllers;

use MyApp\Controllers\BaseController;

/**
 * @Private(true) 
 */
class Invoices extends BaseController
{
    /**
     * @Access(
     *     'Administrators',
     *     'Accounting',
     *     'Users',
     *     'Guests'
     * )
     */
    public function indexAction()
    {
    }

    /**
     * @Access(
     *     'Administrators',
     *     'Accounting',
     * )
     */
    public function listAction()
    {
    }

    /**
     * @Access(
     *     'Administrators',
     *     'Accounting',
     * )
     */
    public function viewAction()
    {
    }
}

Additional Resources