$npx -y skills add aligundogdu/symfony-hexagonal-skill --skill symfony-validationSymfony validation — 3-layer validation strategy, constraints, DTO validation, domain invariants, validation groups, custom validators. Triggers on: validation, validator, constraint, DTO validation, input validation, invariant, assert, validation group
| 1 | # Symfony Validation |
| 2 | |
| 3 | You are an expert in multi-layer validation within Symfony hexagonal architecture. |
| 4 | |
| 5 | ## When to Activate |
| 6 | |
| 7 | - User needs input validation |
| 8 | - User asks about validation strategies |
| 9 | - User needs custom constraints |
| 10 | - User mentions DTO validation or domain invariants |
| 11 | |
| 12 | ## 3-Layer Validation Strategy |
| 13 | |
| 14 | ### Layer 1: Presentation (Input Validation) |
| 15 | **Where**: Controllers, request listeners |
| 16 | **What**: Format validation — is the input well-formed? |
| 17 | **How**: Symfony Validator on request DTOs |
| 18 | |
| 19 | ```php |
| 20 | // Request DTO with constraints |
| 21 | final readonly class CreateUserRequest |
| 22 | { |
| 23 | public function __construct( |
| 24 | #[Assert\NotBlank] |
| 25 | #[Assert\Email] |
| 26 | public string $email, |
| 27 | |
| 28 | #[Assert\NotBlank] |
| 29 | #[Assert\Length(min: 2, max: 100)] |
| 30 | public string $name, |
| 31 | |
| 32 | #[Assert\NotBlank] |
| 33 | #[Assert\Length(min: 8)] |
| 34 | #[Assert\Regex(pattern: '/[A-Z]/', message: 'Must contain uppercase')] |
| 35 | #[Assert\Regex(pattern: '/[0-9]/', message: 'Must contain a number')] |
| 36 | public string $password, |
| 37 | ) { |
| 38 | } |
| 39 | } |
| 40 | ``` |
| 41 | |
| 42 | ### Layer 2: Application (DTO/Command Validation) |
| 43 | **Where**: Command/Query classes, validated by Messenger middleware |
| 44 | **What**: Business rule pre-checks — are the values acceptable? |
| 45 | **How**: Symfony Validator constraints on command properties |
| 46 | |
| 47 | ```php |
| 48 | final readonly class RegisterUser |
| 49 | { |
| 50 | public function __construct( |
| 51 | #[Assert\NotBlank] |
| 52 | #[Assert\Email] |
| 53 | public string $email, |
| 54 | |
| 55 | #[Assert\NotBlank] |
| 56 | public string $name, |
| 57 | |
| 58 | #[Assert\NotBlank] |
| 59 | public string $password, |
| 60 | ) { |
| 61 | } |
| 62 | } |
| 63 | ``` |
| 64 | |
| 65 | The `validation` middleware on the bus auto-validates before handler execution. |
| 66 | |
| 67 | ### Layer 3: Domain (Invariants) |
| 68 | **Where**: Entity constructors, value objects, business methods |
| 69 | **What**: Business invariants — is the operation valid in domain context? |
| 70 | **How**: Pure PHP validation, throw domain exceptions |
| 71 | |
| 72 | ```php |
| 73 | // Value object self-validates |
| 74 | final readonly class Email |
| 75 | { |
| 76 | public function __construct(public string $value) |
| 77 | { |
| 78 | if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { |
| 79 | throw InvalidEmailException::forValue($value); |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | // Entity protects invariants |
| 85 | final class Order |
| 86 | { |
| 87 | public function cancel(string $reason): void |
| 88 | { |
| 89 | if ($this->status === OrderStatus::DELIVERED) { |
| 90 | throw CannotCancelDeliveredException::forOrder($this->id); |
| 91 | } |
| 92 | // ... |
| 93 | } |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | ## References |
| 98 | |
| 99 | See `references/` for detailed guides: |
| 100 | - `validation-layers.md` — Full examples for each layer, custom constraints, groups |