@@ -13,6 +13,11 @@ abstract class FieldValidator
1313 * @var array<mixed>|null
1414 */
1515 protected $ oneOf = null ;
16+ /**
17+ * @var array<array{rule: callable, message: string}>
18+ */
19+ protected array $ validations = [];
20+
1621
1722 /**
1823 * Marks the field as required.
@@ -72,6 +77,83 @@ public function oneOf(array $values): self
7277 return $ this ;
7378 }
7479
80+ /**
81+ * Adds a custom validation rule.
82+ *
83+ * @param callable $validation The validation function that receives (value, key, input).
84+ * @param string $message The error message if validation fails.
85+ * @return $this
86+ */
87+ public function addValidation (callable $ validation , string $ message ): self
88+ {
89+ $ this ->validations [] = ['rule ' => $ validation , 'message ' => $ message ];
90+ return $ this ;
91+ }
92+
93+ /**
94+ * Validates that the value passes ALL of the provided validators.
95+ *
96+ * @param array<FieldValidator> $validators Array of validators that must all pass.
97+ * @param ?string $message Custom error message.
98+ * @return $this
99+ */
100+ public function allOf (array $ validators , ?string $ message = null ): self
101+ {
102+ return $ this ->addValidation (
103+ function ($ value , $ key = null , $ input = null ) use ($ validators ) {
104+ foreach ($ validators as $ validator ) {
105+ [$ valid , , ] = $ validator ->tryValidate ($ value , $ key , $ input );
106+ if (!$ valid ) {
107+ return false ;
108+ }
109+ }
110+ return true ;
111+ },
112+ $ message ?? 'Value must satisfy all validation rules. '
113+ );
114+ }
115+
116+ /**
117+ * Validates that the value passes ANY of the provided validators.
118+ *
119+ * @param array<FieldValidator> $validators Array of validators, at least one must pass.
120+ * @param ?string $message Custom error message.
121+ * @return $this
122+ */
123+ public function anyOf (array $ validators , ?string $ message = null ): self
124+ {
125+ return $ this ->addValidation (
126+ function ($ value , $ key = null , $ input = null ) use ($ validators ) {
127+ foreach ($ validators as $ validator ) {
128+ [$ valid , , ] = $ validator ->tryValidate ($ value , $ key , $ input );
129+ if ($ valid ) {
130+ return true ;
131+ }
132+ }
133+ return false ;
134+ },
135+ $ message ?? 'Value must satisfy at least one validation rule. '
136+ );
137+ }
138+
139+ /**
140+ * Validates that the value does NOT pass the provided validator.
141+ *
142+ * @param FieldValidator $validator The validator that must fail.
143+ * @param ?string $message Custom error message.
144+ * @return $this
145+ */
146+ public function not (FieldValidator $ validator , ?string $ message = null ): self
147+ {
148+ return $ this ->addValidation (
149+ function ($ value , $ key = null , $ input = null ) use ($ validator ) {
150+ [$ valid , , ] = $ validator ->tryValidate ($ value , $ key , $ input );
151+ return !$ valid ;
152+ },
153+ $ message ?? 'Value must not satisfy the validation rule. '
154+ );
155+ }
156+
75157 /**
76158 * Validates the given value against the defined rules.
77159 *
@@ -119,6 +201,19 @@ public function tryValidate(mixed $value, string $key = '', mixed $input = null)
119201
120202 try {
121203 $ validatedValue = $ this ->validateType ($ value , $ key );
204+
205+ // Collect all validation errors
206+ $ validationErrors = [];
207+ foreach ($ this ->validations as $ validation ) {
208+ if (!$ validation ['rule ' ]($ validatedValue , $ key , $ input )) {
209+ $ validationErrors [] = $ validation ['message ' ];
210+ }
211+ }
212+
213+ if (!empty ($ validationErrors )) {
214+ return [false , $ validatedValue , $ validationErrors ];
215+ }
216+
122217 return [true , $ validatedValue , null ];
123218 } catch (ValidationException $ e ) {
124219 return [false , $ value , $ e ->getErrors ()];
0 commit comments