skip to content

Dentro de $form->bind() de Symfony

Intento aclarar cómo funcionan los formularios de Symfony 1

La documentación de formularios de Symfony va mejorando, pero sigue habiendo muchos puntos oscuros. Los formularios siguen siendo una de partes más complejas de entender de Symfony. Hay una forma simple para hacer cosas simples, pero a la que te quieres mover de ahí, es necesario entender cómo funciona todo por dentro.

Y el punto más importante del proceso está en la función bind(). Por ejemplo, si hemos hecho un post validator y no funciona, si estamos trabajando con formularios empotrados (embedded), o simplemente si algo no funciona, necesitaremos entender qué está pasando en este método. Así pues, ¿Qué hace bind()? Según la documentación de formularios de Symfony 1.2 bind() hace lo siguiente:

Cuando el formulario se asocia (bind) a los datos externos usando bind(), el formulario pasa al estado «bound» (ya veremos que esto no significa gran cosa) y se ejecutan las acciones siguientes:

  • Se ejecuta el proceso de validación
  • Los mensajes de error se almacenan en el formulario para que estén disponibles para la template
  • Los valores por defecto del formulario son reemplazados por los datos que ha introducido el usuario.

Vamos a verlo con más detalle. Una función de procesado de formulario típica (bind(), y si es válido, save()) tiene esta forma:

protected function processForm(sfWebRequest $request, sfForm $form)
{
  $form->bind($request->getParameter($form->getName()), $request->getFiles($form->getName()));
  if ($form->isValid())
  {
    $card = $form->save();
    $this->redirect('card/edit?id='.$card->getId());
  }
}

Esta operación tiene dos actores: la $request y el $form. Dentro de todo lo que hay en una $request, si todo ha ido bien habrá un array. En el caso de este objeto (card), nuestra $request contiene este campo:

[postParameters:protected] => Array
        (
            [card] => Array
                (
                    [question] => elefante
                    [answer] => mamífero
                    [id] =>
                    [_csrf_token] => a288d0365a6bb4aab5a11789534a4ddb
                )
        )

Así que tenemos los datos del formulario en un array. Este array es el que enviamos a bind en el primer parámetro al hacer

$form->bind($request->getParameter($form->getName()), $request->getFiles($form->getName()));

¿Qué hace bind con esto? Miremos el código de sfForm->bind() :

 /*
 * Binds the form with input values.
 *
 * It triggers the validator schema validation.
 *
 * @param array $taintedValues An array of input values
 * @param array $taintedFiles  An array of uploaded files (in the $_FILES or $_GET format)
 */
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
  $this->taintedValues = $taintedValues;
  $this->taintedFiles  = $taintedFiles;
  $this->isBound = true;
  $this->resetFormFields();

  if (is_null($this->taintedValues))
  {
    $this->taintedValues = array();
  }

  if (is_null($this->taintedFiles))
  {
    if ($this->isMultipart())
    {
      throw new InvalidArgumentException('This form is multipart, which means you need to supply a files array as the bind() method second argument.');
    }

    $this->taintedFiles = array();
  }

 try
  {
    $this->values = $this->validatorSchema->clean(self::deepArrayUnion($this->taintedValues, self::convertFileInformation($this->taintedFiles)));
    $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);

    // remove CSRF token
    unset($this->values[self::$CSRFFieldName]);
  }
  catch (sfValidatorErrorSchema $e)
  {
    $this->values = array();
    $this->errorSchema = $e;
  }
}

En las dos primeras líneas guarda nuestro array de valores que viene de la request en variables del objeto. La siguiente marca el formulario como bound. Es decir, que aunque haya un desastre en un punto posterior del código de bind(), si llamamos a $form->isBound() nos dirá que efectivamente está «bound». Es decir, que isBound() significa «se ha llamado a bind para este formulario» y no «el bind ha funcionado». resetFormFields() simplemente vacía el array de $this->formFields y también $form->formFieldSchema . El fieldschema es el objeto que realiza el render de un field del formulario cuando lo queremos representar.

Lo siguiente que hace es comprobar que taintedValues y taintedFiles existan y sean coherentes con el formulario (si es un formulario multipart, necesitará taintedFiles).

Tras los preparativos ejecuta una línea que es la que hace los tres puntos de la descripción de bind(). Es decir, la que hace lo que esperamos de bind():

$this->values = $this->validatorSchema->clean(self::deepArrayUnion($this->taintedValues, self::convertFileInformation($this->taintedFiles)));

De dentro a afuera:

  • convertFileInformation() «convierte un array de fichero a un formato que cumple la convención de nombres de $_GET y $_POST».
  • deepArrayUnion() es una función recursiva que fusiona dos arrays.
  • Y este array es el que se le pasa a $this->validatorSchema->clean().

Es decir, que le pasa el muerto al validatorSchema, que es el que carga con el peso del bind. Una conclusión que podemos sacar es que, si hay formularios embedded, sus métodos bind no se van a llamar, pero sí sus validadores. Así que la lógica de sus binds tiene que ir en alguna otra parte.

¿Qué hace $this->validatorSchema->clean()? llama a doClean() que hace esto:

protected function doClean($values)
{
  if (is_null($values))
  {
    $values = array();
  }

  if (!is_array($values))
  {
    throw new InvalidArgumentException('You must pass an array parameter to the clean() method');
  }

  $clean  = array();
  $unused = array_keys($this->fields);
  $errorSchema = new sfValidatorErrorSchema($this);

  // check that post_max_size has not been reached
  if (isset($_SERVER['CONTENT_LENGTH']) && (int) $_SERVER['CONTENT_LENGTH'] > $this->getBytes(ini_get('post_max_size')))
  {
    $errorSchema->addError(new sfValidatorError($this, 'post_max_size'));

    throw $errorSchema;
  }

  // pre validator
  try
  {
    $this->preClean($values);
  }
  catch (sfValidatorErrorSchema $e)
  {
    $errorSchema->addErrors($e);
  }
  catch (sfValidatorError $e)
  {
    $errorSchema->addError($e);
  }
  // validate given values
  foreach ($values as $name => $value)
  {
    // field exists in our schema?
    if (!array_key_exists($name, $this->fields))
    {
      if (!$this->options['allow_extra_fields'])
      {
        $errorSchema->addError(new sfValidatorError($this, 'extra_fields', array('field' => $name)));
      }
      else if (!$this->options['filter_extra_fields'])
      {
        $clean[$name] = $value;
      }

      continue;
    }

    unset($unused[array_search($name, $unused, true)]);

    // validate value
    try
    {
      $clean[$name] = $this->fields[$name]->clean($value);
    }
    catch (sfValidatorError $e)
    {
      $clean[$name] = null;

      $errorSchema->addError($e, (string) $name);
    }
  }
  // are non given values required?
  foreach ($unused as $name)
  {
    // validate value
    try
    {
      $clean[$name] = $this->fields[$name]->clean(null);
    }
    catch (sfValidatorError $e)
    {
      $clean[$name] = null;

      $errorSchema->addError($e, (string) $name);
    }
  }

  // post validator
  try
  {
    $clean = $this->postClean($clean);
  }
  catch (sfValidatorErrorSchema $e)
  {
    $errorSchema->addErrors($e);
  }
  catch (sfValidatorError $e)
  {
    $errorSchema->addError($e);
  }

  if (count($errorSchema))
  {
    throw $errorSchema;
  }
  return $clean;
}

Y aquí está el quid de la cuestión. Tras asegurarse de que los datos de entrada tienen sentido, llama al preValidator, si es que hay uno definido, en preClean(). Entonces llama a la validación campo a campo. Y acaba llamando al postValidator, en postClean(). Hay que fijarse en cosas como que nuestro postValidator realmente devuelva valores, porque si no devuelve nada, si nos fijamos en el código veremos que no retornaremos los valores «cleaned» y estaramos liándola, porque entonces $this->values del formulario quedará vacío.

Resulta complicado encontrar errores así. Por eso es interesante entender qué está pasando al hacer un bind. En este post he tratado de resumirlo y de reunir el código que hay disperso por la API, para que uno pueda tenerlo a mano y entender qué puede estar pasando cuando las cosas fallan.