Secciones

Anotaciones


Resumen

Phalcon introdujo el primer componente analizador de anotaciones escrito en C para PHP. The Phalcon\Annotations namespace contains general purpose components that offers an easy way to parse and cache annotations in PHP applications.

Uso

Las anotaciones son leídas desde docblocks en clases, métodos y propiedades. Una anotación puede colocarse en cualquier posición del docblock:

<?php

/**
 * #01
 *
 * @AmazingClass(true)
 */
class Example
{
    /**
     * #02
     *
     * @SpecialFeature
     */
    protected $someProperty;

    /**
     * #03
     *
     * @SpecialFeature
     */
    public function someMethod()
    {
        // ...
    }
}

Legend

01: This is the class description

02: This a property with a special feature

03: This is a method

Una anotación tiene la siguiente sintaxis:

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

También, una anotación se puede colocar en cualquier parte de un docblock:

<?php

/**
 * #01
 *
 * @SpecialFeature
 *
 * #02
 *
 * @AnotherSpecialFeature(true)
 */

Legend

01: This is a property with a special feature

02: More comments

El analizador es altamente flexible, el siguiente docblock es válido:

<?php

/**
 * Esta es una propiedad con una característica especial @SpecialFeature({
someParameter='the value', false

 })  Más comentarios @AnotherSpecialFeature(true) @MoreAnnotations
 **/

Sin embargo, para hacer el código más mantenible y comprensible se recomienda colocar las anotaciones al final del docblock:

<?php

/**
 * #01
 * #02
 *
 * @SpecialFeature({someParameter='the value', false})
 * @AnotherSpecialFeature(true)
 */

Legend

01: This is a property with a special feature

02: More comments

Un ejemplo para un modelo es:

<?php

use Phalcon\Mvc\Model;

/**
 * #01
 *
 * #02
 *
 * @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;
}

Legend

01: Customers

02: Represents a customer record

Tipos

Las anotaciones pueden tener parámetros o no. Un parámetro podría ser un literal simple (cadenas, número, booleano, null), un vector, una list codificada u otra anotación:

/**
 * @SomeAnnotation
 */

Anotación Simple

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

Anotación con parámetros

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

Anotación con parámetros nombrados

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

Pasando un vector

/**
 * @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])
 */

Pasando una codificación como parámetro

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

Vectores/Codificaciones anidadas

/**
 * @SomeAnnotation(first=@AnotherAnnotation(1, 2, 3))
 */

Anotaciones Anidadas

Adaptadores

Este componente hace uso de adaptadores para cachear o no las anotaciones analizadas y procesadas mejorando el rendimiento:

Adaptador Descripción
Phalcon\Annotations\Adapter\Apcu Usa APCu para almacenar las anotaciones analizadas y procesadas (producción)
Phalcon\Annotations\Adapter\Memory Usa la memoria para almacenar anotaciones (desarrollo)
Phalcon\Annotations\Adapter\Stream Usa un flujo de archivo para almacenar anotaciones. Se debe usar con un caché de byte-code.

Apcu

Phalcon\Annotations\Adapter\Apcu stores the parsed and processed annotations using the APCu cache. Este adaptador es adecuado para sistemas en producción. Sin embargo, una vez que el servidor se reinicia, el caché se limpiará y tendrá que reconstruirse. 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,
    ]
);

Internamente, el adaptador almacena los datos prefijando cada clave con _PHAN. Este ajuste no se puede cambiar. Sin embargo, esto le da la opción de escanear APCu en busca de claves que tengan el prefijo _PHAN y limpiarlas si es necesario.

<?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. Este adaptador es adecuado para sistemas en desarrollo. 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();

Flujo (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.

Personalizado

Phalcon\Annotations\Adapter\AdapterInterface is available. Ampliar esta interfaz le permitirá crear sus propios adaptadores.

Fábrica (Factory)

newInstance

Podemos crear fácilmente una clase adaptador de anotaciones usando la palabra clave new. However, Phalcon offers the Phalcon\Annotations\AnnotationsFactory class, so that developers can easily instantiate annotations adapters. La fábrica aceptará un vector de opciones que a su vez se usarán para instanciar la clase adaptador necesaria. The factory always returns a new instance that implements the Phalcon\Annotations\Adapter\AdapterInterface. Los nombre de los adaptadores preconfigurados son:

Nombre Adaptador
apcu Phalcon\Annotations\Adapter\Apcu
memory Phalcon\Annotations\Adapter\Memory
stream Phalcon\Annotations\Adapter\Stream

El ejemplo siguiente muestra como puede crear un adaptador de anotaciones Apcu:

<?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. Este objeto puede ser un vector o un objeto Phalcon\Config, con directivas que se usarán para configurar el adaptador. El objeto requiere el elemento adapter, así como el elemento options con las directivas necesarias.

<?php

use Phalcon\Annotations\AnnotationsFactory;

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

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

Leyendo anotaciones

Se implementa un reflector para obtener fácilmente las anotaciones definidas en una clase usando una interfaz orientada a objetos. 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());
}

En el ejemplo anterior primero creamos el adaptador de anotaciones de memoria. Luego se llama a get para cargar las anotaciones de la clase Invoices. The getClassAnnotations will return a Phalcon\Annotations\Collection class. Iteramos sobre la colección e imprimimos el nombre (getName), el número de argumentos (numberArguments) y luego imprimimos todos los argumentos (getArguments) por pantalla.

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

Excepciones

Any exceptions thrown in the Phalcon\Annotations namespace will be of type Phalcon\Annotations\Exception. Puede usar estas excepciones para capturar selectivamente sólo las excepciones lanzadas desde este componente.

<?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();
        }
    }
}

Ejemplos

Acceso basado en controlador

Puede usar anotaciones para definir qué áreas se controlan por la ACL. Podemos hacer esto registrando un plugin en el gestor de eventos que escucha el evento beforeExceuteRoute, o simplemente implementar el método en nuestro controlador base.

Primero necesitamos configurar el gestor de anotaciones en nuestro contenedor DI:

<?php

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

$container = new FactoryDefault();

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

y ahora en el controlador base implementamos el método beforeExceuteRoute:

<?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.

y en nuestros controladores podemos especificar:

<?php

namespace MyApp\Controllers;

use MyApp\Controllers\BaseController;

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

Acceso basado en grupo

Podría querer ampliar lo anterior y ofrecer un control de acceso más granular para su aplicación. Para esto, también usaremos beforeExecuteRoute en el controlador pero añadiremos los metadatos de acceso en cada acción. If we need a specific controller to be locked we can also use the initialize method.

Primero necesitamos configurar el gestor de anotaciones en nuestro contenedor DI:

<?php

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

$container = new FactoryDefault();

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

y ahora en el controlador base implementamos el método beforeExceuteRoute:

<?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;
    }
}

y en nuestros controladores:

<?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()
    {
    }
}

Recursos Adicionales