Custom validator
API Platform agent skills
npx -y skills add api-platform/skillset --skill custom-validatorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 24 stars24 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Creates custom validation constraints for API Platform resources. Use whenever a rule goes beyond built-in Symfony constraints — rate or plan limits, uniqueness checks, domain-specific formats, cross-field or conditional validation, or any 'reject the request when X' business rule on write operations, even if the user doesn't say 'validator'.
SKILL.md
4.8 KB, as published. Nobody here has run it
Custom Validation Constraints
Create custom validators when built-in Symfony constraints are insufficient.
Constraint Class
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class IsValidAccountLimit extends Constraint
{
public string $message = 'Account limit reached for your current plan.';
public int $limit = 50;
}
Validator Class
Convention: {ConstraintName}Validator in the same namespace.
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class IsValidAccountLimitValidator extends ConstraintValidator
{
public function __construct(
private readonly UserHelper $userHelper,
private readonly AccountRepository $accountRepository,
) {}
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof IsValidAccountLimit) {
throw new \InvalidArgumentException('Unexpected constraint type');
}
if (null === $value || '' === $value) {
return;
}
$user = $this->userHelper->getUser();
$count = $this->accountRepository->countByUser($user->getId());
if ($count >= $constraint->limit) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Usage on Properties
use App\Validator\IsValidAccountLimit;
use Symfony\Component\Validator\Constraints as Assert;
class Account
{
#[Assert\NotBlank(groups: ['account:write'])]
#[Assert\Email(groups: ['account:write'])]
#[IsValidAccountLimit(groups: ['account:write'])]
public ?string $address;
}
Class-Level Constraint (UniqueEntity)
For constraints that validate across multiple fields:
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
class MongoDBUnique extends Constraint
{
public array $fields = [];
public string $message = 'This value is already used.';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}
Usage:
#[MongoDBUnique(fields: ['address'], groups: ['account:write', 'account:patch'])]
class Account {}
Validation Groups with Operations
Target a constraint to specific operations by passing groups: (as shown above)
and wiring those groups into each operation's validationContext — see
operations for the full per-operation setup.
Callback Validation
For quick one-off validation on DTOs:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class SendMessage
{
public ?string $text = null;
public ?string $html = null;
#[Assert\Callback]
public function validateContent(ExecutionContextInterface $context): void
{
if (null === $this->text && null === $this->html) {
$context->buildViolation('Either text or html content is required.')
->atPath('text')
->addViolation();
}
}
}
Laravel
Laravel does not use Symfony Constraints/ConstraintValidator. Validation is
declared with Laravel validation rules via the
rules option on #[ApiResource] or per-operation (rules can be an array, a closure,
or a FormRequest class-string). The ValidateProvider runs them before the
processor and emits the same 422 ConstraintViolationList shape.
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
#[ApiResource(rules: ['title' => 'required|min:2'])]
#[Post(rules: ['isbn' => ['required', 'string']])] // per-operation override
class Book extends Model {}
For business rules beyond built-in rules, write a Laravel custom rule (a class
implementing Illuminate\Contracts\Validation\ValidationRule, scaffold with
php artisan make:rule) or a closure rule, and reference it in rules. A
FormRequest class-string is also accepted — its authorize()/rules() run, and an
AuthorizationException maps to 403, ValidationException to 422. Validation groups
and Symfony validationContext do not apply on Laravel; scope rules per operation
instead. Partial PATCH can relax required to sometimes via
partial_patch_validation in config/api-platform.php.