4

问题

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

情况:

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

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

代替:

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

我们可以有:

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

在 validate() 情况下, preValidate() 将作为 Event 自动发送(当然还要检查 Entity 是否有这样的方法)。

案例分析:

  1. 我有一个将页面/子页面存储为实体的系统。可以有 10 或 10000 个页面/子页面
  2. 页面/子页面可以有文件。
  3. 实体只存储文件名(因为我们不能存储 SplFileInfo - 资源序列化限制)
  4. 虽然 Entity->file 属性是字符串类型,但当我想让它成为 File 的实例时(所以我们可以对 File 类型进行验证),我有类似的东西:

/**
 * @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 注意到)都会被刷新。

它会被冲洗掉。

因此,我们有数百个或数千个毫无意义的 sql 更新。

顺便提一句。

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

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

这是我尝试按照 Symfony 食谱中所述进行文件上传时发生的问题http://symfony.com/doc/current/controller/upload_file.html

我的解决方案是,在 postLoad() 中,在不是 Doctrine 列的属性中创建 File 实例,并注明有断言等。

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

任何人有更好的解决方案?或者知道如果 ->validate() 发生了如何调度事件?

4

1 回答 1

2

你好沃尔特,

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


Symfony 2.3+,Symfony < 3

在这种情况下,由于 symfony 和大多数其他包都使用参数来定义服务类,我在这种情况下所做的就是扩展该服务。查看下面的示例,有关扩展服务的更多信息,请查看此链接

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

最后一步,您需要将类值参数添加到 config.yml 中的“参数”配置块中,如下所示:

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

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

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


对于 Symfony 3.0 -> 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:Your\Name\Space\MyValidation

这是我能找到的侵入性最小的方法。不是那么干净,当您将 symfony 升级到未来版本时,它可能需要一些维护。

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

亚历山德鲁·科索伊

于 2016-09-22T05:54:18.547 回答