Lines
100.00%
27 / 27
Methods
100.00%
11 / 11
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| mobile | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| password | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| alpha | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| alphanumeric | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| hexColor | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| safePattern | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 3 | ||
| defaultMessage | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| validate | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | ||
| [Lucent\Validation\Constraint] withMessage | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| [Lucent\Validation\Constraint] message | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 3 | ||
| 26 | final class Matches extends Constraint | |
| 27 | { | |
| 28 | /** | |
| 29 | * Create a constraint matching an international phone number (E.164). | |
| 30 | * | |
| 31 | * @return self A new Matches instance. | |
| 32 | */ | |
| 33 | public static function mobile(): self | |
| 34 | { | |
| 35 | return (new self('/^\+?[1-9]\d{1,14}$/')) // E.164 | |
| 36 | ->withMessage('Phone number must be in a valid international format.'); | |
| 37 | } | |
| 38 | ||
| 39 | /** | |
| 40 | * Create a constraint matching a strong password. | |
| 41 | * | |
| 42 | * Requires at least one lowercase letter, one uppercase letter, and a | |
| 43 | * minimum length of 8 characters. | |
| 44 | * | |
| 45 | * @return self A new Matches instance. | |
| 46 | */ | |
| 47 | public static function password(): self | |
| 48 | { | |
| 49 | return (new self('/^(?=.*[a-z])(?=.*[A-Z]).{8,}$/')) | |
| 50 | ->withMessage('Password must contain at least one lowercase letter, one uppercase letter, and be at least 8 characters long.'); | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Create a constraint matching a string of letters only. | |
| 55 | * | |
| 56 | * @return self A new Matches instance. | |
| 57 | */ | |
| 58 | public static function alpha(): self | |
| 59 | { | |
| 60 | return (new self('/^[a-zA-Z]+$/')) | |
| 61 | ->withMessage('Must contain only letters.'); | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Create a constraint matching a string of letters and numbers only. | |
| 66 | * | |
| 67 | * @return self A new Matches instance. | |
| 68 | */ | |
| 69 | public static function alphanumeric(): self | |
| 70 | { | |
| 71 | return (new self('/^[a-zA-Z0-9]+$/')) | |
| 72 | ->withMessage('Must contain only letters and numbers.'); | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Create a constraint matching a HEX color code. | |
| 77 | * | |
| 78 | * Accepts 3- or 6-digit codes with an optional leading `#`. | |
| 79 | * | |
| 80 | * @return self A new Matches instance. | |
| 81 | */ | |
| 82 | public static function hexColor(): self | |
| 83 | { | |
| 84 | return (new self('/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/')) | |
| 85 | ->withMessage('Must be a valid HEX color code (e.g., #FFF or #FFFFFF).'); | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Create a regex-based match constraint. | |
| 90 | * | |
| 91 | * @param string $pattern The PCRE pattern to match against. | |
| 92 | * @param int $flags Optional `preg_match` flags. | |
| 93 | */ | |
| 94 | public function __construct(private readonly string $pattern, private readonly int $flags = 0) {} | |
| 95 | ||
| 96 | /** | |
| 97 | * Create a regex-based match constraint from a ReDoS-safe pattern. | |
| 98 | * | |
| 99 | * Rejects patterns containing nested quantifiers (e.g. `(a+)+`), which | |
| 100 | * are the classic source of catastrophic backtracking, and verifies the | |
| 101 | * pattern compiles. Use this when the pattern may come from an untrusted | |
| 102 | * source (config, user input) rather than a hard-coded developer constant. | |
| 103 | * | |
| 104 | * PHP has no built-in ReDoS detector, so this is a best-effort guard: it | |
| 105 | * catches the most common catastrophic-backtracking shape (a quantified | |
| 106 | * group containing a quantifier) and rejects patterns that fail to | |
| 107 | * compile (via {@see preg_last_error()}). It cannot prove a pattern is | |
| 108 | * safe, so patterns should still be reviewed. | |
| 109 | * | |
| 110 | * @param string $pattern The PCRE pattern to match against. | |
| 111 | * @param int $flags Optional `preg_match` flags. | |
| 112 | * @return self A new Matches instance. | |
| 113 | * @throws \InvalidArgumentException If the pattern contains a nested quantifier or does not compile. | |
| 114 | */ | |
| 115 | public static function safePattern(string $pattern, int $flags = 0): self | |
| 116 | { | |
| 117 | if (preg_match('/\([^()]*[+*{][^()]*\)[+*?]/', $pattern)) { | |
| 118 | throw new \InvalidArgumentException('Pattern contains a nested quantifier and is not ReDoS-safe.'); | |
| 119 | } | |
| 120 | ||
| 121 | // Verify the pattern compiles. The subject string is arbitrary — it only | |
| 122 | // exists to trigger compilation. A compile failure sets a PREG_*_ERROR | |
| 123 | // regardless of the subject, and a valid pattern against a single | |
| 124 | // character can never hit a backtrack/recursion limit, so 'x' is safe | |
| 125 | // and unambiguous. preg_last_error() distinguishes a compile error | |
| 126 | // from a legitimate no-match. | |
| 127 | @preg_match($pattern, 'x'); | |
| 128 | if (preg_last_error() !== PREG_NO_ERROR) { | |
| 129 | throw new \InvalidArgumentException('Pattern is not a valid PCRE pattern.'); | |
| 130 | } | |
| 131 | ||
| 132 | return new self($pattern, $flags); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * @return string|Closure(FieldContext): string The default error message. | |
| 137 | */ | |
| 138 | #[Override] | |
| 139 | protected function defaultMessage(): string|Closure|null | |
| 140 | { | |
| 141 | return fn(FieldContext $ctx) => "The {$ctx->field} field didn't match the expected pattern."; | |
| 142 | } | |
| 143 | ||
| 144 | /** | |
| 145 | * Validate that the value matches the configured regex pattern. | |
| 146 | * | |
| 147 | * @param FieldContext $ctx The context of the field being validated. | |
| 148 | * @return bool True if the value matches the pattern, false otherwise. | |
| 149 | */ | |
| 150 | #[Override] | |
| 151 | public function validate(FieldContext $ctx): bool | |
| 152 | { | |
| 153 | return is_string($ctx->value) && preg_match($this->pattern, $ctx->value, flags: $this->flags); | |
| 154 | } | |
| 155 | } |
Inherited from Lucent\Validation\Constraint
| 38 | final public function withMessage(string|\Closure $message): static | |
| 39 | { | |
| 40 | $this->customMessage = $message; | |
| 41 | return $this; | |
| 42 | } |
| 56 | final public function message(FieldContext $ctx): ?string | |
| 57 | { | |
| 58 | $message = $this->customMessage ?? $this->defaultMessage(); | |
| 59 | ||
| 60 | if ($message === null) { | |
| 61 | return null; | |
| 62 | } | |
| 63 | ||
| 64 | if ($message instanceof \Closure) { | |
| 65 | return call_user_func($message, $ctx); | |
| 66 | } | |
| 67 | ||
| 68 | return $message; | |
| 69 | } |