JMSAopBundle

Este paquete añade capacidades AOP (Aspect-oriented programming o Programación orientada a aspectos) a Symfony2.

Si todavía no has oído hablar de AOP, básicamente te permite separar un asunto omnipresente (por ejemplo, la comprobación de seguridad) en una clase específica, y no tener que repetir ese código en todos los lugares donde se necesite.

En otras palabras, te permite ejecutar código personalizado antes y después de invocar ciertos métodos en tu capa de servicios o controladores. También puedes optar por omitir la invocación del método original, o simplemente lanzar excepciones.

Instalando

Consigue una copia del código:

git submodule add https://github.com/schmittjoh/JMSAopBundle.git src/JMS/AopBundle

Finalmente, registra el paquete en tu núcleo:

// en AppKernel::registerBundles()
$bundles = array(
    // ...
    new JMS\AopBundle\JMSAopBundle(),
    // ...
);

Este paquete además necesita la biblioteca CG para generar código:

git submodule add https://github.com/schmittjoh/cg-library.git vendor/cg-library

Asegúrate de registrar los espacios de nombres en tu cargador automático:

// app/autoload.php
$loader->registerNamespaces(array(
    // ...
    'JMS'              => __DIR__.'/../vendor/bundles',
    'CG'               => __DIR__.'/../vendor/cg-library/src',
    // ...
));

Configurando

jms_aop:
    cache_dir: %kernel.cache_dir%/jms_aop

Usando

A fin de ejecutar código personalizado, necesitas dos clases. En primer lugar, necesitas un así llamado punto de corte (pointcut). El propósito de esta clase es tomar la decisión de si cierto interceptor debe capturar una llamada a un método. Esta decisión se tiene que hacer estáticamente sólo en base de la firma del método en sí.

La segunda clase es el interceptor. Esta clase se llama en lugar del método original. Esta contiene el código personalizado que quieres ejecutar. En este punto, tienes acceso al objeto en el cual es invocado el método, y todos los argumentos suministrados a ese método.

Ejemplos

1. Registro cronológico

En este ejemplo, implementaremos el registro cronológico de todos los métodos que contienen «delete»

Punto de corte

<?php

use JMS\AopBundle\Aop\PointcutInterface;

class LoggingPointcut implements PointcutInterface
{
    public function matchesClass(\ReflectionClass $class)
    {
        return true;
    }

    public function matchesMethod(\ReflectionMethod $method)
    {
        return false !== strpos($method->name, 'delete');
    }
}
# services.yml
services:
    my_logging_pointcut:
        class: LoggingPointcut
        tags:
                - { name: jms_aop.pointcut, interceptor: logging_interceptor }

Interceptor de registro

<?php

use CG\Proxy\MethodInterceptorInterface;
use CG\Proxy\MethodInvocation;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;

class LoggingInterceptor implements MethodInterceptorInterface
{
    private $context;
    private $logger;

    public function __construct(SecurityContextInterface $context,
                                LoggerInterface $logger)
    {
        $this->context = $context;
        $this->logger = $logger;
    }

    public function intercept(MethodInvocation $invocation)
    {
        $user = $this->context->getToken()->getUsername();
        $this->logger->info(sprintf('User "%s" invoked method "%s".', $user, $invocation->reflection->name));

        // se asegura de proceder con la invocación, de lo contrario
        // el método original nunca se llamará
        return $invocation->proceed();
    }
}
# services.yml
services:
    logging_interceptor:
        class: LoggingInterceptor
        arguments: [@security.context, @logger]

2. Gestionando transacciones

En este ejemplo, añadimos una anotación @Transactional, y automáticamente ajustamos todos los métodos donde se declara esta anotación en una transacción.

Punto de corte

use Doctrine\Common\Annotations\Reader;
use JMS\AopBundle\Aop\PointcutInterface;
use JMS\DiExtraBundle\Annotation as DI;

/**
 * @DI\Service
 * @DI\Tag("jms_aop.pointcut", attributes = {"interceptor" = "aop.transactional_interceptor"})
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class TransactionalPointcut implements PointcutInterface
{
    private $reader;

    /**
     * @DI\InjectParams({
     *     "reader" = @DI\Inject("annotation_reader"),
     * })
     * @param Reader $reader
     */
    public function __construct(Reader $reader)
    {
        $this->reader = $reader;
    }

    public function matchesClass(\ReflectionClass $class)
    {
        return true;
    }

    public function matchesMethod(\ReflectionMethod $method)
    {
        return null !== $this->reader->getMethodAnnotation($method, 'Annotation\Transactional');
    }
}

Interceptor

use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use CG\Proxy\MethodInvocation;
use CG\Proxy\MethodInterceptorInterface;
use Doctrine\ORM\EntityManager;
use JMS\DiExtraBundle\Annotation as DI;

/**
 * @DI\Service("aop.transactional_interceptor")
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class TransactionalInterceptor implements MethodInterceptorInterface
{
    private $em;
    private $logger;

    /**
     * @DI\InjectParams
     * @param EntityManager $em
     */
    public function __construct(EntityManager $em, LoggerInterface $logger)
    {
        $this->em = $em;
        $this->logger = $logger;
    }

    public function intercept(MethodInvocation $invocation)
    {
        $this->logger->info('Beginning transaction for method "'.$invocation.'")');
        $this->em->getConnection()->beginTransaction();
        try {
            $rs = $invocation->proceed();

            $this->logger->info(sprintf('Comitting transaction for method "%s" (method invocation successful)', $invocation));
            $this->em->getConnection()->commit();

            return $rs;
        } catch (\Exception $ex) {
            if ($ex instanceof NotFoundHttpException) {
                $this->logger->info(sprintf('Committing transaction for method "%s" (exception thrown, but no rollback)', $invocation));
                $this->em->getConnection()->commit();
            } else {
                $this->logger->info(sprintf('Rolling back transaction for method "%s" (exception thrown)', $invocation));
                $this->em->getConnection()->rollBack();
            }

            throw $ex;
        }
    }
}
Bifúrcame en GitHub