Completed
Push — master ( d67281...0290cb )
by Jeroen De
01:40
created

Processor   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 491
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 75.42%

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 9
dl 0
loc 491
ccs 135
cts 179
cp 0.7542
rs 2.96
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A newDefault() 0 3 1
A newFromOptions() 0 3 1
A getOptions() 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 33 4
A doParamProcessing() 0 9 3
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
C setFunctionParams() 0 74 13
A processOneParam() 0 34 5

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
/**
6
 * Class for parameter validation of a single parser hook or other parametrized construct.
7
 *
8
 * @since 0.1
9
 *
10
 * @licence GNU GPL v2+
11
 * @author Jeroen De Dauw < [email protected] >
12
 * @author Daniel Werner
13
 */
14
class Processor {
15
16
	/**
17
	 * Flag for unnamed default parameters used in Processor::setFunctionParams() to determine that
18
	 * a parameter should not have a named fallback.
19
	 *
20
	 * @since 0.4.13
21
	 */
22
	const PARAM_UNNAMED = 1;
23
24
	/**
25
	 * @var Param[]
26
	 */
27
	private $params;
28
29
	/**
30
	 * Associative array containing parameter names (keys) and their user-provided data (values).
31
	 * This list is needed because additional parameter definitions can be added to the $parameters
32
	 * field during validation, so we can't determine in advance if a parameter is unknown.
33
	 * @var string[]
34
	 */
35
	private $rawParameters = [];
36
37
	/**
38
	 * Array containing the names of the parameters to handle, ordered by priority.
39
	 * @var string[]
40
	 */
41
	private $paramsToHandle = [];
42
43
	/**
44
	 * @var IParamDefinition[]
45
	 */
46
	private $paramDefinitions = [];
47
48
	/**
49
	 * @var ProcessingError[]
50
	 */
51
	private $errors = [];
52
53
	private $options;
54
55 15
	public function __construct( Options $options ) {
56 15
		$this->options = $options;
57 15
	}
58
59
	/**
60
	 * Constructs and returns a Validator object based on the default options.
61
	 *
62
	 * @since 1.0
63
	 *
64
	 * @return Processor
65
	 */
66 4
	public static function newDefault(): self {
67 4
		return new Processor( new Options() );
68
	}
69
70
	/**
71
	 * Constructs and returns a Validator object based on the provided options.
72
	 *
73
	 * @since 1.0
74
	 *
75
	 * @param Options $options
76
	 *
77
	 * @return Processor
78
	 */
79 11
	public static function newFromOptions( Options $options ): self {
80 11
		return new Processor( $options );
81
	}
82
83
	/**
84
	 * Returns the options used by this Validator object.
85
	 *
86
	 * @since 1.0
87
	 *
88
	 * @return Options
89
	 */
90 1
	public function getOptions(): Options {
91 1
		return $this->options;
92
	}
93
94
	/**
95
	 * Determines the names and values of all parameters. Also takes care of default parameters.
96
	 * After that the resulting parameter list is passed to Processor::setParameters
97
	 *
98
	 * @since 0.4
99
	 *
100
	 * @param string[] $rawParams
101
	 * @param array $parameterDefinitions
102
	 * @param array $defaultParams array of strings or array of arrays to define which parameters can be used unnamed.
103
	 *        The second value in array-form is reserved for flags. Currently, Processor::PARAM_UNNAMED determines that
104
	 *        the parameter has no name which can be used to set it. Therefore all these parameters must be set before
105
	 *        any named parameter. The effect is, that '=' within the string won't confuse the parameter anymore like
106
	 *        it would happen with default parameters that still have a name as well.
107
	 */
108 1
	public function setFunctionParams( array $rawParams, array $parameterDefinitions, array $defaultParams = [] ) {
109 1
		$parameters = [];
110
111 1
		$nr = 0;
112 1
		$defaultNr = 0;
113 1
		$lastUnnamedDefaultNr = -1;
114
115
		/*
116
		 * Find last parameter with self::PARAM_UNNAMED set. Tread all parameters in front as
117
		 * the flag were set for them as well to ensure that there can't be any unnamed params
118
		 * after the first named param. Wouldn't be possible to determine which unnamed value
119
		 * belongs to which parameter otherwise.
120
		 */
121 1
		for( $i = count( $defaultParams ) - 1; $i >= 0 ; $i-- ) {
122
			$dflt = $defaultParams[$i];
123
			if( is_array( $dflt ) && !empty( $dflt[1] ) && ( $dflt[1] | self::PARAM_UNNAMED ) ) {
124
				$lastUnnamedDefaultNr = $i;
125
				break;
126
			}
127
		}
128
129 1
		foreach ( $rawParams as $arg ) {
130
			// Only take into account strings. If the value is not a string,
131
			// it is not a raw parameter, and can not be parsed correctly in all cases.
132 1
			if ( is_string( $arg ) ) {
133 1
				$parts = explode( '=', $arg, ( $nr <= $lastUnnamedDefaultNr ? 1 : 2 ) );
134
135
				// If there is only one part, no parameter name is provided, so try default parameter assignment.
136
				// Default parameters having self::PARAM_UNNAMED set for having no name alias go here in any case.
137 1
				if ( count( $parts ) == 1 ) {
138
					// Default parameter assignment is only possible when there are default parameters!
139
					if ( count( $defaultParams ) > 0 ) {
140
						$defaultParam = array_shift( $defaultParams );
141
						if( is_array( $defaultParam ) ) {
142
							$defaultParam = $defaultParam[0];
143
						}
144
						$defaultParam = strtolower( $defaultParam );
145
146
						$parameters[$defaultParam] = [
147
							'original-value' => trim( $parts[0] ),
148
							'default' => $defaultNr,
149
							'position' => $nr
150
						];
151
						$defaultNr++;
152
					}
153
					else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
154
						// It might be nice to have some sort of warning or error here, as the value is simply ignored.
155
					}
156
				} else {
157 1
					$paramName = trim( strtolower( $parts[0] ) );
158
159 1
					$parameters[$paramName] = [
160 1
						'original-value' => trim( $parts[1] ),
161
						'default' => false,
162 1
						'position' => $nr
163
					];
164
165
					// Let's not be evil, and remove the used parameter name from the default parameter list.
166
					// This code is basically a remove array element by value algorithm.
167 1
					$newDefaults = [];
168
169 1
					foreach( $defaultParams as $defaultParam ) {
170
						if ( $defaultParam != $paramName ) $newDefaults[] = $defaultParam;
171
					}
172
173 1
					$defaultParams = $newDefaults;
174
				}
175
			}
176
177 1
			$nr++;
178
		}
179
180 1
		$this->setParameters( $parameters, $parameterDefinitions );
181 1
	}
182
183
	/**
184
	 * Loops through a list of provided parameters, resolves aliasing and stores errors
185
	 * for unknown parameters and optionally for parameter overriding.
186
	 *
187
	 * @param array $parameters Parameter name as key, parameter value as value
188
	 * @param IParamDefinition[] $paramDefinitions List of parameter definitions. Either ParamDefinition objects or equivalent arrays.
189
	 */
190 13
	public function setParameters( array $parameters, array $paramDefinitions ) {
191 13
		$this->paramDefinitions = ParamDefinition::getCleanDefinitions( $paramDefinitions );
192
193
		// Loop through all the user provided parameters, and distinguish between those that are allowed and those that are not.
194 13
		foreach ( $parameters as $paramName => $paramData ) {
195 11
			if ( $this->options->lowercaseNames() ) {
196 9
				$paramName = strtolower( $paramName );
197
			}
198
199 11
			if ( $this->options->trimNames() ) {
200 9
				$paramName = trim( $paramName );
201
			}
202
203 11
			$paramValue = is_array( $paramData ) ? $paramData['original-value'] : $paramData;
204
205 11
			$this->rawParameters[$paramName] = $paramValue;
206
		}
207 13
	}
208
209
	/**
210
	 * @param string $message
211
	 * @param mixed $tags string or array
212
	 * @param integer $severity
213
	 */
214 4
	private function registerNewError( $message, $tags = [], $severity = ProcessingError::SEVERITY_NORMAL ) {
215 4
		$this->registerError(
216 4
			new ProcessingError(
217 4
				$message,
218 4
				$severity,
219 4
				$this->options->getName(),
220 4
				(array)$tags
221
			)
222
		);
223 4
	}
224
225 5
	private function registerError( ProcessingError $error ) {
226 5
		$error->element = $this->options->getName();
227 5
		$this->errors[] = $error;
228 5
		ProcessingErrorHandler::addError( $error );
229 5
	}
230
231
	/**
232
	 * Validates and formats all the parameters (but aborts when a fatal error occurs).
233
	 *
234
	 * @since 0.4
235
	 * @deprecated since 1.0, use processParameters
236
	 */
237
	public function validateParameters() {
238
		$this->doParamProcessing();
239
	}
240
241 9
	public function processParameters(): ProcessingResult {
242 9
		$this->doParamProcessing();
243
244 9
		if ( !$this->hasFatalError() && $this->options->unknownIsInvalid() ) {
245
			// Loop over the remaining raw parameters.
246
			// These are unrecognized parameters, as they where not used by any parameter definition.
247 7
			foreach ( $this->rawParameters as $paramName => $paramValue ) {
248 2
				$this->registerNewError(
249 2
					$paramName . ' is not a valid parameter', // TODO
250 2
					$paramName
251
				);
252
			}
253
		}
254
255 9
		return $this->newProcessingResult();
256
	}
257
258 9
	private function newProcessingResult(): ProcessingResult {
259 9
		$parameters = [];
260
261 9
		if ( !is_array( $this->params ) ) {
262 3
			$this->params = [];
263
		}
264
265
		/**
266
		 * @var Param $parameter
267
		 */
268 9
		foreach ( $this->params as $parameter ) {
269
			// TODO
270 6
			$processedParam = new ProcessedParam(
271 6
				$parameter->getName(),
272 6
				$parameter->getValue(),
273 6
				$parameter->wasSetToDefault()
274
			);
275
276
			// TODO: it is possible these values where set even when the value defaulted,
277
			// so this logic is not correct and could be improved
278 6
			if ( !$parameter->wasSetToDefault() ) {
279 5
				$processedParam->setOriginalName( $parameter->getOriginalName() );
280 5
				$processedParam->setOriginalValue( $parameter->getOriginalValue() );
281
			}
282
283 6
			$parameters[$processedParam->getName()] = $processedParam;
284
		}
285
286 9
		return new ProcessingResult(
287 9
			$parameters,
288 9
			$this->getErrors()
289
		);
290
	}
291
292
	/**
293
	 * Does the actual parameter processing.
294
	 */
295 9
	private function doParamProcessing() {
296 9
		$this->errors = [];
297
298 9
		$this->getParamsToProcess( [], $this->paramDefinitions );
299
300 9
		while ( $this->paramsToHandle !== [] && !$this->hasFatalError() ) {
301 8
			$this->processOneParam();
302
		}
303 9
	}
304
305 8
	private function processOneParam() {
306 8
		$paramName = array_shift( $this->paramsToHandle );
307 8
		$definition = $this->paramDefinitions[$paramName];
308
309 8
		$param = new Param( $definition );
310
311 8
		$setUserValue = $this->attemptToSetUserValue( $param );
312
313
		// If the parameter is required but not provided, register a fatal error and stop processing.
314 8
		if ( !$setUserValue && $param->isRequired() ) {
315 2
			$this->registerNewError(
316 2
				"Required parameter '$paramName' is missing", // FIXME: i18n validator_error_required_missing
317 2
				[ $paramName, 'missing' ],
318 2
				ProcessingError::SEVERITY_FATAL
319
			);
320 2
			return;
321
		}
322
323 6
		$this->params[$param->getName()] = $param;
324
325 6
		$initialSet = $this->paramDefinitions;
326
327 6
		$param->process( $this->paramDefinitions, $this->params, $this->options );
328
329 6
		foreach ( $param->getErrors() as $error ) {
330 3
			$this->registerError( $error );
331
		}
332
333 6
		if ( $param->hasFatalError() ) {
334 1
			return;
335
		}
336
337 5
		$this->getParamsToProcess( $initialSet, $this->paramDefinitions );
338 5
	}
339
340
	/**
341
	 * Gets an ordered list of parameters to process.
342
	 * @throws \UnexpectedValueException
343
	 */
344 9
	private function getParamsToProcess( array $initialParamSet, array $resultingParamSet ) {
345 9
		if ( $initialParamSet === [] ) {
346 9
			$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...
347
		}
348
		else {
349 5
			if ( !is_array( $this->paramsToHandle ) ) {
350
				$this->paramsToHandle = [];
351
			}
352
353 5
			foreach ( $resultingParamSet as $paramName => $parameter ) {
354 5
				if ( !array_key_exists( $paramName, $initialParamSet ) ) {
355 5
					$this->paramsToHandle[] = $paramName;
356
				}
357
			}
358
		}
359
360 9
		$this->paramsToHandle = $this->getParameterNamesInEvaluationOrder( $this->paramDefinitions, $this->paramsToHandle );
361 9
	}
362
363
	/**
364
	 * @param IParamDefinition[] $paramDefinitions
365
	 * @param string[] $paramsToHandle
366
	 *
367
	 * @return array
368
	 */
369 9
	private function getParameterNamesInEvaluationOrder( array $paramDefinitions, array $paramsToHandle ): array {
370 9
		$dependencyList = [];
371
372 9
		foreach ( $paramsToHandle as $paramName ) {
373 8
			$dependencies = [];
374
375 8
			if ( !array_key_exists( $paramName, $paramDefinitions ) ) {
376
				throw new \UnexpectedValueException( 'Unexpected parameter name "' . $paramName . '"' );
377
			}
378
379 8
			if ( !is_object( $paramDefinitions[$paramName] ) || !( $paramDefinitions[$paramName] instanceof IParamDefinition ) ) {
380
				throw new \UnexpectedValueException( 'Parameter "' . $paramName . '" is not a IParamDefinition' );
381
			}
382
383
			// Only include dependencies that are in the list of parameters to handle.
384 8
			foreach ( $paramDefinitions[$paramName]->getDependencies() as $dependency ) {
385
				if ( in_array( $dependency, $paramsToHandle ) ) {
386
					$dependencies[] = $dependency;
387
				}
388
			}
389
390 8
			$dependencyList[$paramName] = $dependencies;
391
		}
392
393 9
		$sorter = new TopologicalSort( $dependencyList, true );
394
395 9
		return $sorter->doSort();
396
	}
397
398
	/**
399
	 * Tries to find a matching user provided value and, when found, assigns it
400
	 * to the parameter, and removes it from the raw values. Returns a boolean
401
	 * indicating if there was any user value set or not.
402
	 */
403 8
	private function attemptToSetUserValue( Param $param ): bool {
404 8
		if ( array_key_exists( $param->getName(), $this->rawParameters ) ) {
405 6
			$param->setUserValue( $param->getName(), $this->rawParameters[$param->getName()], $this->options );
406 6
			unset( $this->rawParameters[$param->getName()] );
407 6
			return true;
408
		}
409
		else {
410 5
			foreach ( $param->getDefinition()->getAliases() as $alias ) {
411
				if ( array_key_exists( $alias, $this->rawParameters ) ) {
412
					$param->setUserValue( $alias, $this->rawParameters[$alias], $this->options );
413
					unset( $this->rawParameters[$alias] );
414
					return true;
415
				}
416
			}
417
		}
418
419 5
		return false;
420
	}
421
422
	/**
423
	 * Returns the parameters.
424
	 *
425
	 * @since 0.4
426
	 * @deprecated since 1.0
427
	 *
428
	 * @return IParam[]
429
	 */
430
	public function getParameters(): array {
431
		return $this->params;
432
	}
433
434
	/**
435
	 * Returns a single parameter.
436
	 *
437
	 * @since 0.4
438
	 * @deprecated since 1.0
439
	 *
440
	 * @param string $parameterName The name of the parameter to return
441
	 *
442
	 * @return IParam
443
	 */
444
	public function getParameter( string $parameterName ): IParam {
445
		return $this->params[$parameterName];
446
	}
447
448
	/**
449
	 * Returns an associative array with the parameter names as key and their
450
	 * corresponding values as value.
451
	 */
452
	public function getParameterValues(): array {
453
		$parameters = [];
454
455
		foreach ( $this->params as $parameter ) {
456
			$parameters[$parameter->getName()] = $parameter->getValue();
457
		}
458
459
		return $parameters;
460
	}
461
462
	/**
463
	 * @return ProcessingError[]
464
	 */
465 9
	public function getErrors(): array {
466 9
		return $this->errors;
467
	}
468
469
	/**
470
	 * @return string[]
471
	 */
472
	public function getErrorMessages(): array {
473
		$errors = [];
474
475
		foreach ( $this->errors as $error ) {
476
			$errors[] = $error->getMessage();
477
		}
478
479
		return $errors;
480
	}
481
482
	/**
483
	 * Returns if there where any errors during validation.
484
	 */
485
	public function hasErrors(): bool {
486
		return !empty( $this->errors );
487
	}
488
489
	/**
490
	 * Returns false when there are no fatal errors or an ProcessingError when one is found.
491
	 *
492
	 * @return ProcessingError|boolean false
493
	 */
494 9
	public function hasFatalError() {
495 9
		foreach ( $this->errors as $error ) {
496 5
			if ( $error->isFatal() ) {
497 5
				return $error;
498
			}
499
		}
500
501 9
		return false;
502
	}
503
504
}
505