Validate Polymorphic Collection Items with Symfony Constraints
Handling validation for polymorphic collections of objects can be quite challenging, especially in Symfony. In this article, we'll explore a custom constraint validator that can validate polymorphic collection items directly from the HTTP request.
Imagine a scenario where you have an HTTP request containing an array of objects with different types. You need to validate each object based on its type, and the validation rules may vary for each type. The validation process should be as simple and efficient as possible.
Introducing the PolymorphicCollectionItemValidator
Here's a custom Symfony constraint validator that can handle the polymorphic validation of collections. It validates each item in the collection based on a discriminator field and applies the corresponding validation rules.
PolymorphicCollectionItem Constraint
<?php
declare(strict_types=1);
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class PolymorphicCollectionItem extends Constraint
{
public string $discriminatorField = 'type';
/** @var array<string, Constraint> */
public array $mapping = [];
/** @param array{discriminatorField: string, mapping: array<string, Constraint>} $options */
public function __construct($options = null)
{
parent::__construct($options);
}
public function getRequiredOptions(): array
{
return [
'discriminatorField',
'mapping',
];
}
}
PolymorphicCollectionItemValidator
<?php
declare(strict_types=1);
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class PolymorphicCollectionItemValidator extends ConstraintValidator
{
/**
* {@inheritdoc}
*/
public function validate($object, Constraint $constraint): void
{
if (!$constraint instanceof PolymorphicCollectionItem) {
throw new UnexpectedTypeException($constraint, PolymorphicCollectionItem::class);
}
if (null === $object) {
return;
}
if (!\is_array($object) && !($object instanceof \Traversable && $object instanceof \ArrayAccess)) {
throw new UnexpectedValueException($object, 'array|(Traversable&ArrayAccess)');
}
$discriminatorField = $constraint->discriminatorField;
$type = $object[$discriminatorField] ?? null;
if ($type === null) {
return;
}
$constraintClassName = $constraint->mapping[$type] ?? null;
if ($constraintClassName === null) {
$this
->context
->buildViolation("The '$discriminatorField' is not valid.")
->atPath($discriminatorField)
->setParameter('{{ type }}', $type)
->addViolation()
;
return;
}
$this->context->getValidator()
->inContext($this->context)
->atPath('')
->validate($object, $constraintClassName)
->getViolations()
;
}
}
Usage
Here's an example of how to use this custom constraint validator in your Symfony application:
<?php
declare(strict_types=1);
namespace App\Validator;
use App\Validator\Constraints\AllowExtraFieldsCollection;
use App\Validator\Constraints\PolymorphicCollectionItem;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints;
class BulkObjectValidator
{
public function getConstraint(): Constraint
{
return new Constraints\Collection([
'fields' => [
'objects' => [
new Constraints\All([
'constraints' => [
new Constraints\Collection([
'fields' => [
'type' => [
new Constraints\NotNull(),
new Constraints\Choice(['choices' => ['TYPE_1', 'TYPE_2']]),
],
],
]),
new PolymorphicCollectionItem([
'discriminatorField' => 'type',
'mapping' => [
'TYPE_1' => new Constraints\Collection(['fields' => ['value' => new Constraints\Choice(['choices' => ['VALUE_1', 'VALUE_2']]]),
'TYPE_2' => new Constraints\Collection(['fields' => ['value' => new Constraints\Choice(['choices' => ['VALUE_3', 'VALUE_4']]]),
],
]),
],
]),
],
],
]);
}
}
For instance, you can use the BulkObjectValidator
in a Symfony controller to validate the data from an HTTP request.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Validator\BulkObjectValidator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route('/bulk-objects', methods: ['POST'])]
class BulkObjectsController
{
public function __construct(private BulkObjectValidator $validator, private ValidatorInterface $validatorInterface)
{
}
public function __invoke(Request $request): Response
{
$data = $request->toArray();
$constraint = $this->validator->getConstraint();
$violations = $this->validatorInterface->validate($data, $constraint);
if ($violations->count() > 0) {
// Handle validation errors
// ...
}
// Process the valid data
// ...
return new Response('Success', Response::HTTP_OK);
}
}
By using the PolymorphicCollectionItemValidator
, you can validate the polymorphic data in the objects
array directly from the HTTP request. The validator iterates through each item and applies the appropriate validation rules based on the type
field.
In conclusion, the PolymorphicCollectionItemValidator
offers a simple and powerful way to handle polymorphic validation in Symfony applications. It allows you to validate data directly from the HTTP request and apply different validation rules based on the object's type. Give it a try and let me know your thoughts on Twitter!