44
55use PSFS \base \types \helpers \InjectorHelper ;
66use PSFS \base \types \helpers \MetadataReader ;
7- use PSFS \base \types \helpers \attributes \CsrfField ;
87use PSFS \base \types \helpers \attributes \CsrfProtected ;
9- use PSFS \base \types \helpers \attributes \DefaultValue ;
108use PSFS \base \types \helpers \attributes \DtoConstraintAttributeContract ;
11- use PSFS \base \types \helpers \attributes \Nullable ;
129use PSFS \base \types \helpers \attributes \Values ;
1310use ReflectionClass ;
1411use ReflectionProperty ;
@@ -54,51 +51,51 @@ private function run(): ValidationResult
5451 private function applyDefaultValues (array $ publicProperties ): void
5552 {
5653 foreach ($ publicProperties as $ property ) {
57- if ($ property ->getValue ($ this ->dto ) !== null ) {
54+ $ defaultValue = DtoValidationHelper::defaultValueFor ($ property );
55+ if ($ defaultValue === null || $ property ->getValue ($ this ->dto ) !== null ) {
5856 continue ;
5957 }
6058
61- $ defaultAttr = $ this ->propertyAttribute ($ property , DefaultValue::class);
62- $ doc = (string )($ property ->getDocComment () ?: '' );
63- $ defaultValue = $ defaultAttr instanceof DefaultValue
64- ? $ defaultAttr ->value
65- : MetadataReader::getTagValue ('default ' , $ doc , null , $ property );
66- if ($ defaultValue === null ) {
67- continue ;
68- }
69-
70- $ varType = MetadataReader::extractVarType ($ property , $ doc ) ?: 'string ' ;
59+ $ varType = MetadataReader::extractVarType (
60+ $ property ,
61+ DtoValidationHelper::propertyDoc ($ property )
62+ ) ?: 'string ' ;
7163 $ property ->setValue ($ this ->dto , ($ this ->castValue )($ defaultValue , $ varType ));
7264 }
7365 }
7466
7567 /**
7668 * @param array<int, ReflectionProperty> $publicProperties
7769 */
78- private function validateUnknownFields (array $ publicProperties , ReflectionClass $ reflector , ValidationResult $ result ): void
79- {
70+ private function validateUnknownFields (
71+ array $ publicProperties ,
72+ ReflectionClass $ reflector ,
73+ ValidationResult $ result
74+ ): void {
8075 if (!$ this ->context ->strictUnknownFields ) {
8176 return ;
8277 }
8378
84- $ allowed = [];
85- foreach ($ publicProperties as $ property ) {
86- $ allowed [$ property ->getName ()] = true ;
79+ $ allowed = DtoValidationHelper::allowedPayloadFields (
80+ $ publicProperties ,
81+ $ reflector ,
82+ $ this ->context ->enforceCsrf
83+ );
84+ foreach ($ this ->context ->payload as $ field => $ value ) {
85+ $ this ->validatePayloadField ($ field , $ allowed , $ result );
8786 }
87+ }
8888
89- $ csrfProtected = $ this ->classAttribute ($ reflector , CsrfProtected::class);
90- if ($ csrfProtected instanceof CsrfProtected && $ this ->context ->enforceCsrf !== false ) {
91- $ csrfField = $ this ->classAttribute ($ reflector , CsrfField::class);
92- $ allowed [$ csrfField ?->tokenField ?? '_csrf ' ] = true ;
93- $ allowed [$ csrfField ?->tokenKeyField ?? '_csrf_key ' ] = true ;
89+ /**
90+ * @param array<string, bool> $allowed
91+ */
92+ private function validatePayloadField (mixed $ field , array $ allowed , ValidationResult $ result ): void
93+ {
94+ if (!is_string ($ field ) || array_key_exists ($ field , $ allowed )) {
95+ return ;
9496 }
9597
96- foreach ($ this ->context ->payload as $ field => $ value ) {
97- if (!is_string ($ field ) || array_key_exists ($ field , $ allowed )) {
98- continue ;
99- }
100- $ result ->addError ($ field , 'unknown_field ' , $ this ->messageNotAllowed ($ field ));
101- }
98+ $ result ->addError ($ field , 'unknown_field ' , $ this ->messageNotAllowed ($ field ));
10299 }
103100
104101 /**
@@ -114,37 +111,91 @@ private function validateProperties(array $publicProperties, ValidationResult $r
114111 private function validateProperty (ReflectionProperty $ property , ValidationResult $ result ): void
115112 {
116113 $ name = $ property ->getName ();
117- $ doc = ( string )( $ property-> getDocComment () ?: '' );
114+ $ doc = DtoValidationHelper:: propertyDoc ( $ property );
118115 $ value = $ property ->getValue ($ this ->dto );
119116 $ existsInPayload = array_key_exists ($ name , $ this ->context ->payload );
120117 $ required = (bool )MetadataReader::getTagValue ('required ' , $ doc , false , $ property );
121118
122- if ($ required && !$ existsInPayload && $ value === null ) {
123- $ result ->addError ($ name , 'required ' , $ this ->messageRequired ($ name ));
119+ if (!$ this ->validateRequiredProperty ($ name , $ value , $ existsInPayload , $ required , $ result )) {
124120 return ;
125121 }
126122
127- if ($ value === null ) {
128- if ($ existsInPayload && !$ this ->allowsNull ($ property ) && $ required ) {
129- $ result ->addError ($ name , 'null_not_allowed ' , $ this ->messageRequired ($ name ));
130- }
123+ if (!$ this ->validateNullableProperty ($ property , $ name , $ value , $ existsInPayload , $ required , $ result )) {
131124 return ;
132125 }
133126
134127 $ varType = MetadataReader::extractVarType ($ property , $ doc );
135- if (is_string ($ varType ) && !$ this ->matchesDeclaredType ($ value , $ varType )) {
136- $ result ->addError ($ name , 'invalid_type ' , $ this ->messageInvalidFormat ($ name ));
128+ if (!$ this ->validatePropertyType ($ name , $ value , $ varType , $ result )) {
137129 return ;
138130 }
139131
140- if (!$ this ->hasAttribute ($ property , Values::class)) {
141- $ values = InjectorHelper::getValues ($ doc , $ property );
142- if (is_array ($ values ) && !in_array ($ value , $ values , true )) {
143- $ result ->addError ($ name , 'invalid_enum ' , $ this ->messageInvalidFormat ($ name ));
144- }
132+ $ this ->validateLegacyEnum ($ property , $ name , $ value , $ doc , $ result );
133+ $ this ->validateConstraintAttributes ($ property , $ name , $ value , $ result );
134+ }
135+
136+ private function validateRequiredProperty (
137+ string $ field ,
138+ mixed $ value ,
139+ bool $ existsInPayload ,
140+ bool $ required ,
141+ ValidationResult $ result
142+ ): bool {
143+ if (!$ required || $ existsInPayload || $ value !== null ) {
144+ return true ;
145145 }
146146
147- $ this ->validateConstraintAttributes ($ property , $ name , $ value , $ result );
147+ $ result ->addError ($ field , 'required ' , $ this ->messageRequired ($ field ));
148+ return false ;
149+ }
150+
151+ private function validateNullableProperty (
152+ ReflectionProperty $ property ,
153+ string $ field ,
154+ mixed $ value ,
155+ bool $ existsInPayload ,
156+ bool $ required ,
157+ ValidationResult $ result
158+ ): bool {
159+ if ($ value !== null ) {
160+ return true ;
161+ }
162+
163+ if ($ existsInPayload && $ required && !DtoValidationHelper::allowsNull ($ property )) {
164+ $ result ->addError ($ field , 'null_not_allowed ' , $ this ->messageRequired ($ field ));
165+ }
166+
167+ return false ;
168+ }
169+
170+ private function validatePropertyType (
171+ string $ field ,
172+ mixed $ value ,
173+ ?string $ varType ,
174+ ValidationResult $ result
175+ ): bool {
176+ if (!is_string ($ varType ) || DtoValidationHelper::matchesDeclaredType ($ value , $ varType )) {
177+ return true ;
178+ }
179+
180+ $ result ->addError ($ field , 'invalid_type ' , $ this ->messageInvalidFormat ($ field ));
181+ return false ;
182+ }
183+
184+ private function validateLegacyEnum (
185+ ReflectionProperty $ property ,
186+ string $ field ,
187+ mixed $ value ,
188+ string $ doc ,
189+ ValidationResult $ result
190+ ): void {
191+ if (DtoValidationHelper::hasAttribute ($ property , Values::class)) {
192+ return ;
193+ }
194+
195+ $ values = InjectorHelper::getValues ($ doc , $ property );
196+ if (is_array ($ values ) && !in_array ($ value , $ values , true )) {
197+ $ result ->addError ($ field , 'invalid_enum ' , $ this ->messageInvalidFormat ($ field ));
198+ }
148199 }
149200
150201 private function validateConstraintAttributes (
@@ -167,83 +218,22 @@ private function validateConstraintAttributes(
167218
168219 private function validateCsrfIfRequired (ReflectionClass $ reflector , ValidationResult $ result ): void
169220 {
170- $ csrfProtected = $ this ->classAttribute ($ reflector , CsrfProtected::class);
171- if (!$ csrfProtected instanceof CsrfProtected || $ this ->context ->enforceCsrf === false ) {
221+ if (!DtoValidationHelper::requiresCsrfValidation ($ reflector , $ this ->context ->enforceCsrf )) {
172222 return ;
173223 }
174224
175- $ csrfField = $ this ->classAttribute ($ reflector , CsrfField::class);
176- $ tokenField = $ csrfField ?->tokenField ?? '_csrf ' ;
177- $ tokenKeyField = $ csrfField ?->tokenKeyField ?? '_csrf_key ' ;
178- $ formKey = $ csrfProtected ->formKey !== '' ? $ csrfProtected ->formKey : $ reflector ->getShortName ();
179-
180- $ token = $ this ->payloadScalar ($ tokenField );
181- $ tokenKey = $ this ->payloadScalar ($ tokenKeyField );
182- if ($ token === '' ) {
183- $ token = (string )($ this ->context ->header ($ csrfProtected ->headerName ) ?? '' );
184- }
185- if ($ tokenKey === '' && $ csrfProtected ->headerKeyName !== '' ) {
186- $ tokenKey = (string )($ this ->context ->header ($ csrfProtected ->headerKeyName ) ?? '' );
187- }
225+ $ csrfProtected = DtoValidationHelper::csrfProtection ($ reflector );
226+ assert ($ csrfProtected instanceof CsrfProtected);
227+ $ csrfField = DtoValidationHelper::csrfField ($ reflector );
228+ [$ tokenField ] = DtoValidationHelper::csrfFieldNames ($ csrfField );
229+ $ formKey = DtoValidationHelper::csrfFormKey ($ csrfProtected , $ reflector );
230+ [$ token , $ tokenKey ] = DtoValidationHelper::csrfTokensFromRequest ($ this ->context , $ csrfProtected , $ csrfField );
188231
189232 if (!CsrfValidator::validateSubmission ($ token , $ tokenKey , $ formKey )) {
190233 $ result ->addError ($ tokenField , 'invalid_csrf ' , t ('Invalid form ' ));
191234 }
192235 }
193236
194- private function payloadScalar (string $ field ): string
195- {
196- if (!array_key_exists ($ field , $ this ->context ->payload ) || !is_scalar ($ this ->context ->payload [$ field ])) {
197- return '' ;
198- }
199- return (string )$ this ->context ->payload [$ field ];
200- }
201-
202- private function allowsNull (ReflectionProperty $ property ): bool
203- {
204- $ nullable = $ this ->propertyAttribute ($ property , Nullable::class);
205- return $ nullable instanceof Nullable && $ nullable ->allowsNull ();
206- }
207-
208- private function hasAttribute (ReflectionProperty $ property , string $ attributeClass ): bool
209- {
210- return !empty ($ property ->getAttributes ($ attributeClass ));
211- }
212-
213- private function matchesDeclaredType (mixed $ value , string $ type ): bool
214- {
215- $ normalized = strtolower (trim ($ type ));
216- if (str_contains ($ normalized , '| ' )) {
217- foreach (array_map ('trim ' , explode ('| ' , $ normalized )) as $ candidate ) {
218- if ($ this ->matchesDeclaredType ($ value , $ candidate )) {
219- return true ;
220- }
221- }
222- return false ;
223- }
224-
225- return match ($ normalized ) {
226- 'string ' => is_string ($ value ),
227- 'int ' , 'integer ' => is_int ($ value ),
228- 'bool ' , 'boolean ' => is_bool ($ value ),
229- 'float ' , 'double ' , 'number ' => is_float ($ value ) || is_int ($ value ),
230- 'array ' => is_array ($ value ),
231- default => true ,
232- };
233- }
234-
235- private function propertyAttribute (ReflectionProperty $ property , string $ attributeClass ): mixed
236- {
237- $ attrs = $ property ->getAttributes ($ attributeClass );
238- return empty ($ attrs ) ? null : $ attrs [0 ]->newInstance ();
239- }
240-
241- private function classAttribute (ReflectionClass $ reflector , string $ attributeClass ): mixed
242- {
243- $ attrs = $ reflector ->getAttributes ($ attributeClass );
244- return empty ($ attrs ) ? null : $ attrs [0 ]->newInstance ();
245- }
246-
247237 private function messageRequired (string $ field ): string
248238 {
249239 return str_replace ('%s ' , "<strong> {$ field }</strong> " , t ('Field %s is required ' ));
@@ -259,4 +249,3 @@ private function messageNotAllowed(string $field): string
259249 return str_replace ('%s ' , "<strong> {$ field }</strong> " , t ('Field %s is not allowed ' ));
260250 }
261251}
262-
0 commit comments