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

src/PackagePrivate/Param.php (5 issues)

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\PackagePrivate;
4
5
use Exception;
6
use ParamProcessor\IParam;
7
use ParamProcessor\IParamDefinition;
8
use ParamProcessor\Options;
9
use ParamProcessor\ParamDefinition;
10
use ParamProcessor\ParamDefinitionFactory;
11
use ParamProcessor\ProcessingError;
12
use ValueParsers\NullParser;
13
use ValueParsers\ParseException;
14
use ValueParsers\ValueParser;
15
16
/**
17
 * Package private!
18
 *
19
 * Parameter class, representing the "instance" of a parameter.
20
 * Holds a ParamDefinition, user provided input (name & value) and processing state.
21
 *
22
 * @licence GNU GPL v2+
23
 * @author Jeroen De Dauw < [email protected] >
24
 */
25
class Param implements IParam {
0 ignored issues
show
Deprecated Code introduced by
The interface ParamProcessor\IParam has been deprecated with message: since 1.0

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...
26
27
	/**
28
	 * Indicates whether parameters not found in the criteria list
29
	 * should be stored in case they are not accepted. The default is false.
30
	 *
31
	 * @since 1.0
32
	 *
33
	 * @var boolean
34
	 */
35
	public static $accumulateParameterErrors = false;
36
37
	/**
38
	 * The original parameter name as provided by the user. This can be the
39
	 * main name or an alias.
40
	 *
41
	 * @since 1.0
42
	 *
43
	 * @var string
44
	 */
45
	protected $originalName;
46
47
	/**
48
	 * The original value as provided by the user. This is mainly retained for
49
	 * usage in error messages when the parameter turns out to be invalid.
50
	 *
51
	 * @since 1.0
52
	 *
53
	 * @var string
54
	 */
55
	protected $originalValue;
56
57
	/**
58
	 * The value of the parameter.
59
	 *
60
	 * @since 1.0
61
	 *
62
	 * @var mixed
63
	 */
64
	protected $value;
65
66
	/**
67
	 * Keeps track of how many times the parameter has been set by the user.
68
	 * This is used to detect overrides and for figuring out a parameter is missing.
69
	 *
70
	 * @since 1.0
71
	 *
72
	 * @var integer
73
	 */
74
	protected $setCount = 0;
75
76
	/**
77
	 * List of validation errors for this parameter.
78
	 *
79
	 * @since 1.0
80
	 *
81
	 * @var ProcessingError[]
82
	 */
83
	protected $errors = [];
84
85
	/**
86
	 * Indicates if the parameter was set to it's default.
87
	 *
88
	 * @since 1.0
89
	 *
90
	 * @var boolean
91
	 */
92
	protected $defaulted = false;
93
94
	/**
95
	 * @since 1.0
96
	 *
97
	 * @var ParamDefinition
98
	 */
99
	protected $definition;
100
101
	/**
102
	 * Constructor.
103
	 *
104
	 * @since 1.0
105
	 *
106
	 * @param IParamDefinition $definition
107
	 */
108 66
	public function __construct( IParamDefinition $definition ) {
109 66
		$this->definition = $definition;
0 ignored issues
show
Documentation Bug introduced by
$definition is of type object<ParamProcessor\IParamDefinition>, but the property $definition was declared to be of type object<ParamProcessor\ParamDefinition>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
110 66
	}
111
112
	/**
113
	 * Sets and cleans the original value and name.
114
	 * @see IParam::setUserValue
115
	 *
116
	 * @since 1.0
117
	 *
118
	 * @param string $paramName
119
	 * @param string $paramValue
120
	 * @param Options $options
121
	 *
122
	 * @return boolean
123
	 */
124 64
	public function setUserValue( $paramName, $paramValue, Options $options ) {
125 64
		if ( $this->setCount > 0 && !$options->acceptOverriding() ) {
126
			// TODO
127
			return false;
128
		}
129
		else {
130 64
			$this->originalName = $paramName;
131 64
			$this->originalValue = $paramValue;
132
133 64
			$this->cleanValue( $options );
134
135 64
			$this->setCount++;
136
137 64
			return true;
138
		}
139
	}
140
141
	/**
142
	 * @since 1.0
143
	 *
144
	 * @param mixed $value
145
	 */
146 57
	public function setValue( $value ) {
147 57
		$this->value = $value;
148 57
	}
149
150
	/**
151
	 * Sets the $value to a cleaned value of $originalValue.
152
	 *
153
	 * @since 1.0
154
	 *
155
	 * @param Options $options
156
	 */
157 64
	protected function cleanValue( Options $options ) {
158 64
		if ( $this->definition->isList() ) {
159
			$this->value = explode( $this->definition->getDelimiter(), $this->originalValue );
160
		}
161
		else {
162 64
			$this->value = $this->originalValue;
163
		}
164
165 64
		if ( $this->shouldTrim( $options ) ) {
166 64
			$this->trimValue();
167
		}
168
169 64
		if ( $this->shouldLowercase( $options ) ) {
170
			$this->lowercaseValue();
171
		}
172 64
	}
173
174 64
	private function shouldTrim( Options $options ): bool {
175 64
		$trim = $this->definition->trimDuringClean();
176
177 64
		if ( $trim === true ) {
178
			return true;
179
		}
180
181 64
		return is_null( $trim ) && $options->trimValues();
182
	}
183
184 64
	private function trimValue() {
185 64
		if ( is_string( $this->value ) ) {
186 64
			$this->value = trim( $this->value );
187
		}
188 54
		elseif ( $this->definition->isList() ) {
189
			foreach ( $this->value as &$element ) {
0 ignored issues
show
The expression $this->value of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
190
				if ( is_string( $element ) ) {
191
					$element = trim( $element );
192
				}
193
			}
194
		}
195 64
	}
196
197 64
	private function shouldLowercase( Options $options ): bool {
198 64
		if ( $options->lowercaseValues() ) {
199
			return true;
200
		}
201
202 64
		$definitionOptions = $this->definition->getOptions();
203
204 64
		return array_key_exists( 'tolower', $definitionOptions ) && $definitionOptions['tolower'];
205
	}
206
207
	private function lowercaseValue() {
208
		if ( $this->definition->isList() ) {
209
			foreach ( $this->value as &$element ) {
210
				if ( is_string( $element ) ) {
211
					$element = strtolower( $element );
212
				}
213
			}
214
		}
215
		elseif ( is_string( $this->value ) ) {
216
			$this->value = strtolower( $this->value );
217
		}
218
	}
219
220
	/**
221
	 * Parameter processing entry point.
222
	 * Processes the parameter. This includes parsing, validation and additional formatting.
223
	 *
224
	 * @since 1.0
225
	 *
226
	 * @param $definitions array of IParamDefinition
227
	 * @param $params array of IParam
228
	 * @param Options $options
229
	 *
230
	 * @throws Exception
231
	 */
232 65
	public function process( array &$definitions, array $params, Options $options ) {
233 65
		if ( $this->setCount == 0 ) {
234 1
			if ( $this->definition->isRequired() ) {
235
				// This should not occur, so throw an exception.
236
				throw new Exception( 'Attempted to validate a required parameter without first setting a value.' );
237
			}
238
			else {
239 1
				$this->setToDefault();
240
			}
241
		}
242
		else {
243 64
			$this->parseAndValidate( $options );
244
		}
245
246 65
		if ( !$this->hasFatalError() && ( $this->definition->shouldManipulateDefault() || !$this->wasSetToDefault() ) ) {
247 57
			$this->definition->format( $this, $definitions, $params );
0 ignored issues
show
Deprecated Code introduced by
The method ParamProcessor\ParamDefinition::format() has been deprecated.

This method has been deprecated.

Loading history...
248
		}
249 65
	}
250
251 64
	public function getValueParser( Options $options ): ValueParser {
252 64
		$parser = $this->definition->getValueParser();
253
254 64
		if ( get_class( $parser ) === NullParser::class ) {
255 64
			$parserType = $options->isStringlyTyped() ? 'string-parser' : 'typed-parser';
256
257
			// TODO: inject factory
258 64
			$parserClass = ParamDefinitionFactory::singleton()->getComponentForType( $this->definition->getType(), $parserType );
0 ignored issues
show
Deprecated Code introduced by
The method ParamProcessor\ParamDefinitionFactory::singleton() has been deprecated with message: since 1.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
259
260 64
			if ( $parserClass !== NullParser::class ) {
261 50
				$parser = new $parserClass( new \ValueParsers\ParserOptions() );
262
			}
263
		}
264
265 64
		return $parser;
266
	}
267
268
	/**
269
	 * @since 1.0
270
	 *
271
	 * @param Options $options
272
	 */
273 64
	protected function parseAndValidate( Options $options ) {
274 64
		$parser = $this->getValueParser( $options );
275
276 64
		if ( $this->definition->isList() ) {
277
			$values = [];
278
279
			foreach ( $this->getValue() as $value ) {
280
				$parsedValue = $this->parseAndValidateValue( $parser, $value );
281
282
				if ( is_array( $parsedValue ) ) {
283
					$values[] = $parsedValue[0];
284
				}
285
			}
286
287
			$this->value = $values;
288
		}
289
		else {
290 64
			$parsedValue = $this->parseAndValidateValue( $parser, $this->getValue() );
291
292 64
			if ( is_array( $parsedValue ) ) {
293 64
				$this->value = $parsedValue[0];
294
			}
295
		}
296
297 64
		$this->setToDefaultIfNeeded();
298 64
	}
299
300
	/**
301
	 * Parses and validates the provided with with specified parser.
302
	 * The result is returned in an array on success. On fail, false is returned.
303
	 * The result is wrapped in an array since we need to be able to distinguish
304
	 * between the method returning false and the value being false.
305
	 *
306
	 * Parsing and validation errors get added to $this->errors.
307
	 *
308
	 * @since 1.0
309
	 *
310
	 * @param ValueParser $parser
311
	 * @param mixed $value
312
	 *
313
	 * @return array|bool
314
	 */
315 64
	protected function parseAndValidateValue( ValueParser $parser, $value ) {
316
		try {
317 64
			$value = $parser->parse( $value );
318
		}
319 7
		catch ( ParseException $parseException ) {
320 7
			$this->registerProcessingError( $parseException->getMessage() );
321 7
			return false;
322
		}
323
324 64
		if ( $value instanceof \DataValues\DataValue ) {
325 64
			$value = $value->getValue();
326
		}
327
328 64
		$this->validateValue( $value );
329
330 64
		return [ $value ];
331
	}
332
333 21
	protected function registerProcessingError( string $message ) {
334 21
		$this->errors[] = $this->newProcessingError( $message );
335 21
	}
336
337 21
	protected function newProcessingError( string $message ): ProcessingError {
338 21
		$severity = $this->isRequired() ? ProcessingError::SEVERITY_FATAL : ProcessingError::SEVERITY_NORMAL;
339 21
		return new ProcessingError( $message, $severity );
340
	}
341
342
	/**
343
	 * @since 1.0
344
	 *
345
	 * @param mixed $value
346
	 */
347 64
	protected function validateValue( $value ) {
348 64
		$validationCallback = $this->definition->getValidationCallback();
349
350 64
		if ( $validationCallback !== null && $validationCallback( $value ) !== true ) {
351 7
			$this->registerProcessingError( 'Validation callback failed' );
352
		}
353
		else {
354 64
			$validator = $this->definition->getValueValidator();
355 64
			if ( method_exists( $validator, 'setOptions' ) ) {
356 64
				$validator->setOptions( $this->definition->getOptions() );
357
			}
358 64
			$validationResult = $validator->validate( $value );
359
360 64
			if ( !$validationResult->isValid() ) {
361 16
				foreach ( $validationResult->getErrors() as $error ) {
362 16
					$this->registerProcessingError( $error->getText() );
363
				}
364
			}
365
		}
366 64
	}
367
368
	/**
369
	 * Sets the parameter value to the default if needed.
370
	 *
371
	 * @since 1.0
372
	 */
373 64
	protected function setToDefaultIfNeeded() {
374 64
		if ( $this->shouldSetToDefault() ) {
375 1
			$this->setToDefault();
376
		}
377 64
	}
378
379 64
	private function shouldSetToDefault(): bool {
380 64
		if ( $this->hasFatalError() ) {
381 20
			return false;
382
		}
383
384 56
		if ( $this->definition->isList() ) {
385
			return $this->errors !== [] && $this->value === [];
386
		}
387
388 56
		return $this->errors !== [];
389
	}
390
391
	/**
392
	 * Returns the original use-provided name.
393
	 *
394
	 * @since 1.0
395
	 *
396
	 * @throws Exception
397
	 * @return string
398
	 */
399
	public function getOriginalName(): string {
400
		if ( $this->setCount == 0 ) {
401
			throw new Exception( 'No user input set to the parameter yet, so the original name does not exist' );
402
		}
403
		return $this->originalName;
404
	}
405
406
	/**
407
	 * Returns the original use-provided value.
408
	 *
409
	 * @since 1.0
410
	 *
411
	 * @throws Exception
412
	 * @return mixed
413
	 */
414
	public function getOriginalValue() {
415
		if ( $this->setCount == 0 ) {
416
			throw new Exception( 'No user input set to the parameter yet, so the original value does not exist' );
417
		}
418
		return $this->originalValue;
419
	}
420
421
	/**
422
	 * Returns all validation errors that occurred so far.
423
	 *
424
	 * @since 1.0
425
	 *
426
	 * @return ProcessingError[]
427
	 */
428 50
	public function getErrors() {
429 50
		return $this->errors;
430
	}
431
432
	/**
433
	 * Sets the parameter value to the default.
434
	 *
435
	 * @since 1.0
436
	 */
437 2
	protected function setToDefault() {
438 2
		$this->defaulted = true;
439 2
		$this->value = $this->definition->getDefault();
440 2
	}
441
442
	public function wasSetToDefault(): bool {
443
		return $this->defaulted;
444
	}
445
446 65
	public function hasFatalError(): bool {
447 65
		foreach ( $this->errors as $error ) {
448 21
			if ( $error->isFatal() ) {
449 20
				return true;
450
			}
451
		}
452
453 57
		return false;
454
	}
455
456
	/**
457
	 * Returns the IParamDefinition this IParam was constructed from.
458
	 *
459
	 * @since 1.0
460
	 *
461
	 * @return IParamDefinition
462
	 */
463
	public function getDefinition() {
464
		return $this->definition;
465
	}
466
467
	/**
468
	 * Returns the parameters value.
469
	 *
470
	 * @since 1.0
471
	 *
472
	 * @return mixed
473
	 */
474 65
	public function &getValue() {
475 65
		return $this->value;
476
	}
477
478
	/**
479
	 * Returns if the parameter is required or not.
480
	 *
481
	 * @since 1.0
482
	 *
483
	 * @return boolean
484
	 */
485 22
	public function isRequired() {
486 22
		return $this->definition->isRequired();
487
	}
488
489
	/**
490
	 * Returns if the name of the parameter.
491
	 *
492
	 * @since 1.0
493
	 *
494
	 * @return string
495
	 */
496
	public function getName() {
497
		return $this->definition->getName();
498
	}
499
500
	/**
501
	 * Returns the parameter name aliases.
502
	 *
503
	 * @since 1.0
504
	 *
505
	 * @return string[]
506
	 */
507
	public function getAliases(): array {
508
		return $this->definition->getAliases();
509
	}
510
511
}