Processor   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 75.69%

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 8
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 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 59
	public function __construct( Options $options ) {
58 59
		$this->options = $options;
59 59
	}
60
61
	/**
62
	 * Constructs and returns a Validator object based on the default options.
63
	 */
64 48
	public static function newDefault(): self {
65 48
		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 42
	public function setParameterDefinitions( array $paramDefinitions ) {
175 42
		$this->paramDefinitions = $paramDefinitions;
176 42
	}
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 57
	public function setParameters( array $parameters, array $paramDefinitions = [] ) {
186 57
		$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 57
		foreach ( $parameters as $paramName => $paramData ) {
190 55
			if ( $this->options->lowercaseNames() ) {
191 53
				$paramName = strtolower( $paramName );
192
			}
193
194 55
			if ( $this->options->trimNames() ) {
195 53
				$paramName = trim( $paramName );
196
			}
197
198 55
			$paramValue = is_array( $paramData ) ? $paramData['original-value'] : $paramData;
199
200 55
			$this->rawParameters[$paramName] = $paramValue;
201
		}
202 57
	}
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 19
	private function registerError( ProcessingError $error ) {
221 19
		$error->element = $this->options->getName();
222 19
		$this->errors[] = $error;
223 19
		ProcessingErrorHandler::addError( $error );
224 19
	}
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 53
	public function processParameters(): ProcessingResult {
237 53
		$this->doParamProcessing();
238
239 53
		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 47
			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 53
		return $this->newProcessingResult();
251
	}
252
253 53
	private function newProcessingResult(): ProcessingResult {
254 53
		$parameters = [];
255
256 53
		if ( !is_array( $this->params ) ) {
257 3
			$this->params = [];
258
		}
259
260 53
		foreach ( $this->params as $parameter ) {
261
			// TODO
262 50
			$processedParam = new ProcessedParam(
263 50
				$parameter->getName(),
264 50
				$parameter->getValue(),
265 50
				$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 50
			if ( !$parameter->wasSetToDefault() ) {
271 40
				$processedParam->setOriginalName( $parameter->getOriginalName() );
272 40
				$processedParam->setOriginalValue( $parameter->getOriginalValue() );
273
			}
274
275 50
			$parameters[$processedParam->getName()] = $processedParam;
276
		}
277
278 53
		return new ProcessingResult(
279 53
			$parameters,
280 53
			$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 53
	private function doParamProcessing() {
285 53
		$this->errors = [];
286
287 53
		$this->getParamsToProcess( [], $this->paramDefinitions );
288
289 53
		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 52
			$this->processOneParam();
291
		}
292 53
	}
293
294 52
	private function processOneParam() {
295 52
		$paramName = array_shift( $this->paramsToHandle );
296 52
		$definition = $this->paramDefinitions[$paramName];
297
298 52
		$param = new Param( $definition );
299
300 52
		$setUserValue = $this->attemptToSetUserValue( $param );
301
302
		// If the parameter is required but not provided, register a fatal error and stop processing.
303 52
		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 50
		$this->params[$param->getName()] = $param;
313
314 50
		$initialSet = $this->paramDefinitions;
315
316 50
		$param->process( $this->paramDefinitions, $this->params, $this->options );
317
318 50
		foreach ( $param->getErrors() as $error ) {
319 16
			$this->registerError( $error );
320
		}
321
322 50
		if ( $param->hasFatalError() ) {
323 4
			return;
324
		}
325
326 46
		$this->getParamsToProcess( $initialSet, $this->paramDefinitions );
327 46
	}
328
329
	/**
330
	 * Gets an ordered list of parameters to process.
331
	 * @throws \UnexpectedValueException
332
	 */
333 53
	private function getParamsToProcess( array $initialParamSet, array $resultingParamSet ) {
334 53
		if ( $initialParamSet === [] ) {
335 53
			$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 46
			if ( !is_array( $this->paramsToHandle ) ) {
339
				$this->paramsToHandle = [];
340
			}
341
342 46
			foreach ( $resultingParamSet as $paramName => $parameter ) {
343 46
				if ( !array_key_exists( $paramName, $initialParamSet ) ) {
344 1
					$this->paramsToHandle[] = $paramName;
345
				}
346
			}
347
		}
348
349 53
		$this->paramsToHandle = $this->getParameterNamesInEvaluationOrder( $this->paramDefinitions, $this->paramsToHandle );
350 53
	}
351
352
	/**
353
	 * @param ParamDefinition[] $paramDefinitions
354
	 * @param string[] $paramsToHandle
355
	 *
356
	 * @return array
357
	 */
358 53
	private function getParameterNamesInEvaluationOrder( array $paramDefinitions, array $paramsToHandle ): array {
359 53
		$dependencyList = [];
360
361 53
		foreach ( $paramsToHandle as $paramName ) {
362 52
			$dependencies = [];
363
364 52
			if ( !array_key_exists( $paramName, $paramDefinitions ) ) {
365
				throw new \UnexpectedValueException( 'Unexpected parameter name "' . $paramName . '"' );
366
			}
367
368 52
			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 52
			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 52
			$dependencyList[$paramName] = $dependencies;
380
		}
381
382 53
		$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 53
		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 52
	private function attemptToSetUserValue( Param $param ): bool {
393 52
		if ( array_key_exists( $param->getName(), $this->rawParameters ) ) {
394 50
			$param->setUserValue( $param->getName(), $this->rawParameters[$param->getName()], $this->options );
395 50
			unset( $this->rawParameters[$param->getName()] );
396 50
			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 53
	public function getErrors(): array {
446 53
		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 53
	public function hasFatalError() {
475 53
		foreach ( $this->errors as $error ) {
476 19
			if ( $error->isFatal() ) {
477 7
				return $error;
478
			}
479
		}
480
481 53
		return false;
482
	}
483
484
}
485