Lines 100.00% 19 / 19
Methods 100.00% 6 / 6
Classes 100.00% 1 / 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 100.00% 6 / 6 100.00% 1 / 1 3
 isEmpty 100.00% 3 / 3 100.00% 1 / 1 4
 [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
20final class Optional extends Constraint
21{
22    /**
23     * Create an optional wrapper around another constraint.
24     *
25     * @param Constraint $inner The constraint to apply when a value is present.
26     */
27    public function __construct(private readonly Constraint $inner) {}
28
29    /**
30     * Delegate the error message to the wrapped constraint.
31     *
32     * @return string|Closure(FieldContext): string The inner constraint's message or closure.
33     */
34    #[Override]
35    protected function defaultMessage(): string|Closure|null
36    {
37        return fn(FieldContext $ctx) => $this->inner->message($ctx);
38    }
39
40    /**
41     * Validate the wrapped constraint, skipping it when the field is empty.
42     *
43     * Validation is skipped when the field was not present in the request
44     * body, or when its value is null, an empty string, or an empty array.
45     * This mirrors the set of values {@see \Lucent\Validation\Constraints\Required}
46     * treats as missing, so an optional field may be omitted or left blank.
47     * Any other value is validated by the inner constraint.
48     *
49     * @param FieldContext $ctx The context of the field being validated.
50     * @return bool True when the field is empty, otherwise the result of the wrapped constraint.
51     */
52    #[Override]
53    public function validate(FieldContext $ctx): bool
54    {
55        if (!$ctx->present) {
56            return true;
57        }
58
59        if ($this->isEmpty($ctx->value)) {
60            // Normalize a present-but-empty value to null so the result
61            // reflects the documented behaviour of an optional field that was
62            // left blank. Absent fields are left untouched so they do not
63            // appear in the result.
64            $ctx->normalize(null);
65            return true;
66        }
67
68        return $this->inner->validate($ctx);
69    }
70
71    /**
72     * Determine whether a value counts as empty.
73     *
74     * @param mixed $value The value to inspect.
75     * @return bool True if the value is null, an empty string, or an empty array.
76     */
77    private function isEmpty(mixed $value): bool
78    {
79        if ($value === null || $value === '') {
80            return true;
81        }
82
83        return is_array($value) && $value === [];
84    }
85}

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    }