Más con Symfony

8.1. Creando un comportamiento de Doctrine

En esta sección se explica cómo crear un nuevo comportamiento haciendo uso de Doctrine 1.2. El ejemplo utilizado permitirá mantener una cache del número de relaciones de un registro para no tener que hacer esa consulta todo el rato.

La funcionalidad es realmente simple: en todas las relaciones en las que quieras controlar su número, el comportamiento añade una columna a su modelo para almacenar un contador.

8.1.1. El esquema

Inicialmente se va a utilizar el siguiente esquema. Más adelante se modifica para añadir la definición actAs del comportamiento que se va a crear:

# config/doctrine/schema.yml
Thread:
  columns:
    title:
      type: string(255)
      notnull: true

Post:
  columns:
    thread_id:
      type: integer
      notnull: true
    body:
      type: clob
      notnull: true
  relations:
    Thread:
      onDelete: CASCADE
      foreignAlias: Posts

Seguidamente se construyen todas las clases del esquema:

$ php symfony doctrine:build --all

8.1.2. La plantilla

En primer lugar se crea la clase básica de tipo Doctrine_Template que será la responsable de añadir las columnas al modelo que guardará los contadores.

Añade la siguiente clase dentro de cualquier directorio lib/ del proyecto para que symfony pueda cargarla de forma automática:

// lib/count_cache/CountCache.class.php
class CountCache extends Doctrine_Template
{
  public function setTableDefinition()
  {
  }

  public function setUp()
  {
  }
}

A continuación se modifica el modelo Post para añadir el comportamiento CountCache mediante actAs:

# config/doctrine/schema.yml
Post:
  actAs:
    CountCache: ~
  # ...

Ahora que el modelo Post hace uso del comportamiento CountCache, su funcionamiento es el siguiente: cuando se instancia la información de mapeo de un modelo, se invocan los métodos setTableDefinition() y setUp() de todos sus comportamientos asociados. Esto es lo mismo que sucede con la clase BasePost en lib/model/doctrine/base/BasePost.class.php. Esta característica permite añadir elementos de todo tipo a un modelo, como columnas, relaciones, eventos, etc.

Ahora que está más claro su funcionamiento interno, se añade toda la lógica interna del comportamiento CountCache:

class CountCache extends Doctrine_Template
{
  protected $_options = array(
    'relations' => array()
  );

  public function setTableDefinition()
  {
    foreach ($this->_options['relations'] as $relation => $options)
    {
      // si no se dispone del nombre de la columna, se crea
      if (!isset($options['columnName']))
      {
        $this->_options['relations'][$relation]['columnName'] = 'num_'.Doctrine_Inflector::tableize($relation);
      }

      // añadir la columna al modelo relacionado
      $columnName = $this->_options['relations'][$relation]['columnName'];
      $relatedTable = $this->_table->getRelation($relation)->getTable();
      $this->_options['relations'][$relation]['className'] = $relatedTable->getOption('name');
      $relatedTable->setColumn($columnName, 'integer', null, array('default' => 0));
    }
  }
}

El código superior añade columnas para mantener los contadores de los modelos relacionados. Por tanto, en este caso se añade el comportamiento en el modelo Post para su relación Thread. De esta forma, el número de posts de cualquier Thread se almacena en una columna llamada num_posts. A continuación, modifica el esquema YAML para definir las opciones adicionales del comportamiento:

code.34720752b17772471994a609c6d180fdee6cac0ephp class CountCache extends Doctrine_Template { // ...

public function setTableDefinition() { // ...

$this->addListener(new CountCacheListener($this->_options));

} } code.65708ada59fe69c44c4d5970022173ac2ab752c3php // lib/model/count_cache/CountCacheListener.class.php

class CountCacheListener extends Doctrine_Record_Listener { protected $_options;

public function __construct(array $options) { $this->_options = $options; } } code.3d188004b308550888dad35cc71f43fd229fec21php class CountCacheListener extends Doctrine_Record_Listener { // ...

public function postInsert(Doctrine_Event $event) { $invoker = $event->getInvoker(); foreach ($this->_options['relations'] as $relation => $options) { $table = Doctrine::getTable($options['className']); $relation = $table->getRelation($options['foreignAlias']);

  $table
    ->createQuery()
    ->update()
    ->set($options['columnName'], $options['columnName'].' + 1')
    ->where($relation['local'].' = ?', $invoker->$relation['foreign'])
    ->execute();
}

} } code.3ca928d5842179c661f78197b2ea3756f236c7fbphp $post = new Post(); $post->thread_id = 1; $post->body = 'contenido del post'; $post->save(); code.8780233e6f2f4bd59b24324b15564f7baa688109php class CountCacheListener extends Doctrine_Record_Listener { // ...

public function postDelete(Doctrine_Event $event) { $invoker = $event->getInvoker(); foreach ($this->_options['relations'] as $relation => $options) { $table = Doctrine::getTable($options['className']); $relation = $table->getRelation($options['foreignAlias']);

  $table
    ->createQuery()
    ->update()
    ->set($options['columnName'], $options['columnName'].' - 1')
    ->where($relation['local'].' = ?', $invoker->$relation['foreign'])
    ->execute();
}

} } code.9362e9b88a78ce6bafd4423ff1ad7bc10a38f97aphp $post->delete(); code.7cfb988a77916fd5f3af6d545fb066c95d5ec9baphp class CountCacheListener extends Doctrine_Record_Listener { // ...

public function preDqlDelete(Doctrine_Event $event) { foreach ($this->_options['relations'] as $relation => $options) { $table = Doctrine::getTable($options['className']); $relation = $table->getRelation($options['foreignAlias']);

  $q = clone $event->getQuery();
  $q->select($relation['foreign']);
  $ids = $q->execute(array(), Doctrine::HYDRATE_NONE);
  foreach ($ids as $id)
  {
    $id = $id[0];
    $table
      ->createQuery()
      ->update()
      ->set($options['columnName'], $options['columnName'].' - 1')
      ->where($relation['local'].' = ?', $id)
      ->execute();
  }
}

} } code.a59c4b0fb327a7fd9c298f470c62ee63e412bafephp Doctrine::getTable('Post') ->createQuery() ->delete() ->where('id = ?', 1) ->execute(); code.760e6bb96862904c7fc6468079f5b0a6c1bf36ffphp Doctrine::getTable('Post') ->createQuery() ->delete() ->where('body LIKE ?', '%cool%') ->execute(); code.a06d067f5308d876f4c3d158d15d89105604ab81php $manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true); code.b076a6222ed1da33edf284412214e878219d9a31yaml