Skip to content
Caution: You are browsing the legacy symfony 1.x part of this website.

Utilizando la herencia de tablas de Doctrine

Language

por Hugo Hamon

Doctrine se ha convertido oficialmente en el ORM por defecto de Symfony 1.3, mientras que el desarrollo de Propel ha deca铆do en los 煤ltimos meses. A煤n as铆, el proyecto Propel todav铆a est谩 activo y sigue mejorando gracias al esfuerzo de varios miembros de la comunidad Symfony.

La versi贸n 1.2 de Doctrine se ha convertido en el ORM por defecto de Symfony porque es m谩s f谩cil de utilizar que Propel y porque incluye un mont贸n de utilidades como comportamientos, consultas DQL sencillas, migraciones y herencia de tablas.

Este cap铆tulo describe lo que es la herencia de tablas y c贸mo se integra con Symfony 1.3. Haciendo uso de un proyecto real, este cap铆tulo muestra c贸mo aprovechar la herencia de tablas de Doctrine para hacer que el c贸digo sea m谩s flexible y est茅 mejor organizado.

Herencia de tablas de Doctrine

Aunque la herencia de tablas no es muy conocida ni utilizada por la mayor铆a de programadores, se trata de una de las caracter铆sticas m谩s interesantes de Doctrine. La herencia de tablas permite al programador crear tablas de base de datos que heredan de otras tablas de la misma forma que las clases pueden heredar de otras clases en los lenguajes orientados a objetos. La herencia de tablas es una forma sencilla de que dos o m谩s tablas compartan informaci贸n en una 煤nica tabla padre. Observa el siguiente diagrama para comprender mejor el funcionamiento de la herencia de tablas.

Esquema de la herencia de tablas de Doctrine

Doctrine incluye tres estrategias diferentes para gestionar la herencia de tablas en funci贸n de las necesidades de la aplicaci贸n (rendimiento, atomicidad, simplicidad...): __simple__, __column aggregation__ y __concrete__. Aunque todas estas estrategias se describen en el libro de Doctrine, las siguientes secciones explican cada una de ellas y las circunstancias en las que son 煤tiles.

La estrategia simple de herencia de tablas

La estrategia simple de herencia de tablas es la m谩s b谩sica ya que guarda todas las columnas, incluso las de las tablas hijo, dentro de la tabla padre. Si el esquema del modelo es como el siguiente c贸digo YAML, Doctrine genera una tabla llamada Person que incluye tanto las columnas de la tabla Professor como las de la tabla Student.

Person:
  columns:
    first_name:
      type:           string(50)
      notnull:        true
    last_name:
      type:           string(50)
      notnull:        true
 
Professor:
  inheritance:
    type:             simple
    extends:          Person
  columns:
    specialty:
      type:           string(50)
      notnull:        true
 
Student:
  inheritance:
    type:             simple
    extends:          Person
  columns:
    graduation:
      type:           string(20)
      notnull:        true
    promotion:
      type:           integer(4)
      notnull:        true

En la estrategia simple de herencia de tablas, las columnas specialty, graduation y promotion se a帽aden autom谩ticamente en el modelo Person aunque Doctrine genera una clase de modelo tanto para Student como para Professor.

Esquema de la herencia simple de tablas

El inconveniente de esta estrategia es que la tabla padre Person no incluye ninguna columna que identifique el tipo de cada registro. En otras palabras, no es posible obtener solamente los objetos de tipo Professor o Student. La siguiente instrucci贸n de Doctrine devuelve un objeto Doctrine_Collection con todos los registros de la tabla (registros Student y Professor).

$professors = Doctrine_Core::getTable('Professor')->findAll();

La estrategia estrategia simple de herencia de tablas no suele ser muy 煤til en los ejemplos del mundo real, ya que normalmente es necesario seleccionar objetos de un determinado tipo. As铆 que no se usar谩 m谩s esta estrategia en este cap铆tulo.

La estrategia de agregaci贸n de columnas en la herencia de tablas

La estrategia de agregaci贸n de columnas en la herencia de tablas es similar a la estrategia simple excepto por el hecho de que a帽ade una columna llamada type que identifica los diferentes tipos de registros. De esta forma, cuando se guarda un objeto en la base de datos, se a帽ade autom谩ticamente un valor a la columna type que indica el tipo de clase del objeto.

Person:
  columns:
    first_name:
      type:           string(50)
      notnull:        true
    last_name:
      type:           string(50)
      notnull:        true
 
Professor:
  inheritance:
    type:             column_aggregation
    extends:          Person
    keyField:         type
    keyValue:         1
  columns:
    specialty:
      type:           string(50)
      notnull:        true
 
Student:
  inheritance:
    type:             column_aggregation
    extends:          Person
    keyField:         type
    keyValue:         2
  columns:
    graduation:
      type:           string(20)
      notnull:        true
    promotion:
      type:           integer(4)
      notnull:        true

En el esquema YAML anterior se ha modificado el tipo de herencia al valor column_aggregation y se han a帽adido dos nuevos atributos. El primer atributo se denomina keyField y especifica la columna que se crea para guardar el tipo de informaci贸n del registro. El atributo keyField es una columna de de texto llamada type, que es el nombre por defecto cuando no se especifica el atributo keyField. El segundo atributo (keyValue) define el valor del tipo de cada registro que pertenece a la clase Professor o Student.

Esquema de la herencia de tablas basada en la agregaci贸n de columnas

La estrategia de agregaci贸n de columnas es una forma de herencia de tablas muy interesante porque crea una 煤nica tabla (Person) que contiene todos los campos definidos adem谩s del campo type adicional. De esta forma, no es necesario crear varias tablas para unirlas despu茅s mediante una consulta SQL. A continuaci贸n se muestran algunos ejemplos de c贸mo realizar consultas en las tablas y el tipo de resultados devueltos:

// Devuelve un Doctrine_Collection de objetos Professor
$professors = Doctrine_Core::getTable('Professor')->findAll();
 
// Devuelve un Doctrine_Collection de objetos Student
$students = Doctrine_Core::getTable('Student')->findAll();
 
// Devuelve un objeto Professor
$professor = Doctrine_Core::getTable('Professor')->findOneBySpeciality('physics');
 
// Devuelve un objeto Student
$student = Doctrine_Core::getTable('Student')->find(42);
 
// Devuelve un onjeto Student
$student = Doctrine_Core::getTable('Person')->findOneByIdAndType(array(42, 2));

Cuando se obtienen datos de una subclase (Professor, Student), Doctrine a帽ade autom谩ticamente la cl谩usula WHERE necesaria de SQL para realizar la consulta con el valor correspondiente de la columna type.

No obstante, en algunos casos la agregaci贸n de columnas presenta inconvenientes. En primer lugar, la agregaci贸n de columnas impide que los campos de cada sub-tabla puedan ser configurados como required. Dependiendo de cuantos campos haya, la tabla Person puede contener registros con varios campos vac铆os.

El segundo inconveniente est谩 relacionado con el n煤mero de sub-tablas y campos. Si el esquema declara muchas sub-tablas y cada una declara a su vez muchos campos, la tabla padre final contendr谩 un gran n煤mero de columnas. Por tanto, esta tabla padre puede ser dif铆cil de mantener.

La estrategia concreta de herencia de tablas

La estrategia concreta de herencia de tablas es un compromiso entre las ventajas de la agregaci贸n de columnas, el rendimiento de la aplicaci贸n y su facilidad de mantenimiento. Efectivamente, esta estrategia crea una tabla independiente por cada subclase conteniendo todas las columnas: tanto las columnas compartidas como las columnas exclusivas de cada modelo.

Person:
  columns:
    first_name:
      type:           string(50)
      notnull:        true
    last_name:
      type:           string(50)
      notnull:        true
 
Professor:
  inheritance:
    type:             concrete
    extends:          Person
  columns:
    specialty:
      type:           string(50)
      notnull:        true
 
Student:
  inheritance:
    type:             concrete
    extends:          Person
  columns:
    graduation:
      type:           string(20)
      notnull:        true
    promotion:
      type:           integer(4)
      notnull:        true

El esquema anterior genera una tabla Professor con los siguientes campos: id, first_name, last_name y specialty.

Esquema de la herencia concreta de tablas

Esta estrategia tiene varias ventajas respecto de las anteriores. La primera es que cada tabla permanece aislada y es independiente de las otras tablas. Adem谩s, ya no se guardan columnas vac铆as y tampoco se incluye la columna adicional type. El resultado es que cada tabla es mucho m谩s peque帽a y est谩 aislada del resto.

note

El hecho de que las columnas comunes est茅n duplicadas en las sub-tablas es una mejora del rendimiento y de la escalabilidad, ya que Doctrine no tiene que hacer uniones SQL autom谩ticas con la tabla padre para obtener los datos compartidos que pertenecen a un registro de una sub-tabla.

Las 煤nicas dos desventajas de la herencia concreta de tabla son la duplicaci贸n de las columnas compartidas (aunque la duplicaci贸n es buena para mejorar el rendimiento) y el hecho de que la tabla padre generada siempre estar谩 vac铆a. Efectivamente, Doctrine genera una tabla Person aunque nunca guarde informaci贸n en ella ni la utilice en las consultas. Como toda la informaci贸n se guarda en las sub-tablas, esta tabla padre no se utilizar谩 en ninguna consulta.

Hasta ahora s贸lo se han presentado las tres estrategias de herencias de tablas de Doctrine, pero todav铆a no se han utilizado en ning煤n proyecto real desarrollado con Symfony. La siguiente parte de este cap铆tulo explica c贸mo aprovechar la herencia de tablas de Doctrine en Symfony 1.3, sobre todo en el modelo y en el framework de formularios.

Integraci贸n de la herencia de tablas en Symfony

Antes de Symfony 1.3, la herencia de tablas de Doctrine no estaba integrada en el framework, ya que los formularios y los filtros no heredaban correctamente de la clase base. Por tanto, los programadores que quer铆an utilizar la herencia de tablas deb铆an modificar los formularios y filtros, adem谩s de redefinir muchos m茅todos.

Gracias al apoyo de la comunidad de usuarios, los desarrolladores de Symfony han mejorado los formularios y los filtros para que Symfony 1.3 soporte la herencia de tablas de Doctrine de forma sencilla pero completa.

El resto de este cap铆tulo explica c贸mo usar la herencia de tablas de Doctrine y c贸mo aprovecharla en varios ejemplos adopt谩ndola en los modelos, formularios, filtros y generadores de la parte de administraci贸n. Los casos de uso reales que se presentan permiten entender mejor c贸mo funciona la herencia de tablas en Symfony de forma que puedas hacer uso de ella en tus propias aplicaciones.

Introduciendo casos de estudio reales

A lo largo de este cap铆tulo se van a presentar varios casos de uso reales que muestran las grandes ventajas de usar la herencia de tablas de Doctrine en varios elementos: modelos, formularios, filtros y el generador de la parte de administraci贸n.

El primer ejemplo fue utilizado en una aplicaci贸n desarrollada por Sensio para una empresa francesa muy conocida. Este ejemplo muestra c贸mo la herencia de tablas es una buena soluci贸n para manejar una docena de conjuntos de datos id茅nticos de forma que se puedan reaprovechar m茅todos y propiedades para evitar la duplicaci贸n de c贸digo.

El segundo ejemplo muestra c贸mo hacer uso de la herencia concreta de tablas en los formularios creando un modelo simple que gestione archivos num茅ricos.

Por 煤ltimo, el tercer ejemplo muestra c贸mo utilizar la herencia de tablas con el generador de la parte de administraci贸n para hacerlo m谩s flexible.

Herencia de tablas en el modelo

Al igual que la programaci贸n orientada a objetos, la herencia de tablas fomenta que se comparta la informaci贸n. Por tanto, es posible compartir m茅todos y propiedades cuando se trabaja con las clases generadas por el modelo. La herencia de tablas de Doctrine es una buena forma de compartir y redefinir las acciones de los objetos heredados. A continuaci贸n se explica este uso con un ejemplo real.

El problema

Muchas aplicaciones web requieren el uso de datos "referenciales" para funcionar. Por referencial se entiende un conjunto normalmente peque帽o de datos que se representan mediante una tabla sencilla que contiene al menos dos campos (por ejemplo id y label). No obstante, en algunos casos los datos referenciales contienen informaci贸n adicional como los campos is_active o is_default. Este fue el caso al que se enfrent贸 recientemente la empresa Sensio al desarrollar una aplicaci贸n para un cliente.

El cliente quer铆a gestionar un gran conjunto de datos a trav茅s de los formularios y plantillas de la aplicaci贸n. Todas las tablas referenciales presentaban la misma estructura b谩sica: id, label, position y is_default. El campo position se emplea para ordenar los registros mediante una funcionalidad tipo drag & drop construida con AJAX. El campo is_default indica si el registro se debe mostrar o no seleccionado por defecto cuando se muestra en una lista desplegable de HTML.

La soluci贸n

Gestionar dos o m谩s tablas id茅nticas es uno de los problemas que m谩s f谩cilmente se resuelven con la herencia de tablas. En este caso, se eligi贸 la herencia concreta de tablas como mejor estrategia para que los m茅todos de cada objeto se encontraran en una 煤nica clase. Veamos un esquema de datos simplificado para ilustrar el problema.

sfReferential:
  columns:
    id:
      type:        integer(2)
      notnull:     true
    label:
      type:        string(45)
      notnull:     true
    position:
      type:        integer(2)
      notnull:     true
    is_default:
      type:        boolean
      notnull:     true
      default:     false
 
sfReferentialContractType:
  inheritance:
    type:          concrete
    extends:       sfReferential
 
sfReferentialProductType:
  inheritance:
    type:          concrete
    extends:       sfReferential

La herencia concreta de tablas es la mejor en este caso porque crea tablas aisladas e independientes y porque el campo position se debe gestionar para los registros que sean del mismo tipo.

Construye el modelo y ver谩s que Doctrine y Symfony generan tres tablas SQL y seis clases del modelo en el directorio lib/model/doctrine:

  • sfReferential: gestiona los registros de tipo the sf_referential
  • sfReferentialTable: gestiona la tabla sf_referential
  • sfReferentialContractType: gestiona los registros de tipo sf_referential_contract_type
  • sfReferentialContractTypeTable: gestiona la tabla sf_referential_contract_type
  • sfReferentialProductType: gestiona los registros de tipo sf_referential_product_type
  • sfReferentialProductTypeTable: gestiona la tabla sf_referential_product_type

Si exploras las clases generadas, ver谩s que las clases base de sfReferentialContractType y sfReferentialProductType heredan de la clase sfReferential. Por tanto, todos los m茅todos y propiedades de tipo public o protected de la clase sfReferential se comparten entre las dos sub-clases y pueden ser redefinidos f谩cilmente si es necesario. Esto es justamente lo que necesitamos.

La clase sfReferential ahora puede contener m茅todos que gestionen cualquier tipo de dato referencial, como por ejemplo:

// lib/model/doctrine/sfReferential.class.php
class sfReferential extends BasesfReferential
{
  public function promote()
  {
    // sube un elemento dentro de la lista
  }
 
  public function demote()
  {
    // baja un elemento dentro de la lista
  }
 
  public function moveToFirstPosition()
  {
    // posiciona el elemento como el primero de la lista
  }
 
  public function moveToLastPosition()
  {
    // posiciona el elemento como el 煤ltimo de la lista
  }
 
  public function moveToPosition($position)
  {
    // coloca el elemento en la posici贸n indicada
  }
 
  public function makeDefault($forceSave = true, $conn = null)
  {
    $this->setIsDefault(true);
 
    if ($forceSave)
    {
      $this->save($conn);
    }
  }
}

Gracias a la herencia concreta de tablas de Doctrine, todo el c贸digo se encuentra centralizado en un 煤nico sitio. Por tanto, el c贸digo es m谩s sencillo de depurar, mantener, mejorar y probar con pruebas unitarias.

La anterior es la primera gran ventaja de trabajar con la herencia de tablas. Adem谩s, gracias a esta estrategia, los objetos del modelo se pueden utilizar para centralizar el c贸digo de las acciones, tal y como se muestra a continuaci贸n. La clase sfBaseReferentialActions es un tipo especial de clase de acciones que gestiona el modelo referencial y que heredan cada una de las clases de acciones.

// lib/actions/sfBaseReferentialActions.class.php
class sfBaseReferentialActions extends sfActions
{
  /**
   * Acci贸n AJAX que guarda la nueva posici贸n resultante despu茅s de que
   * el usuario reordene los elementos de la lista.
   *
   * Esta acci贸n est谩 relacionada gracias a una ruta ~sfDoctrineRoute~ que
   * facilita la b煤squeda de los objetos referenciales.
   *
   * @param sfWebRequest $request
   */
  public function executeMoveToPosition(sfWebRequest $request)
  {
    $this->forward404Unless($request->isXmlHttpRequest());
 
    $referential = $this->getRoute()->getObject();
 
    $referential->moveToPosition($request->getParameter('position', 1));
 
    return sfView::NONE;
  }
}

驴Qu茅 hubiera sucedido si el esquema no utiliza herencia de tablas? El c贸digo deber铆a haberse duplicado en cada una de las sub-clases referenciales. Esta estrategia no ser铆a muy DRY (Don't Repeat Yourself) sobre todo en una aplicaci贸n que dispone de una docena de tablas referenciales.

Herencia de tablas en los formularios

Sigamos con el recorrido de todas las ventajas de la herencia de tablas de Doctrine. En la secci贸n anterior se ha mostrado lo 煤til que es la herencia para compartir m茅todos y propiedades entre varios modelos. A continuaci贸n se muestra su utilidad en los formularios generados autom谩ticamente por Symfony.

El modelo de ejemplo

El siguiente esquema YAML describe un modelo para gestionar documentos num茅ricos. El objetivo es guardar informaci贸n gen茅rica en la tabla File e informaci贸n espec铆fica en las sub-tablas Video y PDF.

File:
  columns:
    filename:
      type:            string(50)
      notnull:         true
    mime_type:
      type:            string(50)
      notnull:         true
    description:
      type:            clob
      notnull:         true
    size:
      type:            integer(8)
      notnull:         true
      default:         0
 
Video:
  inheritance:
    type:              concrete
    extends:           File
  columns:
    format:
      type:            string(30)
      notnull:         true
    duration:
      type:            integer(8)
      notnull:         true
      default:         0
    encoding:
      type:            string(50)
 
PDF:
  tableName:           pdf
  inheritance:
    type:              concrete
    extends:           File
  columns:
    pages:
      type:            integer(8)
      notnull:         true
      default:         0
    paper_size:
      type:            string(30)
    orientation:
      type:            enum
      default:         portrait
      values:          [portrait, landscape]
    is_encrypted:
      type:            boolean
      default:         false
      notnull:         true

Las tablas PDF y Video comparten la misma tabla File, que contiene informaci贸n general sobre archivos num茅ricos. El modelo Video encapsula la informaci贸n relativa a los objetos de tipo v铆deo como su formato (columna format) (4/3, 16/9, ...) o su duraci贸n (columna duration), mientras que el modelo PDF contiene informaci贸n como el n煤mero de p谩ginas (columna pages) y la orientaci贸n del documento (columna orientation). Ejecuta la siguiente tarea para generar el modelo y sus correspondientes formularios.

$ php symfony doctrine:build --all

La siguiente secci贸n describe c贸mo aprovechar la herencia de tablas en las clases de los formularios gracias al nuevo m茅todo setupInheritance().

Descubre el m茅todo setupInheritance()

Como era de esperar, Doctrine ha generado seis clases de formulario en los directorios lib/form/doctrine y lib/form/doctrine/base:

  • BaseFileForm
  • BaseVideoForm
  • BasePDFForm

  • FileForm

  • VideoForm
  • PDFForm

Si abres las tres clases Base de los formularios, ver谩s algo nuevo en el m茅todo setup(). Symfony 1.3 a帽ade un nuevo m茅todo llamado setupInheritance(). Inicialmente este m茅todo est谩 vac铆o.

Lo m谩s importante es que la herencia de formularios se mantiene porque tanto BaseVideoForm como BasePDFForm heredan de las clases FileForm y BaseFileForm. Por tanto, cada una de ellas hereda de la clase File y pueden compartir sus m茅todos.

El siguiente c贸digo redefine el m茅todo setupInheritance() y configura la clase FileForm para que pueda ser utilizar en cualquier sub-formulario de forma m谩s efectiva.

// lib/form/doctrine/FileForm.class.php
class FileForm extends BaseFileForm
{
  protected function setupInheritance()
  {
    parent::setupInheritance();
 
    $this->useFields(array('filename', 'description'));
 
    $this->widgetSchema['filename']    = new sfWidgetFormInputFile();
    $this->validatorSchema['filename'] = new sfValidatorFile(array(
      'path' => sfConfig::get('sf_upload_dir')
    ));
  }
}

El m茅todo setupInheritance(), invocado por las sub-clases VideoForm y PDFForm, elimina todos los campos salvo filename y description. El widget del campo filename se ha transformado en un widget de archivo y su validador asociado se ha cambiado a sfValidatorFile. De esta forma, el usuario podr谩 subir un archivo y guardarlo en el servidor.

Personalizando los formularios heredados mediante el m茅todo setupInheritance()

Estableciendo el tama帽o y tipo MIME del archivo

Aunque los formularios ya est谩n preparados, todav铆a falta configurar una cosa m谩s antes de poder utilizarlos. Como los campos mime_type y size se han eliminado del objeto FileForm, es preciso a帽adirlos en la aplicaci贸n. El mejor lugar para a帽adirlos es el m茅todo generateFilenameFilename() de la clase File.

// lib/model/doctrine/File.class.php
class File extends BaseFile
{
  /**
   * Generates a filename for the current file object.
   *
   * @param sfValidatedFile $file
   * @return string
   */
  public function generateFilenameFilename(sfValidatedFile $file)
  {
    $this->setMimeType($file->getType());
    $this->setSize($file->getSize());
 
    return $file->generateFilename();
  }
}

Este nuevo m茅todo se encarga de generar un nombre de archivo propio para guardar el archivo en el sistema de archivos. Aunque el m茅todo generateFilenameFilename() devuelve por defecto un nombre de archivo generado autom谩ticamente, tambi茅n establece las propiedades mime_type y size gracias al objeto de tipo sfValidatedFile que se pasa como primer argumento.

Como Symfony 1.3 ya soporta la herencia de tablas de Doctrine, los formularios ahora pueden guardar un objeto y todos sus valores heredados. Gracias al soporte nativo de la herencia de tablas, es posible crear formularios muy potentes y funcionales a帽adiendo una peque帽a cantidad de c贸digo propio.

El c贸digo anterior puede mejorarse mucho de forma sencilla gracias a la herencia de clases. Por ejemplo las clases VideoForm y PDFForm podr铆an redefinir el validador de filename para que utilizara un validador propio m谩s espec铆fico como sfValidatorVideo o sfValidatorPDF.

Herencia de tablas en los filtros

Como los filtros en realidad son formularios, tambi茅n heredan los m茅todos y propiedades de los filtros padre. De esta forma, los objetos VideoFormFilter y PDFFormFilter heredan de la clase FileFormFilter y se pueden personalizar utilizando el m茅todo setupInheritance().

De la misma forma, tanto VideoFormFilter como PDFFormFilter pueden compartir los mismos m茅todos propios de la clase FileFormFilter.

Herencia de tablas en el generador de la parte de administraci贸n

A continuaci贸n se muestra c贸mo aprovechar la herencia de tablas de Doctrine junto con una de las nuevas caracter铆sticas del generador de la parte de administraci贸n: la definici贸n de una clase base de las acciones. El generador de la parte de administraci贸n es una de las caracter铆sticas que m谩s ha mejorado Symfony desde su versi贸n 1.0.

En noviembre de 2008 Symfony introdujo el nuevo generador de administraci贸n como parte de Symfony 1.2. Esta herramienta incluye muchas funcionalidades listas para usar, como las operaciones CRUD b谩sicas, paginaci贸n y filtrado de listados, borrado m煤ltiple, etc. El generador de administraciones es una herramienta muy poderosa que facilita y acelera el desarrollo y personalizaci贸n del backend de las aplicaciones.

Introducci贸n al ejemplo pr谩ctico

El objetivo de esta 煤ltima parte del cap铆tulo consiste en ilustrar el uso de la herencia de tablas de Doctrine junto con el generador de la parte de administraci贸n. Para ello, se va a crear un backend sencillo que gestione dos tablas que contienen informaci贸n que se puede ordenar y priorizar.

Como el lema de Symfony es "no reinventes la rueda", el modelo de Doctrine va a utilizar el plugin csDoctrineActAsSortablePlugin para que se encargue de todo lo relacionado con la ordenaci贸n de objetos. El plugin csDoctrineActAsSortablePlugin lo desarrolla y mantiene una empresa llamada CentreSource y que es una de las empresas m谩s activas dentro del ecosistema de Symfony.

El modelo de datos es muy sencillo, ya que est谩 formado por tres clases llamadas sfItem, sfTodoItem y sfShoppingItem, que permiten gestionar una lista de tareas y una lista de la compra. Cada elemento de las dos listas es ordenable de forma que se pueda asignar la prioridad de los elementos en base a su posici贸n.

sfItem:
  actAs:             [Timestampable]
  columns:
    name:
      type:          string(50)
      notnull:       true
 
sfTodoItem:
  actAs:             [Sortable]
  inheritance:
    type:            concrete
    extends:         sfItem
  columns:
    priority:
      type:          string(20)
      notnull:       true
      default:       minor
    assigned_to:
      type:          string(30)
      notnull:       true
      default:       me
 
sfShoppingItem:
  actAs:             [Sortable]
  inheritance:
    type:            concrete
    extends:         sfItem
  columns:
    quantity:
      type:          integer(3)
      notnull:       true
      default:       1

El esquema anterior describe el modelo de datos basado en tres clases diferentes. Las dos sub-clases (sfTodoItem y sfShoppingItem) utilizan los comportamientos Sortable y Timestampable. El comportamiento Sortable est谩 disponible gracias al plugin csDoctrineActAsSortablePlugin y a帽ade en cada tabla una columna de tipo entero llamada position. Las dos clases heredan de la clase base sfItem. Esta clase contiene dos columnas llamadas id y name.

Para poder probar el backend se crea el siguiente archivo de datos. Como es habitual, este archivo de datos se guarda en el archivo data/fixtures.yml del proyecto Symfony.

sfTodoItem:
  sfTodoItem_1:
    name:           "Write a new symfony book"
    priority:       "medium"
    assigned_to:    "Fabien Potencier"
  sfTodoItem_2:
    name:           "Release Doctrine 2.0"
    priority:       "minor"
    assigned_to:    "Jonathan Wage"
  sfTodoItem_3:
    name:           "Release symfony 1.4"
    priority:       "major"
    assigned_to:    "Kris Wallsmith"
  sfTodoItem_4:
    name:           "Document Lime 2 Core API"
    priority:       "medium"
    assigned_to:    "Bernard Schussek"
 
sfShoppingItem:
  sfShoppingItem_1:
    name:           "Apple MacBook Pro 15.4 inches"
    quantity:       3
  sfShoppingItem_2:
    name:           "External Hard Drive 320 GB"
    quantity:       5
  sfShoppingItem_3:
    name:           "USB Keyboards"
    quantity:       2
  sfShoppingItem_4:
    name:           "Laser Printer"
    quantity:       1

Despu茅s de instalar el plugin csDoctrineActAsSortablePlugin y despu茅s de crear el modelo de datos, es necesario activar el nuevo plugin en la clase ProjectConfiguration del archivo config/ProjectConfiguration.class.php:

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins(array(
      'sfDoctrinePlugin',
      'csDoctrineActAsSortablePlugin'
    ));
  }
}

A continuaci贸n se puede generar la base de datos, el modelo, los formularios y los filtros, adem谩s de cargar los datos de prueba en las tablas de la reci茅n creada base de datos. Todo esto se puede realizar con una 煤nica tarea llamada doctrine:build:

$ php symfony doctrine:build --all --no-confirmation

Para completar el proceso es necesario borrar la cache de Symfony y tambi茅n se deben enlazar los recursos del plugin desde el directorio web/ del proyecto:

$ php symfony cache:clear
$ php symfony plugin:publish-assets

La siguiente secci贸n explica c贸mo crear los m贸dulos del backend con las herramientas de generaci贸n de administraciones y tambi茅n explica c贸mo aprovechar la nueva clase base de las acciones.

Creando el backend

Esta secci贸n describe los pasos necesarios para crear una nueva aplicaci贸n de backend que contenga dos m贸dulos generados autom谩ticamente para gestionar las listas de tareas y las listas de la compra. Por tanto, el primer paso consiste en generar una nueva aplicaci贸n llamada backend:

$ php symfony generate:app backend

Aunque el generador de administraciones es una herramienta muy completa, antes de Symfony 1.3 el programador deb铆a duplicar todo el c贸digo com煤n de los diferentes m贸dulos. Ahora en cambio, la tarea doctrine:generate-admin incluye una nueva opci贸n llamada --actions-base-class que permite al programador definir la clase base de las acciones del m贸dulo.

Como los dos m贸dulos son muy parecidos, es seguro que compartir谩n el c贸digo de las acciones gen茅ricas. Este c贸digo compartido se puede incluir en una clase base de las acciones que se encuentra en el directorio lib/actions, tal y como se muestra a continuaci贸n:

// lib/actions/sfSortableModuleActions.class.php
class sfSortableModuleActions extends sfActions
{
 
}

Una vez que se ha creado la nueva clase sfSortableModuleActions y despu茅s de borrar la cache, ya es posible generar los dos m贸dulos de la aplicaci贸n backend:

$ php symfony doctrine:generate-admin --module=shopping --actions-base-class=sfSortableModuleActions backend sfShoppingItem
$ php symfony doctrine:generate-admin --module=todo --actions-base-class=sfSortableModuleActions backend sfTodoItem

El generador de la parte de administraci贸n genera los m贸dulos en dos directorios diferentes. El primer directorio obviamente es apps/backend/modules. No obstante, la mayor铆a de los archivos generados se encuentran en el directorio cache/backend/dev/modules. Los archivos que se encuentran en ese directorio se regeneran cada vez que se borra la cache o cuando se modifica la cofiguraci贸n del m贸dulo.

note

Investigar los archivos generados en la cache es una de las mejores formas de aprender c贸mo funciona internamente el generador de administraciones de Symfony. Las nuevas sub-clases de sfSortableModuleActions las puedes encontrar en cache/backend/dev/modules/autoShopping/actions/actions.class.php y cache/backend/dev/modules/autoTodo/actions/actions.class.php respectivamente. Symfony genera por defecto estas clases para que hereden directamente de sfActions.

Gesti贸n por defecto de la lista de tareas

Gesti贸n por defecto de la lista de la compra

Los dos m贸dulos del backend ya est谩n listos para utilizarlos y personalizar su comportamiento. No obstante, en este cap铆tulo no se trata la configuraci贸n de los m贸dulos generados autom谩ticamente. Afortunadamente existe mucha documentaci贸n sobre este aspecto, como por ejemplo la Referencia de Symfony.

Modificando la posici贸n de un elemento

La secci贸n anterior describe c贸mo crear dos m贸dulos de administraci贸n completamente funcionales, cada uno de ellos heredando de la misma clase base de acciones. El siguiente paso consiste en crear una acci贸n compartida que permita reordenar los elementos de una lista. Este requerimiento es bastante sencillo ya que el plugin que acabamos de instalar incluye una completa API para reordenar los objetos.

En primer lugar se crean dos nuevas rutas preparadas para mover un registro hacia arriba o hacia abajo. Como el generador de administraciones utiliza la ruta sfDoctrineRouteCollection, se pueden a帽adir f谩cilmente nuevas rutas a la colecci贸n mediante el archivo de configuraci贸n config/generator.yml de cada m贸dulo:

# apps/backend/modules/shopping/config/generator.yml
generator:
  class: sfDoctrineGenerator
  param:
    model_class:           sfShoppingItem
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          sf_shopping_item
    with_doctrine_route:   true
    actions_base_class:    sfSortableModuleActions
 
    config:
      actions: ~
      fields:  ~
      list:
        max_per_page:      100
        sort:              [position, asc]
        display:           [position, name, quantity]
        object_actions:
          moveUp:          { label: "move up", action: "moveUp" }
          moveDown:        { label: "move down", action: "moveDown" }
          _edit:      ~
          _delete:    ~
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

Los cambios anteriores tambi茅n hay que incluirlos en el m贸dulo todo:

# apps/backend/modules/todo/config/generator.yml
generator:
  class: sfDoctrineGenerator
  param:
    model_class:           sfTodoItem
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          sf_todo_item
    with_doctrine_route:   true
    actions_base_class:    sfSortableModuleActions
 
    config:
      actions: ~
      fields:  ~
      list:
        max_per_page:      100
        sort:              [position, asc]
        display:           [position, name, priority, assigned_to]
        object_actions:
          moveUp:          { label: "move up", action: "moveUp" }
          moveDown:        { label: "move down", action: "moveDown" }
          _edit:      ~
          _delete:    ~
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

Los dos archivos YAML describen la configuraci贸n de los m贸dulos shopping y todo. Cada uno ha sido configurado para que se adapte a las necesidades del usuario. En primer lugar, los listados se ordenan de forma ascendente seg煤n la columna position. Adem谩s, el m谩ximo n煤mero de elementos por p谩gina se ha incrementado hasta 100 para evitar en lo posible la paginaci贸n de elementos.

Por 煤ltimo, el n煤mero de columnas que se muestran se han reducido a position, name, priority, assigned_to y quantity. Adem谩s, cada m贸dulo dispone de dos nuevas acciones: moveUp y moveDown. El aspecto final de los listados debe ser id茅ntico al de las siguientes im谩genes:

Administraci贸n personalizada de la lista de tareas

Administraci贸n personalizada de la lista de la compra

Estas dos nuevas acciones se han declarado pero todav铆a no hacen nada. Las dos se deben crear en la clase base de las acciones sfSortableModuleActions, tal y como se describe a continuaci贸n. El plugin csDoctrineActAsSortablePlugin incluye dos m茅todos muy 煤tiles en la clase de cada modelo: promote() y demote(). Los dos se utilizan para crear las acciones moveUp y moveDown.

// lib/actions/sfSortableModuleActions.class.php
class sfSortableModuleActions extends sfActions
{
  /**
   * Moves an item up in the list.
   *
   * @param sfWebRequest $request
   */
  public function executeMoveUp(sfWebRequest $request)
  {
    $this->item = $this->getRoute()->getObject();
 
    $this->item->promote();
 
    $this->redirect($this->getModuleName());
  }
 
  /**
   * Moves an item down in the list.
   *
   * @param sfWebRequest $request
   */
  public function executeMoveDown(sfWebRequest $request)
  {
    $this->item = $this->getRoute()->getObject();
 
    $this->item->demote();
 
    $this->redirect($this->getModuleName());
  }
}

Gracias a estas dos nuevas acciones compartidas, tanto la lista de tareas como la lista de la compra permiten reordenar sus elementos. Adem谩s, estas acciones son f谩ciles de mantener y de probar con las pruebas funcionales. Si quieres puedes mejorar el aspecto de los dos m贸dulos redefiniendo la plantilla de las acciones del objeto para eliminar el primer enlace move up y el 煤ltimo enlace move down.

Mejorando la experiencia de usuario

Antes de concluir este cap铆tulo se van a retocar las dos listas para mejorar la experiencia de usuario. Casi todo el mundo est谩 de acuerdo en que mover un elemento pinchando un enlace no es una forma muy intuitiva para la mayor铆a de usuarios. Una forma mucho mejor de hacerlo consiste en utilizar comportamientos de AJAX y JavaScript. En este 煤ltimo caso, todas las filas de la tabla HTML son reordenables simplemente arrastrando y soltando (drag & drop) gracias al plugin Table Drag and Drop de jQuery. Cada vez que el usuario mueva una fila de la tabla HTML, se realizar谩 una llamada mediante AJAX.

En primer lugar, descarga el framework jQuery e inst谩lalo en el directorio web/js. Repite este proceso para el plugin Table Drag and Drop cuyo c贸digo puedes encontrar en un repositorio de Google Code.

Para que esta nueva caracter铆stica funcione, los listados de cada m贸dulo deben en primer lugar incluir cierto c贸digo JavaScript y las dos tablas necesitan un atributo id. Como todas las plantillas y elementos parciales del generador de administraciones se pueden redefinir, localiza el archivo _list.php de la cache y copialo en los dos m贸dulos.

Sin embargo, copiar el mismo archivo _list.php en el directorio templates/ de cada m贸dulo no es una pr谩ctica muy DRY (Don't Repeat Yourself). Por tanto, copiar el archivo cache/backend/dev/modules/autoShopping/templates/_list.php en el directorio apps/backend/templates/ y cambia su nombre a _table.php. Reemplaza su contenido actual por el siguiente c贸digo:

<div class="sf_admin_list">
  <?php if (!$pager->getNbResults()): ?>
    <p><?php echo __('No result', array(), 'sf_admin') ?></p>
  <?php else: ?>
    <table cellspacing="0" id="sf_item_table">
      <thead>
        <tr>
          <th id="sf_admin_list_batch_actions"><input id="sf_admin_list_batch_checkbox" type="checkbox" onclick="checkAll();" /></th>
          <?php include_partial(
            $sf_request->getParameter('module').'/list_th_tabular',
            array('sort' => $sort)
          ) ?>
          <th id="sf_admin_list_th_actions">
            <?php echo __('Actions', array(), 'sf_admin') ?>
          </th>
        </tr>
      </thead>
      <tfoot>
        <tr>
          <th colspan="<?php echo $colspan ?>">
            <?php if ($pager->haveToPaginate()): ?>
              <?php include_partial(
                $sf_request->getParameter('module').'/pagination',
                array('pager' => $pager)
              ) ?>
            <?php endif; ?>
            <?php echo format_number_choice(
              '[0] no result|[1] 1 result|(1,+Inf] %1% results', 
              array('%1%' => $pager->getNbResults()),
              $pager->getNbResults(), 'sf_admin'
            ) ?>
            <?php if ($pager->haveToPaginate()): ?>
              <?php echo __('(page %%page%%/%%nb_pages%%)', array(
                '%%page%%' => $pager->getPage(), 
                '%%nb_pages%%' => $pager->getLastPage()), 
                'sf_admin'
              ) ?>
            <?php endif; ?>
          </th>
        </tr>
      </tfoot>
      <tbody>
      <?php foreach ($pager->getResults() as $i => $item): ?>
        <?php $odd = fmod(++$i, 2) ? 'odd' : 'even' ?>
        <tr class="sf_admin_row <?php echo $odd ?>">
          <?php include_partial(
            $sf_request->getParameter('module').'/list_td_batch_actions',
            array(
              'sf_'. $sf_request->getParameter('module') .'_item' => $item,
              'helper' => $helper
          )) ?>
          <?php include_partial(
            $sf_request->getParameter('module').'/list_td_tabular', 
            array(
              'sf_'. $sf_request->getParameter('module') .'_item' => $item
          )) ?>
            <?php include_partial(
              $sf_request->getParameter('module').'/list_td_actions',
              array(
                'sf_'. $sf_request->getParameter('module') .'_item' => $item, 
                'helper' => $helper
            )) ?>
        </tr>
      <?php endforeach; ?>
      </tbody>
    </table>
  <?php endif; ?>
  </div>
  <script type="text/javascript">
    /* <![CDATA[ */
    function checkAll() {
      var boxes = document.getElementsByTagName('input'); 
      for (var index = 0; index < boxes.length; index++) { 
        box = boxes[index]; 
        if (
          box.type == 'checkbox' 
          && 
          box.className == 'sf_admin_batch_checkbox'
        ) 
        box.checked = document.getElementById('sf_admin_list_batch_checkbox').checked 
      }
      return true;
    }
    /* ]]> */
  </script>

Por 煤ltimo, crea un archivo _list.php en el directorio templates/ de cada m贸dulo y coloca el siguiente c贸digo en cada uno:

// apps/backend/modules/shopping/templates/_list.php
<?php include_partial('global/table', array(
  'pager' => $pager,
  'helper' => $helper,
  'sort' => $sort,
  'colspan' => 5
)) ?>
// apps/backend/modules/shopping/templates/_list.php
<?php include_partial('global/table', array(
  'pager' => $pager,
  'helper' => $helper,
  'sort' => $sort,
  'colspan' => 8
)) ?>

Para modificar la posici贸n de una fila, los dos m贸dulos deben implementar una nueva acci贸n que procese la petici贸n AJAX entrante. Como se ha explicado anteriormente, la nueva acci贸n compartida executeMove() debe a帽adirse a la clase base de acciones sfSortableModuleActions:

// lib/actions/sfSortableModuleActions.class.php
class sfSortableModuleActions extends sfActions
{
  /**
   * Performs the Ajax request, moves an item to a new position.
   *
   * @param sfWebRequest $request
   */
  public function executeMove(sfWebRequest $request)
  {
    $this->forward404Unless($request->isXmlHttpRequest());
    $this->forward404Unless($item = Doctrine_Core::getTable($this->configuration->getModel())->find($request->getParameter('id')));
 
    $item->moveToPosition((int) $request->getParameter('rank', 1));
 
    return sfView::NONE;
  }
}

La acci贸n executeMove() requiere un m茅todo llamado getModel() en el objeto de la configuraci贸n. Implementa este nuevo m茅todo en las clases todoGeneratorConfiguration y shoppingGeneratorConfiguration tal y como se muestra a continuaci贸n:

// apps/backend/modules/shopping/lib/shoppingGeneratorConfiguration.class.php
class shoppingGeneratorConfiguration extends BaseShoppingGeneratorConfiguration
{
  public function getModel()
  {
    return 'sfShoppingItem';
  }
}
// apps/backend/modules/todo/lib/todoGeneratorConfiguration.class.php
class todoGeneratorConfiguration extends BaseTodoGeneratorConfiguration
{
  public function getModel()
  {
    return 'sfTodoItem';
  }
}

Todav铆a queda una 煤ltima tarea que hacer. Por el momento, las filas de las tablas no se pueden arrastrar y no se realiza ninguna llamada AJAX cuando se recoloca una fila. Para implementar estas caracter铆sticas, los dos m贸dulos necesitan una ruta espec铆fica para acceder a su correspondiente acci贸n move. Por tanto, a帽ade en el archivo apps/backend/config/routing.yml las dos siguientes rutas:

<?php foreach (array('shopping', 'todo') as $module) : ?>
 
<?php echo $module ?>_move:
  class: sfRequestRoute
  url: /<?php echo $module ?>/move
  param:
    module: "<?php echo $module ?>"
    action: move
  requirements:
    sf_method: [get]
 
<?php endforeach ?>

Para evitar el c贸digo duplicado, las dos nuevas rutas se generan mediante una instrucci贸n foreach y hacen uso del nombre del m贸dulo para utilizarlas f谩cilmente desde la vista. Por 煤ltimo, el archivo apps/backend/templates/_table.php debe incluir el siguiente c贸digo JavaScript para a帽adir el comportamiento de "arrastrar y soltar" en las filas de las tablas y para realizar la correspondiente llamada AJAX:

<script type="text/javascript" charset="utf-8">
  $().ready(function() {
    $("#sf_item_table").tableDnD({
      onDrop: function(table, row) {
        var rows = table.tBodies[0].rows;
 
        // Get the moved item's id
        var movedId = $(row).find('td input:checkbox').val();
 
        // Calculate the new row's position
        var pos = 1;
        for (var i = 0; i<rows.length; i++) {
          var cells = rows[i].childNodes;
          // Perform the ajax request for the new position
          if (movedId == $(cells[1]).find('input:checkbox').val()) {
            $.ajax({
              url:"<?php echo url_for('@'. $sf_request->getParameter('module').'_move') ?>?id="+ movedId +"&rank="+ pos,
              type:"GET"
            });
            break;
          }
          pos++;
        }
      },
    });
  });
</script>

La tabla HTML ya es completamente funcional. Sus filas se pueden arrastrar y soltar y la nueva posici贸n de los elementos se guarda autom谩ticamente gracias a las llamadas AJAX. A帽adiendo un poco de c贸digo en las acciones y plantillas, la usabilidad del backend ha mejorado enormemente, mejorando tambi茅n la experiencia de usuario. El generador de la parte de administraci贸n es suficientemente flexible como para extenderlo y personalizarlo, adem谩s de soportar todas las caracter铆sticas de la herencia de tablas de Doctrine.

Si quieres ahora puedes mejorar los dos m贸dulos eliminando las acciones moveUp y moveDown obsoletas y a帽adiendo cualquier otro cambio que se ajuste a tus necesidades.

Conclusi贸n

Este cap铆tulo ha mostrado c贸mo la herencia de tablas de Doctrine es una utilidad muy poderosa que permite al programador crear c贸digo m谩s r谩pidamente y mejorar la organizaci贸n del c贸digo. Esta caracter铆stica de Doctrine se encuentra completamente integrada en varios niveles de Symfony, por lo que animamos a todos los programadores a que la utilicen para aumentar su eficiencia y mejoren la organizaci贸n de su c贸digo.

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.