La sección anterior explica cómo personalizar los formularios generados automáticamente con la tarea propel:build-forms
. En esta sección se personaliza el flujo de trabajo del formulario, empezando por el código generado mediante la tarea propel:generate-crud
.
4.6.1. Valores por defecto
Una instancia de un formulario Propel siempre está asociada a un objeto Propel. Este objeto asociado siempre es de la clase devuelta por el método getModelName()
. El formulario AutorForm
por ejemplo sólo puede estar asociado a objetos que sean de la clase Autor
. Este objeto puede ser un objeto vacío (una nueva instancia de la clase Autor
) o el objeto que se indica como primer argumento del constructor. A diferencia del constructor de un formulario normal, al que se le pasa como primer argumento un array de valores, al constructor de un formulario Propel se le pasa un objeto Propel. Este objeto se utiliza para determinar el valor por defecto de cada campo del formulario. El método getObject()
devuelve el objeto relacionado con la actual instancia del formulario y el método isNew()
permite descubrir si el objeto se ha obtenido a través del constructor:
// Crear un nuevo objeto
$autorForm = new AutorForm();
print $autorForm->getObject()->getId(); // Muestra null
print $autorForm->isNew(); // Muestra true
// Modificar un objeto existente
$autor = AutorPeer::retrieveByPk(1);
$autorForm = new AutorForm($autor);
print $autorForm->getObject()->getId(); // Muestra 1
print $autorForm->isNew(); // Muestra false
4.6.2. Modificar el flujo de trabajo
Como se explica al principio de este capítulo, la acción edit
mostrada en el listado 4-23 gestiona el flujo de trabajo del formulario.
Listado 4-23 - El método executeEdit
del módulo autor
// apps/frontend/modules/autor/actions/actions.class.php
class autorActions extends sfActions
{
// ...
public function executeEdit($request)
{
$autor = AutorPeer::retrieveByPk($request->getParameter('id'));
$this->form = new AutorForm($autor);
if ($request->isMethod('post'))
{
$this->form->bind($request->getParameter('autor'));
if ($this->form->isValid())
{
$autor = $this->form->save();
$this->redirect('autor/edit?id='.$autor->getId());
}
}
}
}
Aunque la acción edit
se parece a las acciones mostradas en los capítulos anteriores, en realidad presenta diferencias importantes:
- El primer argumento del constructor del formulario es un objeto Propel de la clase
Autor
:
$autor = AutorPeer::retrieveByPk($request->getParameter('id'));
$this->form = new AutorForm($autor);
- El formato del atributo
name
de los widgets se modifica automáticamente para que sea posible obtener la información del usuario en un array PHP que se llama igual que la tabla relacionada (autor
en este caso):
$this->form->bind($request->getParameter('autor'));
- Si el formulario es válido, sólo es necesario ejecutar el método
save()
para crear o actualizar el objeto Propel relacionado con el formulario:
$autor = $this->form->save();
4.6.3. Creando y modificando objetos Propel
El listado 4-23 utiliza un solo método para crear y modificar objetos de la clase Autor
:
- Crear un nuevo objeto de tipo
Autor
:- Se ejecuta la acción
index
sin un parámetroid
($request->getParameter('id')
esnull
) - La llamada al método
retrieveByPk()
devuelvenull
- El objeto
form
se asocia con un objeto Propel vacío de tipoAutor
. - La instrucción
$this->form->save()
crea por tanto un nuevo objeto de tipoAutor
siempre que los datos del formulario sean válidos.
- Se ejecuta la acción
- Modificar un objeto de tipo
Autor
:- Se ejecuta la acción
index
con un parámetroid
obtenido mediante la instrucción$request->getParameter('id')
y que representa la clave primaria del objetoAutor
que se va a modificar. - La llamada al método
retriveByPk()
devuelve el objeto de tipoAutor
relacionado con la clave primaria indicada. - El objeto
form
se asocia al objeto de tipoAutor
que se ha obtenido de la base de datos. - La instrucción
$this->form->save()
actualiza los datos del objetoAutor
siempre que los datos del formulario sean válidos.
- Se ejecuta la acción
4.6.4. El método save()
Cuando los datos de un formulario Propel son válidos, el método save()
actualiza el objeto asociado y lo almacena en la base de datos. Este método no sólo guarda el objeto principal, sino que también guarda todos los objetos relacionados. El formulario ArticuloForm
por ejemplo actualiza las etiquetas relacionadas con el artículo. La relación entre las tablas articulo
y etiqueta
es de tipo n-n y las etiquetas relacionadas con un artículo se guardan en la tabla articulo_etiqueta
utilizando el método saveArticuloEtiquetaList()
.
Para asegurar que los datos guardados son consistentes, el método save()
realiza todas las actualizaciones en una transacción.
Nota En el capítulo 9 se explica que el método save()
también actualiza de forma automática todas las tablas internacionalizadas.
4.6.5. Trabajando con archivos subidos
El método save()
actualiza de forma automática los objetos Propel, pero no puede encargarse de otras cosas importantes como la gestión de los archivos subidos.
A continuación se muestra cómo adjuntar un archivo a cada artículo. Los archivos se almacenan en el directorio web/uploads/
y en el campo archivo
de la tabla articulo
de la base de datos se guarda la ruta hasta el archivo, tal y como muestra el listado 4-24.
Listado 4-24 - Esquema de la tabla articulo
con archivos adjuntos
// config/schema.yml
propel:
articulo:
// ...
archivo: varchar(255)
Después de actualizar el esquema, es necesario actualizar el modelo de objetos, la base de datos y todos los formularios relacionados:
$ ./symfony propel:build-all
Nota Ten en cuenta que la tarea propel:build-all
borra todas las tablas del esquema y las vuelve a crear. Por lo tanto, se borran todos los datos de todas las tablas. Este es el motivo por el que se crean archivos con datos de prueba (llamados fixtures) y que se pueden cargar en la base de datos cada vez que se modifica el modelo.
El listado 4-25 muestra cómo modificar la clase ArticuloForm
para asociar un widget y un validador al campo archivo
.
Listado 4-25 - Modificando el campo archivo
del formulario ArticuloForm
class ArticuloForm extends BaseArticuloForm
{
public function configure()
{
// ...
$this->widgetSchema['archivo'] = new sfWidgetFormInputFile();
$this->validatorSchema['archivo'] = new sfValidatorFile();
}
}
Como sucede en todos los formularios que permite subir archivos, no olvides añadir el atributo enctype
a la etiqueta <form>
de la plantilla (en el capítulo 2 se explica detalladamente el trabajo con los archivos subidos).
El listado 4-26 muestra las modificaciones necesarias en la acción que guarda los datos del formulario para guardar el archivo en el servidor y para guardar la ruta hasta el archivo en el objeto articulo
.
Listado 4-26 - Guardando el objeto articulo
y el archivo subido
public function executeEdit($request)
{
$autor = ArticuloPeer::retrieveByPk($request->getParameter('id'));
$this->form = new ArticuloForm($autor);
if ($request->isMethod('post'))
{
$this->form->bind($request->getParameter('articulo'), $request->getFiles('articulo'));
if ($this->form->isValid())
{
$archivo = $this->form->getValue('archivo');
$nombreArchivo = sha1($archivo->getOriginalName()).$archivo->getExtension($archivo->getOriginalExtension());
$archivo->save(sfConfig::get('sf_upload_dir').'/'.$nombreArchivo);
$articulo = $this->form->save();
$this->redirect('articulo/edit?id='.$articulo->getId());
}
}
}
Después de guardar el archivo subido en el servidor, el objeto sfValidatedFile
ya conoce la ruta hasta el archivo. En el método save()
, se utilizan los valores de cada campo para actualizar el objeto asociado. Además, el objeto sfValidatedFile
se convierte en una cadena de texto utilizando el método __toString()
, que devuelve la ruta completa hasta el archivo. La columna archivo
de la tabla articulo
almacena esta ruta completa.
Nota Si se quiere almacenar la ruta del archivo de forma relativa respecto al directorio sfConfig::get('sf_upload_dir')
, se puede crear una clase que herede de sfValidatedFile
y se puede utilizar la opción validated_file_class
para indicar al validador sfValidatorFile
el nombre de la nueva clase. De esta forma, el validador devuelve una instancia de esa nueva clase. En lo que queda de capítulo se muestra otra forma de conseguirlo, que consiste en modificar el valor de la columna archivo
antes de guardar el objeto en la base de datos.
4.6.6. Personalizando el método save()
En la sección anterior se explica cómo guardar un archivo subido en la acción edit
. Por otra parte, uno de los principios de la programación orientada a objetos es la reutilización del código cuando se encapsula en clases. De esta forma, en vez de duplicar el código necesario para guardar los archivos subidos en todas las acciones del formulario ArticuloForm
, se guarda ese código en la propia clase ArticuloForm
. El listado 4-27 muestra cómo redefinir el método save()
para que también guarde el archivo subido y para que pueda borrar un archivo existente.
Listado 4-27 - Redefiniendo el método save()
de la clase ArticuloForm
class ArticuloForm extends BaseFormPropel
{
// ...
public function save($con = null)
{
if (file_exists($this->getObject()->getArchivo()))
{
unlink($this->getObject()->getArchivo());
}
$archivo = $this->getValue('archivo');
$nombreArchivo = sha1($archivo->getOriginalName()).$archivo->getExtension($archivo->getOriginalExtension());
$archivo->save(sfConfig::get('sf_upload_dir').'/'.$nombreArchivo);
return parent::save($con);
}
}
Después de incluir todo el código en el propio formulario, la nueva acción edit
es idéntica a la que genera automáticamente la tarea propel:generate-crud
.
4.6.7. Personalizando el método doSave()
Como se ha comentado en las secciones anteriores, la actualización de los objetos se realiza mediante transacciones que aseguran que todas las operaciones relacionadas con esa actualización se realizan correctamente. Cuando se redefine el método save()
(como por ejemplo en la sección anterior para guardar los archivos subidos) el código que se ejecuta no se incluye en esta transacción.
El listado 4-28 muestra cómo utilizar el método doSave()
para incluir en la transacción global el código propio que guarda los archivos subidos.
Listado 4-28 - Redefiniendo el método doSave()
en el formulario ArticuloForm
class ArticuloForm extends BaseFormPropel
{
// ...
public function doSave($con = null)
{
if (file_exists($this->getObject()->getArchivo()))
{
unlink($this->getObject()->getArchivo());
}
$archivo = $this->getValue('archivo');
$nombreArchivo = sha1($archivo->getOriginalName()).$archivo->getExtension($archivo->getOriginalExtension());
$archivo->save(sfConfig::get('sf_upload_dir').'/'.$nombreArchivo);
return parent::doSave($con);
}
}
De esta forma, si el método save()
del objeto archivo
produce una excepción, el objeto articulo
tampoco se guarda porque el código propio se ha incluido dentro del método doSave()
que realiza la transacción.
4.6.8. Personalizando el método updateObject()
En ocasiones es necesario modificar el objeto asociado al formulario entre la acción de actualizar sus datos y la acción de guardarlo en la base de datos.
En el ejemplo anterior de los archivos subidos, en vez de guardar en la columna archivo
la ruta completa hasta el archivo subido, sólo se guarda la ruta relativa respecto al directorio sfConfig::get('sf_upload_dir')
.
El listado 4-29 muestra cómo redefinir el método updateObject()
del formulario ArticuloForm
para modificar el valor de la columna archivo
después de actualizar automáticamente el objeto pero antes de guardarlo en la base de datos.
Listado 4-29 - Redefiniendo el método updateObject()
y la clase ArticuloForm
class ArticuloForm extends BaseFormPropel
{
// ...
public function updateObject()
{
$objeto = parent::updateObject();
$objeto->setArchivo(str_replace(sfConfig::get('sf_upload_dir').'/', '', $objeto->getArchivo()));
return $objeto;
}
}
El método updateObject()
se invoca desde el método doSave()
antes de guardar el objeto en la base de datos.