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

Processor   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 75.69%

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 9
dl 0
loc 469
ccs 137
cts 181
cp 0.7569
rs 2.88
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getOptions() 0 3 1
A __construct() 0 3 1
A newDefault() 0 3 1
A newFromOptions() 0 3 1
C setFunctionParams() 0 73 13
A setParameterDefinitions() 0 3 1
A setParameters() 0 18 5
A registerNewError() 0 10 1
A registerError() 0 5 1
A validateParameters() 0 3 1
A processParameters() 0 16 4
A newProcessingResult() 0 30 4
A doParamProcessing() 0 9 3
A processOneParam() 0 34 5
A getParamsToProcess() 0 18 5
B getParameterNamesInEvaluationOrder() 0 28 7
A attemptToSetUserValue() 0 18 4
A getParameters() 0 3 1
A getParameter() 0 3 1
A getParameterValues() 0 9 2
A getErrors() 0 3 1
A getErrorMessages() 0 9 2
A hasErrors() 0 3 1
A hasFatalError() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like Processor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Processor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ParamProcessor;
4
5
use ParamProcessor\PackagePrivate\Param;
0 ignored issues
show
Bug introduced by
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 );
0 ignored issues
show
Documentation introduced by
$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
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 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() ) {
0 ignored issues
show
Deprecated Code introduced by
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 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()
0 ignored issues
show
Deprecated Code introduced by
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 12
	private function doParamProcessing() {
285 12
		$this->errors = [];
286
287 12
		$this->getParamsToProcess( [], $this->paramDefinitions );
288
289 12
		while ( $this->paramsToHandle !== [] && !$this->hasFatalError() ) {
0 ignored issues
show
Deprecated Code introduced by
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 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 );
0 ignored issues
show
Deprecated Code introduced by
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 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