Lines 100.00% 34 / 34
Methods 100.00% 7 / 7
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 2
 defaultMessage 100.00% 1 / 1 100.00% 1 / 1 1
 validate 100.00% 9 / 9 100.00% 1 / 1 3
 resolveCase 100.00% 8 / 8 100.00% 1 / 1 5
 allowedValues 100.00% 6 / 6 100.00% 1 / 1 3
 [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
23final class Enum extends Constraint
24{
25    /**
26     * Create an enum constraint.
27     *
28     * @param class-string<\UnitEnum> $enum The enum class whose cases are allowed.
29     * @throws \InvalidArgumentException If the given class is not an enum.
30     */
31    public function __construct(private readonly string $enum)
32    {
33        if (!enum_exists($enum)) {
34            throw new \InvalidArgumentException("Enum constraint requires a valid enum class, got '{$enum}'.");
35        }
36    }
37
38    /**
39     * @return string|Closure(FieldContext): string The default error message.
40     */
41    #[Override]
42    protected function defaultMessage(): string|Closure|null
43    {
44        return fn(FieldContext $ctx) => "The {$ctx->field} must be one of: " . implode(', ', $this->allowedValues());
45    }
46
47    /**
48     * Validate and normalize a value against the enum's cases.
49     *
50     * A value that is already an instance of the enum passes as-is. A backed
51     * enum also accepts its backing value (string or int); a pure enum
52     * accepts its case name. On success the value is normalized to the enum
53     * case.
54     *
55     * @param FieldContext $ctx The context of the field being validated.
56     * @return bool True if the value matches an enum case, false otherwise.
57     */
58    #[Override]
59    public function validate(FieldContext $ctx): bool
60    {
61        $value = $ctx->value;
62
63        if ($value instanceof $this->enum) {
64            // Store the instance so it is retrievable from the result (raw
65            // object values are not seeded by the validator).
66            $ctx->normalize($value);
67            return true;
68        }
69
70        $case = $this->resolveCase($value);
71
72        if ($case === null) {
73            return false;
74        }
75
76        $ctx->normalize($case);
77        return true;
78    }
79
80    /**
81     * Resolve a raw value to an enum case, or null if it matches none.
82     *
83     * @param mixed $value The raw value.
84     * @return \UnitEnum|null The matching enum case, or null.
85     */
86    private function resolveCase(mixed $value): ?\UnitEnum
87    {
88        foreach ($this->enum::cases() as $case) {
89            if ($case instanceof \BackedEnum) {
90                if ($value === $case->value) {
91                    return $case;
92                }
93                continue;
94            }
95
96            if ($value === $case->name) {
97                return $case;
98            }
99        }
100
101        return null;
102    }
103
104    /**
105     * The human-readable list of allowed values for the error message.
106     *
107     * @return list<string> The backing values (or case names) of the enum.
108     */
109    private function allowedValues(): array
110    {
111        $values = [];
112
113        foreach ($this->enum::cases() as $case) {
114            $values[] = $case instanceof \BackedEnum
115                ? (string) $case->value
116                : $case->name;
117        }
118
119        return $values;
120    }
121}

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    }