Completed
Pull Request — master (#25)
by mw
04:47 queued 02:41
created

Processor   C

Complexity

Total Complexity 68

Size/Duplication

Total Lines 570
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 58.86%

Importance

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