数据转换器 vs.Constraints

Data Transformer vs.Constraints

我偶然发现了一个关于 Symfony's DataTransformers and how to properly use them. While I know how to implement and add them to my form field, I was wondering how DataTransformers are supposed to be combined with Constraints 的问题。

The following code shows my use case.

形式

<?php

namespace AppBundle\Form;

use AppBundle\Form\DataTransformer\Consent\ConsentTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\IsTrue;

class ConsentTestForm extends AbstractType
{
    /** @var ConsentTransformer $consentTransformer */
    private $consentTransformer;

    /**
     * ConsentTestForm constructor.
     * @param ConsentTransformer $consentTransformer
     */
    public function __construct(ConsentTransformer $consentTransformer)
    {
        $this->consentTransformer = $consentTransformer;
    }

    /**
     * @inheritDoc
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('accountConsent', CheckboxType::class, [
            'constraints' => [
                new IsTrue()
            ]
        ]);
        $builder->get('accountConsent')->addModelTransformer($this->consentTransformer);

        $builder->add('submit', SubmitType::class);
    }
}

模特

<?php

class User extends Concrete implements \Pimcore\Model\DataObject\DirtyIndicatorInterface
{
    protected $accountConsent;

    /**
     * ...
     */
    public function getAccountConsent () {
        // ...
    }

    /**
     * ...
     */
    public function setAccountConsent ($accountConsent) {
        // ...
    }
}

A lot of code was omitted for the sake of brevity. The model is a Pimcore class.

DataTransformer

<?php

namespace Passioneight\Bundle\FormBuilderBundle\Form\DataTransformer\Consent;

use Pimcore\Model\DataObject\Data\Consent;
use Symfony\Component\Form\DataTransformerInterface;

class ConsentTransformer implements DataTransformerInterface
{
    /**
     * @inheritDoc
     * @param Consent|null $consent
     */
    public function transform($consent)
    {
        return $consent instanceof Consent && $consent->getConsent();
    }

    /**
     * @inheritDoc
     * @param bool|null $consented
     */
    public function reverseTransform($consented)
    {
        $consent = new Consent();
        $consent->setConsent($consented ?: false);
        return $consent;
    }
}

As you can see any submitted value (i.e., null, true, false) will be converted to a Consent and vice-versa.

控制器

<?php

namespace AppBundle\Controller;

use AppBundle\Form\ConsentTestForm;
use AppBundle\Model\DataObject\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Class TestController
 * @package AppBundle\Controller
 *
 * @Route("/test")
 */
class TestController extends AbstractFrontendController
{
    /**
     * @Route("/form")
     * @param Request $request
     * @return Response
     */
    public function formAction(Request $request)
    {
        $user = new User();

        $form = $this->createForm(ConsentTestForm::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            if ($form->isValid()) {
                p_r("VALID");
                p_r($user);
            } else {
                p_r("NOT VALID");
            }
        };

        return $this->renderTemplate(':Test:form.html.twig', [
            "form" => $form->createView()
        ]);
    }
}

Note how a new User() is passed as entity in order to automatically populate it with the submitted values.

景观

{{ form(form) }}

问题

可以很好地构建表单,最终显示带有我指定标签的复选框。由于转换器,checked 状态甚至可以正确显示,因为 transform 方法将 Users Consent 转换为 boolean.

但是提交表单时,提示需要account-consent。虽然在未经同意的情况下提交表单这很好,但在真正同意时这并不是理想的结果。

同意后,提交的值将转换为 Consent,然后将保留值 true。但是由于 转换是在验证提交的值之前完成的 ,所以会显示上述错误。发生这种情况是因为在表单中添加的 accountConsent 字段有一个 Constraint 集,即 IsTrue。因此,IsTrueValidator 验证了 Consent(而不是实际提交的值)。

Obviously, the IsTrueValidator cannot know about Pimcore's Consent class.

问题

所有这些给我留下了一个问题:我如何正确地将 IsTrue-约束与我的 ConsentDataTransformer 结合起来?

验证问题是您试图将对象验证为布尔类型。当您在提交表单时尝试验证和转换时,约束总是被执行。所以你已经转换了数据,这就是为什么 IsBool 验证失败的原因,因为该值是 Consent 对象类型;不是布尔值。

要解决这个问题,您必须创建新的验证约束来覆盖 IsTrue。

<?php

namespace App\Form\Validator;

use Symfony\Component\Validator\Constraints\IsTrue;

class IsConsented extends IsTrue
{
    public $message = 'You need to consent!';
}

和同一命名空间上的验证器;

<?php

namespace App\Form\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\IsTrueValidator;

class IsConsentedValidator extends IsTrueValidator
{
    public function validate($value, Constraint $constraint)
    {
        return parent::validate($value->getConsent(), $constraint);
    }
}

然后你需要用 IsConsented 改变你的 IsTrue 约束,如下所示;

<?php

namespace App\Form;

use App\Entity\User;
use App\Form\DataTransformer\ConsentTransformer;
use App\Form\Validator\IsConsented;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ConsentTestFormType extends AbstractType
{
    /** @var ConsentTransformer $consentTransformer */
    private $consentTransformer;

    /**
     * ConsentTestForm constructor.
     * @param ConsentTransformer $consentTransformer
     */
    public function __construct(ConsentTransformer $consentTransformer)
    {
        $this->consentTransformer = $consentTransformer;
    }

    /**
     * @inheritDoc
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('accountConsent', CheckboxType::class, [
            'constraints' => [
                new IsConsented()
            ]
        ]);
        $builder->get('accountConsent')->addModelTransformer($this->consentTransformer);

        $builder->add('submit', SubmitType::class);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}

就是这样。您的表格现在有效。输出应如下所示;

FormController.php on line 30:
"VALID"