agentsclimarketplace

Custom validator

Skill api-platform/skillset/skills/custom-validator

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'.From its SKILL.md

Install
npx -y skills add api-platform/skillset --skill custom-validator

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing 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.

SKILL.md

4.8 KB, 967 tokens by cl100k_base, 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.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.