19

I was wondering if there is a way to compare old and new values in a validator within an entity prior to a flush.

I have a Server entity which renders to a form fine. The entity has a relationship to status (N->1) which, when the status is changed from Unracked to Racked, needs to check for SSH and FTP access to the server. If access is not achieved, the validator should fail.

I have mapped a validator callback to the method isServerValid() within the Server entity as described here http://symfony.com/doc/current/reference/constraints/Callback.html. I can obviously access the 'new' values via $this->status, but how can I get the original value?

In pseudo code, something like this:

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

Thanks in advance!

Andy
  • 314
  • 1
  • 2
  • 7

4 Answers4

38

A complete example for Symfony 2.5 (http://symfony.com/doc/current/cookbook/validation/custom_constraint.html)

In this example, the new value for the field "integerField" of the entity "NoDecreasingInteger" must be higher of the stored value.

Creating the constraint:

// 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';
  }
}

Creating the constraint validator:

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

Binding the validator to entity:

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

Injecting the EntityManager to 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 }
targzeta
  • 436
  • 4
  • 2
  • 1
    This **getOriginalEntityData()** method is really handy! – Veelkoov Oct 14 '14 at 10:48
  • Really nice explanation. Works perfectly with SF3.1 – A.D. Sep 08 '16 at 14:48
  • 2
    Nice idea, but it doesn´t work for collections because the collection in an `$object`s property is the same as the collection inside the `$old_data` (same reference). So if elements get deleted in the form you don´t get the old values. – goulashsoup Nov 08 '17 at 11:19
6

Accessing the EntityManager inside a custom validator in symfony2

you could check for the previous value inside your controller action ... but that would not really be a clean solution!

normal form-validation will only access the data bound to the form ... no "previous" data accessible by default.

The callback constraint you're trying to use does not have access to the container or any other service ... therefore you cant easily access the entity-manager (or whatever previous-data provider) to check for the previous value.

What you need is a custom validator on class level. class-level is needed because you need to access the whole object not only a single value if you want to fetch the entity.

The validator itself might look like this:

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

... afterwards register the validator as a service and tag it as validator

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 }

finally use the constraint for your status entity ( i.e. using annotations )

use Vendor\YourBundle\Validation\Constraints as MyValidation;

/**
 * @MyValidation\StatusValidator
 */
class Status 
{
Nicolai Fröhlich
  • 51,330
  • 11
  • 126
  • 130
  • 5
    Quick note: Be aware that Doctrine might give you the exact same object for `$previousState` due to internal object caching. Had my issues with that. :[ – althaus May 07 '14 at 09:54
  • In this case, you need to clone the previous object in a new one (with the PHP's "clone()" function) before working/binding/etc. the new one. – Bastien Libersa Jul 31 '14 at 13:05
  • 1
    I'm having problems with Doctrine... it's giving me the same object/values even though I'm using `$query->useResultCache(false);` in the Validator. Cloning the object before manipulating it in the form/controller ends up duplicating the database entries... so what's the right way to do all this? – caponica Aug 22 '14 at 20:30
  • @nifr You should inject instead of the container the needed service. :) – CSchulz Sep 17 '14 at 23:17
  • @CSchulz I'm totally aware of the performance and testability improvements by injecting direct dependencies instead of the container. This answer is 1.5 years old and just serves as a quick example - feel free to edit/improve, that's what stackoverflow is about. – Nicolai Fröhlich Sep 18 '14 at 11:30
1

For the record, here is the way to do it with Symfony5.

First, you need to inject your EntityManagerInterface service in the constructor of your validator. Then, use it to retrieve the original entity.

/** @var EntityManagerInterface */
private $entityManager;

/**
 * MyValidator constructor.
 * @param EntityManagerInterface $entityManager
 */
public function __construct(EntityManagerInterface $entityManager)
{
    $this->entityManager = $entityManager;
}

/**
 * @param string $value
 * @param Constraint $constraint
 */
public function validate($value, Constraint $constraint)
{    
    $originalEntity = $this->entityManager
        ->getUnitOfWork()
        ->getOriginalEntityData($this->context->getObject());

    // ...
}
Tim
  • 1,238
  • 1
  • 14
  • 24
0

Previous answers are perfectly valid, and may fit your use case.

For "simple" use case, it may fill heavy though. In the case of an entity editable through (only) a form, you can simply add the constraint on the 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()])
                ]
            ])
        ;
    }
}

This is valid for any Symfony 2+ version.

romaricdrigon
  • 1,497
  • 12
  • 16