在实体验证开始之前调度的Validator事件

时间:2016-09-21 11:26:43

标签: doctrine symfony symfony-validator

问题

Symfony 2.8+ / 3.x +是否可以在开始实体验证之前调度事件?

情况:

假设我们有100个实体,他们有@LifeCycleCallbacks,他们有@postLoad事件做某事,但其结果仅用于实体的valiation,在99%的情况下@postLoad的结果对系统不重要。因此,如果我们有从数据库获取的数据库或数千个实体,那么对于不重要的数据,将会有很多机器周期丢失。

运行某种类型的事件会很好,它会运行方法,在验证开始之前填充该特定实体的数据。

而不是:

$entity->preValidate();
$validator = $this->get('validator');
$errors = $validator->validate($entity);

我们可以:

$validator = $this->get('validator');
$errors = $validator->validate($entity);

在validate()情况下,preValidate()将作为Event自动调度(当然,检查实体是否有这样的方法)。

案例研究:

  1. 我有一个将页面/子页面存储为实体的系统。可以有10或10000页/子页
  2. 页面/子页面可以包含文件。
  3. 实体仅存储文件名(因为我们无法存储SplFileInfo - 资源序列化限制)
  4. 虽然Entity->文件属性是字符串的类型,但是当我想让它成为File的实例时(所以我们可以对File类型进行验证)我有类似的东西:
  5. /**
     * @postLoad()
     */
    public function postLoad()
    {
        //magicly we get $rootPath
        $this->file = new File($rootPath . '/' . $this->file);
    }
    /**
     * @prePersist()
     * @preUpdate()
     */
    public function preSave()
    {
        if ($this->file instance of File)
           $this->file = $this->file->getFilename();
        }
    }
    

    好的,但是postLoad()会更改属性,而Doctrine会通知它。所以在下一个

    $entityManager->flush()
    

    所有实体都将被刷新 - 即使preSave()将其恢复为以前的字符串。

    所以,如果我有任何其他实体,让我们说TextEntity,我想删除它

    $entityManager->remove($textEntity);
    $entityManager->flush();
    

    所有其他以某种方式更改的实体(更改被Doctrine注意到)都会刷新,无论文件属性的值是否与DB中的相同(并且更改只是暂时的)。

    它会被冲洗。

    所以我们有无意义的sql更新或者无用的sql更新。

    顺便说一下。

    1。 - > flush($ textEntity)将抛出异常,因为 - >> remove($ textEntity)已经“删除”了该实体。

    2。实体属性 - >文件必须是File for Assert / File类型,因为FileValidator只能接受File或absolute-path-to-file的值。 但是我不会存储绝对路径到文件,因为它在Dev,Stage和Production环境中会完全不同。

    这是我尝试进行文件上传时遇到的问题,如Symfony cookbook http://symfony.com/doc/current/controller/upload_file.html中所述。

    我的解决方法是,在postLoad()中,在属性中创建不是Doctrine列的File实例,并且用于声明等等。

    这有效,但无用的postLoad()问题仍然存在,我想到了事件。这可能是有弹性的,也是非常优雅的解决方案 - 而不是控制器变得“胖”。

    任何人都有更好的解决方案吗?或者知道如果 - > validate()发生事件会如何发送事件?

1 个答案:

答案 0 :(得分:1)

Hello Voult,

编辑:第一种方法在symfony 3中被弃用,作为评论中提到的线程。检查为symfony 3制作的第二种方法。

Symfony 2.3 +,Symfony< 3

在这种情况下我做了什么,因为symfony和大多数其他bundle使用参数进行服务类定义,就是扩展该服务。请查看下面的示例,有关扩展服务的更多信息,请查看此链接

http://symfony.com/doc/current/bundles/override.html

首先,您需要为需要预验证的实体添加一些标记。我通常使用类似

这样的东西的接口
namespace Your\Name\Space;

interface PreValidateInterface
{
   public function preValidate();
}

在此之后,您将扩展验证器服务

<?php

namespace Your\Name\Space;

use Symfony\Component\Validator\Validator;

class MyValidator extends Validator //feel free to rename this to your own liking
{
    /**
     * @inheritdoc
     */
    public function validate($value, $groups = null, $traverse = false, $deep = false)
    {
        if (is_object($value) && $value instanceof PreValidateInterface) {
            $value->preValidate();
        }
        return parent::validate($value, $groups, $traverse, $deep);
    }
}

最后一步,您需要将类值参数添加到&#39;参数&#39; config.yml中的config block,如下所示:

parameters:
    validator.class: Your\Name\Space\MyValidator

这是基本的想法。现在你可以将这个想法与你想要达到的目标混合在一起。例如,不是在实体上调用方法(我通常喜欢将业务逻辑保留在我的实体之外),您可以查找接口,如果它在那里,您可以在其上启动pre.validate事件,并且使用听众来完成这项工作。之后,您可以保留parent :: validate的结果,并启动post.validate事件。你知道我要去哪里。你基本上可以在那个验证方法中做任何你喜欢的事情。

PS:上面的例子是简单的方法。如果你想进入事件路线,服务扩展将更难,因为你需要将调度程序注入其中。检查我在开头提供的链接,看看扩展服务的其他方式,如果您需要帮助,请告诉我。

对于Symfony 3.0 - &gt; 3.1

在这种情况下,他们设法使扩展

更难和更脏

第1步:

创建自己的验证器,如下所示:

<?php

namespace Your\Name\Space;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception;
use Symfony\Component\Validator\MetadataInterface;
use Symfony\Component\Validator\Validator\ContextualValidatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class myValidator implements ValidatorInterface
{
    /**
     * @var ValidatorInterface
     */
    protected $validator;

    /**
     * @param ValidatorInterface $validator
     */
    public function __construct(ValidatorInterface $validator)
    {
        $this->validator = $validator;
    }

    /**
     * Returns the metadata for the given value.
     *
     * @param mixed $value Some value
     *
     * @return MetadataInterface The metadata for the value
     *
     * @throws Exception\NoSuchMetadataException If no metadata exists for the given value
     */
    public function getMetadataFor($value)
    {
        return $this->validator->getMetadataFor($value);
    }

    /**
     * Returns whether the class is able to return metadata for the given value.
     *
     * @param mixed $value Some value
     *
     * @return bool Whether metadata can be returned for that value
     */
    public function hasMetadataFor($value)
    {
        return $this->validator->hasMetadataFor($value);
    }

    /**
     * Validates a value against a constraint or a list of constraints.
     *
     * If no constraint is passed, the constraint
     * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed.
     *
     * @param mixed $value The value to validate
     * @param Constraint|Constraint[] $constraints The constraint(s) to validate
     *                                             against
     * @param array|null $groups The validation groups to
     *                                             validate. If none is given,
     *                                             "Default" is assumed
     *
     * @return ConstraintViolationListInterface A list of constraint violations.
     *                                          If the list is empty, validation
     *                                          succeeded
     */
    public function validate($value, $constraints = null, $groups = null)
    {
        //the code you are doing all of this for
        if (is_object($value) && $value instanceof PreValidateInterface) {
            $value->preValidate();
        }
        //End of code

        return $this->validator->validate($value, $constraints, $groups);
    }

    /**
     * Validates a property of an object against the constraints specified
     * for this property.
     *
     * @param object $object The object
     * @param string $propertyName The name of the validated property
     * @param array|null $groups The validation groups to validate. If
     *                                 none is given, "Default" is assumed
     *
     * @return ConstraintViolationListInterface A list of constraint violations.
     *                                          If the list is empty, validation
     *                                          succeeded
     */
    public function validateProperty($object, $propertyName, $groups = null)
    {
        $this->validator->validateProperty($object, $propertyName, $groups);
    }

    /**
     * Validates a value against the constraints specified for an object's
     * property.
     *
     * @param object|string $objectOrClass The object or its class name
     * @param string $propertyName The name of the property
     * @param mixed $value The value to validate against the
     *                                     property's constraints
     * @param array|null $groups The validation groups to validate. If
     *                                     none is given, "Default" is assumed
     *
     * @return ConstraintViolationListInterface A list of constraint violations.
     *                                          If the list is empty, validation
     *                                          succeeded
     */
    public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
    {
        $this->validator->validatePropertyValue($objectOrClass, $propertyName, $value, $groups);
    }

    /**
     * Starts a new validation context and returns a validator for that context.
     *
     * The returned validator collects all violations generated within its
     * context. You can access these violations with the
     * {@link ContextualValidatorInterface::getViolations()} method.
     *
     * @return ContextualValidatorInterface The validator for the new context
     */
    public function startContext()
    {
        $this->validator->startContext();
    }

    /**
     * Returns a validator in the given execution context.
     *
     * The returned validator adds all generated violations to the given
     * context.
     *
     * @param ExecutionContextInterface $context The execution context
     *
     * @return ContextualValidatorInterface The validator for that context
     */
    public function inContext(ExecutionContextInterface $context)
    {
        $this->validator->inContext($context);
    }
}

第2步:

扩展Symfony \ Component \ Validator \ ValidatorBuilder,如下所示:

namespace Your\Name\Space;

use Symfony\Component\Validator\ValidatorBuilder;

class myValidatorBuilder extends ValidatorBuilder
{
    public function getValidator()
    {
        $validator =  parent::getValidator();

        return new  MyValidator($validator);
    }

}

您需要覆盖Symfony \ Component \ Validator \ Validation。这是丑陋/肮脏的部分,因为这个类是最终的,所以你不能扩展它,并且没有实现的接口,所以你必须注意未来的symfony版本,以防后向兼容性被破坏。它是这样的:

namespace Your\Name\Space;

final class MyValidation
{
    /**
     * The Validator API provided by Symfony 2.4 and older.
     *
     * @deprecated use API_VERSION_2_5_BC instead.
     */
    const API_VERSION_2_4 = 1;

    /**
     * The Validator API provided by Symfony 2.5 and newer.
     */
    const API_VERSION_2_5 = 2;

    /**
     * The Validator API provided by Symfony 2.5 and newer with a backwards
     * compatibility layer for 2.4 and older.
     */
    const API_VERSION_2_5_BC = 3;

    /**
     * Creates a new validator.
     *
     * If you want to configure the validator, use
     * {@link createValidatorBuilder()} instead.
     *
     * @return ValidatorInterface The new validator.
     */
    public static function createValidator()
    {
        return self::createValidatorBuilder()->getValidator();
    }

    /**
     * Creates a configurable builder for validator objects.
     *
     * @return ValidatorBuilderInterface The new builder.
     */
    public static function createValidatorBuilder()
    {
        return new MyValidatorBuilder();
    }

    /**
     * This class cannot be instantiated.
     */
    private function __construct()
    {
    }
}

最后一步覆盖config.yml中的参数validator.builder.factory.class:

参数:     validator.builder.factory.class:您的\ Name \ Space \ MyValidation

这是最难侵入的方式,我能找到。当你将symfony升级到未来版本时,它可能需要一些维护。

希望这会有所帮助,并且编码很快

Alexandru Cosoi