Lines 100.00% 38 / 38
Methods 100.00% 7 / 7
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 of 100.00% 1 / 1 100.00% 1 / 1 1
 defaultMessage 100.00% 3 / 3 100.00% 1 / 1 2
 validate 100.00% 19 / 19 100.00% 1 / 1 7
 [Lucent\Validation\Concerns\RecordsConstraintFailure] recordConstraintFailure 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
32final class AllOrNothing extends Constraint
33{
34    use RecordsConstraintFailure;
35
36    /**
37     * @param array<string, Constraint> $constraints Constraints keyed by field name.
38     */
39    private function __construct(private readonly array $constraints) {}
40
41    /**
42     * Create an "all or nothing" combinator from a map of constraints.
43     *
44     * @param array<string, Constraint> $constraints Constraints keyed by field name.
45     * @return self A new AllOrNothing instance.
46     */
47    public static function of(array $constraints): self
48    {
49        return new self($constraints);
50    }
51
52    /**
53     * @return string|\Closure(FieldContext): ?string|null The default error message.
54     */
55    #[Override]
56    protected function defaultMessage(): string|\Closure|null
57    {
58        return fn(FieldContext $ctx) => $ctx->childFailed
59            ? null
60            : "The {$ctx->field} fields must be provided together.";
61    }
62
63    /**
64     * Validate that the group of fields is all present or all absent.
65     *
66     * Fails when the value is not an array, when only some of the group's
67     * fields are present, or when any present field fails its constraint.
68     * When a child fails, its specific error is already recorded on the
69     * result at its dotted path, so {@see defaultMessage()} returns null to
70     * avoid a redundant generic error.
71     *
72     * @param FieldContext $ctx The context of the field being validated.
73     * @return bool True if the group is all present (and valid) or all absent.
74     */
75    #[Override]
76    public function validate(FieldContext $ctx): bool
77    {
78        $ctx->childFailed = false;
79
80        if (!is_array($ctx->value)) {
81            return false;
82        }
83
84        $present = [];
85        foreach (array_keys($this->constraints) as $name) {
86            $present[$name] = array_key_exists($name, $ctx->value);
87        }
88
89        $presentCount = count(array_filter($present));
90
91        // All absent: the group is optional. Normalize to null so the result
92        // reflects that the group was not provided (mirroring Optional).
93        if ($presentCount === 0) {
94            $ctx->normalize(null);
95            return true;
96        }
97
98        // Partial group: some fields present, some absent.
99        if ($presentCount !== count($this->constraints)) {
100            return false;
101        }
102
103        // All present: validate each field.
104        $ctx->result->ensureContainer($ctx->field);
105
106        foreach ($this->constraints as $name => $constraint) {
107            $child = $ctx->child($name, $ctx->value[$name], true);
108            $child->seedRaw();
109
110            if (!$this->recordConstraintFailure($constraint, $child)) {
111                $ctx->childFailed = true;
112            }
113        }
114
115        return !$ctx->childFailed;
116    }
117}

From Lucent\Validation\Concerns\RecordsConstraintFailure

18trait RecordsConstraintFailure
19{
20    /**
21     * Validate a constraint and record its error if it fails.
22     *
23     * Runs the constraint against the context. On failure, resolves the
24     * constraint's message and records it on the result at the context's
25     * field path, unless the message is null (meaning a child already
26     * recorded its specific error).
27     *
28     * @param Constraint $constraint The constraint to validate.
29     * @param FieldContext $ctx The context of the field being validated.
30     * @return bool True if the constraint passed, false otherwise.
31     */
32    protected function recordConstraintFailure(Constraint $constraint, FieldContext $ctx): bool
33    {
34        if ($constraint->validate($ctx)) {
35            return true;
36        }
37
38        $message = $constraint->message($ctx);
39        if ($message !== null) {
40            $ctx->result->addError($ctx->field, $message);
41        }
42
43        return false;
44    }
45}

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    }