Más con Symfony

6.9. Ignorando los formularios embebidos

La implementación actual de ProductForm sufre una carencia importante. Como los campos filename y caption son obligatorios en ProductPhotoForm, la validación del formulario principal siempre falla a menos que el usuario suba dos nuevas fotos. En otras palabras, el usuario no puede modificar solamente el precio de Product sin tener que subir dos nuevas fotos del producto.

Error de validación de las fotos en el formulario del producto

Figura 6.3 Error de validación de las fotos en el formulario del producto

Por este motivo se van a redefinir los requisitos de la aplicación para que si el usuario deja vacíos todos los campos de tipo ProductPhotoForm, el formulario principal los ignore. No obstante, si al menos un campo de este tipo tiene información (sea el caption o el filename), el formulario realiza la validación tradicional y se guarda normalmente. Para conseguirlo, se va a utilizar una técnica avanzada que hace uso de un post-validador propio.

El primer paso consiste en modificar el formulario ProductPhotoForm para hacer que los campos caption y filename sean opcionales:

// lib/form/doctrine/ProductPhotoForm.class.php
public function configure()
{
  $this->setValidator('filename', new sfValidatorFile(array(
    'mime_types' => 'web_images',
    'path' => sfConfig::get('sf_upload_dir').'/products',
    'required' => false,
  )));

  $this->validatorSchema['caption']->setOption('required', false);
}

El código anterior establece la opción required a false al redefinir el validador por defecto del campo filename. Además, también se ha establecido de forma explícita la opción required del campo caption a false.

Seguidamente se añade el post-validador en el formulario ProductPhotoCollectionForm:

// lib/form/doctrine/ProductPhotoCollectionForm.class.php
public function configure()
{
  // ...

  $this->mergePostValidator(new ProductPhotoValidatorSchema());
}

Un post-validador es un tipo especial de validador que tiene acceso a todos los valores enviados, a diferencia de un validador que sólo pude acceder a un único campo del formulario. Uno de los post-validadores más utilizados es sfValidatorSchemaCompare que comprueba por ejemplo que el valor de un campo sea inferior al del otro campo.

6.9.1. Creando un validador propio

Afortunadamente es muy sencillo crear un validador propio. Crea un nuevo archivo llamado ProductPhotoValidatorSchema.class.php y guárdalo en el directorio lib/validator/ (debes crear este directorio a mano):

// lib/validator/ProductPhotoValidatorSchema.class.php
class ProductPhotoValidatorSchema extends sfValidatorSchema
{
  protected function configure($options = array(), $messages = array())
  {
    $this->addMessage('caption', 'The caption is required.');
    $this->addMessage('filename', 'The filename is required.');
  }

  protected function doClean($values)
  {
    $errorSchema = new sfValidatorErrorSchema($this);

    foreach($values as $key => $value)
    {
      $errorSchemaLocal = new sfValidatorErrorSchema($this);

      // se ha rellenado el campo filename pero no el campo caption
      if ($value['filename'] && !$value['caption'])
      {
        $errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'caption');
      }

      // se ha rellenado el campo caption pero no el campo filename
      if ($value['caption'] && !$value['filename'])
      {
        $errorSchemaLocal->addError(new sfValidatorError($this, 'required'), 'filename');
      }

      // no se ha rellenado ni caption ni filename, se eliminan los valores vacíos
      if (!$value['filename'] && !$value['caption'])
      {
        unset($values[$key]);
      }

      // algun error para este formulario embebido
      if (count($errorSchemaLocal))
      {
        $errorSchema->addError($errorSchemaLocal, (string) $key);
      }
    }

    // lanza un error para el formulario principal
    if (count($errorSchema))
    {
      throw new sfValidatorErrorSchema($this, $errorSchema);
    }

    return $values;
  }
}

Nota Todos los validadores heredan de sfValidatorBase y solamente requieren el método doClean(). También se puede emplear el método configure() para añadir opciones o mensajes al validador. En este caso, se han añadido dos mensajes al validador. Igualmente, se pueden añadir opciones adicionales mediante el método addOption().

El método doClean() se encarga de limpiar y validar los datos enviados. La propia lógica del validador es muy simple:

  • Si la foto enviada solamente tiene un nombre de archivo o un título, se lanza un error (sfValidatorErrorSchema) con el mensaje apropiado
  • Si la foto enviada no tiene ni nombre de archivo ni título, se eliminan los dos valores para evitar guardar una foto vacía
  • Si no se producen errores de validación, el método devuelve un array con los datos limpios

Nota Como en este caso el validador propio se va a utilizar como post-validador, su método doClean() espera un array con los valores enviados por el usuario y devuelve un array con los valores limpios. Los validadores propios para campos individuales también se pueden crear igual de fácil. En este caso, el método doClean() solamente espera un valor (el valor enviado por el usuario) y devuelve un único valor.

El último paso consiste en redefinir el método saveEmbeddedForms() de ProductForm para eliminar los formularios de fotos vacíos de forma que no se guarde una foto vacía en la base de datos (ya que se lanzaría una excepción porque la columna caption es obligatoria):

public function saveEmbeddedForms($con = null, $forms = null)
{
  if (null === $forms)
  {
    $photos = $this->getValue('newPhotos');
    $forms = $this->embeddedForms;
    foreach ($this->embeddedForms['newPhotos'] as $name => $form)
    {
      if (!isset($photos[$name]))
      {
        unset($forms['newPhotos'][$name]);
      }
    }
  }

  return parent::saveEmbeddedForms($con, $forms);
}