Passed
Pull Request — master (#33)
by Stephan
02:03
created

Processor   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 557
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 81.67%

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 9
dl 0
loc 557
ccs 147
cts 180
cp 0.8167
rs 2.88
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
C setFunctionParams() 0 76 14
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 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
/**
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
	 * Array containing the parameters.
26
	 *
27
	 * @since 0.4
28
	 *
29
	 * @var Param[]
30
	 */
31
	private $params;
32
33
	/**
34
	 * Associative array containing parameter names (keys) and their user-provided data (values).
35
	 * This list is needed because additional parameter definitions can be added to the $parameters
36
	 * field during validation, so we can't determine in advance if a parameter is unknown.
37
	 *
38
	 * @since 0.4
39
	 *
40
	 * @var string[]
41
	 */
42
	private $rawParameters = [];
43
44
	/**
45
	 * Array containing the names of the parameters to handle, ordered by priority.
46
	 *
47
	 * @since 0.4
48
	 *
49
	 * @var string[]
50
	 */
51
	private $paramsToHandle = [];
52
53
	/**
54
	 *
55
	 *
56
	 * @since 1.0
57
	 *
58
	 * @var IParamDefinition[]
59
	 */
60
	private $paramDefinitions = [];
61
62
	/**
63
	 * List of ProcessingError.
64
	 *
65
	 * @since 0.4
66
	 *
67
	 * @var ProcessingError[]
68
	 */
69
	private $errors = [];
70
71
	/**
72
	 * Options for this validator object.
73
	 *
74
	 * @since 1.0
75
	 *
76
	 * @var Options
77
	 */
78
	private $options;
79
80
	/**
81
	 * Constructor.
82
	 *
83
	 * @param Options $options
84
	 *
85
	 * @since 1.0
86
	 */
87 19
	public function __construct( Options $options ) {
88 19
		$this->options = $options;
89 19
	}
90
91
	/**
92
	 * Constructs and returns a Validator object based on the default options.
93
	 *
94
	 * @since 1.0
95
	 *
96
	 * @return Processor
97
	 */
98 4
	public static function newDefault() {
99 4
		return new Processor( new Options() );
100
	}
101
102
	/**
103
	 * Constructs and returns a Validator object based on the provided options.
104
	 *
105
	 * @since 1.0
106
	 *
107
	 * @param Options $options
108
	 *
109
	 * @return Processor
110
	 */
111 15
	public static function newFromOptions( Options $options ) {
112 15
		return new Processor( $options );
113
	}
114
115
	/**
116
	 * Returns the options used by this Validator object.
117
	 *
118
	 * @since 1.0
119
	 *
120
	 * @return Options
121
	 */
122 1
	public function getOptions() {
123 1
		return $this->options;
124
	}
125
126
	/**
127
	 * Determines the names and values of all parameters. Also takes care of default parameters.
128
	 * After that the resulting parameter list is passed to Processor::setParameters
129
	 *
130
	 * @since 0.4
131
	 *
132
	 * @param array $rawParams
133
	 * @param array $parameterInfo
134
	 * @param array $defaultParams array of strings or array of arrays to define which parameters can be used unnamed.
135
	 *        The second value in array-form is reserved for flags. Currently, Processor::PARAM_UNNAMED determines that
136
	 *        the parameter has no name which can be used to set it. Therefore all these parameters must be set before
137
	 *        any named parameter. The effect is, that '=' within the string won't confuse the parameter anymore like
138
	 *        it would happen with default parameters that still have a name as well.
139
	 */
140 4
	public function setFunctionParams( array $rawParams, array $parameterInfo, array $defaultParams = [] ) {
141 4
		$parameters = [];
142
143 4
		$nr = 0;
144 4
		$defaultNr = 0;
145 4
		$lastUnnamedDefaultNr = -1;
146
147
		/*
148
		 * Find last parameter with self::PARAM_UNNAMED set. Tread all parameters in front as
149
		 * the flag were set for them as well to ensure that there can't be any unnamed params
150
		 * after the first named param. Wouldn't be possible to determine which unnamed value
151
		 * belongs to which parameter otherwise.
152
		 */
153 4
		for( $i = count( $defaultParams ) - 1; $i >= 0 ; $i-- ) {
154 1
			$dflt = $defaultParams[$i];
155 1
			if( is_array( $dflt ) && !empty( $dflt[1] ) && ( $dflt[1] | self::PARAM_UNNAMED ) ) {
156
				$lastUnnamedDefaultNr = $i;
157
				break;
158
			}
159
		}
160
161 4
		$definedParams = array_keys( $parameterInfo );
162
163 4
		foreach ( $rawParams as $arg ) {
164
			// Only take into account strings. If the value is not a string,
165
			// it is not a raw parameter, and can not be parsed correctly in all cases.
166 4
			if ( is_string( $arg ) ) {
167 4
				$parts = explode( '=', $arg, ( $nr <= $lastUnnamedDefaultNr ? 1 : 2 ) );
168 4
				$paramName = trim( strtolower( $parts[0] ) );
169
170
				// If there is only one part, no parameter name is provided, so try default parameter assignment.
171
				// Default parameters having self::PARAM_UNNAMED set for having no name alias go here in any case.
172 4
				if ( count( $parts ) == 1 || !in_array( $paramName, $definedParams ) ) {
173
					// Default parameter assignment is only possible when there are default parameters!
174 1
					if ( count( $defaultParams ) > 0 ) {
175 1
						$defaultParam = array_shift( $defaultParams );
176 1
						if( is_array( $defaultParam ) ) {
177
							$defaultParam = $defaultParam[0];
178
						}
179 1
						$defaultParam = strtolower( $defaultParam );
180
181 1
						$parameters[$defaultParam] = [
182 1
							'original-value' => trim( $arg ),
183 1
							'default' => $defaultNr,
184 1
							'position' => $nr
185
						];
186 1
						$defaultNr++;
187
					}
188 1
					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...
189
						// It might be nice to have some sort of warning or error here, as the value is simply ignored.
190
					}
191
				} else {
192
193 3
					$parameters[$paramName] = [
194 3
						'original-value' => trim( $parts[1] ),
195
						'default' => false,
196 3
						'position' => $nr
197
					];
198
199
					// Let's not be evil, and remove the used parameter name from the default parameter list.
200
					// This code is basically a remove array element by value algorithm.
201 3
					$newDefaults = [];
202
203 3
					foreach( $defaultParams as $defaultParam ) {
204
						if ( $defaultParam != $paramName ) $newDefaults[] = $defaultParam;
205
					}
206
207 3
					$defaultParams = $newDefaults;
208
				}
209
			}
210
211 4
			$nr++;
212
		}
213
214 4
		$this->setParameters( $parameters, $parameterInfo );
215 4
	}
216
217
	/**
218
	 * Loops through a list of provided parameters, resolves aliasing and stores errors
219
	 * for unknown parameters and optionally for parameter overriding.
220
	 *
221
	 * @param array $parameters Parameter name as key, parameter value as value
222
	 * @param IParamDefinition[] $paramDefinitions List of parameter definitions. Either ParamDefinition objects or equivalent arrays.
223
	 */
224 16
	public function setParameters( array $parameters, array $paramDefinitions ) {
225 16
		$this->paramDefinitions = ParamDefinition::getCleanDefinitions( $paramDefinitions );
226
227
		// Loop through all the user provided parameters, and distinguish between those that are allowed and those that are not.
228 16
		foreach ( $parameters as $paramName => $paramData ) {
229 14
			if ( $this->options->lowercaseNames() ) {
230 12
				$paramName = strtolower( $paramName );
231
			}
232
233 14
			if ( $this->options->trimNames() ) {
234 12
				$paramName = trim( $paramName );
235
			}
236
237 14
			$paramValue = is_array( $paramData ) ? $paramData['original-value'] : $paramData;
238
239 14
			$this->rawParameters[$paramName] = $paramValue;
240
		}
241 16
	}
242
243
	/**
244
	 * @param string $message
245
	 * @param mixed $tags string or array
246
	 * @param integer $severity
247
	 */
248 4
	private function registerNewError( $message, $tags = [], $severity = ProcessingError::SEVERITY_NORMAL ) {
249 4
		$this->registerError(
250 4
			new ProcessingError(
251 4
				$message,
252 4
				$severity,
253 4
				$this->options->getName(),
254 4
				(array)$tags
255
			)
256
		);
257 4
	}
258
259 4
	private function registerError( ProcessingError $error ) {
260 4
		$error->element = $this->options->getName();
261 4
		$this->errors[] = $error;
262 4
		ProcessingErrorHandler::addError( $error );
263 4
	}
264
265
	/**
266
	 * Validates and formats all the parameters (but aborts when a fatal error occurs).
267
	 *
268
	 * @since 0.4
269
	 * @deprecated since 1.0, use processParameters
270
	 */
271
	public function validateParameters() {
272
		$this->doParamProcessing();
273
	}
274
275
	/**
276
	 * @since 1.0
277
	 *
278
	 * @return ProcessingResult
279
	 */
280 12
	public function processParameters() {
281 12
		$this->doParamProcessing();
282
283 12
		if ( !$this->hasFatalError() && $this->options->unknownIsInvalid() ) {
284
			// Loop over the remaining raw parameters.
285
			// These are unrecognized parameters, as they where not used by any parameter definition.
286 11
			foreach ( $this->rawParameters as $paramName => $paramValue ) {
287 2
				$this->registerNewError(
288 2
					$paramName . ' is not a valid parameter', // TODO
289 2
					$paramName
290
				);
291
			}
292
		}
293
294 12
		return $this->newProcessingResult();
295
	}
296
297
	/**
298
	 * @return ProcessingResult
299
	 */
300 12
	private function newProcessingResult() {
301 12
		$parameters = [];
302
303 12
		if ( !is_array( $this->params ) ) {
304 3
			$this->params = [];
305
		}
306
307
		/**
308
		 * @var Param $parameter
309
		 */
310 12
		foreach ( $this->params as $parameter ) {
311
			// TODO
312 9
			$processedParam = new ProcessedParam(
313 9
				$parameter->getName(),
314 9
				$parameter->getValue(),
315 9
				$parameter->wasSetToDefault()
316
			);
317
318
			// TODO: it is possible these values where set even when the value defaulted,
319
			// so this logic is not correct and could be improved
320 9
			if ( !$parameter->wasSetToDefault() ) {
321 8
				$processedParam->setOriginalName( $parameter->getOriginalName() );
322 8
				$processedParam->setOriginalValue( $parameter->getOriginalValue() );
323
			}
324
325 9
			$parameters[$processedParam->getName()] = $processedParam;
326
		}
327
328 12
		return new ProcessingResult(
329 12
			$parameters,
330 12
			$this->getErrors()
331
		);
332
	}
333
334
	/**
335
	 * Does the actual parameter processing.
336
	 */
337 12
	private function doParamProcessing() {
338 12
		$this->errors = [];
339
340 12
		$this->getParamsToProcess( [], $this->paramDefinitions );
341
342 12
		while ( $this->paramsToHandle !== [] && !$this->hasFatalError() ) {
343 11
			$this->processOneParam();
344
		}
345 12
	}
346
347 11
	private function processOneParam() {
348 11
		$paramName = array_shift( $this->paramsToHandle );
349 11
		$definition = $this->paramDefinitions[$paramName];
350
351 11
		$param = new Param( $definition );
352
353 11
		$setUserValue = $this->attemptToSetUserValue( $param );
354
355
		// If the parameter is required but not provided, register a fatal error and stop processing.
356 11
		if ( !$setUserValue && $param->isRequired() ) {
357 2
			$this->registerNewError(
358 2
				"Required parameter '$paramName' is missing", // FIXME: i18n validator_error_required_missing
359 2
				[ $paramName, 'missing' ],
360 2
				ProcessingError::SEVERITY_FATAL
361
			);
362 2
			return;
363
		}
364
365 9
		$this->params[$param->getName()] = $param;
366
367 9
		$initialSet = $this->paramDefinitions;
368
369 9
		$param->process( $this->paramDefinitions, $this->params, $this->options );
370
371 9
		foreach ( $param->getErrors() as $error ) {
372 2
			$this->registerError( $error );
373
		}
374
375 9
		if ( $param->hasFatalError() ) {
376
			return;
377
		}
378
379 9
		$this->getParamsToProcess( $initialSet, $this->paramDefinitions );
380 9
	}
381
382
	/**
383
	 * Gets an ordered list of parameters to process.
384
	 *
385
	 * @since 0.4
386
	 *
387
	 * @param array $initialParamSet
388
	 * @param array $resultingParamSet
389
	 *
390
	 * @throws \UnexpectedValueException
391
	 */
392 12
	private function getParamsToProcess( array $initialParamSet, array $resultingParamSet ) {
393 12
		if ( $initialParamSet === [] ) {
394 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...
395
		}
396
		else {
397 9
			if ( !is_array( $this->paramsToHandle ) ) {
398
				$this->paramsToHandle = [];
399
			}
400
401 9
			foreach ( $resultingParamSet as $paramName => $parameter ) {
402 9
				if ( !array_key_exists( $paramName, $initialParamSet ) ) {
403 9
					$this->paramsToHandle[] = $paramName;
404
				}
405
			}
406
		}
407
408 12
		$this->paramsToHandle = $this->getParameterNamesInEvaluationOrder( $this->paramDefinitions, $this->paramsToHandle );
409 12
	}
410
411
	/**
412
	 * @param IParamDefinition[] $paramDefinitions
413
	 * @param string[] $paramsToHandle
414
	 *
415
	 * @return array
416
	 */
417 12
	private function getParameterNamesInEvaluationOrder( array $paramDefinitions, array $paramsToHandle ) {
418 12
		$dependencyList = [];
419
420 12
		foreach ( $paramsToHandle as $paramName ) {
421 11
			$dependencies = [];
422
423 11
			if ( !array_key_exists( $paramName, $paramDefinitions ) ) {
424
				throw new \UnexpectedValueException( 'Unexpected parameter name "' . $paramName . '"' );
425
			}
426
427 11
			if ( !is_object( $paramDefinitions[$paramName] ) || !( $paramDefinitions[$paramName] instanceof IParamDefinition ) ) {
428
				throw new \UnexpectedValueException( 'Parameter "' . $paramName . '" is not a IParamDefinition' );
429
			}
430
431
			// Only include dependencies that are in the list of parameters to handle.
432 11
			foreach ( $paramDefinitions[$paramName]->getDependencies() as $dependency ) {
433
				if ( in_array( $dependency, $paramsToHandle ) ) {
434
					$dependencies[] = $dependency;
435
				}
436
			}
437
438 11
			$dependencyList[$paramName] = $dependencies;
439
		}
440
441 12
		$sorter = new TopologicalSort( $dependencyList, true );
442
443 12
		return $sorter->doSort();
444
	}
445
446
	/**
447
	 * Tries to find a matching user provided value and, when found, assigns it
448
	 * to the parameter, and removes it from the raw values. Returns a boolean
449
	 * indicating if there was any user value set or not.
450
	 *
451
	 * @since 0.4
452
	 *
453
	 * @param Param $param
454
	 *
455
	 * @return boolean
456
	 */
457 11
	private function attemptToSetUserValue( Param $param ) {
458 11
		if ( array_key_exists( $param->getName(), $this->rawParameters ) ) {
459 9
			$param->setUserValue( $param->getName(), $this->rawParameters[$param->getName()], $this->options );
460 9
			unset( $this->rawParameters[$param->getName()] );
461 9
			return true;
462
		}
463
		else {
464 6
			foreach ( $param->getDefinition()->getAliases() as $alias ) {
465
				if ( array_key_exists( $alias, $this->rawParameters ) ) {
466
					$param->setUserValue( $alias, $this->rawParameters[$alias], $this->options );
467
					unset( $this->rawParameters[$alias] );
468
					return true;
469
				}
470
			}
471
		}
472
473 6
		return false;
474
	}
475
476
	/**
477
	 * Returns the parameters.
478
	 *
479
	 * @since 0.4
480
	 * @deprecated since 1.0
481
	 *
482
	 * @return IParam[]
483
	 */
484
	public function getParameters() {
485
		return $this->params;
486
	}
487
488
	/**
489
	 * Returns a single parameter.
490
	 *
491
	 * @since 0.4
492
	 * @deprecated since 1.0
493
	 *
494
	 * @param string $parameterName The name of the parameter to return
495
	 *
496
	 * @return IParam
497
	 */
498
	public function getParameter( $parameterName ) {
499
		return $this->params[$parameterName];
500
	}
501
502
	/**
503
	 * Returns an associative array with the parameter names as key and their
504
	 * corresponding values as value.
505
	 *
506
	 * @since 0.4
507
	 *
508
	 * @return array
509
	 */
510
	public function getParameterValues() {
511
		$parameters = [];
512
513
		foreach ( $this->params as $parameter ) {
514
			$parameters[$parameter->getName()] = $parameter->getValue();
515
		}
516
517
		return $parameters;
518
	}
519
520
	/**
521
	 * Returns the errors.
522
	 *
523
	 * @since 0.4
524
	 *
525
	 * @return ProcessingError[]
526
	 */
527 12
	public function getErrors() {
528 12
		return $this->errors;
529
	}
530
531
	/**
532
	 * @since 0.4.6
533
	 *
534
	 * @return string[]
535
	 */
536
	public function getErrorMessages() {
537
		$errors = [];
538
539
		foreach ( $this->errors as $error ) {
540
			$errors[] = $error->getMessage();
541
		}
542
543
		return $errors;
544
	}
545
546
	/**
547
	 * Returns if there where any errors during validation.
548
	 *
549
	 * @return boolean
550
	 */
551
	public function hasErrors() {
552
		return !empty( $this->errors );
553
	}
554
555
	/**
556
	 * Returns false when there are no fatal errors or an ProcessingError when one is found.
557
	 *
558
	 * @return ProcessingError|boolean false
559
	 */
560 12
	public function hasFatalError() {
561 12
		foreach ( $this->errors as $error ) {
562 4
			if ( $error->isFatal() ) {
563 4
				return $error;
564
			}
565
		}
566
567 12
		return false;
568
	}
569
570
}
571