A menudo te encontrarás con la necesidad de transformar en alguna otra cosa los datos que el usuario introdujo en un formulario para usarlos en tu programa. Lo podrías hacer fácilmente a mano en tu controlador, pero, ¿qué pasa si quieres utilizar ese formulario específico en diferentes sitios?
Digamos que tienes una relación uno a uno entre una Tarea y una Incidencia, por ejemplo, una Tarea opcionalmente está vinculada a una Incidencia. Añadir un cuadro de lista con todas las posibles Incidencias finalmente te puede conducir a una lista realmente larga en la cual es imposible encontrar algo. En su lugar mejor querrás añadir un cuadro de texto, en el cual el usuario sencillamente puede introducir el número de la incidencia.
Podrías intentar hacer esto en tu controlador, pero no es la mejor solución. Sería mejor si esta incidencia se buscara y convirtiera automáticamente a un objeto Incidencia, para usarla en tu acción. Aquí es donde entran en juego los Transformadores de datos.
En primer lugar, crea una clase IssueToNumberTransformer — esta clase será la responsable de la conversión hacia y desde el número de incidencia y el objeto Incidencia:
// src/Acme/TaskBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace Acme\TaskBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\TaskBundle\Entity\Issue;
class IssueToNumberTransformer implements DataTransformerInterface
{
/**
* @var ObjectManager
*/
private $om;
/**
* @param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* Transforma un objeto (``issue``) a una cadena (``number``).
*
* @param Issue|null $issue
* @return string
*/
public function transform($issue)
{
if (null === $issue) {
return "";
}
return $issue->getNumber();
}
/**
* Transforma una cadena (``number``) a un objeto (``issue``).
*
* @param string $number
*
* @return Issue|null
*
* @throws TransformationFailedException si no encuentra el objeto (issue).
*/
public function reverseTransform($number)
{
if (!$number) {
return null;
}
$issue = $this->om
->getRepository('AcmeTaskBundle:Issue')
->findOneBy(array('number' => $number))
;
if (null === $issue) {
throw new TransformationFailedException(sprintf(
'An issue with number "%s" does not exist!',
$number
));
}
return $issue;
}
}
Truco
Si quieres crear una nueva incidencia al introducir un número desconocido, puedes crear una nueva instancia en lugar de lanzar una TransformationFailedException.
Ahora que ya tienes incorporado el transformador, solamente lo tienes que añadir a tu campo incidencia en algún formulario.
También puedes utilizar transformadores sin necesidad de crear un nuevo tipo de formulario personalizado llamando a addModelTransformer (o a addViewTransformer — consulta la sección Transformadores de modelo y vista) en cualquier campo del constructor:
use Symfony\Component\Form\FormBuilderInterface; use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // ... // este asume que el gestor de la entidad se pasó como una // opción $entityManager = $options['em']; $transformer = new IssueToNumberTransformer($entityManager); // agrega un campo de texto normal, pero le añade tu transformador $builder->add( $builder->create('issue', 'text') ->addModelTransformer($transformer) ); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Acme\TaskBundle\Entity\Task', )); $resolver->setRequired(array( 'em', )); $resolver->setAllowedTypes(array( 'em' => 'Doctrine\Common\Persistence\ObjectManager', )); // ... } // ... }
Este ejemplo requiere que pases como una opción el gestor de la entidad al crear el formulario. Más tarde, aprenderás cómo puedes crear un tipo de campo incidencia personalizado para evitar la necesidad de hacer esto en tu controlador:
$taskForm = $this->createForm(new TaskType(), $task, array(
'em' => $this->getDoctrine()->getEntityManager(),
));
¡Estupendo, ya está! El usuario será capaz de introducir un número de incidencia en el campo de texto y se transformará en un objeto Incidencia. Esto significa que, después de vincularlo satisfactoriamente, la infraestructura del formulario pasa un objeto Incidencia real a Task::setIssue() en vez de el número de incidencia.
Si no encuentra la incidencia, creará un error en el formulario para ese campo y puedes controlar su mensaje de error con la opción invalid_message del campo.
Prudencia
Ten en cuenta que al añadir un transformador necesitas usar una sintaxis un poco más complicada cuando agregas el campo. Lo siguiente es incorrecto, debido a que puedes aplicar el transformador a todo el formulario, en lugar de sólo a este campo:
// ESTE ESTÁ MAL - LA TRANSFORMACIÓN SE APLICARÁ AL
// FORMULARIO COMPLETO
// ve el código correcto en el ejemplo anterior
$builder->add('issue', 'text')
->addModelTransformer($transformer);
Nuevo en la versión 2.1: Los nombres y métodos de los transformadores cambiaron en Symfony 2.1. prependNormTransformer se convirtió en addModelTransformer y appendClientTransformer cambió a addViewTransformer.
En el ejemplo anterior, el transformador se utilizó como un transformador del «modelo». De hecho, hay dos diferentes tipos de transformadores y tres diferentes tipos de datos subyacentes.
En cualquier formulario, los 3 diferentes tipos de datos son los siguientes:
Los 2 diferentes tipos de transformadores te ayudan a convertir hacia y desde cada uno de estos tipos de datos:
¿Qué transformador necesitas? depende de tu situación.
Para utilizar el transformador de la vista, llama a addViewTransformer.
En este ejemplo, el campo es un campo text, y un campo de texto siempre se espera que sea un sencillo, formato escalar en los formatos norm y view. Por esta razón, el transformador más adecuado fue el transformador del «modelo» (que se convierte a/desde el formato normal — cadena a número — al formato del modelo — emisión de objeto).
La diferencia entre los transformadores es sutil y siempre se debe pensar en lo que deben ser realmente los datos «normales» para un campo. Por ejemplo, los datos «normales» de un campo text son una cadena, pero es un objeto DateTime para un campo date.
En el ejemplo anterior, aplicaste el transformador a un campo text normal. Esto fue fácil, pero tiene dos inconvenientes:
Debido a esto, puedes optar por crear un tipo de campo personalizado. En primer lugar, crea la clase del tipo de campo personalizado:
// src/Acme/TaskBundle/Form/Type/IssueSelectorType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class IssueSelectorType extends AbstractType
{
/**
* @var ObjectManager
*/
private $om;
/**
* @param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new IssueToNumberTransformer($this->om);
$builder->addModelTransformer($transformer);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'invalid_message' => 'The selected issue does not exist',
));
}
public function getParent()
{
return 'text';
}
public function getName()
{
return 'issue_selector';
}
}
A continuación, registra el tipo como un servicio y etiquétalo con form.type, para que sea reconocido como un tipo de campo personalizado:
services:
acme_demo.type.issue_selector:
class: Acme\TaskBundle\Form\Type\IssueSelectorType
arguments: ["@doctrine.orm.entity_manager"]
tags:
- { name: form.type, alias: issue_selector }
<service id="acme_demo.type.issue_selector" class="Acme\TaskBundle\Form\Type\IssueSelectorType">
<argument type="service" id="doctrine.orm.entity_manager"/>
<tag name="form.type" alias="issue_selector" />
</service>
$container
->setDefinition('acme_demo.type.issue_selector', array(
new Reference('doctrine.orm.entity_manager'),
))
->addTag('form.type', array(
'alias' => 'issue_selector',
))
;
Ahora, cada vez que necesites utilizar tu tipo de campo issue_selector especial, es muy fácil:
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('dueDate', null, array('widget' => 'single_text'));
->add('issue', 'issue_selector');
}
public function getName()
{
return 'task';
}
}