diff --git a/book/forms.rst b/book/forms.rst
index 38186bba172..71b6695bb75 100644
--- a/book/forms.rst
+++ b/book/forms.rst
@@ -1141,6 +1141,8 @@ the choice is ultimately up to you.
$form->get('dueDate')->setData(new \DateTime());
+.. _form-as-services:
+
Defining your Forms as Services
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/cookbook/form/data_transformers.rst b/cookbook/form/data_transformers.rst
index 850db1a5adb..cfc8cf1035b 100644
--- a/cookbook/form/data_transformers.rst
+++ b/cookbook/form/data_transformers.rst
@@ -4,51 +4,171 @@
How to Use Data Transformers
============================
-You'll often find the need to transform the data the user entered in a form into
-something else for use in your program. You could easily do this manually in your
-controller, but what if you want to use this specific form in different places?
-
-Say you have a one-to-one relation of Task to Issue, e.g. a Task optionally has an
-issue linked to it. Adding a listbox with all possible issues can eventually lead to
-a really long listbox in which it is impossible to find something. You might
-want to add a textbox instead, where the user can simply enter the issue number.
-
-You could try to do this in your controller, but it's not the best solution.
-It would be better if this issue were automatically converted to an Issue object.
-This is where Data Transformers come into play.
+Data transformers are used to translate the data for a field into a format that can
+be displayed in a form (and back on submit). They're already used internally for
+many field types. For example, the :doc:`date field type `
+can be rendered as a ``yyyy-MM-dd``-formatted input textbox. Internally, a data transformer
+converts the starting ``DateTime`` value of the field into the ``yyyy-MM-dd`` string
+to render the form, and then back into a ``DateTime`` object on submit.
.. caution::
When a form field has the ``inherit_data`` option set, Data Transformers
won't be applied to that field.
+Simple Example: Sanitizing HTML on User Input
+---------------------------------------------
+
+Suppose you have a Task form with a description ``textarea`` type::
+
+ // src/AppBundle/Form/TaskType.php
+
+ use Symfony\Component\Form\FormBuilderInterface;
+ use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+ // ...
+ class TaskType extends AbstractType
+ {
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('description', 'textarea');
+ }
+
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'data_class' => 'AppBundle\Entity\Task'
+ ));
+ }
+
+ // ...
+ }
+
+But, there are two complications:
+
+#. Your users are allowed to use *some* HTML tags, but not others: you need a way
+ to call :phpfunction:`striptags` after the form is submitted;
+
+#. To be friendly, you want to convert ``
`` tags into line breaks (``\n``) before
+ rendering the field so the text is easier to edit.
+
+This is a *perfect* time to attach a custom data transformer to the ``description``
+field. The easiest way to do this is with the :class:`Symfony\\Component\\Form\\CallbackTransformer`
+class::
+
+ // src/AppBundle/Form/TaskType.php
+
+ use Symfony\Component\Form\CallbackTransformer;
+ use Symfony\Component\Form\FormBuilderInterface;
+ // ...
+
+ class TaskType extends AbstractType
+ {
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('description', 'textarea');
+
+ $builder->get('description')
+ ->addModelTransformer(new CallbackTransformer(
+ // transform
to \n so the textarea reads easier
+ function ($originalDescription) {
+ return preg_replace('#
#i', "\n", $originalDescription);
+ },
+ function ($submittedDescription) {
+ // remove most HTML tags (but not br,p)
+ $cleaned = strip_tags($submittedDescription, '
');
+
+ // transform any \n to real
+ return str_replace("\n", '
', $cleaned);
+ }
+ ));
+ }
+
+ // ...
+ }
+
+The ``CallbackTransformer`` takes to callback functions as arguments. The first transforms
+the original value into a format that'll be used to render the field. The second
+does the reverse: it transforms the submitted value back into the format you'll use
+in your code.
+
+.. tip::
+
+ The ``addModelTransformer()`` method accepts *any* object that implements
+ :class:`Symfony\\Component\\Form\\DataTransformerInterface` - so you can create
+ your own classes, instead of putting all the logic in the form (see the next section).
+
+You can also add the transformer, right when adding the field by changing the format
+slightly::
+
+ $builder->add(
+ $builder->create('description', 'textarea')
+ ->addModelTransformer(...)
+ );
+
+Harder Example: Transforming an Issue Number into an Issue Entity
+-----------------------------------------------------------------
+
+Say you have a many-to-one relation from the Task entity to an Issue entity (i.e. each
+Task has an optional foreign key to its related Issue). Adding a listbox with all
+possible issues could eventually get *really* long and take a long time to load.
+Instead, you decide you want to add a textbox, where the user can simply enter the
+issue number.
+
+Start by setting up the text field like normal::
+
+ // src/AppBundle/Form/TaskType.php
+
+ // ...
+ class TaskType extends AbstractType
+ {
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('description', 'textarea')
+ ->add('issue', 'text');
+ }
+
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'data_class' => 'AppBundle\Entity\Task'
+ ));
+ }
+
+ // ...
+ }
+
+Good start! But if you stopped here and submitted the form, the Task's ``issue``
+property would be a string (e.g. "55"). How can you transform this into an ``Issue``
+entity on submit?
+
Creating the Transformer
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+You could use the ``CallbackTransformer`` like earlier. But since this is a bit more
+complex, creating a new transformer class will keep the ``TaskType`` form class simpler.
-First, create an ``IssueToNumberTransformer`` class - this class will be responsible
-for converting to and from the issue number and the ``Issue`` object::
+Create an ``IssueToNumberTransformer`` class: it will be responsible for converting
+to and from the issue number and the ``Issue`` object::
// src/AppBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace AppBundle\Form\DataTransformer;
use AppBundle\Entity\Issue;
- use Doctrine\Common\Persistence\ObjectManager;
+ use Doctrine\Common\Persistence\EntityManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IssueToNumberTransformer implements DataTransformerInterface
{
- /**
- * @var ObjectManager
- */
- private $om;
+ private $entityManager;
- /**
- * @param ObjectManager $om
- */
- public function __construct(ObjectManager $om)
+ public function __construct(EntityManager $entityManager)
{
- $this->om = $om;
+ $this->entityManager = $entityManager;
}
/**
@@ -63,31 +183,36 @@ for converting to and from the issue number and the ``Issue`` object::
return '';
}
- return $issue->getNumber();
+ return $issue->getId();
}
/**
* Transforms a string (number) to an object (issue).
*
- * @param string $number
+ * @param string $issueNumber
* @return Issue|null
* @throws TransformationFailedException if object (issue) is not found.
*/
- public function reverseTransform($number)
+ public function reverseTransform($issueNumber)
{
- if (!$number) {
- return null;
+ // no issue number? It's optional, so that's ok
+ if (!$issueNumber) {
+ return;
}
- $issue = $this->om
+ $issue = $this->entityManager
->getRepository('AppBundle:Issue')
- ->findOneBy(array('number' => $number))
+ // query for the issue with this id
+ ->find($issueNumber)
;
if (null === $issue) {
+ // causes a validation error
+ // this message is not shown to the user
+ // see the invalid_message option
throw new TransformationFailedException(sprintf(
'An issue with number "%s" does not exist!',
- $number
+ $issueNumber
));
}
@@ -95,10 +220,15 @@ for converting to and from the issue number and the ``Issue`` object::
}
}
-.. tip::
+Just like in the first example, a transformer has two directions. The ``transform()``
+method is responsible for converting the data used in your code to a format that
+can be rendered in your form (e.g. an ``Issue`` object to its ``id``, a string).
+The ``reverseTransform()`` method does the reverse: it converts the submitted value
+back into the format you want (e.g. convert the ``id`` back to the ``Issue`` object).
- If you want a new issue to be created when an unknown number is entered, you
- can instantiate it rather than throwing the ``TransformationFailedException``.
+To cause a validation error, throw a :class:`Symfony\\Component\\Form\\Exception\\TransformationFailedException`.
+But the message you pass to this exception won't be shown to the user. You'll set
+that message with the ``invalid_message`` option (see below).
.. note::
@@ -107,142 +237,60 @@ for converting to and from the issue number and the ``Issue`` object::
an empty string, 0 for integers or 0.0 for floats).
Using the Transformer
----------------------
-
-As seen above our transformer requires an instance of an object manager. While for most
-use-cases it is sufficient to use the default entity manager, you will sometimes need
-to explicitly choose the one to use. To achieve this, you can use a factory::
-
- // src/AppBundle/Form/DataTransformer/IssueToNumberTransformerFactory.php
- namespace AppBundle\Form\DataTransformer;
-
- use Doctrine\Common\Persistence\ManagerRegistry;
-
- class IssueToNumberTransformerFactory
- {
- /**
- * @var ManagerRegistry
- */
- private $registry;
-
- public function __construct(ManagerRegistry $registry)
- {
- $this->registry = $registry;
- }
-
- public function create($om)
- {
- return new IssueToNumberTransformer($this->registry->getManager($om));
- }
- }
-
-.. configuration-block::
-
- .. code-block:: yaml
-
- services:
- app.issue_transformer_factory:
- class: AppBundle\Form\DataTransformer\IssueToNumberTransformerFactory
- arguments: ["@doctrine"]
- public: false
-
- app.type.task:
- class: AppBundle\Form\TaskType
- arguments: ["@app.issue_transformer_factory"]
- tags:
- - { name: form.type, alias: app_task }
-
- .. code-block:: xml
-
-