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.
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
.
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
.
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
.
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 thesf_referential
sfReferentialTable
: gestiona la tablasf_referential
sfReferentialContractType
: gestiona los registros de tiposf_referential_contract_type
sfReferentialContractTypeTable
: gestiona la tablasf_referential_contract_type
sfReferentialProductType
: gestiona los registros de tiposf_referential_product_type
sfReferentialProductTypeTable
: gestiona la tablasf_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.
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
.
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:
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.