27 сентября 2012 в 17:38

Использование Data Transformers в Symfony2 из песочницы

Формы – в Symfony2 один из самых мощных инструментов, они представляют множество возможностей. Много секретов работы с Symfony2 описано в Книге рецетов. Хочу представить вам перевод одного рецепта работы с формами, в Symfony 2 – использование дата трансформеров.
Часто возникает необходимость преобразовывать данные, введенные пользователем в форму в другой формат для использования в вашей программе. Можно легко сделать это вручную в контроллере, но как поступить, если вы хотите использовать эту форму в разных местах? Скажем, у вас есть объект «Task» (задачи) связанный соотношением кодин-к-одному с объектом «Issue» (проблемы), для каждой «Task» моможет быть указана опционально «Issue», которую она решает. Если в форму редактирования задач «Task», добавить выпадающий список из проблем «Issue», то нам будет очень тяжело в нем ориентироваться. Можна добавить текстовое поле вместо, выпадающего списка и вводить просто номер «Issue».
Вы можете попробовать сделать преобразование в контроллере, но это не самое лучшая идея. Было бы намного лучше, если бы номер «Issue» автоматически преобразовался объект «Issue». В этом случае в игру вступают «Data Transformers» (трансформеры данных).

Создание трансформеров.


Сначала создадим IssueToNumberTransformer класс – это класс будет отвечать за преобразование и из номера «Issue» в объект «Issue»:
// 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;
    }

    /**
     * Transforms an object (issue) to a string (number).
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($issue)
    {
        if (null === $issue) {
            return "";
        }

        return $issue->getNumber();
    }

    /**
     * Transforms a string (number) to an object (issue).
     *
     * @param  string $number
     * @return Issue|null
     * @throws TransformationFailedException if object (issue) is not found.
     */
    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;
    }
}

Можно создать новый объект «Issue», когда пользователь ввел неизвестный номер а не выкидывать TransformationFailedException.

Использование трансформеров


Теперь у нас есть трансформер, нужно просто добавить его к нашему полю «Issue» в той или иной форме.
use Symfony\Component\Form\FormBuilderInterface;
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // ...

        // this assumes that the entity manager was passed in as an option
        $entityManager = $options['em'];
        $transformer = new IssueToNumberTransformer($entityManager);

        // add a normal text field, but add our transformer to it
        $builder->add(
            $builder->create('issue', 'text')
                ->addModelTransformer($transformer)
        );
    }

    // ...
}


В этом примере необходимо передать EntityManager в качестве опции при создании формы. Позже вы узнаете, как можно создать пользовательское поле для номера «Issue», чтобы избежать необходимости передачи EntityManager
$taskForm = $this->createForm(new TaskType(), $task, array(
    'em' => $this->getDoctrine()->getEntityManager(),
));


Круто, мы это сделали! Теперь пользователь сможет ввести номер в текстовом поле, и оно будет преобразовано объект «Issue». Это означает, что после успешного связывания ($form->bindRequest($request)), фреймворк форм предаст реальный объект «Issue» в метод :: setIssue () вместо номера «Issue».
Обратите внимание, что при добавление трансформера необходимо использовать несколько более сложный синтаксис чем при добавлении поля. Ниже приведен не правильный пример, как трансформер будет применяться ко всей форме, а не к конкретному полю:
// Это не правильно - трансформер будет применен ко всей форме
// смотрите пример выше для правильного использования трансформера
$builder->add('issue', 'text')
    ->addModelTransformer($transformer);


Модель вид и трансформеры


Новое в версии 2.1: название методов трансформетров были изменены в Symfony 2.1. prependNormTransformer стал addModelTransformer и appendClientTransformer стал addViewTransformer.
В приведенном выше примере, трансформер был использован в качестве «трансформера модели». В самом деле, есть два различных типа трансформеров и три различных типа исходных данных.
В любой форме, есть три различных типа данных:

  1. Model data — это данные в том формате которое используются в вашем приложениии (например, например обьект типа «Issue»). Если вы вызовете в форме методы :: GetData или :: SetData, вы будете имеете дело с «Model data».
  2. Norm Data — это нормализированные версии ваших данных, они обычно такие же, как «Model data» данные (хотя и не в нашем примере). На прямую они не часто используются.
  3. View Data — это формат, который используется для заполнения полей формы. Это также формат, в котором пользователь будет передавать данные (сабмитить форму). Когда мы вызываем метод Form::bind($data), $data представляется в формате «View Data»

Есть два различных типа трансформеров, которые помогают нам преобразовывать данные из одного представления в другой.
  • «Model transformer» трансформеры модели
    • transform: «model data» => «norm data»
    • reverseTransform: «norm data» => «model data»

  • «View transformer» трансформеры видов:
    • transform: «norm data» => «view data»
    • reverseTransform: «view data» => «norm data»


Какой трансформер вам нужен, зависит от конкретной ситуации.
Чтобы использовать «View Transformer», вызывайте метод addViewTransformer.

Так зачем нам использовать трансформеры данных?


В нашем примере, поле — текстовое поле, и мы ожидаем что текстовое поле будет всегда возвращать скалярные данные в «norm» и «view» форматах. И в данном случае наиболее приемлемый трансформер — «model transformer», который преобразует «norm data» в «model data» и обратно (номер «Issue» в объект «Isuuse» и обратно).
Разница между трансформерами очень тонкая и вы всегда должны думать что из себя должны представлять «norm» нормализированные данные. Например для текстового поля «norm» нормализированные данные — текстовая сторока, а для поля «date» — объект DateTime.

Использование трансформеров в «кастомных полях»


В примере который мы описали выше, мы используем трансформер для текстового поля. Сделать это было довольно просто, но у этого подхода есть два недостатка:
  1. Вы должны всегда помнить, о применении трансформера когда используете поле «isuue»
  2. Вы должны заботится о передаче в опции em=>EntityManager всякий раз, когда вы создаете форму, которая использует трансформер.

Потому нам наверное нужно создать пользовательский «кастомный» тип поля. Для начала создадим класс для пользователького типа поля:
// 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';
    }
}


Далее, зарегистрируем свой тип сервиса и пометим его тегом form.type так, чтобы поле распознавалось как пользовательский тип:
<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>


Теперь можно использовать наш специальный тип issue_selector:
// 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';
    }
}
+11
2249
51
vkalmuk 3,0

Комментарии (4)

–1
Ringtail, #
С каждой статьей с примерами того, как в Symfony 2 сделать вообще хоть что-нибудь, я очень радуюсь, что больше с ней не работаю.
+4
Davert, #
Первый постулат Symfony2 way: если что-то можно сделать просто, значит ты делашь это неправильно.
+2
zIs, #
Знаете, мне сначала тоже так казалось.
А потом я увидел за этим непередаваемую красоту и обаяние. В СФ2 программист обладает полной свободой.
Можно написать всё в одном контроллере, и оно, конечно, будет работать.
Но когда используешь Symfony way, приложение получается гибким и масштабируемым засчёт слабых связей.
Вы можете на лету менять поведение, заменяя одни объекты другими, можете легко изменять стандартное поведение классов фреймворка на своё, написав пару строчек в конфиге.
0
warsoul, #
Ничего нового. Абсолютно то же о sf1 (1.2 и выше) можно было сказать.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.