*/ private array $data; /** @var array code d'erreur par champ */ private array $errors = []; /** @param array $data */ public function __construct(array $data) { $this->data = $data; } public function has(string $key): bool { return array_key_exists($key, $this->data); } public function value(string $key, mixed $default = null): mixed { return $this->data[$key] ?? $default; } /** Définit manuellement une erreur sur un champ (ex: "not_found" après lookup BDD). */ public function setError(string $key, string $code): self { $this->errors[$key] = $code; return $this; } public function required(string $key): self { if (!$this->has($key) || $this->data[$key] === null || $this->data[$key] === '') { $this->errors[$key] ??= 'required'; } return $this; } public function string(string $key, ?int $max = null, ?int $min = null): self { if (!$this->has($key) || $this->data[$key] === null) { return $this; } $v = $this->data[$key]; if (!is_string($v)) { $this->errors[$key] ??= 'must_be_string'; return $this; } $len = mb_strlen($v); if ($min !== null && $len < $min) $this->errors[$key] ??= 'too_short'; if ($max !== null && $len > $max) $this->errors[$key] ??= 'too_long'; return $this; } public function integer(string $key, ?int $min = null, ?int $max = null): self { if (!$this->has($key) || $this->data[$key] === null) { return $this; } $v = $this->data[$key]; if (is_int($v)) { $i = $v; } elseif (is_string($v) && preg_match('/^-?\d+$/', $v)) { $i = (int) $v; } else { $this->errors[$key] ??= 'must_be_integer'; return $this; } if ($min !== null && $i < $min) $this->errors[$key] ??= 'too_small'; if ($max !== null && $i > $max) $this->errors[$key] ??= 'too_large'; return $this; } public function boolean(string $key): self { if (!$this->has($key) || $this->data[$key] === null) { return $this; } $v = $this->data[$key]; if (!is_bool($v) && !in_array($v, [0, 1, '0', '1', 'true', 'false'], true)) { $this->errors[$key] ??= 'must_be_boolean'; } return $this; } /** @param array $allowed */ public function in(string $key, array $allowed): self { if (!$this->has($key) || $this->data[$key] === null) { return $this; } if (!in_array($this->data[$key], $allowed, true)) { $this->errors[$key] ??= 'invalid_value'; } return $this; } public function url(string $key, int $max = 1024): self { if (!$this->has($key) || $this->data[$key] === null || $this->data[$key] === '') { return $this; } $v = $this->data[$key]; if (!is_string($v) || filter_var($v, FILTER_VALIDATE_URL) === false || mb_strlen($v) > $max) { $this->errors[$key] ??= 'invalid_url'; } return $this; } public function email(string $key): self { if (!$this->has($key) || $this->data[$key] === null || $this->data[$key] === '') { return $this; } if (!is_string($this->data[$key]) || filter_var($this->data[$key], FILTER_VALIDATE_EMAIL) === false) { $this->errors[$key] ??= 'invalid_email'; } return $this; } /** Format ISO date YYYY-MM-DD strict. */ public function date(string $key): self { if (!$this->has($key) || $this->data[$key] === null || $this->data[$key] === '') { return $this; } $v = $this->data[$key]; if (!is_string($v)) { $this->errors[$key] ??= 'invalid_date'; return $this; } $d = \DateTimeImmutable::createFromFormat('Y-m-d', $v); if ($d === false || $d->format('Y-m-d') !== $v) { $this->errors[$key] ??= 'invalid_date'; } return $this; } public function colorHex(string $key): self { if (!$this->has($key) || $this->data[$key] === null || $this->data[$key] === '') { return $this; } if (!is_string($this->data[$key]) || !preg_match('/^#[0-9A-Fa-f]{6}$/', $this->data[$key])) { $this->errors[$key] ??= 'invalid_color_hex'; } return $this; } public function fails(): bool { return $this->errors !== []; } /** @return array */ public function errors(): array { return $this->errors; } /** Termine la validation : lance ApiException 422 si au moins une erreur. */ public function check(): void { if ($this->errors !== []) { throw new \ApiException(422, [ 'error' => 'validation_failed', 'fields' => $this->errors, ]); } } }