1

I'm going to describe the situation first and ask questions in the end.

I need to validate a scalar value using the Symfony Validator component. The value is an associative array. Now I have defined the following validation constraints according to the expected structure:

new Constraints\Collection([
            'Call' => new Constraints\NotBlank(),
            'Start Time' => new Constraints\DateTime(['format' => 'd M Y H:i:s']),
            'Caller ID' => new Nullable(),
            'Calling Party Number' => new Nullable(new Phone()),
            'Dialed Party Number' => new Nullable(new Phone()),
            'Call Duration' => new Constraints\Regex(['pattern' => '/^\d{1,2}:\d{1,2}:\d{1,2}$/']),
            'Receiving Agent' => new Nullable(),
            'Is Abandoned' => new Constraints\Regex(['pattern' => '/^(True|False)$/']),
            'Total Queue Duration' => new Constraints\Regex(['pattern' => '/^\d{1,2}:\d{1,2}:\d{1,2}$/']),
            'Account Code' => new Nullable(),
            'Initial Tag' => new Constraints\NotBlank(),
        ]);

where Nullable and Phone are custom constraints.

These constraints must be reusable, and to achieve this I've created the following validation service:

use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class Validator
{
    private $validator;

    public function __construct(ValidatorInterface $validator)
    {
        $this->validator = $validator;
    }

    public function validate($value, ConstraintsProvider $constraintsProvider): ConstraintViolationListInterface
    {
        $violations = $this->validator->validate($value, $constraintsProvider->getConstraints());
        return $violations;
    }
}

And defined the ConstraintsProvider interface:

interface ConstraintsProvider
{
    public function getConstraints(): array;
}

which is going to be implemented by newables, encapsulating validation constraints like this:

class Call implements ConstraintsProvider
{
    public function getConstraints(): array
    {
        return [
            new Constraints\Collection([
                'Call' => new Constraints\NotBlank(),
                'Start Time' => new Constraints\DateTime(['format' => 'd M Y H:i:s']),
                'Caller ID' => new Nullable(),
                'Calling Party Number' => new Nullable(new Phone()),
                'Dialed Party Number' => new Nullable(new Phone()),
                'Call Duration' => new Constraints\Regex(['pattern' => '/^\d{1,2}:\d{1,2}:\d{1,2}$/']),
                'Receiving Agent' => new Nullable(),
                'Is Abandoned' => new Constraints\Regex(['pattern' => '/^(True|False)$/']),
                'Total Queue Duration' => new Constraints\Regex(['pattern' => '/^\d{1,2}:\d{1,2}:\d{1,2}$/']),
                'Account Code' => new Nullable(),
                'Initial Tag' => new Constraints\NotBlank(),
            ])
        ];
    }
}

And the validation is carried out as

$validator=new Validator();
$validator->validate(['foo'=>'bar'],new Call());

What I was wondering is:

  1. Is it a common practice to add an extra abstraction level in order to encapsulate some data which need to be reusable throughout the app? I my case it's the validation constraints.
  2. If it's common, are there any patterns addressing this issue?
  3. Is there any better way to resolve the problem I faced?

Update

The Call class is not an entity, it's just a wrapper around the constraints from where the validation service takes them. The validation service is used to validate the data before filling an entity with them. The corresponding entity has its own validation defined in its config.

  • What do you mean by "common?" Are you going to make this decision based on popularity, or are you going to make it based on how well the technique fits your specific requirements? – Robert Harvey Jun 1 '18 at 16:04
  • @RobertHarvey meaning how people usually solve this problem. The only requirment here is the reusability of once defined constraints throughout the app. Keeping constraints in classes implementing ConstraintsProvider interface, how I made it, looks ok to me. The answer below suggests keeping the constraints in static properties. I'm just in doubts that I'm doing it right – super.t Jun 2 '18 at 11:30
0

If the constraints are used by the validator, then they should be a static field on the Validator class itself. Furthermore, the validator class is too generic to be of any use. It should specialize in validating a Call. You can use it in a generic sense by defining an interface for validators:

interface Validator
{
    function validate($value) : ConstraintViolationListInterface
}

And then your CallValidator can implement the Validator interface and own the constraints:

class CallValidator implements Validator
{
    private $validator;

    private static $constraints = new Constraints\Collection([
        'Call' => new Constraints\NotBlank(),
        'Start Time' => new Constraints\DateTime(['format' => 'd M Y H:i:s']),
        'Caller ID' => new Nullable(),
        'Calling Party Number' => new Nullable(new Phone()),
        'Dialed Party Number' => new Nullable(new Phone()),
        'Call Duration' => new Constraints\Regex(['pattern' => '/^\d{1,2}:\d{1,2}:\d{1,2}$/']),
        'Receiving Agent' => new Nullable(),
        'Is Abandoned' => new Constraints\Regex(['pattern' => '/^(True|False)$/']),
        'Total Queue Duration' => new Constraints\Regex(['pattern' => '/^\d{1,2}:\d{1,2}:\d{1,2}$/']),
        'Account Code' => new Nullable(),
        'Initial Tag' => new Constraints\NotBlank(),
    ]);

    public function __construct(ValidatorInterface $validator)
    {
        $this->validator = $validator;
    }

    public function validate($value): ConstraintViolationListInterface
    {
        $violations = $this->validator->validate($value, self::$constraints);
        return $violations;
    }
}

Now the validator owns the constraints that it checks, they are static (so only 1 copy is held in memory), and nobody else can access those constraints without going through the validator.

|improve this answer|||||
  • There was a misunderstanding about the Call class, this is not an entity, it's just a wrapper around the constraints. I just needed to keep them somewhere so that they would be reusable. My goal is to validate the data (say from an HTTP request or from a CSV doc) before filling an entity with these data. The entity would have its own validation defined in its config files. Alternatively I could make an abstract class with ValidatorInterface injected and create multiple validation classes inheriting it like CallValidator, UserValidaor, SomethingElseValidator... – super.t Jun 1 '18 at 14:04
  • ... , but keeping constraints apart from the service sounds better to me for some reason – super.t Jun 1 '18 at 14:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.