Los formularios de Symfony 1.4

4.6. Serializando formularios

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ámetro id ($request->getParameter('id') es null)
    • La llamada al método retrieveByPk() devuelve null
    • El objeto form se asocia con un objeto Propel vacío de tipo Autor.
    • La instrucción $this->form->save() crea por tanto un nuevo objeto de tipo Autor siempre que los datos del formulario sean válidos.
  • Modificar un objeto de tipo Autor:
    • Se ejecuta la acción index con un parámetro id obtenido mediante la instrucción $request->getParameter('id') y que representa la clave primaria del objeto Autor que se va a modificar.
    • La llamada al método retriveByPk() devuelve el objeto de tipo Autor relacionado con la clave primaria indicada.
    • El objeto form se asocia al objeto de tipo Autor que se ha obtenido de la base de datos.
    • La instrucción $this->form->save() actualiza los datos del objeto Autor siempre que los datos del formulario sean válidos.

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.