Completed
Pull Request — master (#41)
by Jeroen De
08:38 queued 06:16
created

Processor::getParamsToProcess()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.2

Importance

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