Completed
Push — master ( 709546...161d08 )
by Jeroen De
04:22 queued 02:02
created

src/Processor.php (1 issue)

Severity

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
/**
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
	public function __construct( Options $options ) {
88
		$this->options = $options;
89
	}
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
	public static function newDefault() {
99
		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
	public static function newFromOptions( Options $options ) {
112
		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
	public function getOptions() {
123
		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 {
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, false );
0 ignored issues
show
The call to Processor::setParameters() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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
	public function setParameters( array $parameters, array $paramDefinitions ) {
223
		$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
		foreach ( $parameters as $paramName => $paramData ) {
227
			if ( $this->options->lowercaseNames() ) {
228
				$paramName = strtolower( $paramName );
229
			}
230
231
			if ( $this->options->trimNames() ) {
232
				$paramName = trim( $paramName );
233
			}
234
235
			$paramValue = is_array( $paramData ) ? $paramData['original-value'] : $paramData;
236
237
			$this->rawParameters[$paramName] = $paramValue;
238
		}
239
	}
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
	private function registerNewError( $message, $tags = [], $severity = ProcessingError::SEVERITY_NORMAL ) {
251
		$this->registerError(
252
			new ProcessingError(
253
				$message,
254
				$severity,
255
				$this->options->getName(),
256
				(array)$tags
257
			)
258
		);
259
	}
260
	
261
	/**
262
	 * Registers an error.
263
	 * 
264
	 * @since 0.4
265
	 * 
266
	 * @param ProcessingError $error
267
	 */
268
	private function registerError( ProcessingError $error ) {
269
		$error->element = $this->options->getName();
270
		$this->errors[] = $error;
271
		ProcessingErrorHandler::addError( $error );
272
	}
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
	public function processParameters() {
290
		$this->doParamProcessing();
291
292
		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
			foreach ( $this->rawParameters as $paramName => $paramValue ) {
296
				$this->registerNewError(
297
					$paramName . ' is not a valid parameter', // TODO
298
					$paramName
299
				);
300
			}
301
		}
302
303
		return $this->newProcessingResult();
304
	}
305
306
	/**
307
	 * @return ProcessingResult
308
	 */
309
	private function newProcessingResult() {
310
		$parameters = [];
311
312
		if ( !is_array( $this->params ) ) {
313
			$this->params = [];
314
		}
315
316
		/**
317
		 * @var Param $parameter
318
		 */
319
		foreach ( $this->params as $parameter ) {
320
			// TODO
321
			$processedParam = new ProcessedParam(
322
				$parameter->getName(),
323
				$parameter->getValue(),
324
				$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
			if ( !$parameter->wasSetToDefault() ) {
330
				$processedParam->setOriginalName( $parameter->getOriginalName() );
331
				$processedParam->setOriginalValue( $parameter->getOriginalValue() );
332
			}
333
334
			$parameters[$processedParam->getName()] = $processedParam;
335
		}
336
337
		return new ProcessingResult(
338
			$parameters,
339
			$this->getErrors()
340
		);
341
	}
342
	
343
	/**
344
	 * Does the actual parameter processing. 
345
	 * 
346
	 * @since 0.4
347
	 */
348
	private function doParamProcessing() {
349
		$this->getParamsToProcess( [], $this->paramDefinitions );
350
351
		while ( $this->paramsToHandle !== [] && !$this->hasFatalError() ) {
352
			$this->processOneParam();
353
		}
354
	}
355
356
	private function processOneParam() {
357
		$paramName = array_shift( $this->paramsToHandle );
358
		$definition = $this->paramDefinitions[$paramName];
359
360
		$param = new Param( $definition );
361
362
		$setUserValue = $this->attemptToSetUserValue( $param );
363
364
		// If the parameter is required but not provided, register a fatal error and stop processing.
365
		if ( !$setUserValue && $param->isRequired() ) {
366
			$this->registerNewError(
367
				"Required parameter '$paramName' is missing", // FIXME: i18n validator_error_required_missing
368
				[ $paramName, 'missing' ],
369
				ProcessingError::SEVERITY_FATAL
370
			);
371
			return;
372
		}
373
374
		$this->params[$param->getName()] = $param;
375
376
		$initialSet = $this->paramDefinitions;
377
378
		$param->process( $this->paramDefinitions, $this->params, $this->options );
379
380
		foreach ( $param->getErrors() as $error ) {
381
			$this->registerError( $error );
382
		}
383
384
		if ( $param->hasFatalError() ) {
385
			return;
386
		}
387
388
		$this->getParamsToProcess( $initialSet, $this->paramDefinitions );
389
	}
390
	
391
	/**
392
	 * Gets an ordered list of parameters to process.
393
	 * 
394
	 * @since 0.4
395
	 * 
396
	 * @param array $initialParamSet
397
	 * @param array $resultingParamSet
398
	 *
399
	 * @throws \UnexpectedValueException
400
	 */
401
	private function getParamsToProcess( array $initialParamSet, array $resultingParamSet ) {
402
		if ( $initialParamSet === [] ) {
403
			$this->paramsToHandle = array_keys( $resultingParamSet );
404
		}
405
		else {
406
			if ( !is_array( $this->paramsToHandle ) ) {
407
				$this->paramsToHandle = [];
408
			}			
409
			
410
			foreach ( $resultingParamSet as $paramName => $parameter ) {
411
				if ( !array_key_exists( $paramName, $initialParamSet ) ) {
412
					$this->paramsToHandle[] = $paramName;
413
				}
414
			}	
415
		}
416
		
417
		$this->paramsToHandle = $this->getParameterNamesInEvaluationOrder( $this->paramDefinitions, $this->paramsToHandle );
418
	}
419
420
	/**
421
	 * @param IParamDefinition[] $paramDefinitions
422
	 * @param string[] $paramsToHandle
423
	 *
424
	 * @return array
425
	 */
426
	private function getParameterNamesInEvaluationOrder( array $paramDefinitions, array $paramsToHandle ) {
427
		$dependencyList = [];
428
429
		foreach ( $paramsToHandle as $paramName ) {
430
			$dependencies = [];
431
432
			if ( !array_key_exists( $paramName, $paramDefinitions ) ) {
433
				throw new \UnexpectedValueException( 'Unexpected parameter name "' . $paramName . '"' );
434
			}
435
436
			if ( !is_object( $paramDefinitions[$paramName] ) || !( $paramDefinitions[$paramName] instanceof IParamDefinition ) ) {
437
				throw new \UnexpectedValueException( 'Parameter "' . $paramName . '" is not a IParamDefinition' );
438
			}
439
440
			// Only include dependencies that are in the list of parameters to handle.
441
			foreach ( $paramDefinitions[$paramName]->getDependencies() as $dependency ) {
442
				if ( in_array( $dependency, $paramsToHandle ) ) {
443
					$dependencies[] = $dependency;
444
				}
445
			}
446
447
			$dependencyList[$paramName] = $dependencies;
448
		}
449
450
		$sorter = new TopologicalSort( $dependencyList, true );
451
452
		return $sorter->doSort();
453
	}
454
	
455
	/**
456
	 * Tries to find a matching user provided value and, when found, assigns it
457
	 * to the parameter, and removes it from the raw values. Returns a boolean
458
	 * indicating if there was any user value set or not.
459
	 * 
460
	 * @since 0.4
461
	 *
462
	 * @param Param $param
463
	 * 
464
	 * @return boolean
465
	 */
466
	private function attemptToSetUserValue( Param $param ) {
467
		if ( array_key_exists( $param->getName(), $this->rawParameters ) ) {
468
			$param->setUserValue( $param->getName(), $this->rawParameters[$param->getName()], $this->options );
469
			unset( $this->rawParameters[$param->getName()] );
470
			return true;
471
		}
472
		else {
473
			foreach ( $param->getDefinition()->getAliases() as $alias ) {
474
				if ( array_key_exists( $alias, $this->rawParameters ) ) {
475
					$param->setUserValue( $alias, $this->rawParameters[$alias], $this->options );
476
					unset( $this->rawParameters[$alias] );
477
					return true;
478
				}
479
			}
480
		}
481
		
482
		return false;
483
	}
484
	
485
	/**
486
	 * Returns the parameters.
487
	 * 
488
	 * @since 0.4
489
	 * @deprecated since 1.0
490
	 * 
491
	 * @return IParam[]
492
	 */
493
	public function getParameters() {
494
		return $this->params;
495
	}
496
	
497
	/**
498
	 * Returns a single parameter.
499
	 * 
500
	 * @since 0.4
501
	 * @deprecated since 1.0
502
	 *
503
	 * @param string $parameterName The name of the parameter to return
504
	 * 
505
	 * @return IParam
506
	 */
507
	public function getParameter( $parameterName ) {
508
		return $this->params[$parameterName];
509
	}
510
	
511
	/**
512
	 * Returns an associative array with the parameter names as key and their
513
	 * corresponding values as value.
514
	 * 
515
	 * @since 0.4
516
	 * 
517
	 * @return array
518
	 */
519
	public function getParameterValues() {
520
		$parameters = [];
521
522
		foreach ( $this->params as $parameter ) {
523
			$parameters[$parameter->getName()] = $parameter->getValue();
524
		}
525
		
526
		return $parameters;
527
	}
528
	
529
	/**
530
	 * Returns the errors.
531
	 *
532
	 * @since 0.4
533
	 *
534
	 * @return ProcessingError[]
535
	 */
536
	public function getErrors() {
537
		return $this->errors;
538
	}
539
	
540
	/**
541
	 * @since 0.4.6
542
	 * 
543
	 * @return string[]
544
	 */
545
	public function getErrorMessages() {
546
		$errors = [];
547
		
548
		foreach ( $this->errors as $error ) {
549
			$errors[] = $error->getMessage();
550
		}
551
		
552
		return $errors;
553
	}
554
	
555
	/**
556
	 * Returns if there where any errors during validation. 
557
	 * 
558
	 * @return boolean
559
	 */
560
	public function hasErrors() {
561
		return !empty( $this->errors );
562
	}
563
	
564
	/**
565
	 * Returns false when there are no fatal errors or an ProcessingError when one is found.
566
	 * 
567
	 * @return ProcessingError|boolean false
568
	 */
569
	public function hasFatalError() {
570
		foreach ( $this->errors as $error ) {
571
			if ( $error->isFatal() ) {
572
				return $error;
573
			}
574
		}
575
		
576
		return false;
577
	}	
578
	
579
}
580