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');
}
}
@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