Skip to content

Instantly share code, notes, and snippets.

@n3b
Created July 6, 2011 21:22
Show Gist options
  • Save n3b/1068357 to your computer and use it in GitHub Desktop.
Save n3b/1068357 to your computer and use it in GitHub Desktop.
symfony2 dynamic validator constraints
<?php
namespace n3b\Bundle\Shop\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormEvents;
class CheckoutFullType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('deliver', 'checkbox')
->add('user_save', 'checkbox')
->add('checkout', new CheckoutType());
$builder->addEventSubscriber(new EventSubscriber\PreBindDataSubscriber());
$builder->addValidator(new Validator\CheckoutDeliveryValidator());
}
public function getDefaultOptions(array $options)
{
return array(
'validation_groups' => array('pass_through'),
'data_class' => 'n3b\Bundle\Shop\Entity\Customer',
);
}
}
<?php
namespace n3b\Bundle\Shop\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
use n3b\Bundle\Shop\Model\Customer as BaseCustomer;
/**
* @ORM\Entity
*/
class Customer extends BaseCustomer
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(unique="true", nullable="true")
* @Assert\NotBlank(groups={"registration"})
* @Assert\Blank(groups={"pass_through"})
*/
protected $login;
}
<?php
namespace Symfony\Component\Form;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\Event\FilterDataEvent;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class Form implements \IteratorAggregate, FormInterface
{
...
//этого метода нет в дефолтной комплектации
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
}
}
<?php
namespace n3b\Bundle\Shop\Form\EventSubscriber;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PreBindDataSubscriber implements EventSubscriberInterface
{
public function onPreBindData(DataEvent $event)
{
$data = $event->getData();
if(isset($data['user_save']))
$event->getForm()->get('checkout')->get('customer')->setAttribute('validation_groups', array('registration'));
}
static public function getSubscribedEvents()
{
return array(FormEvents::PRE_BIND => 'onPreBindData');
}
}
@yethee
Copy link

yethee commented Jul 7, 2011

Как вариант, в PreBindDataSubscriber можно инжектировать FormFactory и при обработке события заново создать форму с типом CheckoutType:

if (isset($data['user_save'])) {
    $event->getForm()->add($this->formFactory->createNamed(new CheckoutType(), 'checkout', null, array(
        'validation_groups' => 'registration'
    ));
}

В CheckoutFullType:

$builder->addEventSubscriber(new EventSubscriber\PreBindDataSubscriber($builder->getFormFactory()));

@n3b
Copy link
Author

n3b commented Jul 7, 2011

Вариант интересный, но, во-первых, меня смущает повторная сборка формы. А во-вторых - в контролере модель связывается с CheckoutType до байнда формы:

$form = $this->services['ff']->create(new CheckoutFullType());
$form->get('checkout')->setData($checkout);
...
$form->bindRequest($this->services['request']);

@yethee
Copy link

yethee commented Jul 7, 2011

Да, повторная инициализация формы сопровождается некоторыми издержками, но на производительности, как мне кажется, не должно существенно сказаться.
Тоже возникала необходимость изменить атрибуты у уже созданного экземпляра формы, но склоняюсь к тому, что предполагаемый метод в Form вряд ли добавят. Нарушение инкапсуляции - как аргумент, может быть.

На счет второго, можно данные для формы передать в метод фабрики, третий аргумент, вместо
null -> $event->getForm()->get('checkout')->getData().

@n3b
Copy link
Author

n3b commented Jul 7, 2011

Насчет второго - да, сам уже догадался, но спасибо за идею )
В любом случае, я уверен, формы еще рефакторить будут. Там еще и с валидаторами довольно серьезное упущение обнаружил, тут отписался http://groups.google.com/group/symfony-devs/browse_thread/thread/6755929b4d70a38f?hl=en

@n3b
Copy link
Author

n3b commented Jul 7, 2011

Поковырялся - $event->getForm()->get('checkout')->getData() возвращает null. Оно и верно, форма еще байнд не начинала. Ну а раз не начинала - достаточно просто создать новый объект класса:

public function onPreBindData(DataEvent $event)
{
    $data = $event->getData();
    if(isset($data['user_save'])) {
        $event->getForm()->get('checkout')->remove('customer');
        $newCustomerForm = $this->ff->createNamed(new NewCustomerType(), 'customer', new Customer(), array(
            'validation_groups' => array('registration')
            ));
        $event->getForm()->get('checkout')->add($newCustomerForm);
    }
}

@yethee
Copy link

yethee commented Jul 8, 2011

Странно... Если до события FormEvents::PRE_BIND для формы вызывался метод setData(), то по идее не должно возвращать null. А если попробовать получить данные для корневой формы $event->getForm()->getData(), тоже будет null или нет?

Да, компонент форм еще сыроват, но уже на многое способен и имеет достаточно гибкую архитектуру :) Мне кажется, значительного рефакторинга до 2.1 не будет, сейчас комманда сосредоточена на стабилизации ядра.

По поводу, валидаторов и атрибута required. Между валидаторами и атрибутом нет связи. Не знаю, правильно ли понял Вас, но если речь про зависимость состояния атрибута от наличия валидаторов для данных элемента формы, то такого не должно быть. Валидаторы могут быть сопоставлены с моделью (объектом), т.е. данными формы (как в Вашем случае в аннотациях модели), так и относиться к элементу формы (можно добавить через опцию validation_constraint). В первом случае, форма ничего не знает про валидаторы, не считая названия группы валидаторов.

@n3b
Copy link
Author

n3b commented Jul 8, 2011

Странно, сейчас проверил - $event->getForm()->get('checkout')->getData() действительно возвращает пустой объект модели (да, именно этот объект привязывается до байнда), однако родительская форма $event->getForm()->getData() отдает все же null. Аналогично с дочерней формой $event->getForm()->get('checkout')->get('customer') тоже возвращает null. Видимо, связывание билдером модели с формой происходит позже.

Насчет required связь есть. В FormFactory:

public function createBuilderForProperty($class, $property, $data = null, array $options = array())
{
    if (!$this->guesser) {
        $this->loadGuesser();
    }

    $typeGuess = $this->guesser->guessType($class, $property);
    $maxLengthGuess = $this->guesser->guessMaxLength($class, $property);
    $minLengthGuess = $this->guesser->guessMinLength($class, $property);
    $requiredGuess = $this->guesser->guessRequired($class, $property);
    ...

Эти guesser'ы и задают атрибуты формам, которые в дальнейшем выводятся при генерации вида формы. А сами guesser'ы проверяют в аннотациях только соответствие классам. Таким образом, @Assert\NotBlank(groups={"registration"}) будет проводить валидацию только для групп registration, а вот атрибут формы будет, вне зависимости от группы, required. Что с учетом html5 вообще не даст отправить форму.
З.Ы. посмотрите класс Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser

@yethee
Copy link

yethee commented Jul 8, 2011

То, что родительская форма возвращает null ($event->getForm()->getData()) вроде как правильно, судя по этому коду:

$form = $this->services['ff']->create(new CheckoutFullType());
$form->get('checkout')->setData($checkout);

Т.к. setData() вызывается непосредственно для элемента checkout.
А в объекте $checkout для формы $event->getForm()->get('checkout')->get('customer') есть данные или null?

Спасибо за информацию о guesser'ах и FormFactory, не знал про эту фичу. В таком случае Вы правы, это баг - что не учитывается группа валидации при определении валидаторов.

@n3b
Copy link
Author

n3b commented Jul 8, 2011

Я не стал там ковыряться, решил инициализировать модель после байнда:

    $form = $this->services['ff']->create(new CheckoutFullType());

    if ($this->services['request']->getMethod() == 'POST') {
        $form->bindRequest($this->services['request']);
        $checkout = $form->get('checkout')->getData();

при таком раскладе в onPreBindData все поголовно в нулях.
По guesser'ам ждем фиксов. Пока накидал цепочку инжектов опций, начиная от билдера и заканчивая непосредственно guesser'ами.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment