【问题标题】:How to use Symfony2 Validation Constraint Callback with validation groups to validate a single property如何使用 Symfony2 验证约束回调和验证组来验证单个属性
【发布时间】:2015-03-20 05:23:57
【问题描述】:

我的实体:

<?php

namespace Acme\AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * PriceQuoteRequest
 *
 * @ORM\Table(name="pricequote")
 * @ORM\Entity
 *
 * @Assert\Callback(methods={"isGutterValid"}, groups={"flow_dormerRequest_step2"})
 */
class PriceQuoteRequest
{
    /**
     * @var integer $dormerInnerHeight
     *
     * @ORM\Column(name="h", type="float")
     * @Assert\NotBlank(groups={"flow_dormerRequest_step2"})
     * @Assert\Range(min="80", max="1000", groups={"flow_dormerRequest_step2"})
     */
    public $dormerInnerHeight;

    /**
     * @var boolean $dormerGutter
     *
     * @ORM\Column(name="gutter", type="boolean")
     *
     * @Assert\Type(type="boolean", groups={"flow_dormerRequest_step2"})
     * Assert\Callback(methods={"isGutterValid"}, groups={"flow_dormerRequest_step2"})
     */
    public $dormerGutter;

    /**
     * @ORM\OneToOne(targetEntity="Acme\AppBundle\Entity\GutterMaterial")
     *
     * Assert\Type(type="Acme\AppBundle\Entity\GutterMaterial", groups={"flow_dormerRequest_step2"})
     */
    public $dormerGutterMaterial;

    /**
     *
     * @param ExecutionContextInterface $context
     *
     * Assert\Callback(groups={"flow_dormerRequest_step2"})
     */
    public function isGutterValid(ExecutionContextInterface $context)
    {
        if ($this->getDormerGutter() === true && $this->getDormerGutterMaterial() === null) {
            $context->buildViolation('Gutter checked but no Material selected.')
                ->atPath('dormerGutter')
                ->addViolation();
        }
    }
}

我的测试:

<?php

namespace Acme\AppBundle\Tests\Entity;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Validator\Validation;

class RequestFunctionalTest extends KernelTestCase
{
    /**
     * @var \Doctrine\ORM\EntityManager
     */
    private $em;

    /**
     * {@inheritDoc}
     */
    public function setUp()
    {
        self::bootKernel();
        $this->em = static::$kernel->getContainer()
            ->get('doctrine')
            ->getManager()
        ;
    }

    public function testDormerGutterValidator()
    {
        $priceQuoteRequest = new PriceQuoteRequest();

        $priceQuoteRequest->setDormerGutter(true);

        // Validating on single property will not call "isGutterValid" Callback, exptected only 1 error otherwise
        // $violationList = $this->getValidator()->validateProperty($priceQuoteRequest, 'dormerGutter', array('flow_dormerRequest_step2'));

        // Validating full object will call "isGutterValid" but return all errors resp. > 1
        $violationList = $this->getValidator()->validate($priceQuoteRequest, array('flow_dormerRequest_step2'));

        dump($violationList);

        $this->assertEquals(1, count($violationList));
    }
}

如果 DormerGutter 设置为 true(分别签入表单),则必须选择 DormerGutterMaterial。

isGutterValid 方法应该检查这一点,否则将违规指向 DormerGutter 字段:

if ($this->getDormerGutter() === true && $this->getDormerGutterMaterial() === null) {
    $context->buildViolation('Gutter checked but no Material selected.')
        ->atPath('dormerGutter')
        ->addViolation();
} 

测试将 DormerGutter 设置为 true,但未选择 DormerGutterMaterial。 验证属性 DormerGutter 我希望得到 isGutterValid 回调返回的 1 个错误。

相反,它将返回 0 个错误,因为在单个属性上调用 validateProperty 不会调用整个对象的“全局”回调。类。

相反,我可以验证完整的对象。这确实会调用 isGutterValid 方法。 它将返回至少 2 个错误,因为这也验证了所有其他属性,例如dormerInnerHeight 不幸的是 - 在我的用例中 - 我只希望返回 1 个错误。

有没有办法将回调指向单个属性,仅使我能够调用 validateProperty 方法并仍然触发回调?

替代/解决方法当然是使用表达式约束:

/**
 * @var boolean $dormerGutter
 *
 * @ORM\Column(name="gutter", type="boolean")
 *
 * @Assert\Type(type="boolean", groups={"flow_dormerRequest_step2"})
 * @Assert\Expression(
 *  "this.getDormerGutterMaterial() !== null or this.getDormerGutter() == 0",
 *  message="Gutter checked but no Material selected.",
 *  groups={"flow_dormerRequest_step2"}
 * )
 */
public $dormerGutter; 

Ps.:所有验证组都成功链接到flow_dormerRequest_step2。

GitHub Gist 上的完整代码here

【问题讨论】:

    标签: validation symfony callback


    【解决方案1】:

    我建议您使用实体验证器。

    例如,在一个项目中,我有一个 Vote 实体,它链接了 2 个用户。 (一个在voter中,一个在votable->getTargetUser中)。

    这是投票:

    /**
     * Vote est l'entitĂŠ de jointure entre un juge, un jugĂŠ
     * et la votable choisie par le juge.
     *
     * @ORM\Table(name="vote")
     *     @ORM\Entity(repositoryClass="Face\Bundle\VoteBundle\Entity\Repository\VoteRepository")
     * @Serializer\ExclusionPolicy("all")
     * @Assert\GroupSequence({"properties", "Vote"})
     * @AssertVote\UniqueCharacteristic
     * @AssertVote\VoterNotTargetUser
     */
    class Vote
    {
    
    /**
     * Vote's id.
     *
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @Serializer\Expose
     */
    private $id;
    
    /**
     * User who gave the vote.
     *
     * @var User
     * @ORM\ManyToOne(targetEntity="Face\Bundle\UserBundle\Entity\User", cascade={"persist"})
     * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
     * @Assert\NotNull(message = "vote.voter_user.not_null", groups={"properties"})
     * @Serializer\Expose
     */
    private $voterUser;
    
    /**
     * The votable (ie: profile) which the voterUser voted on.
     *
     * @var Votable
     * @ORM\ManyToOne(
     *  targetEntity="Face\Bundle\VoteBundle\Entity\AbstractVotable",
     *  inversedBy="votes",
     *  cascade={"persist"}
     * )
     * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
     * @Assert\NotNull(message = "vote.votable.not_null", groups={"properties"})
     */
    private $votable;
    

    我必须确保我的选民与目标用户不同。

    然后我创建了一个新的约束:

    /**
     * Block votes of one User on its profile
     *
     * @package Face\Bundle\UserBundle\Validator\Validator
     */
    class VoterNotTargetUserValidator extends ConstraintValidator
    {
    
        /**
         * The entity manager.
         *
         * @var EntityManager
         */
        private $em;
    
        /**
         * Default constructor.
         *
         * @param EntityManager $em
         */
        public function __construct(EntityManager $em)
        {
            $this->em = $em;
        }
    
        /**
         * Block votes of one User on its profile
         *
         * @param Vote       $value      The value that should be validated
         * @param Constraint $constraint The constraint for the validation
         *
         */
            public function validate($value, Constraint $constraint)
            {
            if ($value->getVoterUser() === $value->getVotable()->getTargetUser()) {
                $this->context->addViolation($constraint->message);
            }
        }
    }
    

    以及投票中使用的验证器:

    /**
     * Block votes of one User on its profile
     *
     * @package Face\Bundle\UserBundle\Validator\Validator
     */
    class VoterNotTargetUserValidator extends ConstraintValidator
    {
    
        /**
         * The entity manager.
         *
         * @var EntityManager
         */
        private $em;
    
        /**
         * Default constructor.
         *
         * @param EntityManager $em
         */
        public function __construct(EntityManager $em)
        {
            $this->em = $em;
        }
    
        /**
         * Block votes of one User on its profile
         *
         * @param Vote       $value      The value that should be validated
         * @param Constraint $constraint The constraint for the validation
         *
         * @api
         */
        public function validate($value, Constraint $constraint)
        {
            if ($value->getVoterUser() === $value->getVotable()->getTargetUser()) {
                $this->context->addViolation($constraint->message);
            }
        }
    }
    

    您可以查看文档以获取更多详细信息,例如如何将其添加到您要验证的字段。

    http://symfony.com/fr/doc/current/cookbook/validation/custom_constraint.html

    【讨论】:

    • 感谢@Grégory Elhaimer!我喜欢创建自定义约束来替代我提到的 Symfony 2.6 表达式约束的想法。不幸的是,我认为当涉及到我的 Test 类时,这最终会遇到同样的问题,因为注释附加到整个类而不是单个元素。您只能将错误的路径设置为元素。当我仅在该特定字段上运行验证器方法“validateProperty”时会调用您的自定义约束,还是必须在整个类上运行验证?
    • 我的错,我读得太快了:/。我猜你必须在全班上运行它。由于您需要另一个属性来验证另一个属性,因此您必须确保每个属性都有效。你可以用 dormerGutter 的有效数据模拟整个对象。然后您的验证将向您发送 1 个错误。
    • @bjo3rnf 的另一个聪明方法是给元素一个额外的验证组,然后只在测试的“验证”方法中使用该组来减少预期错误的数量。我在我的要点上为该解决方法添加了一个示例:gist.github.com/webdevilopers/7dbf2f072ab394e1311b
    猜你喜欢
    • 1970-01-01
    • 2014-04-05
    • 1970-01-01
    • 2014-02-03
    • 2015-02-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多