|
| 1 | +# Lemmon/Validator Technical Spec |
| 2 | + |
| 3 | +## Core Concepts |
| 4 | +- **Form-Safe Coercion:** `coerce()` converts types but treats empty strings `''` as `null` for `Int`, `Float`, and `Bool` to prevent dangerous defaults (0/false). |
| 5 | +- **Optional by Default:** All fields accept `null` unless `required()` is called. |
| 6 | +- **Null Handling:** |
| 7 | + - **Validations** (`satisfies`, `min`, `email`): Skip `null` values. |
| 8 | + - **Transformations**: |
| 9 | + - `pipe(callable)`: Skips `null` values (type-preserving, expects specific type). |
| 10 | + - `transform(callable)`: Executes on `null` (can handle null and change types). |
| 11 | +- **Pipeline Order:** Operations execute in the exact order defined (e.g., `pipe('trim')->nullifyEmpty()->required()`). |
| 12 | +- **Transformation Types:** |
| 13 | + - `pipe(callable)`: Intended for sanitization. Preserves type context and applies type-coercion to result. |
| 14 | + - `transform(callable)`: Intended for type conversion. Updates type context; no coercion. |
| 15 | + |
| 16 | +## Core Patterns |
| 17 | +- **Namespace:** `Lemmon\Validator` |
| 18 | +- **Entry Point:** `Validator` static factory (e.g., `Validator::isString()`). |
| 19 | +- **Fluency:** Validators are mutable during configuration but used sequentially. |
| 20 | +- **Execution:** |
| 21 | + - `validate(mixed $value, string $key = '', array $input = [])` throws `ValidationException`. |
| 22 | + - `tryValidate(mixed $value, string $key = '', mixed $input = null)` returns tuple `[bool $valid, mixed $data, ?array $errors]`. |
| 23 | +- **Error Structure:** `ValidationException` contains nested array of error messages. Use `getFlattenedErrors()` for API format `[{path: string, message: string}]`. |
| 24 | +- **Null Handling:** Validators skip `null` values unless `required()` is called. |
| 25 | +- **Pipeline:** Supports both validation (`satisfies`) and transformation (`transform`, `pipe`). |
| 26 | +- **Coercion:** `coerce()` enables smart type conversion (e.g., "true" -> `true`). Non-coercible values pass through unchanged. |
| 27 | +- **Form Safety:** Empty strings `''` are treated as `null` for primitives if coerced. |
| 28 | + |
| 29 | +## API Reference |
| 30 | + |
| 31 | +### Factory (`Lemmon\Validator\Validator`) |
| 32 | +```php |
| 33 | +static isString(): StringValidator |
| 34 | +static isInt(): IntValidator |
| 35 | +static isFloat(): FloatValidator |
| 36 | +static isBool(): BoolValidator |
| 37 | +static isArray(): ArrayValidator |
| 38 | +static isAssociative(array<string, FieldValidator> $schema = []): AssociativeValidator |
| 39 | +static isObject(array<string, FieldValidator> $schema = []): ObjectValidator |
| 40 | +static anyOf(array<FieldValidator> $validators, ?string $message = null): FieldValidator |
| 41 | +static allOf(array<FieldValidator> $validators, ?string $message = null): FieldValidator |
| 42 | +static not(FieldValidator $validator, ?string $message = null): FieldValidator |
| 43 | +``` |
| 44 | + |
| 45 | +### Base API (`FieldValidator`) |
| 46 | +*Available on all validators* |
| 47 | +```php |
| 48 | +required(?string $message = null): static |
| 49 | +default(mixed $value): static |
| 50 | +coerce(): static |
| 51 | +nullifyEmpty(): static (transforms '' or [] to null) |
| 52 | +clone(): static (deep copy with bound closures) |
| 53 | + |
| 54 | +// Custom Validation |
| 55 | +satisfies(callable|FieldValidator $rule, ?string $message = null): static |
| 56 | +satisfiesAny(array<callable|FieldValidator> $rules, ?string $message = null): static |
| 57 | +satisfiesAll(array<callable|FieldValidator> $rules, ?string $message = null): static |
| 58 | +satisfiesNone(array<callable|FieldValidator> $rules, ?string $message = null): static |
| 59 | + |
| 60 | +// Transformation |
| 61 | +transform(callable $fn): static (can change type) |
| 62 | +pipe(callable ...$fns): static (preserves type, skips null) |
| 63 | +``` |
| 64 | + |
| 65 | +### StringValidator |
| 66 | +```php |
| 67 | +email(?string $message = null): static |
| 68 | +url(?string $message = null): static |
| 69 | +uuid(UuidVariant $variant = UuidVariant::Any, ?string $message = null): static |
| 70 | +ip(IpVersion $version = IpVersion::Any, ?string $message = null): static |
| 71 | +hostname(?string $message = null): static |
| 72 | +domain(?string $message = null): static // Requires dot |
| 73 | +time(?string $message = null): static // HH:MM or HH:MM:SS |
| 74 | +base64(Base64Variant $variant = Base64Variant::Standard, ?string $message = null): static |
| 75 | +hex(?string $message = null): static |
| 76 | +regex(string $pattern, ?string $message = null): static // Alias for pattern() |
| 77 | +pattern(string $pattern, ?string $message = null): static |
| 78 | +datetime(string $format = 'Y-m-d\TH:i:s', ?string $message = null): static |
| 79 | +date(string $format = 'Y-m-d', ?string $message = null): static |
| 80 | +minLength(int $min, ?string $message = null): static |
| 81 | +maxLength(int $max, ?string $message = null): static |
| 82 | +length(int $exact, ?string $message = null): static |
| 83 | +oneOf(array $values, ?string $message = null): static |
| 84 | +``` |
| 85 | + |
| 86 | +### IntValidator / FloatValidator |
| 87 | +*Both support NumericConstraintsTrait* |
| 88 | +```php |
| 89 | +min(int|float $min, ?string $message = null): static |
| 90 | +max(int|float $max, ?string $message = null): static |
| 91 | +gt(int|float $threshold, ?string $message = null): static |
| 92 | +gte(int|float $threshold, ?string $message = null): static |
| 93 | +lt(int|float $threshold, ?string $message = null): static |
| 94 | +lte(int|float $threshold, ?string $message = null): static |
| 95 | +multipleOf(int|float $divisor, ?string $message = null): static |
| 96 | +positive(?string $message = null): static |
| 97 | +negative(?string $message = null): static |
| 98 | +nonPositive(?string $message = null): static |
| 99 | +nonNegative(?string $message = null): static |
| 100 | +clampToRange(int|float $min, int|float $max): static // Transformation, not validation |
| 101 | +oneOf(array $values, ?string $message = null): static |
| 102 | +``` |
| 103 | +*IntValidator Only:* |
| 104 | +```php |
| 105 | +port(?string $message = null): static // 1-65535 |
| 106 | +``` |
| 107 | + |
| 108 | +### ArrayValidator (`Validator::isArray()`) |
| 109 | +*Indexed arrays / Lists* |
| 110 | +```php |
| 111 | +items(FieldValidator $validator): static |
| 112 | +filterEmpty(): static (removes null/'', reindexes) |
| 113 | +minItems(int $min, ?string $message = null): static |
| 114 | +maxItems(int $max, ?string $message = null): static |
| 115 | +contains(mixed|FieldValidator $valueOrValidator, ?string $message = null): static |
| 116 | +``` |
| 117 | + |
| 118 | +### AssociativeValidator / ObjectValidator |
| 119 | +*Schema validation* |
| 120 | +```php |
| 121 | +// Constructor accepts array<string, FieldValidator> |
| 122 | +coerceAll(): static // Recursively enables coercion on schema fields |
| 123 | +``` |
| 124 | + |
| 125 | +### BoolValidator |
| 126 | +```php |
| 127 | +oneOf(array $values, ?string $message = null): static |
| 128 | +// Coercion handles: 'true','on','1' -> true; 'false','off','0' -> false |
| 129 | +``` |
| 130 | + |
| 131 | +## Enums |
| 132 | + |
| 133 | +### IpVersion |
| 134 | +`Any`, `IPv4`, `IPv6` |
| 135 | + |
| 136 | +### UuidVariant |
| 137 | +`Any`, `V1`, `V2`, `V3`, `V4`, `V5`, `V7` |
| 138 | + |
| 139 | +### Base64Variant |
| 140 | +`Standard`, `UrlSafe`, `Any` |
| 141 | + |
| 142 | +### PipelineType |
| 143 | +`VALIDATION`, `TRANSFORMATION` |
| 144 | + |
| 145 | +## Quick Examples |
| 146 | +```php |
| 147 | +// Pipeline order: pipe (same type) -> validate -> transform (type change) |
| 148 | +Validator::isString() |
| 149 | + ->pipe('trim') // String operations (preserves type) |
| 150 | + ->nullifyEmpty() |
| 151 | + ->required() |
| 152 | + ->date('Y-m-d') |
| 153 | + ->transform(fn($s) => DateTime::createFromFormat('Y-m-d', $s)); // String → DateTime |
| 154 | + |
| 155 | +// Form-safe coercion: '' -> null (not 0/false) |
| 156 | +Validator::isInt()->coerce()->validate(''); // null (if not required) |
| 157 | + |
| 158 | +// Schema with defaults |
| 159 | +Validator::isAssociative([ |
| 160 | + 'name' => Validator::isString()->required(), |
| 161 | + 'age' => Validator::isInt()->default(0) |
| 162 | +]); |
| 163 | + |
| 164 | +// Error handling |
| 165 | +try { |
| 166 | + $data = $validator->validate($input); |
| 167 | +} catch (ValidationException $e) { |
| 168 | + $errors = $e->getFlattenedErrors(); // [{path: 'field', message: '...'}] |
| 169 | +} |
| 170 | +``` |
0 commit comments