Processor::registerError()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace ParamProcessor;
4
5
use ParamProcessor\PackagePrivate\Param;
0 ignored issues
show
Bug introduced by Jeroen De Dauw
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 ParamDefinition[]
47
	 */
48
	private $paramDefinitions = [];
49
50
	/**
51
	 * @var ProcessingError[]
52
	 */
53
	private $errors = [];
54
55
	private $options;
56
57 67
	public function __construct( Options $options ) {
58 67
		$this->options = $options;
59 67
	}
60
61
	/**
62
	 * Constructs and returns a Validator object based on the default options.
63
	 */
64 56
	public static function newDefault(): self {
65 56
		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 50
	public function setParameterDefinitions( array $paramDefinitions ) {
175 50
		$this->paramDefinitions = $paramDefinitions;
176 50
	}
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 65
	public function setParameters( array $parameters, array $paramDefinitions = [] ) {
186 65
		$this->paramDefinitions = ParamDefinition::getCleanDefinitions( $paramDefinitions );
0 ignored issues
show
Documentation introduced by jeroendedauw
$paramDefinitions is of type array<integer,object<Par...ParamDefinition>|array>, but the function expects a array<integer,object<Par...essor\ParamDefinition>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by jeroendedauw
The method ParamProcessor\ParamDefi...::getCleanDefinitions() has been deprecated with message: since 1.7 - use ParamDefinitionFactory

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
187
188
		// Loop through all the user provided parameters, and distinguish between those that are allowed and those that are not.
189 65
		foreach ( $parameters as $paramName => $paramData ) {
190 63
			if ( $this->options->lowercaseNames() ) {
191 61
				$paramName = strtolower( $paramName );
192
			}
193
194 63
			if ( $this->options->trimNames() ) {
195 61
				$paramName = trim( $paramName );
196
			}
197
198 63
			$paramValue = is_array( $paramData ) ? $paramData['original-value'] : $paramData;
199
200 63
			$this->rawParameters[$paramName] = $paramValue;
201
		}
202 65
	}
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 25
	private function registerError( ProcessingError $error ) {
221 25
		$error->element = $this->options->getName();
222 25
		$this->errors[] = $error;
223 25
		ProcessingErrorHandler::addError( $error );
224 25
	}
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 61
	public function processParameters(): ProcessingResult {
237 61
		$this->doParamProcessing();
238
239 61
		if ( !$this->hasFatalError() && $this->options->unknownIsInvalid() ) {
0 ignored issues
show
Deprecated Code introduced by jeroendedauw
The method ParamProcessor\Processor::hasFatalError() has been deprecated with message: since 1.7 - use processParameters() return value

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
240
			// Loop over the remaining raw parameters.
241
			// These are unrecognized parameters, as they where not used by any parameter definition.
242 54
			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 61
		return $this->newProcessingResult();
251
	}
252
253 61
	private function newProcessingResult(): ProcessingResult {
254 61
		$parameters = [];
255
256 61
		if ( !is_array( $this->params ) ) {
257 3
			$this->params = [];
258
		}
259
260 61
		foreach ( $this->params as $parameter ) {
261
			// TODO
262 58
			$processedParam = new ProcessedParam(
263 58
				$parameter->getName(),
264 58
				$parameter->getValue(),
265 58
				$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 58
			if ( !$parameter->wasSetToDefault() ) {
271 43
				$processedParam->setOriginalName( $parameter->getOriginalName() );
272 43
				$processedParam->setOriginalValue( $parameter->getOriginalValue() );
273
			}
274
275 58
			$parameters[$processedParam->getName()] = $processedParam;
276
		}
277
278 61
		return new ProcessingResult(
279 61
			$parameters,
280 61
			$this->getErrors()
0 ignored issues
show
Deprecated Code introduced by jeroendedauw
The method ParamProcessor\Processor::getErrors() has been deprecated with message: since 1.7 - use processParameters() return value

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
281
		);
282
	}
283
284 61
	private function doParamProcessing() {
285 61
		$this->errors = [];
286
287 61
		$this->getParamsToProcess( [], $this->paramDefinitions );
288
289 61
		while ( $this->paramsToHandle !== [] && !$this->hasFatalError() ) {
0 ignored issues
show
Deprecated Code introduced by jeroendedauw
The method ParamProcessor\Processor::hasFatalError() has been deprecated with message: since 1.7 - use processParameters() return value

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
290 60
			$this->processOneParam();
291
		}
292 61
	}
293
294 60
	private function processOneParam() {
295 60
		$paramName = array_shift( $this->paramsToHandle );
296 60
		$definition = $this->paramDefinitions[$paramName];
297
298 60
		$param = new Param( $definition );
299
300 60
		$setUserValue = $this->attemptToSetUserValue( $param );
301
302
		// If the parameter is required but not provided, register a fatal error and stop processing.
303 60
		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 58
		$this->params[$param->getName()] = $param;
313
314 58
		$initialSet = $this->paramDefinitions;
315
316 58
		$param->process( $this->paramDefinitions, $this->params, $this->options );
317
318 58
		foreach ( $param->getErrors() as $error ) {
319 22
			$this->registerError( $error );
320
		}
321
322 58
		if ( $param->hasFatalError() ) {
323 5
			return;
324
		}
325
326 53
		$this->getParamsToProcess( $initialSet, $this->paramDefinitions );
327 53
	}
328
329
	/**
330
	 * Gets an ordered list of parameters to process.
331
	 * @throws \UnexpectedValueException
332
	 */
333 61
	private function getParamsToProcess( array $initialParamSet, array $resultingParamSet ) {
334 61
		if ( $initialParamSet === [] ) {
335 61
			$this->paramsToHandle = array_keys( $resultingParamSet );
0 ignored issues
show
Documentation Bug introduced by Jeroen De Dauw
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 53
			if ( !is_array( $this->paramsToHandle ) ) {
339
				$this->paramsToHandle = [];
340
			}
341
342 53
			foreach ( $resultingParamSet as $paramName => $parameter ) {
343 53
				if ( !array_key_exists( $paramName, $initialParamSet ) ) {
344 1
					$this->paramsToHandle[] = $paramName;
345
				}
346
			}
347
		}
348
349 61
		$this->paramsToHandle = $this->getParameterNamesInEvaluationOrder( $this->paramDefinitions, $this->paramsToHandle );
350 61
	}
351
352
	/**
353
	 * @param ParamDefinition[] $paramDefinitions
354
	 * @param string[] $paramsToHandle
355
	 *
356
	 * @return array
357
	 */
358 61
	private function getParameterNamesInEvaluationOrder( array $paramDefinitions, array $paramsToHandle ): array {
359 61
		$dependencyList = [];
360
361 61
		foreach ( $paramsToHandle as $paramName ) {
362 60
			$dependencies = [];
363
364 60
			if ( !array_key_exists( $paramName, $paramDefinitions ) ) {
365
				throw new \UnexpectedValueException( 'Unexpected parameter name "' . $paramName . '"' );
366
			}
367
368 60
			if ( !is_object( $paramDefinitions[$paramName] ) || !( $paramDefinitions[$paramName] instanceof ParamDefinition ) ) {
369
				throw new \UnexpectedValueException( 'Parameter "' . $paramName . '" is not a ParamDefinition' );
370
			}
371
372
			// Only include dependencies that are in the list of parameters to handle.
373 60
			foreach ( $paramDefinitions[$paramName]->getDependencies() as $dependency ) {
0 ignored issues
show
Deprecated Code introduced by jeroendedauw
The method ParamProcessor\ParamDefinition::getDependencies() has been deprecated with message: since 1.7
Returns a list of dependencies the parameter has, in the form of
other parameter names.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
374
				if ( in_array( $dependency, $paramsToHandle ) ) {
375
					$dependencies[] = $dependency;
376
				}
377
			}
378
379 60
			$dependencyList[$paramName] = $dependencies;
380
		}
381
382 61
		$sorter = new TopologicalSort( $dependencyList, true );
0 ignored issues
show
Deprecated Code introduced by Jeroen De Dauw
The class ParamProcessor\TopologicalSort has been deprecated with message: since 1.7

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
383
384 61
		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 60
	private function attemptToSetUserValue( Param $param ): bool {
393 60
		if ( array_key_exists( $param->getName(), $this->rawParameters ) ) {
394 58
			$param->setUserValue( $param->getName(), $this->rawParameters[$param->getName()], $this->options );
395 58
			unset( $this->rawParameters[$param->getName()] );
396 58
			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 61
	public function getErrors(): array {
446 61
		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 61
	public function hasFatalError() {
475 61
		foreach ( $this->errors as $error ) {
476 25
			if ( $error->isFatal() ) {
477 8
				return $error;
478
			}
479
		}
480
481 61
		return false;
482
	}
483
484
}
485