Johnny Dew
Johnny Dew

Reputation: 981

Symfony - Validation on multiple elements in Collection form

So, I have a form which has a collection and the user can add new items to this collection. My entity in this collection has an id and an integer. I want to validate that the user cannot add a item after another that has a lower integer than the first one.

For exemple, the form is empty, the user adds a record to the collection by clicking on the Add button. He enters 100 in the input field and adds another record and enters 50 in the input field. When he tries to submit I want the second record to have an error because 50 is lower than 100 but he added it after in the list.

Here are my FormTypes and Entites

class ItemType extends AbstractType
{    
    public function buildForm(FormBuilderInterface $builder, array $options)
    {        
        $builder->add('value');
    }
}

class ItemCollectionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {   
        $builder->add('items', 'collection', array(
            'type' => new ItemType(),
            'allow_add'    => true,
            'allow_delete' => true,
            'by_reference' => false,
            'label' => false,
        ));

        $builder->add('save');
    }
}

class Item
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="integer", nullable=true)
     */
    protected $value;
}

class List
{
    /**
     * @ORM\OneToMany(targetEntity="Item", cascade={"persist"})
     * @Assert\Valid
     **/
    private $items;
}

I think I could do the validation in the Controller but I would like to do it in my Entity and using a Validation or a custom Validation.

** EDIT **

I can also do what DonCallisto suggested but I am trying to get error on the specific row that is in problem not just on the global class like he suggested.

I can't find how to add the propertyPath to the item in problem, I have tried to use the following code but it seems like it's still binding the error on the class rather than on the item itself like I would like.

$context->addViolationAt(
    $context->getPropertyPath().'.items.data.value',
    'Your violation message'
);

It also doesn't take in consideration the current element.

The following code is not working either. Here I'm hardcoding the key of the array just for the purpose of testing.

$context->addViolationAt(
    $context->getPropertyPath().'.items[0].data.value',
    'Your violation message'
);

Can anyone tell me how to correctly set up a path to access a child's value in a collection?

Upvotes: 3

Views: 7466

Answers (2)

Johnny Dew
Johnny Dew

Reputation: 981

I found the answer to my question, I used part of DonCallisto's answer and I moved the code from the List Entity to Item and in a validator that I am calling as a CLASS_CONSTRAINT on my Item Entity.

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

/**
 * @Annotation
 */
class ItemsOrderedValidator extends ConstraintValidator  {

    public function validate($value, Constraint $constraint) {
        foreach ($value->getList()->getItems() as $item) {
            $currentValue = $item->getValue();

            if (isset($prevValue) && $prevValue >= $currentValue && $value === $item) {
                $this->context->buildViolation($constraint->message)
                    ->atPath('value')
                    ->addViolation();
            }
            $prevValue = $currentValue;
        }
    }
}

Upvotes: 2

DonCallisto
DonCallisto

Reputation: 29932

If I understood correctly your question, you could modify your List entity as follows

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ExecutionContext;

/**
 * //Whatever you have here to declare this as an entity
 * @Assert\Callback(methods={"areItemsOrdered"})
 */
class List
{
  /**
   * @ORM\OneToMany(targetEntity="Item", cascade={"persist"})
   * @Assert\Valid
   **/
   private $items;
    
   //whatever methods you have here

   public function areItemsOrdered(ExecutionContext $context)
   {
     foreach ($this->items as $item) {
       $current_value = $item->getValue();
       
       if (!isset($prev_value)) {
         $prev_value = $current_value;
        } else { 
          if ($prev_value > $current_value) {
            $context->addViolation('Your violation message');
          }
        }
     }
   }
}

For more information about entity callback methods: http://symfony.com/doc/2.3/reference/constraints/Callback.html

For more information about ExecutionContext: http://api.symfony.com/2.4/Symfony/Component/Validator/ExecutionContextInterface.html

BTW

Is a correct behaviour to inform user about error? I mean, could you not sort added items yourself? If so, please update your answer and I will be happy to update my code for this purpose.

Upvotes: 1

Related Questions