Completed
Push — master ( ebda25...4c5acd )
by Jeroen De
01:42
created

Processor::setParameterDefinitions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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