Lines 95.23% 20 / 21
Methods 80.00% 4 / 5
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 defaultMessage 100.00% 1 / 1 100.00% 1 / 1 1
 validate 90.90% 10 / 11 0.00% 0 / 1 5.02
 [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
18final class Date extends Constraint
19{
20    /**
21     * Create a date constraint.
22     *
23     * @param string $format The expected date format (defaults to `Y-m-d`).
24     */
25    public function __construct(private readonly string $format = 'Y-m-d') {}
26
27    /**
28     * @return string|Closure(FieldContext): string The default error message.
29     */
30    #[Override]
31    protected function defaultMessage(): string|Closure|null
32    {
33        return fn(FieldContext $ctx) => "The {$ctx->field} must be a valid date in {$this->format} format.";
34    }
35
36    /**
37     * Validate that the value is a date in the expected format.
38     *
39     * A round-trip check rejects rollovers (e.g. `2023-13-31` becoming
40     * `2024-01-31`) and non-strict input (e.g. `2023-1-1`). On success the
41     * value is normalized to a {@see Carbon} instance.
42     *
43     * @param FieldContext $ctx The context of the field being validated.
44     * @return bool True if the value is a valid date, false otherwise.
45     */
46    #[Override]
47    public function validate(FieldContext $ctx): bool
48    {
49        if (!is_string($ctx->value)) {
50            return false;
51        }
52
53        try {
54            $date = Carbon::createFromFormat($this->format, $ctx->value);
55        } catch (\Throwable) {
56            // Carbon throws instead of returning false for some malformed
57            // input (e.g. "not-a-date" or an empty string), and may throw
58            // other exception types for edge cases. Treat any failure as an
59            // invalid date rather than letting the exception propagate.
60            return false;
61        }
62
63        if ($date === false) {
64            return false;
65        }
66
67        // Round-trip check: reject rollovers (e.g. 2023-13-31 -> 2024-01-31)
68        // and non-strict input (e.g. 2023-1-1).
69        if ($date->format($this->format) !== $ctx->value) {
70            return false;
71        }
72
73        $ctx->normalize($date);
74        return true;
75    }
76}

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    }