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; |
||
4 | |||
5 | use ParamProcessor\PackagePrivate\Param; |
||
0 ignored issues
–
show
|
|||
6 | |||
7 | /** |
||
8 | * Class for parameter validation of a single parser hook or other parametrized construct. |
||
9 | * |
||
10 | * @since 0.1 |
||
11 | * |
||
12 | * @licence GNU GPL v2+ |
||
13 | * @author Jeroen De Dauw < [email protected] > |
||
14 | * @author Daniel Werner |
||
15 | */ |
||
16 | class Processor { |
||
17 | |||
18 | /** |
||
19 | * Flag for unnamed default parameters used in Processor::setFunctionParams() to determine that |
||
20 | * a parameter should not have a named fallback. |
||
21 | * |
||
22 | * @since 0.4.13 |
||
23 | */ |
||
24 | const PARAM_UNNAMED = 1; |
||
25 | |||
26 | /** |
||
27 | * @var Param[] |
||
28 | */ |
||
29 | private $params; |
||
30 | |||
31 | /** |
||
32 | * Associative array containing parameter names (keys) and their user-provided data (values). |
||
33 | * This list is needed because additional parameter definitions can be added to the $parameters |
||
34 | * field during validation, so we can't determine in advance if a parameter is unknown. |
||
35 | * @var string[] |
||
36 | */ |
||
37 | private $rawParameters = []; |
||
38 | |||
39 | /** |
||
40 | * Array containing the names of the parameters to handle, ordered by priority. |
||
41 | * @var string[] |
||
42 | */ |
||
43 | private $paramsToHandle = []; |
||
44 | |||
45 | /** |
||
46 | * @var IParamDefinition[] |
||
47 | */ |
||
48 | private $paramDefinitions = []; |
||
49 | |||
50 | /** |
||
51 | * @var ProcessingError[] |
||
52 | */ |
||
53 | private $errors = []; |
||
54 | |||
55 | private $options; |
||
56 | |||
57 | 18 | public function __construct( Options $options ) { |
|
58 | 18 | $this->options = $options; |
|
59 | 18 | } |
|
60 | |||
61 | /** |
||
62 | * Constructs and returns a Validator object based on the default options. |
||
63 | */ |
||
64 | 7 | public static function newDefault(): self { |
|
65 | 7 | return new Processor( new Options() ); |
|
66 | } |
||
67 | |||
68 | /** |
||
69 | * Constructs and returns a Validator object based on the provided options. |
||
70 | */ |
||
71 | 11 | public static function newFromOptions( Options $options ): self { |
|
72 | 11 | return new Processor( $options ); |
|
73 | } |
||
74 | |||
75 | /** |
||
76 | * Returns the options used by this Validator object. |
||
77 | */ |
||
78 | 1 | public function getOptions(): Options { |
|
79 | 1 | return $this->options; |
|
80 | } |
||
81 | |||
82 | /** |
||
83 | * Determines the names and values of all parameters. Also takes care of default parameters. |
||
84 | * After that the resulting parameter list is passed to Processor::setParameters |
||
85 | * |
||
86 | * @since 0.4 |
||
87 | * |
||
88 | * @param string[] $rawParams |
||
89 | * @param ParamDefinition[]|array[] $parameterDefinitions DEPRECATED! Use @see setParameterDefinitions instead |
||
90 | * @param array $defaultParams array of strings or array of arrays to define which parameters can be used unnamed. |
||
91 | * The second value in array-form is reserved for flags. Currently, Processor::PARAM_UNNAMED determines that |
||
92 | * the parameter has no name which can be used to set it. Therefore all these parameters must be set before |
||
93 | * any named parameter. The effect is, that '=' within the string won't confuse the parameter anymore like |
||
94 | * it would happen with default parameters that still have a name as well. |
||
95 | */ |
||
96 | 4 | public function setFunctionParams( array $rawParams, array $parameterDefinitions = [], array $defaultParams = [] ) { |
|
97 | 4 | $lastUnnamedDefaultNr = -1; |
|
98 | |||
99 | /* |
||
100 | * Find last parameter with self::PARAM_UNNAMED set. Tread all parameters in front as |
||
101 | * the flag were set for them as well to ensure that there can't be any unnamed params |
||
102 | * after the first named param. Wouldn't be possible to determine which unnamed value |
||
103 | * belongs to which parameter otherwise. |
||
104 | */ |
||
105 | 4 | for( $i = count( $defaultParams ) - 1; $i >= 0; $i-- ) { |
|
106 | $dflt = $defaultParams[$i]; |
||
107 | if( is_array( $dflt ) && !empty( $dflt[1] ) && ( $dflt[1] | self::PARAM_UNNAMED ) ) { |
||
108 | $lastUnnamedDefaultNr = $i; |
||
109 | break; |
||
110 | } |
||
111 | } |
||
112 | |||
113 | 4 | $parameters = []; |
|
114 | 4 | $nr = 0; |
|
115 | 4 | $defaultNr = 0; |
|
116 | |||
117 | 4 | foreach ( $rawParams as $arg ) { |
|
118 | // Only take into account strings. If the value is not a string, |
||
119 | // it is not a raw parameter, and can not be parsed correctly in all cases. |
||
120 | 4 | if ( is_string( $arg ) ) { |
|
121 | 4 | $parts = explode( '=', $arg, ( $nr <= $lastUnnamedDefaultNr ? 1 : 2 ) ); |
|
122 | |||
123 | // If there is only one part, no parameter name is provided, so try default parameter assignment. |
||
124 | // Default parameters having self::PARAM_UNNAMED set for having no name alias go here in any case. |
||
125 | 4 | if ( count( $parts ) == 1 ) { |
|
126 | // Default parameter assignment is only possible when there are default parameters! |
||
127 | if ( count( $defaultParams ) > 0 ) { |
||
128 | $defaultParam = array_shift( $defaultParams ); |
||
129 | if( is_array( $defaultParam ) ) { |
||
130 | $defaultParam = $defaultParam[0]; |
||
131 | } |
||
132 | $defaultParam = strtolower( $defaultParam ); |
||
133 | |||
134 | $parameters[$defaultParam] = [ |
||
135 | 'original-value' => trim( $parts[0] ), |
||
136 | 'default' => $defaultNr, |
||
137 | 'position' => $nr |
||
138 | ]; |
||
139 | $defaultNr++; |
||
140 | } |
||
141 | } else { |
||
142 | 4 | $paramName = trim( strtolower( $parts[0] ) ); |
|
143 | |||
144 | 4 | $parameters[$paramName] = [ |
|
145 | 4 | 'original-value' => trim( $parts[1] ), |
|
146 | 'default' => false, |
||
147 | 4 | 'position' => $nr |
|
148 | ]; |
||
149 | |||
150 | // Let's not be evil, and remove the used parameter name from the default parameter list. |
||
151 | // This code is basically a remove array element by value algorithm. |
||
152 | 4 | $newDefaults = []; |
|
153 | |||
154 | 4 | foreach( $defaultParams as $defaultParam ) { |
|
155 | if ( $defaultParam != $paramName ) { |
||
156 | $newDefaults[] = $defaultParam; |
||
157 | } |
||
158 | } |
||
159 | |||
160 | 4 | $defaultParams = $newDefaults; |
|
161 | } |
||
162 | } |
||
163 | |||
164 | 4 | $nr++; |
|
165 | } |
||
166 | |||
167 | 4 | $this->setParameters( $parameters, $parameterDefinitions ); |
|
168 | 4 | } |
|
169 | |||
170 | /** |
||
171 | * @since 1.6.0 |
||
172 | * @param ParamDefinition[] $paramDefinitions |
||
173 | */ |
||
174 | 1 | public function setParameterDefinitions( array $paramDefinitions ) { |
|
175 | 1 | $this->paramDefinitions = $paramDefinitions; |
|
176 | 1 | } |
|
177 | |||
178 | /** |
||
179 | * Loops through a list of provided parameters, resolves aliasing and stores errors |
||
180 | * for unknown parameters and optionally for parameter overriding. |
||
181 | * |
||
182 | * @param array $parameters Parameter name as key, parameter value as value |
||
183 | * @param ParamDefinition[]|array[] $paramDefinitions DEPRECATED! Use @see setParameterDefinitions instead |
||
184 | */ |
||
185 | 16 | public function setParameters( array $parameters, array $paramDefinitions = [] ) { |
|
186 | 16 | $this->paramDefinitions = ParamDefinition::getCleanDefinitions( $paramDefinitions ); |
|
187 | |||
188 | // Loop through all the user provided parameters, and distinguish between those that are allowed and those that are not. |
||
189 | 16 | foreach ( $parameters as $paramName => $paramData ) { |
|
190 | 14 | if ( $this->options->lowercaseNames() ) { |
|
191 | 12 | $paramName = strtolower( $paramName ); |
|
192 | } |
||
193 | |||
194 | 14 | if ( $this->options->trimNames() ) { |
|
195 | 12 | $paramName = trim( $paramName ); |
|
196 | } |
||
197 | |||
198 | 14 | $paramValue = is_array( $paramData ) ? $paramData['original-value'] : $paramData; |
|
199 | |||
200 | 14 | $this->rawParameters[$paramName] = $paramValue; |
|
201 | } |
||
202 | 16 | } |
|
203 | |||
204 | /** |
||
205 | * @param string $message |
||
206 | * @param string[] $tags |
||
207 | * @param integer $severity |
||
208 | */ |
||
209 | 5 | private function registerNewError( string $message, array $tags = [], int $severity = ProcessingError::SEVERITY_NORMAL ) { |
|
210 | 5 | $this->registerError( |
|
211 | 5 | new ProcessingError( |
|
212 | 5 | $message, |
|
213 | $severity, |
||
214 | 5 | $this->options->getName(), |
|
215 | 5 | (array)$tags |
|
216 | ) |
||
217 | ); |
||
218 | 5 | } |
|
219 | |||
220 | 8 | private function registerError( ProcessingError $error ) { |
|
221 | 8 | $error->element = $this->options->getName(); |
|
222 | 8 | $this->errors[] = $error; |
|
223 | 8 | ProcessingErrorHandler::addError( $error ); |
|
224 | 8 | } |
|
225 | |||
226 | /** |
||
227 | * Validates and formats all the parameters (but aborts when a fatal error occurs). |
||
228 | * |
||
229 | * @since 0.4 |
||
230 | * @deprecated since 1.0, use processParameters |
||
231 | */ |
||
232 | public function validateParameters() { |
||
233 | $this->doParamProcessing(); |
||
234 | } |
||
235 | |||
236 | 12 | public function processParameters(): ProcessingResult { |
|
237 | 12 | $this->doParamProcessing(); |
|
238 | |||
239 | 12 | if ( !$this->hasFatalError() && $this->options->unknownIsInvalid() ) { |
|
240 | // Loop over the remaining raw parameters. |
||
241 | // These are unrecognized parameters, as they where not used by any parameter definition. |
||
242 | 9 | foreach ( $this->rawParameters as $paramName => $paramValue ) { |
|
243 | 2 | $this->registerNewError( |
|
244 | 2 | $paramName . ' is not a valid parameter', // TODO |
|
245 | 2 | [ $paramName ] |
|
246 | ); |
||
247 | } |
||
248 | } |
||
249 | |||
250 | 12 | return $this->newProcessingResult(); |
|
251 | } |
||
252 | |||
253 | 12 | private function newProcessingResult(): ProcessingResult { |
|
254 | 12 | $parameters = []; |
|
255 | |||
256 | 12 | if ( !is_array( $this->params ) ) { |
|
257 | 3 | $this->params = []; |
|
258 | } |
||
259 | |||
260 | 12 | foreach ( $this->params as $parameter ) { |
|
261 | // TODO |
||
262 | 9 | $processedParam = new ProcessedParam( |
|
263 | 9 | $parameter->getName(), |
|
264 | 9 | $parameter->getValue(), |
|
265 | 9 | $parameter->wasSetToDefault() |
|
266 | ); |
||
267 | |||
268 | // TODO: it is possible these values where set even when the value defaulted, |
||
269 | // so this logic is not correct and could be improved |
||
270 | 9 | if ( !$parameter->wasSetToDefault() ) { |
|
271 | 7 | $processedParam->setOriginalName( $parameter->getOriginalName() ); |
|
272 | 7 | $processedParam->setOriginalValue( $parameter->getOriginalValue() ); |
|
273 | } |
||
274 | |||
275 | 9 | $parameters[$processedParam->getName()] = $processedParam; |
|
276 | } |
||
277 | |||
278 | 12 | return new ProcessingResult( |
|
279 | 12 | $parameters, |
|
280 | 12 | $this->getErrors() |
|
281 | ); |
||
282 | } |
||
283 | |||
284 | 12 | private function doParamProcessing() { |
|
285 | 12 | $this->errors = []; |
|
286 | |||
287 | 12 | $this->getParamsToProcess( [], $this->paramDefinitions ); |
|
288 | |||
289 | 12 | while ( $this->paramsToHandle !== [] && !$this->hasFatalError() ) { |
|
290 | 11 | $this->processOneParam(); |
|
291 | } |
||
292 | 12 | } |
|
293 | |||
294 | 11 | private function processOneParam() { |
|
295 | 11 | $paramName = array_shift( $this->paramsToHandle ); |
|
296 | 11 | $definition = $this->paramDefinitions[$paramName]; |
|
297 | |||
298 | 11 | $param = new Param( $definition ); |
|
299 | |||
300 | 11 | $setUserValue = $this->attemptToSetUserValue( $param ); |
|
301 | |||
302 | // If the parameter is required but not provided, register a fatal error and stop processing. |
||
303 | 11 | if ( !$setUserValue && $param->isRequired() ) { |
|
304 | 3 | $this->registerNewError( |
|
305 | 3 | "Required parameter '$paramName' is missing", // FIXME: i18n validator_error_required_missing |
|
306 | 3 | [ $paramName, 'missing' ], |
|
307 | 3 | ProcessingError::SEVERITY_FATAL |
|
308 | ); |
||
309 | 3 | return; |
|
310 | } |
||
311 | |||
312 | 9 | $this->params[$param->getName()] = $param; |
|
313 | |||
314 | 9 | $initialSet = $this->paramDefinitions; |
|
315 | |||
316 | 9 | $param->process( $this->paramDefinitions, $this->params, $this->options ); |
|
317 | |||
318 | 9 | foreach ( $param->getErrors() as $error ) { |
|
319 | 5 | $this->registerError( $error ); |
|
320 | } |
||
321 | |||
322 | 9 | if ( $param->hasFatalError() ) { |
|
323 | 1 | return; |
|
324 | } |
||
325 | |||
326 | 8 | $this->getParamsToProcess( $initialSet, $this->paramDefinitions ); |
|
327 | 8 | } |
|
328 | |||
329 | /** |
||
330 | * Gets an ordered list of parameters to process. |
||
331 | * @throws \UnexpectedValueException |
||
332 | */ |
||
333 | 12 | private function getParamsToProcess( array $initialParamSet, array $resultingParamSet ) { |
|
334 | 12 | if ( $initialParamSet === [] ) { |
|
335 | 12 | $this->paramsToHandle = array_keys( $resultingParamSet ); |
|
0 ignored issues
–
show
It seems like
array_keys($resultingParamSet) of type array<integer,integer|string> is incompatible with the declared type array<integer,string> of property $paramsToHandle .
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property. Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..
Loading history...
|
|||
336 | } |
||
337 | else { |
||
338 | 8 | if ( !is_array( $this->paramsToHandle ) ) { |
|
339 | $this->paramsToHandle = []; |
||
340 | } |
||
341 | |||
342 | 8 | foreach ( $resultingParamSet as $paramName => $parameter ) { |
|
343 | 8 | if ( !array_key_exists( $paramName, $initialParamSet ) ) { |
|
344 | 1 | $this->paramsToHandle[] = $paramName; |
|
345 | } |
||
346 | } |
||
347 | } |
||
348 | |||
349 | 12 | $this->paramsToHandle = $this->getParameterNamesInEvaluationOrder( $this->paramDefinitions, $this->paramsToHandle ); |
|
350 | 12 | } |
|
351 | |||
352 | /** |
||
353 | * @param IParamDefinition[] $paramDefinitions |
||
354 | * @param string[] $paramsToHandle |
||
355 | * |
||
356 | * @return array |
||
357 | */ |
||
358 | 12 | private function getParameterNamesInEvaluationOrder( array $paramDefinitions, array $paramsToHandle ): array { |
|
359 | 12 | $dependencyList = []; |
|
360 | |||
361 | 12 | foreach ( $paramsToHandle as $paramName ) { |
|
362 | 11 | $dependencies = []; |
|
363 | |||
364 | 11 | if ( !array_key_exists( $paramName, $paramDefinitions ) ) { |
|
365 | throw new \UnexpectedValueException( 'Unexpected parameter name "' . $paramName . '"' ); |
||
366 | } |
||
367 | |||
368 | 11 | if ( !is_object( $paramDefinitions[$paramName] ) || !( $paramDefinitions[$paramName] instanceof IParamDefinition ) ) { |
|
369 | throw new \UnexpectedValueException( 'Parameter "' . $paramName . '" is not a IParamDefinition' ); |
||
370 | } |
||
371 | |||
372 | // Only include dependencies that are in the list of parameters to handle. |
||
373 | 11 | foreach ( $paramDefinitions[$paramName]->getDependencies() as $dependency ) { |
|
374 | if ( in_array( $dependency, $paramsToHandle ) ) { |
||
375 | $dependencies[] = $dependency; |
||
376 | } |
||
377 | } |
||
378 | |||
379 | 11 | $dependencyList[$paramName] = $dependencies; |
|
380 | } |
||
381 | |||
382 | 12 | $sorter = new TopologicalSort( $dependencyList, true ); |
|
383 | |||
384 | 12 | return $sorter->doSort(); |
|
385 | } |
||
386 | |||
387 | /** |
||
388 | * Tries to find a matching user provided value and, when found, assigns it |
||
389 | * to the parameter, and removes it from the raw values. Returns a boolean |
||
390 | * indicating if there was any user value set or not. |
||
391 | */ |
||
392 | 11 | private function attemptToSetUserValue( Param $param ): bool { |
|
393 | 11 | if ( array_key_exists( $param->getName(), $this->rawParameters ) ) { |
|
394 | 9 | $param->setUserValue( $param->getName(), $this->rawParameters[$param->getName()], $this->options ); |
|
395 | 9 | unset( $this->rawParameters[$param->getName()] ); |
|
396 | 9 | return true; |
|
397 | } |
||
398 | else { |
||
399 | 6 | foreach ( $param->getDefinition()->getAliases() as $alias ) { |
|
400 | if ( array_key_exists( $alias, $this->rawParameters ) ) { |
||
401 | $param->setUserValue( $alias, $this->rawParameters[$alias], $this->options ); |
||
402 | unset( $this->rawParameters[$alias] ); |
||
403 | return true; |
||
404 | } |
||
405 | } |
||
406 | } |
||
407 | |||
408 | 6 | return false; |
|
409 | } |
||
410 | |||
411 | /** |
||
412 | * @deprecated since 1.0 |
||
413 | * @return Param[] |
||
414 | */ |
||
415 | public function getParameters(): array { |
||
416 | return $this->params; |
||
417 | } |
||
418 | |||
419 | /** |
||
420 | * @deprecated since 1.0 |
||
421 | */ |
||
422 | public function getParameter( string $parameterName ): Param { |
||
423 | return $this->params[$parameterName]; |
||
424 | } |
||
425 | |||
426 | /** |
||
427 | * Returns an associative array with the parameter names as key and their |
||
428 | * corresponding values as value. |
||
429 | * @deprecated since 1.7 - use processParameters() return value |
||
430 | */ |
||
431 | public function getParameterValues(): array { |
||
432 | $parameters = []; |
||
433 | |||
434 | foreach ( $this->params as $parameter ) { |
||
435 | $parameters[$parameter->getName()] = $parameter->getValue(); |
||
436 | } |
||
437 | |||
438 | return $parameters; |
||
439 | } |
||
440 | |||
441 | /** |
||
442 | * @deprecated since 1.7 - use processParameters() return value |
||
443 | * @return ProcessingError[] |
||
444 | */ |
||
445 | 12 | public function getErrors(): array { |
|
446 | 12 | return $this->errors; |
|
447 | } |
||
448 | |||
449 | /** |
||
450 | * @deprecated since 1.7 - use processParameters() return value |
||
451 | * @return string[] |
||
452 | */ |
||
453 | public function getErrorMessages(): array { |
||
454 | $errors = []; |
||
455 | |||
456 | foreach ( $this->errors as $error ) { |
||
457 | $errors[] = $error->getMessage(); |
||
458 | } |
||
459 | |||
460 | return $errors; |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * @deprecated since 1.7 - use processParameters() return value |
||
465 | */ |
||
466 | public function hasErrors(): bool { |
||
467 | return !empty( $this->errors ); |
||
468 | } |
||
469 | |||
470 | /** |
||
471 | * @deprecated since 1.7 - use processParameters() return value |
||
472 | * @return ProcessingError|boolean false |
||
473 | */ |
||
474 | 12 | public function hasFatalError() { |
|
475 | 12 | foreach ( $this->errors as $error ) { |
|
476 | 8 | if ( $error->isFatal() ) { |
|
477 | 4 | return $error; |
|
478 | } |
||
479 | } |
||
480 | |||
481 | 12 | return false; |
|
482 | } |
||
483 | |||
484 | } |
||
485 |
Let’s assume that you have a directory layout like this:
and let’s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: