Symfony2 — проверка нескольких полей, DTO, без аннотаций

Я знаю, что подобные вопросы задавались здесь, на SO, но ответы меня не удовлетворяют.

Имея следующее:

  1. Стандартная форма с полями foo, bar, baz, bat, x
  2. DTO (не объект, фиктивный объект, без аннотаций)
  3. Ограничения для некоторых полей, прикрепленных с помощью FormBuilder
  4. Форма используется во многих местах.

В некоторых местах используется эта форма, я хотел бы добавить один валидатор в поля foo, bar, baz. Этот валидатор может получить только значения этих полей или всего распространяемого DTO. Он должен иметь доступ к контейнеру DI для вызова службы, которая будет проверять данные по базе данных.

Пока я думаю об одном из двух решений:

  1. Добавление дополнительного ограничения к форме в контроллерах, которые его требуют (звучит грязно)
  2. Добавление дополнительного поля в конструктор формы/DTO (не логическое значение, а бизнес-логика, которая указывает, требуется ли дополнительная проверка) и добавление дополнительного ограничения в форму.

Проблема в том, что я не могу понять, как справиться с любым из этих случаев.

Наконец, я хочу подчеркнуть, что я не хочу использовать группы проверки и аннотации — и то, и другое добавит дополнительные зависимости и логику в DTO.


person ex3v    schedule 23.09.2015    source источник


Ответы (1)


Нашел ответ. Для краткого ответа вы можете проверить Это сообщение Мэтта Даума.

Вот полный пример, демонстрирующий не только то, как создать пользовательский валидатор формы, но и то, как внедрять службы и дополнительные данные в форму (потому что это было в моем случае).

Если вам нужен простой рецепт, идите прямо вниз.

Давайте иметь DTO:

class MyFormDTO
{
    /** @var  string */
    private $name;

    /** @var  string */
    private $surname;

    /** @var  string */
    private $phone;

    /** getters and setters ommited */

}

Теперь определите зависимости в форме. Первые два — это сервисы, последний (Calendar) — дополнительные данные, необходимые от контроллера.

class MyForm extends AbstractType
{
    (fields hidden)

    /**
     * @param Sender                 $sender
     * @param TranslatorInterface    $translator
     * @param Calendar               $calendar
     */
    public function __construct(Sender $sender, TranslatorInterface $translator, Calendar $calendar)
    {
        $this->translator = $translator;
        $this->sender     = $sender;
        $this->calendar   = $calendar;
    }
}

Теперь есть два пути: если вам нужны только сервисы в вашей форме, вы можете просто определить свою форму как сервис. Если вам, как и мне, нужны дополнительные данные, вам нужно написать form factory service:

class MyFormFactory
{

    (fields hidden)

    /**
     * @param Sender                 $sender
     * @param TranslatorInterface    $translator
     */
    public function __construct(Sender $sender, TranslatorInterface $translator)
    {
        $this->sender                 = $sender;
        $this->translator             = $translator;
    }

    /**
     * @param Calendar $calendar
     *
     * @return MyForm
     */
    public function getMyForm(Calendar $calendar)
    {
        return new MyForm($this->sender, $this->translator, $calendar);
    }

}

Давайте определим эту фабрику как службу с правильными зависимостями:

mybundle.form.myform_factory:
    class: MyBundle\Service\FormFactory\MyFormFactory
    arguments: [ @text_message.sender, @translator ]

Как получить форму в контроллере? Просто так:

class MyController extends Controller
{
    /**
     * @ParamConverter("calendar", options={"mapping"={"calendarId":"id"}})
     *
     * @param Request  $request
     * @param Calendar $calendar
     *
     * @return Response
     * @throws Exception
     */
    public function myAction(Request $request, Calendar $calendar)
    {
        $formDTO = new MyFormDto();

        $myForm = $this->get('mybundle.form.myform_factory')->getMyForm($calendar);
        $form = $this->createForm($myForm, $formDTO);

        (handling post hidden)
    }
}

А теперь самая важная часть — мы правильно внедрили сервисы в нашу форму. Как их использовать и проверять выбранные данные? так:

class MyForm extends AbstractType
{
    (fields hidden, constructor shown in previous example)

    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        /** @var MyFormDTO $myDTO */
        $myDTO = $options['data'];

        (build form as usual, using services and data from $options and $this->calendar injected by controller and factory)
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        parent::setDefaultOptions($resolver);

        $resolver->setDefaults([
            'csrf_protection' => true,
            'constraints'     => [
                new Callback(function (MyFormDTO $data, ExecutionContextInterface $context) //notice that we have access to fully propageted DTO here
                {
                    //use injected service 
                    $isValid = $this->sender->validateSomething($data->getSurname(), $data->getPhone());

                    if (false === $isValid)
                    {
                        $context
                            ->buildViolation($this->translator->trans('wrong_surname_phone_pair'))
                            ->addViolation();
                    }

                    return $isValid;
                })
            ],
        ]);
    }
}
person ex3v    schedule 23.09.2015
comment
И только небольшая подсказка. Если ваш Callback обрабатывает сложную логику, вы можете переместить его в отдельный Constraint. вот как это сделать - person ex3v; 23.09.2015