En este capítulo nos concentraremos en la instalación de uno de los Bundles más atractivos para incluir en nuestros proyectos, se trata del StofDoctrineExtensionsBundle por Christophe Coevoet el cual hace una implementación del complemento Doctrine2 behavioral extensions creado por Gediminas Morkevicius, cuyo propósito es proveer de los aclamados Comportamientos (behaviors) de Doctrine; en general explicaremos la configuración de 3 comportamientos ampliamente utilizados, como lo son Timestampable, Sluggable y Loggable, reiteramos que el objetivo del capítulo es la instalación de Bundles de Terceros en Symfony2 y no el de profundizar en todos los comportamientos que provee el StofDoctrineExtensionsBundle.

Paso 1: Instalando el Bundle

En Symfony 2 existen varias formas para la instalación de los bundles de terceros, entre ellas (y la más práctica) es la instalación por medio de submodule en GIT, si no sabes que es GIT te recomiendo visitar http://progit.org/book/es/ y tratar de instalarlo en tu sistema.

Comenzamos instalando la librería principal de Doctrine Behavioral Extensions, para ello accedemos a nuestra consola, nos ubicamos en la raíz del proyecto de symfony y ejecutamos:

~$ git submodule add git://github.com/l3pp4rd/DoctrineExtensions.git vendor/gedmo-doctrine-extensions

[tipexperto titulo = “Nota”]Si tienes problemas puedes descargar manualmente el paquete, sólo debes descomprimir su contenido y copiarlo al directorio /ruta_hacia_proyecto/vendor/gedmo-doctrine-extensions[/tipexperto]

Ahora si añadimos el StofDoctrineExtensionsBundle:

~$ git submodule add git://github.com/stof/StofDoctrineExtensionsBundle.git vendor/bundles/Stof/DoctrineExtensionsBundle

[tipexperto titulo = “Nota”]Si tienes problemas puedes descargar manualmente el paquete, sólo debes descomprimir su contenido y copiarlo al directorio /ruta_hacia_proyecto/vendor/bundles/Stof/DoctrineExtensionsBundle.[/tipexperto]

Registramos los Namespaces en nuestro app/autoload.php:

<?php

use Symfony\Component\ClassLoader\UniversalClassLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;

$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
    'Symfony'          => array(__DIR__.'/../vendor/symfony/src', __DIR__.'/../vendor/bundles'),
    'Sensio'           => __DIR__.'/../vendor/bundles',
    'JMS'              => __DIR__.'/../vendor/bundles',
    'Doctrine\\Common' => __DIR__.'/../vendor/doctrine-common/lib',
    'Doctrine\\DBAL'   => __DIR__.'/../vendor/doctrine-dbal/lib',
    'Doctrine'         => __DIR__.'/../vendor/doctrine/lib',
    'Monolog'          => __DIR__.'/../vendor/monolog/src',
    'Assetic'          => __DIR__.'/../vendor/assetic/src',
    'Metadata'         => __DIR__.'/../vendor/metadata/src',
    // Aquí registramos:
    'Stof'  => __DIR__.'/../vendor/bundles',
    'Gedmo' => __DIR__.'/../vendor/gedmo-doctrine-extensions/lib',
));
// ... resto del archivo

Añadimos el Bundle a nuestro app/AppKernel.php:

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),
            new Symfony\Bundle\MonologBundle\MonologBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
            new Symfony\Bundle\AsseticBundle\AsseticBundle(),
            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
            new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
            new MDW\BlogBundle\MDWBlogBundle(),
            new MDW\DemoBundle\MDWDemoBundle(),
            // Aquí Añadimos:
            new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
        );
// ... resto del archivo

Básicamente es todo lo que se realiza para incluir un Bundle.

Paso 2: Configurando el Bundle

En nuestro caso el StofDoctrineExtensionsBundle para funcionar requiere agregar configuración adicional al archivo app/config/config.yml de la aplicación (la mayoría de los bundles pueden detallar tales configuraciones en su documentación), para ello agregamos estos segmentos:

En la sección Doctrine Configuration, añadimos al final el mapping para StofDoctrineExtensions Bundle:

# Doctrine Configuration
doctrine:
    dbal:
        driver:   %database_driver%
        host:     %database_host%
        port:     %database_port%
        dbname:   %database_name%
        user:     %database_user%
        password: %database_password%
        charset:  UTF8

    orm:
        auto_generate_proxy_classes: %kernel.debug%
        auto_mapping: true
        # Añadimos el Mapping para StofDoctrineExtensionsBundle: -------------
        mappings:
            StofDoctrineExtensionsBundle: ~
# ... resto del archivo

Luego añadimos al final del mismo archivo la siguiente configuración:

# Añadimos las configuraciones específicas para el StofDoctrineExtensionsBundle
stof_doctrine_extensions:
    default_locale: en_US
    orm:
        default:
            sluggable: true
            timestampable: true
            loggable: true
            #demás behaviors para activar

Donde default: representa la configuración para todos los entornos, eso quiere decir que puedes añadir una configuración específica para cada entorno.

Paso 3: Utilizando los Comportamientos (Behaviors) en los modelos

En este tutorial nos concentraremos en los comportamientos sluggable, timestampable y loggable, para hacer las cosas más fáciles se recomienda añadir el siguiente Namespace a cada una de nuestras entidades en donde queramos añadir los comportamientos:

// añadimos luego del Namespace de la Entidad:
use Gedmo\Mapping\Annotation as Gedmo;

Algunos Behaviors como Loggable disponen de Entidades propias que requieren crearse en base de datos, para garantizar ello solo debemos ejecutar en consola el comando siguiente, de este modo Doctrine creará las tablas necesarias para almacenar los datos generados por los comportamientos que lo requieran:

~$ php app/console doctrine:schema:update --force

Sluggable: Permite que Doctrine cree automáticamente el “slug” o la típica cadena optimizada para buscadores utilizada comúnmente al indexar artículos de un blog. Para definir el slug debemos tener un campo destino que es donde se almacenará y uno o más campos origen (los Sluggable) de los cuales se construirá el slug y es tan simple como agregar los siguientes Metadatos a nuestros campos del modelo:

    // … dentro de una Entidad
    // Campo origen:
    /**
     * @var string $title
     *
     * @ORM\Column(name="title", type="string", length=255)
     * @Gedmo\Sluggable()
     */
    private $title;

    // … otras variables

    // Campo Destino:
    /**
     * @var string $slug
     *
     * @ORM\Column(name="slug", type="string", length=255)
     * @Gedmo\Slug(style="camel", separator="_", updatable=false, unique=true)
     */
    private $slug;
    // … dentro de una Entidad

Note que el campo desde donde se creará el slug ($title) tiene el Metadato @Gedmo\Sluggable(), de hecho puede definir más de uno. En cambio el campo de destino ($slug) tiene el Metadato @Gedmo\Slug(…) y por convención debe ser uno solo, los argumentos style, separator, updatable y unique son opcionales y se detallan en la documentación propia del autor, en este ejemplo se tiene una forma básica de configuración.

Cada vez que se cree un registro de la entidad, Doctrine automáticamente generará el slug y lo aplicará al campo destino, en el caso de modificaciones depende del valor del argumento updatable.

Timestampable: permite que Doctrine gestione la actualización del Timestamp en campos específicos al realizar operaciones de inserción y/o actualización. Para definir un campo con Timestampable solo debemos añadir el Metadato Gedmo\Timestampable(on=”action”), donde action puede ser created o updated respectivamente.

    // … dentro de una Entidad
    // Campo created:
    /**
     * @var date $created
     *
     * @ORM\Column(name="created", type="date")
     * @Gedmo\Timestampable(on="create")
     */
    private $created;
    // Campo updated:
    /**
     * @var datetime $updated
     *
     * @ORM\Column(name="updated", type="datetime")
     * @Gedmo\Timestampable(on="update")
     */
    private $updated;
    // … dentro de una Entidad

Doctrine automáticamente aplicará un nuevo Date al campo definido on=”create” al crear un nuevo registro de la entidad y actualizará el Timestamp del campo definido on=”update” al actualizar el registro de la entidad.

Loggable: permite que Doctrine lleve un control de Versiones sobre los campos indicados, permitiendo consultar las versiones y revertir hacia una versión anterior.
Para crear campos con log (control de versión) solo debemos añadir a cada campo el Metadato @Gedmo\Versioned(), además de añadir el Metadato @Gedmo\Loggable() a la Entidad correspondiente:

// ... encabezados del archivo
// definimos el Metadato @Gedmo\Loggable() a la Entidad:
/**
 * MDW\BlogBundle\Entity\Articles
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="MDW\BlogBundle\Entity\ArticlesRepository")
 * @Gedmo\Loggable()
 */
class Articles
{
    // … dentro de una Entidad
    // Campo $content será Versionable:
    /**
     * @var text $content
     *
     * @ORM\Column(name="content", type="text")
     * @Gedmo\Versioned()
     */
    private $content;
    // … demás contenido de la entidad

Doctrine automáticamente supervisará los updates hacia los atributos marcados como Versioned de  la entidad y llevará un control de versiones en la Entidad (Stof\DoctrineExtensionsBundle\Entity\LogEntry), y gracias al Repositorio de dicha entidad (\Gedmo\Loggable\Entity\Repository\LogEntryRepository) podremos consultar las Versiones e incluso Revertir los cambios (función $logRepositoryInstance->revert($Entity, $version);), aquí apreciamos un ejemplo de un controlador que lista los cambios:

    // Ejemplo dentro de un Controller:
    public function updateArticleAction($id) {
      $em = $this->getDoctrine()->getEntityManager();

      $article = $em->getRepository('MDWBlogBundle:Articles')->findOneBy(array('id' => $id));

      $article->setContent('editado');
      $em->persist($article);
      $em->flush();

      $content = '';
      // ver cambios
      $log = $em->getRepository('Stof\DoctrineExtensionsBundle\Entity\LogEntry');
      /* @var $log \Gedmo\Loggable\Entity\Repository\LogEntryRepository */

      $query_changues = $log->getLogEntriesQuery($article); //use $log->getLogEntries() para un result directo
      $changues = $query_changues->getResult();
      /* @var $version Stof\DoctrineExtensionsBundle\Entity\LogEntry */
      foreach ($changues as $version) {
        $fields = $version->getData();
        $content.= ' fecha: ' .
            $version->getLoggedAt()->format('d/m/Y H:i:s') .
            ' accion: "'  . $version->getAction() . '"'.
            ' usuario: "' . $version->getUsername() . '"'.
            ' objeto: "'  . $version->getObjectClass() . '"'.
            ' id: "'      . $version->getObjectId() . '"'.
            ' Version: "' . $version->getVersion() . '"'.
            ' datos:';
            foreach ($fields as $field => $value) {
              $content.= "-- " . $field . ': '. $value . '';
            }
      }
      // generamos una salida básica
      $r = new \Symfony\Component\HttpFoundation\Response();
      $r->setContent($content);
      return $r;
    }

De esta forma podemos aprovecharnos de algunos de los comportamientos más utilizados de Doctrine, reutilizar código y automatizar tareas en nuestros modelos.

Resumen Final

Como pudimos apreciar con Symfony2 disponemos de una amplia variedad de bundles de terceros a incluir para extender las capacidades de nuestras aplicaciones, aprovechar y reutilizar código mejorando considerablemente el tiempo en el desarrollo de nuestros proyectos; aprendimos que existen diversas formas de incluir nuestros bundles y que en dado caso podemos hacer instalaciones a mano, también de lo importante que es seguir la documentación de cada bundle para agregarlo en el Autoload o Kernel según corresponda y aplicar las configuraciones requeridas por el mismo.