Passed
Push — master ( 761e95...f1e6ba )
by Jeroen De
05:40
created

src/Processor.php (2 issues)

Upgrade to new PHP Analysis Engine

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
This use statement conflicts with another class in this namespace, ParamProcessor\Param.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/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 before OtherDir/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:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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
Documentation Bug introduced by
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