Las secciones anteriores explican cómo personalizar los formularios generados automáticamente por la tarea doctrine:build-forms
. En esta sección, se personaliza el flujo de trabajo de los formularios, comenzando por el código generado mediante la tarea doctrine:generate-crud
.
11.5.1. Valores iniciales
Cada instancia de un formulario de Doctrine siempre está conectada con un objeto de Doctrine. El objeto de Doctrine relacionado siempre pertenece a la clase que devuelve el método getModelName()
. El formulario AuthorForm
de los ejemplos anteriores sólo puede estar relacionado con objetos de la clase Author
. El objeto relacionado o es un objeto vacío (una instancia nueva de la clase Author
) o es el objeto utilizado como primer argumento del constructor. Mientras el constructor de un formulario típico utiliza como primer argumento un array de valores, el constructor de un formulario de Doctrine siempre utiliza un objeto de Doctrine. Este objeto es el que se emplea para obtener el valor inicial de cada campo del formulario. El método getObject()
devuelve el objeto asociado con la actual instancia del formulario y el método isNew()
permite averiguar si el objeto se ha enviado mediante el constructor:
// creando un nuevo objeto
$authorForm = new AuthorForm();
print $authorForm->getObject()->getId(); // muestra null
print $authorForm->isNew(); // muestra true
// modificando un objeto existente
$author = Doctrine::getTable('Author')->find(1);
$authorForm = new AuthorForm($author);
print $authorForm->getObject()->getId(); // muestra 1
print $authorForm->isNew(); // muestra false
11.5.2. Flujo de trabajo
Como se explicó al principio de este capítulo, la acción edit
mostrada en el listado 11-23 es la encargada de gestionar el flujo de trabajo del formulario.
Listado 11-23 - El método executeEdit
del módulo author
// apps/frontend/modules/author/actions/actions.class.php
class authorActions extends sfActions
{
// ...
public function executeEdit($request)
{
$author = Doctrine::getTable('Author')->find($request->getParameter('id'));
$this->form = new AuthorForm($author);
if ($request->isMethod('post'))
{
$this->form->bind($request->getParameter('author'));
if ($this->form->isValid())
{
$author = $this->form->save();
$this->redirect('author/edit?id='.$author->getId());
}
}
}
}
Aunque la acción edit
se parece a las acciones que se han descrito en los capítulos anteriores, existen varias diferencias:
- El primer argumento del constructor del formulario es un objeto Doctrine de la clase
Author
:
$author = Doctrine::getTable('Author')->find($request->getParameter('id'));
$this->form = new AuthorForm($author);
- El formato del atributo
name
del widget se adapta automáticamente para poder obtener los datos enviados por el usuario mediante un array de PHP con el mismo nombre que la tabla relacionada (author
):
$this->form->bind($request->getParameter('author'));
- Si el formulario es válido, un simple llamada al método
save()
crea o actualiza el objeto Doctrine relacionado con el formulario:
$author = $this->form->save();
11.5.3. Creando y modificando objetos Doctrine
El código del listado 11-23 dispone de un único método para crear y modificar objetos de la clase Author
:
- Crear nuevos objetos de tipo
Author
:- Se invoca la acción
index
sin ningún parámetroid
($request->getParameter('id')
esnull
) - El método
find()
devuelvenull
- El objeto
form
se asocia con un objeto Doctrine vacío de tipoAuthor
- Si el formulario es válido, la llamada a
$this->form->save()
crea un nuevo objeto de tipoAuthor
- Se invoca la acción
- Modificar objetos de tipo
Author
existentes:- Se invoca la acción
index
con un parámetroid
($request->getParameter('id')
es la clave primaria del objeto de tipoAuthor
que se quiere modificar) - El método
find()
devuelve el objeto de tipoAuthor
relacionado con esa clave primaria - El objeto
form
se asocia con el objeto anterior - Si el formulario es válido, la llamada a
$this->form->save()
actualiza el objeto de tipoAuthor
- Se invoca la acción
11.5.4. El método save()
Cuando un formulario de Doctrine es válido, el método save()
actualiza el objeto relacionado y lo almacena en la base de datos. En realidad, este método no sólo guarda el objeto principal sino que también almacena todos los objetos relacionados. El formulario ArticleForm
por ejemplo también actualiza las etiquetas asociadas con el artículo. Como la relación entre las tablas article
y tag
es de tipo n-n, las etiquetas relacionadas con un artículo se guardan en la tabla article_tag
(utilizando el método saveArticleTagList()
generado automáticamente).
Para asegurar la integridad de los datos guardados, el método save()
realiza todas las actualizaciones en una transacción
Nota Como se explica en el capítulo 9, el método save()
también actualiza las tablas internacionalizadas.
11.5.5. Trabajando con archivos subidos
El método save()
actualiza automáticamente los objetos Doctrine, pero no se encarga de los elementos relacionados como los archivos subidos.
A continuación se adjunta un archivo a cada artículo. Los archivos subidos se almacenan en el directorio web/uploads
y en el campo file
de la tabla article
se almacena la ruta hasta el archivo, tal y como muestra el listado 11-24.
Listado 11-24 - Esquema de la tabla article
con un archivo adjunto
// config/schema.yml
doctrine:
article:
// ...
file: string(255)
Cada vez que se actualiza el esquema de datos es necesario actualizar el modelo de objetos, la base de datos y los formularios:
$ ./symfony doctrine:build-all
Nota Debes tener en cuenta que la tarea doctrine:build-all
borra todas las tablas de la base de datos antes de volver a crearlas. Por lo tanto, se pierde toda la información existente en las tablas. Este es el motivo por el que se recomienda crear archivos con datos de prueba (fixtures
) para cargarlos cada vez que se modifica el modelo de datos.
El listado 11-25 muestra cómo modificar la clase ArticleForm
para asociar un widget y un validador con el campo file
.
Listado 11-25 - Modificando el campo file
del formulario ArticleForm
class ArticleForm extends BaseArticleForm
{
public function configure()
{
// ...
$this->widgetSchema['file'] = new sfWidgetFormInputFile();
$this->validatorSchema['file'] = new sfValidatorFile();
}
}
No olvides que todos los formularios que permiten adjuntar archivos deben incluir un atributo llamado enctype
en la etiqueta <form>
. En el capítulo 2 se explica cómo modificar la etiqueta <form>
de la plantilla para gestionar los archivos subidos.
El listado 11-26 muestra las modificaciones necesarias para guardar el archivo subido en el servidor y para almacenar su ruta en el objeto article
.
Listado 11-26 - Guardando el objeto article
y el archivo subido en la acción
public function executeEdit($request)
{
$author = Doctrine::getTable('Author')->find($request->getParameter('id'));
$this->form = new ArticleForm($author);
if ($request->isMethod('post'))
{
$this->form->bind($request->getParameter('article'), $request->getFiles('article'));
if ($this->form->isValid())
{
$file = $this->form->getValue('file');
$filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
$file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
$article = $this->form->save();
$this->redirect('article/edit?id='.$article->getId());
}
}
}
Después de guardar el archivo subido en algún directorio, el objeto sfValidatedFile
ya conoce la ruta absoluta del archivo. Cuando se invoca el método save()
, se emplean los valores de cada campo para actualizar el objeto. En el caso del campo file
, el objeto sfValidatedFile
se convierte en una cadena de caracteres mediante el método __toString()
y se devuelve el valor de la ruta absoluta del archivo. A continuación, la columna file
de la tabla article
almacena esta ruta absoluta.
Nota Si sólo quieres almacenar la ruta relativa desde el directorio sfConfig::get('sf_upload_dir')
, se puede crear una clase que herede de sfValidatedFile
y que utilice la opción validated_file_class
para enviar el nombre de la nueva clase al validador sfValidatorFile
. De esta forma, el validador devuelve una instancia de tu clase. En lo que resta de capítulo se muestra otra forma de hacerlo, que consiste en modificar el valor de la columna file
antes de guardar el objeto en la base de datos.
11.5.6. Personalizando el método save()
En la sección anterior se explica cómo guardar en la acción edit
un archivo subido. Uno de los principios de la programación orientada a objetos es la reutilización del código mediante su encapsulación en clases. Por tanto, en vez de duplicar en cada acción del formulario ArticleForm
el código que guarda un archivo, es mejor mover ese código a la clase ArticleForm
. El listado 11-27 muestra como redefinir el método save()
para almacenar los archivos subidos y para borrar un archivo existente.
Listado 11-27 - Redefiniendo el método save()
de la clase ArticleForm
class ArticleForm extends BaseFormDoctrine
{
// ...
public function save($con = null)
{
if (file_exists($this->getObject()->getFile()))
{
unlink($this->getObject()->getFile());
}
$file = $this->getValue('file');
$filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
$file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
return parent::save($con);
}
}
Después de mover el código al formulario, la acción edit
es idéntica al código generado automáticamente por la tarea doctrine:generate-crud
.
11.5.7. Personalizando el método doSave()
Como se vio en las secciones anteriores, cuando se guarda un objeto se realiza una transacción para asegurar que todas las operaciones del proceso de guardado se realizan correctamente. Cuando se redefine el método save()
como en la sección anterior al guardar un archivo subido, el código se ejecuta de forma independiente a esa transacción.
El listado 11-28 muestra cómo utilizar el método doSave()
para incluir en la transacción global el código encargado de guardar el archivo subido.
Listado 11-28 - Redefiniendo el método doSave()
en el formulario ArticleForm
class ArticleForm extends BaseFormDoctrine
{
// ...
public function doSave($con = null)
{
if (file_exists($this->getObject()->getFile()))
{
unlink($this->getObject()->getFile());
}
$file = $this->getValue('file');
$filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension());
$file->save(sfConfig::get('sf_upload_dir').'/'.$filename);
return parent::doSave($con);
}
}
Como el método doSave()
se ejecuta en la transacción creada por el método save()
, si la llamada al método save()
del objeto file()
lanza una excepción, el objeto no se guarda.
11.5.8. Personalizando el método updateObject()
En ocasiones es necesario modificar el objeto asociado al formulario después de su actualización automática pero antes de que se almacene en la base de datos.
Siguiendo con el ejemplo de los archivos subidos, en esta ocasión no se quiere almacenar en la columna file
la ruta absoluta del archivo subido, sino que sólo se guarda la ruta relativa respecto al directorio sfConfig::get('sf_upload_dir')
.
El listado 11-29 muestra cómo redefinir el método updateObject()
del formulario ArticleForm
para modificar el valor de la columna file
después de la actualización automática del objeto pero antes de que sea almacenado.
Listado 11-29 - Redefiniendo el método updateObject()
y la clase ArticleForm
class ArticleForm extends BaseFormDoctrine
{
// ...
public function updateObject($values = null)
{
$object = parent::updateObject($values);
$object->setFile(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getFile()));
return $object;
}
}
El método updateObject()
se invoca desde el método doSave()
antes de almacenar el objeto en la base de datos.