77use PSFS \base \types \helpers \attributes \CsrfField ;
88use PSFS \base \types \helpers \attributes \CsrfProtected ;
99use PSFS \base \types \helpers \attributes \DefaultValue ;
10- use PSFS \base \types \helpers \attributes \Length ;
11- use PSFS \base \types \helpers \attributes \Max ;
12- use PSFS \base \types \helpers \attributes \Min ;
10+ use PSFS \base \types \helpers \attributes \DtoConstraintAttributeContract ;
1311use PSFS \base \types \helpers \attributes \Nullable ;
14- use PSFS \base \types \helpers \attributes \Pattern ;
1512use PSFS \base \types \helpers \attributes \Values ;
1613use ReflectionClass ;
1714use ReflectionProperty ;
@@ -30,9 +27,7 @@ public function validate(?ValidationContext $ctx = null): ValidationResult
3027
3128 $ this ->applyDefaultValues ($ publicProperties );
3229 $ this ->validateUnknownFields ($ publicProperties , $ context , $ result );
33- foreach ($ publicProperties as $ property ) {
34- $ this ->validateProperty ($ property , $ context , $ result );
35- }
30+ $ this ->validateProperties ($ publicProperties , $ context , $ result );
3631 $ this ->validateCsrfIfRequired ($ reflector , $ context , $ result );
3732
3833 $ this ->__validationResult = $ result ;
@@ -145,53 +140,31 @@ private function validateProperty(ReflectionProperty $property, ValidationContex
145140 $ existsInPayload = array_key_exists ($ name , $ context ->payload );
146141
147142 $ required = (bool )MetadataReader::getTagValue ('required ' , $ doc , false , $ property );
148- $ nullable = $ this ->propertyAttribute ($ property , Nullable::class)?->value ?? false ;
149- if ($ required && !$ existsInPayload && $ value === null ) {
150- $ result ->addError ($ name , 'required ' , str_replace ('%s ' , "<strong> {$ name }</strong> " , t ('Field %s is required ' )));
143+ if ($ this ->isRequiredMissing ($ required , $ existsInPayload , $ value )) {
144+ $ result ->addError ($ name , 'required ' , $ this ->requiredFieldMessage ($ name ));
151145 return ;
152146 }
153147 if ($ value === null ) {
154- if ($ existsInPayload && !$ nullable && $ required ) {
155- $ result ->addError ($ name , 'null_not_allowed ' , str_replace ( ' %s ' , " <strong> { $ name } </strong> " , t ( ' Field %s is required ' ) ));
148+ if ($ existsInPayload && !$ this -> allowsNull ( $ property ) && $ required ) {
149+ $ result ->addError ($ name , 'null_not_allowed ' , $ this -> requiredFieldMessage ( $ name ));
156150 }
157151 return ;
158152 }
159153
160154 $ varType = MetadataReader::extractVarType ($ property , $ doc );
161155 if (is_string ($ varType ) && !$ this ->matchesDeclaredType ($ value , $ varType )) {
162- $ result ->addError ($ name , 'invalid_type ' , str_replace ( ' %s ' , " <strong> { $ name } </strong> " , t ( ' Field %s has an invalid format ' ) ));
156+ $ result ->addError ($ name , 'invalid_type ' , $ this -> invalidFormatMessage ( $ name ));
163157 return ;
164158 }
165159
166- $ valuesAttr = $ this ->propertyAttribute ($ property , Values::class);
167- $ values = $ valuesAttr instanceof Values
168- ? (is_array ($ valuesAttr ->value ) ? $ valuesAttr ->value : [$ valuesAttr ->value ])
169- : InjectorHelper::getValues ($ doc , $ property );
170- if (is_array ($ values ) && !in_array ($ value , $ values , true )) {
171- $ result ->addError ($ name , 'invalid_enum ' , str_replace ('%s ' , "<strong> {$ name }</strong> " , t ('Field %s has an invalid format ' )));
172- }
173-
174- $ pattern = $ this ->propertyAttribute ($ property , Pattern::class);
175- if ($ pattern instanceof Pattern && is_string ($ value ) && preg_match ($ pattern ->value , $ value ) !== 1 ) {
176- $ result ->addError ($ name , 'pattern_mismatch ' , str_replace ('%s ' , "<strong> {$ name }</strong> " , t ('Field %s has an invalid format ' )));
177- }
178-
179- $ length = $ this ->propertyAttribute ($ property , Length::class);
180- if ($ length instanceof Length && is_string ($ value )) {
181- $ strlen = mb_strlen ($ value );
182- if ($ strlen < $ length ->min || ($ length ->max !== null && $ strlen > $ length ->max )) {
183- $ result ->addError ($ name , 'invalid_length ' , str_replace ('%s ' , "<strong> {$ name }</strong> " , t ('Field %s has an invalid format ' )));
160+ if (!$ this ->hasConstraintAttribute ($ property , Values::class)) {
161+ $ values = InjectorHelper::getValues ($ doc , $ property );
162+ if (is_array ($ values ) && !in_array ($ value , $ values , true )) {
163+ $ result ->addError ($ name , 'invalid_enum ' , $ this ->invalidFormatMessage ($ name ));
184164 }
185165 }
186166
187- $ min = $ this ->propertyAttribute ($ property , Min::class);
188- if ($ min instanceof Min && is_numeric ($ value ) && (float )$ value < $ min ->value ) {
189- $ result ->addError ($ name , 'min_value ' , str_replace ('%s ' , "<strong> {$ name }</strong> " , t ('Field %s has an invalid format ' )));
190- }
191- $ max = $ this ->propertyAttribute ($ property , Max::class);
192- if ($ max instanceof Max && is_numeric ($ value ) && (float )$ value > $ max ->value ) {
193- $ result ->addError ($ name , 'max_value ' , str_replace ('%s ' , "<strong> {$ name }</strong> " , t ('Field %s has an invalid format ' )));
194- }
167+ $ this ->validateConstraintAttributes ($ property , $ name , $ value , $ result );
195168 }
196169
197170 private function validateCsrfIfRequired (
@@ -272,4 +245,61 @@ private function classAttribute(ReflectionClass $reflector, string $attributeCla
272245 }
273246 return $ attrs [0 ]->newInstance ();
274247 }
248+
249+ /**
250+ * @param array<int, ReflectionProperty> $publicProperties
251+ */
252+ private function validateProperties (array $ publicProperties , ValidationContext $ context , ValidationResult $ result ): void
253+ {
254+ foreach ($ publicProperties as $ property ) {
255+ $ this ->validateProperty ($ property , $ context , $ result );
256+ }
257+ }
258+
259+ private function isRequiredMissing (bool $ required , bool $ existsInPayload , mixed $ value ): bool
260+ {
261+ return $ required && !$ existsInPayload && $ value === null ;
262+ }
263+
264+ private function allowsNull (ReflectionProperty $ property ): bool
265+ {
266+ $ nullable = $ this ->propertyAttribute ($ property , Nullable::class);
267+ if (!$ nullable instanceof Nullable) {
268+ return false ;
269+ }
270+ return $ nullable ->allowsNull ();
271+ }
272+
273+ private function requiredFieldMessage (string $ field ): string
274+ {
275+ return str_replace ('%s ' , "<strong> {$ field }</strong> " , t ('Field %s is required ' ));
276+ }
277+
278+ private function invalidFormatMessage (string $ field ): string
279+ {
280+ return str_replace ('%s ' , "<strong> {$ field }</strong> " , t ('Field %s has an invalid format ' ));
281+ }
282+
283+ private function hasConstraintAttribute (ReflectionProperty $ property , string $ attributeClass ): bool
284+ {
285+ return !empty ($ property ->getAttributes ($ attributeClass ));
286+ }
287+
288+ private function validateConstraintAttributes (
289+ ReflectionProperty $ property ,
290+ string $ field ,
291+ mixed $ value ,
292+ ValidationResult $ result
293+ ): void {
294+ foreach ($ property ->getAttributes () as $ reflectionAttribute ) {
295+ $ attribute = $ reflectionAttribute ->newInstance ();
296+ if (!$ attribute instanceof DtoConstraintAttributeContract) {
297+ continue ;
298+ }
299+ if ($ attribute ->validateValue ($ value )) {
300+ continue ;
301+ }
302+ $ result ->addError ($ field , $ attribute ->errorCode (), $ this ->invalidFormatMessage ($ field ));
303+ }
304+ }
275305}
0 commit comments