Symfony2 Form Validator - сравнение старых и новых значений перед запуском

Мне было интересно, есть ли способ сравнить старые и новые значения в валидаторе внутри объекта до флеша.

У меня есть объект Server, который отлично отображает форму. Сущность имеет отношение к status (N- > 1), которое, когда статус изменен с Unracked на Racked, требует проверки доступа SSH и FTP к серверу. Если доступ не достигнут, валидатор должен выйти из строя.

Я сопоставил обратный вызов валидатора с методом isServerValid() внутри объекта Server, как описано здесь http://symfony.com/doc/current/reference/constraints/Callback.html. Я могу, очевидно, получить доступ к "новым" значениям через $this->status, но как получить исходное значение?

В псевдокоде что-то вроде этого:

public function isAuthorValid(ExecutionContextInterface $context)
{
    $original = ... ; // get old values
    if( $this->status !== $original->status && $this->status === 'Racked' && $original->status === 'Unracked' )
    {
        // check ftp and ssh connection
        // $context->addViolationAt('status', 'Unable to connect etc etc');
    }
}

Спасибо заранее!

Ответ 1

Полный пример для Symfony 2.5 (http://symfony.com/doc/current/cookbook/validation/custom_constraint.html)

В этом примере новое значение для поля "integerField" объекта "NoDecreasingInteger" должно быть выше сохраненного значения.

Создание ограничения:

// src/Acme/AcmeBundle/Validator/Constraints/IncrementOnly.php;
<?php
namespace Acme\AcmeBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class IncrementOnly extends Constraint
{
  public $message = 'The new value %new% is least than the old %old%';

  public function getTargets()
  {
    return self::CLASS_CONSTRAINT;
  }

  public function validatedBy()
  {
    return 'increment_only';
  }
}

Создание механизма проверки ограничений:

// src/Acme/AcmeBundle/Validator/Constraints/IncrementOnlyValidator.php
<?php
namespace Acme\AcmeBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

use Doctrine\ORM\EntityManager;

class IncrementOnlyValidator extends ConstraintValidator
{
  protected $em;

  public function __construct(EntityManager $em)
  {
    $this->em = $em;
  }

  public function validate($object, Constraint $constraint)
  {
    $new_value = $object->getIntegerField();

    $old_data = $this->em
      ->getUnitOfWork()
      ->getOriginalEntityData($object);

    // $old_data is empty if we create a new NoDecreasingInteger object.
    if (is_array($old_data) and !empty($old_data))
      {
        $old_value = $old_data['integerField'];

        if ($new_value < $old_value)
          {
            $this->context->buildViolation($constraint->message)
              ->setParameter("%new%", $new_value)
              ->setParameter('%old%', $old_value)
              ->addViolation();
          }
      }
  }
}

Связывание валидатора с сущностью:

// src/Acme/AcmeBundle/Resources/config/validator.yml
Acme\AcmeBundle\Entity\NoDecreasingInteger:
  constraints:
     - Acme\AcmeBundle\Validator\Constraints\IncrementOnly: ~

Внедрение EntityManager в IncrementOnlyValidator:

// src/Acme/AcmeBundle/Resources/config/services.yml
services:
   validator.increment_only:
        class: Acme\AcmeBundle\Validator\Constraints\IncrementOnlyValidator
        arguments: ["@doctrine.orm.entity_manager"]
        tags:
            - { name: validator.constraint_validator, alias: increment_only }

Ответ 2

Доступ к EntityManager внутри настраиваемого валидатора в symfony2

вы можете проверить предыдущее значение внутри действия вашего контроллера... но это не было бы действительно чистым решением!

нормальная форма-валидация будет доступ только к данным, привязанным к форме... нет "предыдущих" данных, доступных по умолчанию.

Ограничение обратного вызова, которое вы пытаетесь использовать, не имеет доступа к контейнеру или какой-либо другой услуге... поэтому вы не можете легко получить доступ к диспетчеру сущностей (или любому другому провайдеру предыдущих данных) для проверки предыдущего значения.

Что вам нужно, это настраиваемый валидатор на уровень класса. класс-уровень необходим, потому что вам нужно получить доступ ко всему объекту не только к одному значению, если вы хотите получить объект.

Сам валидатор может выглядеть так:

namespace Vendor\YourBundle\Validation\Constraints;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class StatusValidator extends ConstraintValidator
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function validate($status, Constraint $constraint)
    {

        $em = $this->container->get('doctrine')->getEntityManager('default');

        $previousStatus = $em->getRepository('YourBundle:Status')->findOneBy(array('id' => $status->getId()));

        // ... do something with the previous status here

        if ( $previousStatus->getValue() != $status->getValue() ) {
            $this->context->addViolationAt('whatever', $constraint->message, array(), null);
        }
    }

    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }

    public function validatedBy()
    {
       return 'previous_value';
    }
}

... после этого зарегистрируйте валидатор как службу и пометьте его как валидатор

services:
    validator.previous_value:
        class: Vendor\YourBundle\Validation\Constraints\StatusValidator

        # example! better inject only the services you need ... 
        # i.e. ... @doctrine.orm.entity_manager

        arguments: [ @service_container ]         
        tags:
            - { name: validator.constraint_validator, alias: previous_value }

наконец, используйте ограничение для вашего объекта статуса (т.е. используя аннотации)

use Vendor\YourBundle\Validation\Constraints as MyValidation;

/**
 * @MyValidation\StatusValidator
 */
class Status 
{

Ответ 3

Предыдущие ответы совершенно верны и могут соответствовать вашему варианту использования.

Для "простого" варианта использования он может заполнить все, хотя. В случае объекта, редактируемого через (только) форму, вы можете просто добавить ограничение на FormBuilder:

<?php

namespace AppBundle\Form\Type;

// ...

use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;

/**
 * Class MyFormType
 */
class MyFormType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('fooField', IntegerType::class, [
                'constraints' => [
                    new GreaterThanOrEqual(['value' => $builder->getData()->getFooField()])
                ]
            ])
        ;
    }
}

Это допустимо для любой версии Symfony 2+.