These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace ParamProcessor\PackagePrivate; |
||
4 | |||
5 | use Exception; |
||
6 | use ParamProcessor\IParam; |
||
7 | use ParamProcessor\IParamDefinition; |
||
8 | use ParamProcessor\Options; |
||
9 | use ParamProcessor\ParamDefinition; |
||
10 | use ParamProcessor\ParamDefinitionFactory; |
||
11 | use ParamProcessor\ProcessingError; |
||
12 | use ValueParsers\NullParser; |
||
13 | use ValueParsers\ParseException; |
||
14 | use ValueParsers\ValueParser; |
||
15 | |||
16 | /** |
||
17 | * Package private! |
||
18 | * |
||
19 | * Parameter class, representing the "instance" of a parameter. |
||
20 | * Holds a ParamDefinition, user provided input (name & value) and processing state. |
||
21 | * |
||
22 | * @licence GNU GPL v2+ |
||
23 | * @author Jeroen De Dauw < [email protected] > |
||
24 | */ |
||
25 | class Param implements IParam { |
||
26 | |||
27 | /** |
||
28 | * Indicates whether parameters not found in the criteria list |
||
29 | * should be stored in case they are not accepted. The default is false. |
||
30 | * |
||
31 | * @since 1.0 |
||
32 | * |
||
33 | * @var boolean |
||
34 | */ |
||
35 | public static $accumulateParameterErrors = false; |
||
36 | |||
37 | /** |
||
38 | * The original parameter name as provided by the user. This can be the |
||
39 | * main name or an alias. |
||
40 | * |
||
41 | * @since 1.0 |
||
42 | * |
||
43 | * @var string |
||
44 | */ |
||
45 | protected $originalName; |
||
46 | |||
47 | /** |
||
48 | * The original value as provided by the user. This is mainly retained for |
||
49 | * usage in error messages when the parameter turns out to be invalid. |
||
50 | * |
||
51 | * @since 1.0 |
||
52 | * |
||
53 | * @var string |
||
54 | */ |
||
55 | protected $originalValue; |
||
56 | |||
57 | /** |
||
58 | * The value of the parameter. |
||
59 | * |
||
60 | * @since 1.0 |
||
61 | * |
||
62 | * @var mixed |
||
63 | */ |
||
64 | protected $value; |
||
65 | |||
66 | /** |
||
67 | * Keeps track of how many times the parameter has been set by the user. |
||
68 | * This is used to detect overrides and for figuring out a parameter is missing. |
||
69 | * |
||
70 | * @since 1.0 |
||
71 | * |
||
72 | * @var integer |
||
73 | */ |
||
74 | protected $setCount = 0; |
||
75 | |||
76 | /** |
||
77 | * List of validation errors for this parameter. |
||
78 | * |
||
79 | * @since 1.0 |
||
80 | * |
||
81 | * @var ProcessingError[] |
||
82 | */ |
||
83 | protected $errors = []; |
||
84 | |||
85 | /** |
||
86 | * Indicates if the parameter was set to it's default. |
||
87 | * |
||
88 | * @since 1.0 |
||
89 | * |
||
90 | * @var boolean |
||
91 | */ |
||
92 | protected $defaulted = false; |
||
93 | |||
94 | /** |
||
95 | * @since 1.0 |
||
96 | * |
||
97 | * @var ParamDefinition |
||
98 | */ |
||
99 | protected $definition; |
||
100 | |||
101 | /** |
||
102 | * Constructor. |
||
103 | * |
||
104 | * @since 1.0 |
||
105 | * |
||
106 | * @param IParamDefinition $definition |
||
107 | */ |
||
108 | 66 | public function __construct( IParamDefinition $definition ) { |
|
109 | 66 | $this->definition = $definition; |
|
110 | 66 | } |
|
111 | |||
112 | /** |
||
113 | * Sets and cleans the original value and name. |
||
114 | * @see IParam::setUserValue |
||
115 | * |
||
116 | * @since 1.0 |
||
117 | * |
||
118 | * @param string $paramName |
||
119 | * @param string $paramValue |
||
120 | * @param Options $options |
||
121 | * |
||
122 | * @return boolean |
||
123 | */ |
||
124 | 64 | public function setUserValue( $paramName, $paramValue, Options $options ) { |
|
125 | 64 | if ( $this->setCount > 0 && !$options->acceptOverriding() ) { |
|
126 | // TODO |
||
127 | return false; |
||
128 | } |
||
129 | else { |
||
130 | 64 | $this->originalName = $paramName; |
|
131 | 64 | $this->originalValue = $paramValue; |
|
132 | |||
133 | 64 | $this->cleanValue( $options ); |
|
134 | |||
135 | 64 | $this->setCount++; |
|
136 | |||
137 | 64 | return true; |
|
138 | } |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * @since 1.0 |
||
143 | * |
||
144 | * @param mixed $value |
||
145 | */ |
||
146 | 57 | public function setValue( $value ) { |
|
147 | 57 | $this->value = $value; |
|
148 | 57 | } |
|
149 | |||
150 | /** |
||
151 | * Sets the $value to a cleaned value of $originalValue. |
||
152 | * |
||
153 | * @since 1.0 |
||
154 | * |
||
155 | * @param Options $options |
||
156 | */ |
||
157 | 64 | protected function cleanValue( Options $options ) { |
|
158 | 64 | if ( $this->definition->isList() ) { |
|
159 | $this->value = explode( $this->definition->getDelimiter(), $this->originalValue ); |
||
160 | } |
||
161 | else { |
||
162 | 64 | $this->value = $this->originalValue; |
|
163 | } |
||
164 | |||
165 | 64 | if ( $this->shouldTrim( $options ) ) { |
|
166 | 64 | $this->trimValue(); |
|
167 | } |
||
168 | |||
169 | 64 | if ( $this->shouldLowercase( $options ) ) { |
|
170 | $this->lowercaseValue(); |
||
171 | } |
||
172 | 64 | } |
|
173 | |||
174 | 64 | private function shouldTrim( Options $options ): bool { |
|
175 | 64 | $trim = $this->definition->trimDuringClean(); |
|
176 | |||
177 | 64 | if ( $trim === true ) { |
|
178 | return true; |
||
179 | } |
||
180 | |||
181 | 64 | return is_null( $trim ) && $options->trimValues(); |
|
182 | } |
||
183 | |||
184 | 64 | private function trimValue() { |
|
185 | 64 | if ( is_string( $this->value ) ) { |
|
186 | 64 | $this->value = trim( $this->value ); |
|
187 | } |
||
188 | 54 | elseif ( $this->definition->isList() ) { |
|
189 | foreach ( $this->value as &$element ) { |
||
0 ignored issues
–
show
|
|||
190 | if ( is_string( $element ) ) { |
||
191 | $element = trim( $element ); |
||
192 | } |
||
193 | } |
||
194 | } |
||
195 | 64 | } |
|
196 | |||
197 | 64 | private function shouldLowercase( Options $options ): bool { |
|
198 | 64 | if ( $options->lowercaseValues() ) { |
|
199 | return true; |
||
200 | } |
||
201 | |||
202 | 64 | $definitionOptions = $this->definition->getOptions(); |
|
203 | |||
204 | 64 | return array_key_exists( 'tolower', $definitionOptions ) && $definitionOptions['tolower']; |
|
205 | } |
||
206 | |||
207 | private function lowercaseValue() { |
||
208 | if ( $this->definition->isList() ) { |
||
209 | foreach ( $this->value as &$element ) { |
||
210 | if ( is_string( $element ) ) { |
||
211 | $element = strtolower( $element ); |
||
212 | } |
||
213 | } |
||
214 | } |
||
215 | elseif ( is_string( $this->value ) ) { |
||
216 | $this->value = strtolower( $this->value ); |
||
217 | } |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Parameter processing entry point. |
||
222 | * Processes the parameter. This includes parsing, validation and additional formatting. |
||
223 | * |
||
224 | * @since 1.0 |
||
225 | * |
||
226 | * @param $definitions array of IParamDefinition |
||
227 | * @param $params array of IParam |
||
228 | * @param Options $options |
||
229 | * |
||
230 | * @throws Exception |
||
231 | */ |
||
232 | 65 | public function process( array &$definitions, array $params, Options $options ) { |
|
233 | 65 | if ( $this->setCount == 0 ) { |
|
234 | 1 | if ( $this->definition->isRequired() ) { |
|
235 | // This should not occur, so throw an exception. |
||
236 | throw new Exception( 'Attempted to validate a required parameter without first setting a value.' ); |
||
237 | } |
||
238 | else { |
||
239 | 1 | $this->setToDefault(); |
|
240 | } |
||
241 | } |
||
242 | else { |
||
243 | 64 | $this->parseAndValidate( $options ); |
|
244 | } |
||
245 | |||
246 | 65 | if ( !$this->hasFatalError() && ( $this->definition->shouldManipulateDefault() || !$this->wasSetToDefault() ) ) { |
|
247 | 57 | $this->definition->format( $this, $definitions, $params ); |
|
248 | } |
||
249 | 65 | } |
|
250 | |||
251 | 64 | public function getValueParser( Options $options ): ValueParser { |
|
252 | 64 | $parser = $this->definition->getValueParser(); |
|
253 | |||
254 | 64 | if ( get_class( $parser ) === NullParser::class ) { |
|
255 | 64 | $parserType = $options->isStringlyTyped() ? 'string-parser' : 'typed-parser'; |
|
256 | |||
257 | // TODO: inject factory |
||
258 | 64 | $parserClass = ParamDefinitionFactory::singleton()->getComponentForType( $this->definition->getType(), $parserType ); |
|
259 | |||
260 | 64 | if ( $parserClass !== NullParser::class ) { |
|
261 | 50 | $parser = new $parserClass( new \ValueParsers\ParserOptions() ); |
|
262 | } |
||
263 | } |
||
264 | |||
265 | 64 | return $parser; |
|
266 | } |
||
267 | |||
268 | /** |
||
269 | * @since 1.0 |
||
270 | * |
||
271 | * @param Options $options |
||
272 | */ |
||
273 | 64 | protected function parseAndValidate( Options $options ) { |
|
274 | 64 | $parser = $this->getValueParser( $options ); |
|
275 | |||
276 | 64 | if ( $this->definition->isList() ) { |
|
277 | $values = []; |
||
278 | |||
279 | foreach ( $this->getValue() as $value ) { |
||
280 | $parsedValue = $this->parseAndValidateValue( $parser, $value ); |
||
281 | |||
282 | if ( is_array( $parsedValue ) ) { |
||
283 | $values[] = $parsedValue[0]; |
||
284 | } |
||
285 | } |
||
286 | |||
287 | $this->value = $values; |
||
288 | } |
||
289 | else { |
||
290 | 64 | $parsedValue = $this->parseAndValidateValue( $parser, $this->getValue() ); |
|
291 | |||
292 | 64 | if ( is_array( $parsedValue ) ) { |
|
293 | 64 | $this->value = $parsedValue[0]; |
|
294 | } |
||
295 | } |
||
296 | |||
297 | 64 | $this->setToDefaultIfNeeded(); |
|
298 | 64 | } |
|
299 | |||
300 | /** |
||
301 | * Parses and validates the provided with with specified parser. |
||
302 | * The result is returned in an array on success. On fail, false is returned. |
||
303 | * The result is wrapped in an array since we need to be able to distinguish |
||
304 | * between the method returning false and the value being false. |
||
305 | * |
||
306 | * Parsing and validation errors get added to $this->errors. |
||
307 | * |
||
308 | * @since 1.0 |
||
309 | * |
||
310 | * @param ValueParser $parser |
||
311 | * @param mixed $value |
||
312 | * |
||
313 | * @return array|bool |
||
314 | */ |
||
315 | 64 | protected function parseAndValidateValue( ValueParser $parser, $value ) { |
|
316 | try { |
||
317 | 64 | $value = $parser->parse( $value ); |
|
318 | } |
||
319 | 7 | catch ( ParseException $parseException ) { |
|
320 | 7 | $this->registerProcessingError( $parseException->getMessage() ); |
|
321 | 7 | return false; |
|
322 | } |
||
323 | |||
324 | 64 | if ( $value instanceof \DataValues\DataValue ) { |
|
325 | 64 | $value = $value->getValue(); |
|
326 | } |
||
327 | |||
328 | 64 | $this->validateValue( $value ); |
|
329 | |||
330 | 64 | return [ $value ]; |
|
331 | } |
||
332 | |||
333 | 21 | protected function registerProcessingError( string $message ) { |
|
334 | 21 | $this->errors[] = $this->newProcessingError( $message ); |
|
335 | 21 | } |
|
336 | |||
337 | 21 | protected function newProcessingError( string $message ): ProcessingError { |
|
338 | 21 | $severity = $this->isRequired() ? ProcessingError::SEVERITY_FATAL : ProcessingError::SEVERITY_NORMAL; |
|
339 | 21 | return new ProcessingError( $message, $severity ); |
|
340 | } |
||
341 | |||
342 | /** |
||
343 | * @since 1.0 |
||
344 | * |
||
345 | * @param mixed $value |
||
346 | */ |
||
347 | 64 | protected function validateValue( $value ) { |
|
348 | 64 | $validationCallback = $this->definition->getValidationCallback(); |
|
349 | |||
350 | 64 | if ( $validationCallback !== null && $validationCallback( $value ) !== true ) { |
|
351 | 7 | $this->registerProcessingError( 'Validation callback failed' ); |
|
352 | } |
||
353 | else { |
||
354 | 64 | $validator = $this->definition->getValueValidator(); |
|
355 | 64 | if ( method_exists( $validator, 'setOptions' ) ) { |
|
356 | 64 | $validator->setOptions( $this->definition->getOptions() ); |
|
357 | } |
||
358 | 64 | $validationResult = $validator->validate( $value ); |
|
359 | |||
360 | 64 | if ( !$validationResult->isValid() ) { |
|
361 | 16 | foreach ( $validationResult->getErrors() as $error ) { |
|
362 | 16 | $this->registerProcessingError( $error->getText() ); |
|
363 | } |
||
364 | } |
||
365 | } |
||
366 | 64 | } |
|
367 | |||
368 | /** |
||
369 | * Sets the parameter value to the default if needed. |
||
370 | * |
||
371 | * @since 1.0 |
||
372 | */ |
||
373 | 64 | protected function setToDefaultIfNeeded() { |
|
374 | 64 | if ( $this->shouldSetToDefault() ) { |
|
375 | 1 | $this->setToDefault(); |
|
376 | } |
||
377 | 64 | } |
|
378 | |||
379 | 64 | private function shouldSetToDefault(): bool { |
|
380 | 64 | if ( $this->hasFatalError() ) { |
|
381 | 20 | return false; |
|
382 | } |
||
383 | |||
384 | 56 | if ( $this->definition->isList() ) { |
|
385 | return $this->errors !== [] && $this->value === []; |
||
386 | } |
||
387 | |||
388 | 56 | return $this->errors !== []; |
|
389 | } |
||
390 | |||
391 | /** |
||
392 | * Returns the original use-provided name. |
||
393 | * |
||
394 | * @since 1.0 |
||
395 | * |
||
396 | * @throws Exception |
||
397 | * @return string |
||
398 | */ |
||
399 | public function getOriginalName(): string { |
||
400 | if ( $this->setCount == 0 ) { |
||
401 | throw new Exception( 'No user input set to the parameter yet, so the original name does not exist' ); |
||
402 | } |
||
403 | return $this->originalName; |
||
404 | } |
||
405 | |||
406 | /** |
||
407 | * Returns the original use-provided value. |
||
408 | * |
||
409 | * @since 1.0 |
||
410 | * |
||
411 | * @throws Exception |
||
412 | * @return mixed |
||
413 | */ |
||
414 | public function getOriginalValue() { |
||
415 | if ( $this->setCount == 0 ) { |
||
416 | throw new Exception( 'No user input set to the parameter yet, so the original value does not exist' ); |
||
417 | } |
||
418 | return $this->originalValue; |
||
419 | } |
||
420 | |||
421 | /** |
||
422 | * Returns all validation errors that occurred so far. |
||
423 | * |
||
424 | * @since 1.0 |
||
425 | * |
||
426 | * @return ProcessingError[] |
||
427 | */ |
||
428 | 50 | public function getErrors() { |
|
429 | 50 | return $this->errors; |
|
430 | } |
||
431 | |||
432 | /** |
||
433 | * Sets the parameter value to the default. |
||
434 | * |
||
435 | * @since 1.0 |
||
436 | */ |
||
437 | 2 | protected function setToDefault() { |
|
438 | 2 | $this->defaulted = true; |
|
439 | 2 | $this->value = $this->definition->getDefault(); |
|
440 | 2 | } |
|
441 | |||
442 | public function wasSetToDefault(): bool { |
||
443 | return $this->defaulted; |
||
444 | } |
||
445 | |||
446 | 65 | public function hasFatalError(): bool { |
|
447 | 65 | foreach ( $this->errors as $error ) { |
|
448 | 21 | if ( $error->isFatal() ) { |
|
449 | 20 | return true; |
|
450 | } |
||
451 | } |
||
452 | |||
453 | 57 | return false; |
|
454 | } |
||
455 | |||
456 | /** |
||
457 | * Returns the IParamDefinition this IParam was constructed from. |
||
458 | * |
||
459 | * @since 1.0 |
||
460 | * |
||
461 | * @return IParamDefinition |
||
462 | */ |
||
463 | public function getDefinition() { |
||
464 | return $this->definition; |
||
465 | } |
||
466 | |||
467 | /** |
||
468 | * Returns the parameters value. |
||
469 | * |
||
470 | * @since 1.0 |
||
471 | * |
||
472 | * @return mixed |
||
473 | */ |
||
474 | 65 | public function &getValue() { |
|
475 | 65 | return $this->value; |
|
476 | } |
||
477 | |||
478 | /** |
||
479 | * Returns if the parameter is required or not. |
||
480 | * |
||
481 | * @since 1.0 |
||
482 | * |
||
483 | * @return boolean |
||
484 | */ |
||
485 | 22 | public function isRequired() { |
|
486 | 22 | return $this->definition->isRequired(); |
|
487 | } |
||
488 | |||
489 | /** |
||
490 | * Returns if the name of the parameter. |
||
491 | * |
||
492 | * @since 1.0 |
||
493 | * |
||
494 | * @return string |
||
495 | */ |
||
496 | public function getName() { |
||
497 | return $this->definition->getName(); |
||
498 | } |
||
499 | |||
500 | /** |
||
501 | * Returns the parameter name aliases. |
||
502 | * |
||
503 | * @since 1.0 |
||
504 | * |
||
505 | * @return string[] |
||
506 | */ |
||
507 | public function getAliases(): array { |
||
508 | return $this->definition->getAliases(); |
||
509 | } |
||
510 | |||
511 | } |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.