Los formularios de Symfony 1.4

11.5. Serialización de formularios

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ámetro id ($request->getParameter('id') es null)
    • El método find() devuelve null
    • El objeto form se asocia con un objeto Doctrine vacío de tipo Author
    • Si el formulario es válido, la llamada a $this->form->save() crea un nuevo objeto de tipo Author
  • Modificar objetos de tipo Author existentes:
    • Se invoca la acción index con un parámetro id ($request->getParameter('id') es la clave primaria del objeto de tipo Author que se quiere modificar)
    • El método find() devuelve el objeto de tipo Author 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 tipo Author

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.