Passed
Push — master ( 761e95...f1e6ba )
by Jeroen De
05:40
created

src/Processor.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace ParamProcessor;
4
5
use ParamProcessor\PackagePrivate\Param;
6
7
/**
8
 * Class for parameter validation of a single parser hook or other parametrized construct.
9
 *
10
 * @since 0.1
11
 *
12
 * @licence GNU GPL v2+
13
 * @author Jeroen De Dauw < [email protected] >
14
 * @author Daniel Werner
15
 */
16
class Processor {
17
18
	/**
19
	 * Flag for unnamed default parameters used in Processor::setFunctionParams() to determine that
20
	 * a parameter should not have a named fallback.
21
	 *
22
	 * @since 0.4.13
23
	 */
24
	const PARAM_UNNAMED = 1;
25
26
	/**
27
	 * @var Param[]
28
	 */
29
	private $params;
30
31
	/**
32
	 * Associative array containing parameter names (keys) and their user-provided data (values).
33
	 * This list is needed because additional parameter definitions can be added to the $parameters
34
	 * field during validation, so we can't determine in advance if a parameter is unknown.
35
	 * @var string[]
36
	 */
37
	private $rawParameters = [];
38
39
	/**
40
	 * Array containing the names of the parameters to handle, ordered by priority.
41
	 * @var string[]
42
	 */
43
	private $paramsToHandle = [];
44
45
	/**
46
	 * @var IParamDefinition[]
47
	 */
48
	private $paramDefinitions = [];
49
50
	/**
51
	 * @var ProcessingError[]
52
	 */
53
	private $errors = [];
54
55
	private $options;
56
57 18
	public function __construct( Options $options ) {
58 18
		$this->options = $options;
59 18
	}
60
61
	/**
62
	 * Constructs and returns a Validator object based on the default options.
63
	 */
64 7
	public static function newDefault(): self {
65 7
		return new Processor( new Options() );
66
	}
67
68
	/**
69
	 * Constructs and returns a Validator object based on the provided options.
70
	 */
71 11
	public static function newFromOptions( Options $options ): self {
72 11
		return new Processor( $options );
73
	}
74
75
	/**
76
	 * Returns the options used by this Validator object.
77
	 */
78 1
	public function getOptions(): Options {
79 1
		return $this->options;
80
	}
81
82
	/**
83
	 * Determines the names and values of all parameters. Also takes care of default parameters.
84
	 * After that the resulting parameter list is passed to Processor::setParameters
85
	 *
86
	 * @since 0.4
87
	 *
88
	 * @param string[] $rawParams
89
	 * @param ParamDefinition[]|array[] $parameterDefinitions DEPRECATED! Use @see setParameterDefinitions instead
90
	 * @param array $defaultParams array of strings or array of arrays to define which parameters can be used unnamed.
91
	 *        The second value in array-form is reserved for flags. Currently, Processor::PARAM_UNNAMED determines that
92
	 *        the parameter has no name which can be used to set it. Therefore all these parameters must be set before
93
	 *        any named parameter. The effect is, that '=' within the string won't confuse the parameter anymore like
94
	 *        it would happen with default parameters that still have a name as well.
95
	 */
96 4
	public function setFunctionParams( array $rawParams, array $parameterDefinitions = [], array $defaultParams = [] ) {
97 4
		$lastUnnamedDefaultNr = -1;
98
99
		/*
100
		 * Find last parameter with self::PARAM_UNNAMED set. Tread all parameters in front as
101
		 * the flag were set for them as well to ensure that there can't be any unnamed params
102
		 * after the first named param. Wouldn't be possible to determine which unnamed value
103
		 * belongs to which parameter otherwise.
104
		 */
105 4
		for( $i = count( $defaultParams ) - 1; $i >= 0; $i-- ) {
106
			$dflt = $defaultParams[$i];
107
			if( is_array( $dflt ) && !empty( $dflt[1] ) && ( $dflt[1] | self::PARAM_UNNAMED ) ) {
108
				$lastUnnamedDefaultNr = $i;
109
				break;
110
			}
111
		}
112
113 4
		$parameters = [];
114 4
		$nr = 0;
115 4
		$defaultNr = 0;
116
117 4
		foreach ( $rawParams as $arg ) {
118
			// Only take into account strings. If the value is not a string,
119
			// it is not a raw parameter, and can not be parsed correctly in all cases.
120 4
			if ( is_string( $arg ) ) {
121 4
				$parts = explode( '=', $arg, ( $nr <= $lastUnnamedDefaultNr ? 1 : 2 ) );
122
123
				// If there is only one part, no parameter name is provided, so try default parameter assignment.
124
				// Default parameters having self::PARAM_UNNAMED set for having no name alias go here in any case.
125 4
				if ( count( $parts ) == 1 ) {
126
					// Default parameter assignment is only possible when there are default parameters!
127
					if ( count( $defaultParams ) > 0 ) {
128
						$defaultParam = array_shift( $defaultParams );
129
						if( is_array( $defaultParam ) ) {
130
							$defaultParam = $defaultParam[0];
131
						}
132
						$defaultParam = strtolower( $defaultParam );
133
134
						$parameters[$defaultParam] = [
135
							'original-value' => trim( $parts[0] ),
136
							'default' => $defaultNr,
137
							'position' => $nr
138
						];
139
						$defaultNr++;
140
					}
141
				} else {
142 4
					$paramName = trim( strtolower( $parts[0] ) );
143
144 4
					$parameters[$paramName] = [
145 4
						'original-value' => trim( $parts[1] ),
146
						'default' => false,
147 4
						'position' => $nr
148
					];
149
150
					// Let's not be evil, and remove the used parameter name from the default parameter list.
151
					// This code is basically a remove array element by value algorithm.
152 4
					$newDefaults = [];
153
154 4
					foreach( $defaultParams as $defaultParam ) {
155
						if ( $defaultParam != $paramName ) {
156
							$newDefaults[] = $defaultParam;
157
						}
158
					}
159
160 4
					$defaultParams = $newDefaults;
161
				}
162
			}
163
164 4
			$nr++;
165
		}
166
167 4
		$this->setParameters( $parameters, $parameterDefinitions );
168 4
	}
169
170
	/**
171
	 * @since 1.6.0
172
	 * @param ParamDefinition[] $paramDefinitions
173
	 */
174 1
	public function setParameterDefinitions( array $paramDefinitions ) {
175 1
		$this->paramDefinitions = $paramDefinitions;
176 1
	}
177
178
	/**
179
	 * Loops through a list of provided parameters, resolves aliasing and stores errors
180
	 * for unknown parameters and optionally for parameter overriding.
181
	 *
182
	 * @param array $parameters Parameter name as key, parameter value as value
183
	 * @param ParamDefinition[]|array[] $paramDefinitions DEPRECATED! Use @see setParameterDefinitions instead
184
	 */
185 16
	public function setParameters( array $parameters, array $paramDefinitions = [] ) {
186 16
		$this->paramDefinitions = ParamDefinition::getCleanDefinitions( $paramDefinitions );
187
188
		// Loop through all the user provided parameters, and distinguish between those that are allowed and those that are not.
189 16
		foreach ( $parameters as $paramName => $paramData ) {
190 14
			if ( $this->options->lowercaseNames() ) {
191 12
				$paramName = strtolower( $paramName );
192
			}
193
194 14
			if ( $this->options->trimNames() ) {
195 12
				$paramName = trim( $paramName );
196
			}
197
198 14
			$paramValue = is_array( $paramData ) ? $paramData['original-value'] : $paramData;
199
200 14
			$this->rawParameters[$paramName] = $paramValue;
201
		}
202 16
	}
203
204
	/**
205
	 * @param string $message
206
	 * @param string[] $tags
207
	 * @param integer $severity
208
	 */
209 5
	private function registerNewError( string $message, array $tags = [], int $severity = ProcessingError::SEVERITY_NORMAL ) {
210 5
		$this->registerError(
211 5
			new ProcessingError(
212 5
				$message,
213
				$severity,
214 5
				$this->options->getName(),
215 5
				(array)$tags
216
			)
217
		);
218 5
	}
219
220 8
	private function registerError( ProcessingError $error ) {
221 8
		$error->element = $this->options->getName();
222 8
		$this->errors[] = $error;
223 8
		ProcessingErrorHandler::addError( $error );
224 8
	}
225
226
	/**
227
	 * Validates and formats all the parameters (but aborts when a fatal error occurs).
228
	 *
229
	 * @since 0.4
230
	 * @deprecated since 1.0, use processParameters
231
	 */
232
	public function validateParameters() {
233
		$this->doParamProcessing();
234
	}
235
236 12
	public function processParameters(): ProcessingResult {
237 12
		$this->doParamProcessing();
238
239 12
		if ( !$this->hasFatalError() && $this->options->unknownIsInvalid() ) {
240
			// Loop over the remaining raw parameters.
241
			// These are unrecognized parameters, as they where not used by any parameter definition.
242 9
			foreach ( $this->rawParameters as $paramName => $paramValue ) {
243 2
				$this->registerNewError(
244 2
					$paramName . ' is not a valid parameter', // TODO
245 2
					[ $paramName ]
246
				);
247
			}
248
		}
249
250 12
		return $this->newProcessingResult();
251
	}
252
253 12
	private function newProcessingResult(): ProcessingResult {
254 12
		$parameters = [];
255
256 12
		if ( !is_array( $this->params ) ) {
257 3
			$this->params = [];
258
		}
259
260 12
		foreach ( $this->params as $parameter ) {
261
			// TODO
262 9
			$processedParam = new ProcessedParam(
263 9
				$parameter->getName(),
264 9
				$parameter->getValue(),
265 9
				$parameter->wasSetToDefault()
266
			);
267
268
			// TODO: it is possible these values where set even when the value defaulted,
269
			// so this logic is not correct and could be improved
270 9
			if ( !$parameter->wasSetToDefault() ) {
271 7
				$processedParam->setOriginalName( $parameter->getOriginalName() );
272 7
				$processedParam->setOriginalValue( $parameter->getOriginalValue() );
273
			}
274
275 9
			$parameters[$processedParam->getName()] = $processedParam;
276
		}
277
278 12
		return new ProcessingResult(
279 12
			$parameters,
280 12
			$this->getErrors()
281
		);
282
	}
283
284 12
	private function doParamProcessing() {
285 12
		$this->errors = [];
286
287 12
		$this->getParamsToProcess( [], $this->paramDefinitions );
288
289 12
		while ( $this->paramsToHandle !== [] && !$this->hasFatalError() ) {
290 11
			$this->processOneParam();
291
		}
292 12
	}
293
294 11
	private function processOneParam() {
295 11
		$paramName = array_shift( $this->paramsToHandle );
296 11
		$definition = $this->paramDefinitions[$paramName];
297
298 11
		$param = new Param( $definition );
299
300 11
		$setUserValue = $this->attemptToSetUserValue( $param );
301
302
		// If the parameter is required but not provided, register a fatal error and stop processing.
303 11
		if ( !$setUserValue && $param->isRequired() ) {
304 3
			$this->registerNewError(
305 3
				"Required parameter '$paramName' is missing", // FIXME: i18n validator_error_required_missing
306 3
				[ $paramName, 'missing' ],
307 3
				ProcessingError::SEVERITY_FATAL
308
			);
309 3
			return;
310
		}
311
312 9
		$this->params[$param->getName()] = $param;
313
314 9
		$initialSet = $this->paramDefinitions;
315
316 9
		$param->process( $this->paramDefinitions, $this->params, $this->options );
317
318 9
		foreach ( $param->getErrors() as $error ) {
319 5
			$this->registerError( $error );
320
		}
321
322 9
		if ( $param->hasFatalError() ) {
323 1
			return;
324
		}
325
326 8
		$this->getParamsToProcess( $initialSet, $this->paramDefinitions );
327 8
	}
328
329
	/**
330
	 * Gets an ordered list of parameters to process.
331
	 * @throws \UnexpectedValueException
332
	 */
333 12
	private function getParamsToProcess( array $initialParamSet, array $resultingParamSet ) {
334 12
		if ( $initialParamSet === [] ) {
335 12
			$this->paramsToHandle = array_keys( $resultingParamSet );
336
		}
337
		else {
338 8
			if ( !is_array( $this->paramsToHandle ) ) {
339
				$this->paramsToHandle = [];
340
			}
341
342 8
			foreach ( $resultingParamSet as $paramName => $parameter ) {
343 8
				if ( !array_key_exists( $paramName, $initialParamSet ) ) {
344 1
					$this->paramsToHandle[] = $paramName;
345
				}
346
			}
347
		}
348
349 12
		$this->paramsToHandle = $this->getParameterNamesInEvaluationOrder( $this->paramDefinitions, $this->paramsToHandle );
350 12
	}
351
352
	/**
353
	 * @param IParamDefinition[] $paramDefinitions
354
	 * @param string[] $paramsToHandle
355
	 *
356
	 * @return array
357
	 */
358 12
	private function getParameterNamesInEvaluationOrder( array $paramDefinitions, array $paramsToHandle ): array {
359 12
		$dependencyList = [];
360
361 12
		foreach ( $paramsToHandle as $paramName ) {
362 11
			$dependencies = [];
363
364 11
			if ( !array_key_exists( $paramName, $paramDefinitions ) ) {
365
				throw new \UnexpectedValueException( 'Unexpected parameter name "' . $paramName . '"' );
366
			}
367
368 11
			if ( !is_object( $paramDefinitions[$paramName] ) || !( $paramDefinitions[$paramName] instanceof IParamDefinition ) ) {
369
				throw new \UnexpectedValueException( 'Parameter "' . $paramName . '" is not a IParamDefinition' );
370
			}
371
372
			// Only include dependencies that are in the list of parameters to handle.
373 11
			foreach ( $paramDefinitions[$paramName]->getDependencies() as $dependency ) {
374
				if ( in_array( $dependency, $paramsToHandle ) ) {
375
					$dependencies[] = $dependency;
376
				}
377
			}
378
379 11
			$dependencyList[$paramName] = $dependencies;
380
		}
381
382 12
		$sorter = new TopologicalSort( $dependencyList, true );
0 ignored issues
show
Deprecated Code introduced by
The class ParamProcessor\TopologicalSort has been deprecated with message: since 1.7

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
383
384 12
		return $sorter->doSort();
385
	}
386
387
	/**
388
	 * Tries to find a matching user provided value and, when found, assigns it
389
	 * to the parameter, and removes it from the raw values. Returns a boolean
390
	 * indicating if there was any user value set or not.
391
	 */
392 11
	private function attemptToSetUserValue( Param $param ): bool {
393 11
		if ( array_key_exists( $param->getName(), $this->rawParameters ) ) {
394 9
			$param->setUserValue( $param->getName(), $this->rawParameters[$param->getName()], $this->options );
395 9
			unset( $this->rawParameters[$param->getName()] );
396 9
			return true;
397
		}
398
		else {
399 6
			foreach ( $param->getDefinition()->getAliases() as $alias ) {
400
				if ( array_key_exists( $alias, $this->rawParameters ) ) {
401
					$param->setUserValue( $alias, $this->rawParameters[$alias], $this->options );
402
					unset( $this->rawParameters[$alias] );
403
					return true;
404
				}
405
			}
406
		}
407
408 6
		return false;
409
	}
410
411
	/**
412
	 * @deprecated since 1.0
413
	 * @return Param[]
414
	 */
415
	public function getParameters(): array {
416
		return $this->params;
417
	}
418
419
	/**
420
	 * @deprecated since 1.0
421
	 */
422
	public function getParameter( string $parameterName ): Param {
423
		return $this->params[$parameterName];
424
	}
425
426
	/**
427
	 * Returns an associative array with the parameter names as key and their
428
	 * corresponding values as value.
429
	 * @deprecated since 1.7 - use processParameters() return value
430
	 */
431
	public function getParameterValues(): array {
432
		$parameters = [];
433
434
		foreach ( $this->params as $parameter ) {
435
			$parameters[$parameter->getName()] = $parameter->getValue();
436
		}
437
438
		return $parameters;
439
	}
440
441
	/**
442
	 * @deprecated since 1.7 - use processParameters() return value
443
	 * @return ProcessingError[]
444
	 */
445 12
	public function getErrors(): array {
446 12
		return $this->errors;
447
	}
448
449
	/**
450
	 * @deprecated since 1.7 - use processParameters() return value
451
	 * @return string[]
452
	 */
453
	public function getErrorMessages(): array {
454
		$errors = [];
455
456
		foreach ( $this->errors as $error ) {
457
			$errors[] = $error->getMessage();
458
		}
459
460
		return $errors;
461
	}
462
463
	/**
464
	 * @deprecated since 1.7 - use processParameters() return value
465
	 */
466
	public function hasErrors(): bool {
467
		return !empty( $this->errors );
468
	}
469
470
	/**
471
	 * @deprecated since 1.7 - use processParameters() return value
472
	 * @return ProcessingError|boolean false
473
	 */
474 12
	public function hasFatalError() {
475 12
		foreach ( $this->errors as $error ) {
476 8
			if ( $error->isFatal() ) {
477 4
				return $error;
478
			}
479
		}
480
481 12
		return false;
482
	}
483
484
}
485