Joyce Babu
Joyce Babu

Reputation: 20674

Symfony constraint to prevent duplicates in overlapping time frame

How can I enforce uniqueness of a value within overlapping date range in Symfony using Doctrine ORM.

I have the following entity

<?php
/**
 * @ORM\Entity
 * @ORM\Table("tax_component")
 */

class TaxComponent
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(name="tax_component_id", type="integer")
     */
    private ?int $id;

    /**
     * @ORM\Column(name="tax_component_name", type="string", length=20)
     */
    private string $name;

    /**
     * @ORM\Column(name="tax_component_rate", type="integer")
     * @Assert\GreaterThanOrEqual(0)
     */
    private int $rate;

    /**
     * @ORM\Column(name="tax_component_applicable_from", type="datetime_immutable")
     */
    private DateTimeInterface $applicableFrom;

    /**
     * @ORM\Column(name="tax_component_applicable_to", type="datetime_immutable")
     */

    public function __construct(string $name, int $rate, ?DateTimeImmutable $applicableFrom = null, ?DateTimeImmutable $applicableTo = null)
    {
        ...
    }

}

I want to make $name unique withing overlapping time frames of $applicableFrom and $applicableTo. For example,

$repository->save(
    new TaxComponent('inter-state', 1800, new DateTime('2018-04-01:00:00:00'), new DateTime('2019-03-31T23:59:59'))
);

// The following should be allowed since there is no overlap between the two time ranges using the name 'inter-state'
$repository->save(
    new TaxComponent('inter-state', 1200, new DateTime('2019-04-01:00:00:00'), new DateTime('2020-03-31T23:59:59'))
);

// The following should fail since 'inter-state' is ambiguous during the period 2019-09-01:00:00:00 to 2020-03-31T23:59:59
$repository->save(
    new TaxComponent('inter-state', 1800, new DateTime('2019-09-01:00:00:00'), new DateTime('2020-09-31T23:59:59'))
);

Is there a constraint to enforce this is Symfony?

I am currency planning to check for existing entities from within TaxComponentRepository::save, before calling $this->entityManager->persist. Is there a better solution?

Upvotes: 4

Views: 557

Answers (1)

Dylan KAS
Dylan KAS

Reputation: 5703

The cleaner way would be to create your own custom assert.

Starting by creating your constraint :

<?php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class TaxComponentConstraint extends Constraint
{
    public $message = 'Another tax component overlap this one: {{ taxComponent}}';

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

    public function validatedBy()
    {
        return 'App\Validator\Constraints\TaxComponentValidator';
    }
}

And now you have to create a validator that will check if there exist an overlap with two tax component.

<?php

namespace App\Validator\Constraints;

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

class TaxComponentValidator extends ConstraintValidator
{
    public function validate($taxComponentObject, Constraint $constraint)
    {
    //Check however you want if the tax component can be created (So no overlap between two existing TaxComponent)
    if($overlappingTaxComponent){
        $this->context->buildViolation($constraint->message)
                        ->setParameter('{{ taxComponent }}', $overlappingTaxComponent->__toString())
                        ->addViolation();
    }

    }
}

Here, $overlappingTaxComponent is a TaxComponent preventing us from making one because of your constraint.

If the constraint is properly done, you can now use it easily in your entity so that it check automatically when submitting the form :

<?php

//...
use App\Validator\Constraints as CustomAssert;

/**
 * @ORM\Entity
 * @ORM\Table("tax_component")
 * @CustomAssert\TaxComponentConstraint
 */
class TaxComponent
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(name="tax_component_id", type="integer")
     */
    private ?int $id;

    /**
     * @ORM\Column(name="tax_component_name", type="string", length=20)
     */
    private string $name;

Upvotes: 5

Related Questions