Passed
Push — master ( 2628a9...59e798 )
by Jeroen De
02:20
created

Param::setToDefaultIfNeeded()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 3
nc 2
nop 0
crap 3
1
<?php
2
3
namespace ParamProcessor;
4
5
use Exception;
6
use ValueParsers\NullParser;
7
use ValueParsers\ParseException;
8
use ValueParsers\ValueParser;
9
10
/**
11
 * Parameter class, representing the "instance" of a parameter.
12
 * Holds a ParamDefinition, user provided input (name & value) and processing state.
13
 *
14
 * NOTE: as of version 1.0, this class is for internal use only!
15
 *
16
 * @since 1.0
17
 *
18
 * @licence GNU GPL v2+
19
 * @author Jeroen De Dauw < [email protected] >
20
 */
21
final 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...
22
23
	/**
24
	 * Indicates whether parameters not found in the criteria list
25
	 * should be stored in case they are not accepted. The default is false.
26
	 *
27
	 * @since 1.0
28
	 *
29
	 * @var boolean
30
	 */
31
	public static $accumulateParameterErrors = false;
32
33
	/**
34
	 * The original parameter name as provided by the user. This can be the
35
	 * main name or an alias.
36
	 *
37
	 * @since 1.0
38
	 *
39
	 * @var string
40
	 */
41
	protected $originalName;
42
43
	/**
44
	 * The original value as provided by the user. This is mainly retained for
45
	 * usage in error messages when the parameter turns out to be invalid.
46
	 *
47
	 * @since 1.0
48
	 *
49
	 * @var string
50
	 */
51
	protected $originalValue;
52
53
	/**
54
	 * The value of the parameter.
55
	 *
56
	 * @since 1.0
57
	 *
58
	 * @var mixed
59
	 */
60
	protected $value;
61
62
	/**
63
	 * Keeps track of how many times the parameter has been set by the user.
64
	 * This is used to detect overrides and for figuring out a parameter is missing.
65
	 *
66
	 * @since 1.0
67
	 *
68
	 * @var integer
69
	 */
70
	protected $setCount = 0;
71
72
	/**
73
	 * List of validation errors for this parameter.
74
	 *
75
	 * @since 1.0
76
	 *
77
	 * @var array of ProcessingError
78
	 */
79
	protected $errors = [];
80
81
	/**
82
	 * Indicates if the parameter was set to it's default.
83
	 *
84
	 * @since 1.0
85
	 *
86
	 * @var boolean
87
	 */
88
	protected $defaulted = false;
89
90
	/**
91
	 * @since 1.0
92
	 *
93
	 * @var ParamDefinition
94
	 */
95
	protected $definition;
96
97
	/**
98
	 * Constructor.
99
	 *
100
	 * @since 1.0
101
	 *
102
	 * @param IParamDefinition $definition
103
	 */
104 65
	public function __construct( IParamDefinition $definition ) {
105 65
		$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...
106 65
	}
107
108
	/**
109
	 * Sets and cleans the original value and name.
110
	 * @see IParam::setUserValue
111
	 *
112
	 * @since 1.0
113
	 *
114
	 * @param string $paramName
115
	 * @param string $paramValue
116
	 * @param Options $options
117
	 *
118
	 * @return boolean
119
	 */
120 64
	public function setUserValue( $paramName, $paramValue, Options $options ) {
121 64
		if ( $this->setCount > 0 && !$options->acceptOverriding() ) {
122
			// TODO
123
			return false;
124
		}
125
		else {
126 64
			$this->originalName = $paramName;
127 64
			$this->originalValue = $paramValue;
128
129 64
			$this->cleanValue( $options );
130
131 64
			$this->setCount++;
132
133 64
			return true;
134
		}
135
	}
136
137
	/**
138
	 * Sets the value.
139
	 *
140
	 * @since 1.0
141
	 *
142
	 * @param mixed $value
143
	 */
144 57
	public function setValue( $value ) {
145 57
		$this->value = $value;
146 57
	}
147
148
	/**
149
	 * Sets the $value to a cleaned value of $originalValue.
150
	 *
151
	 * TODO: the per-parameter lowercaseing and trimming here needs some thought
152
	 *
153
	 * @since 1.0
154
	 *
155
	 * @param Options $options
156
	 */
157 64
	protected function cleanValue( Options $options ) {
158 64
		$this->value = $this->originalValue;
159
160 64
		$trim = $this->getDefinition()->trimDuringClean();
161
162 64
		if ( $trim === true || ( is_null( $trim ) && $options->trimValues() ) ) {
163 64
			if ( is_string( $this->value ) ) {
164 64
				$this->value = trim( $this->value );
165
			}
166
		}
167
168 64
		if ( $this->definition->isList() ) {
169
			$this->value = explode( $this->definition->getDelimiter(), $this->value );
170
171
			if ( $trim === true || ( is_null( $trim ) && $options->trimValues() ) ) {
172
				foreach ( $this->value as &$element ) {
173
					if ( is_string( $element ) ) {
174
						$element = trim( $element );
175
					}
176
				}
177
			}
178
		}
179
180 64
		$definitionOptions = $this->definition->getOptions();
181
182 64
		if ( $options->lowercaseValues() || ( array_key_exists( 'tolower', $definitionOptions ) && $definitionOptions['tolower'] ) ) {
183
			if ( $this->definition->isList() ) {
184
				foreach ( $this->value as &$element ) {
0 ignored issues
show
Bug introduced by
The expression $this->value of type array|string 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...
185
					if ( is_string( $element ) ) {
186
						$element = strtolower( $element );
187
					}
188
				}
189
			}
190
			elseif ( is_string( $this->value ) ) {
191
				$this->value = strtolower( $this->value );
192
			}
193
		}
194 64
	}
195
196
	/**
197
	 * Parameter processing entry point.
198
	 * Processes the parameter. This includes parsing, validation and additional formatting.
199
	 *
200
	 * @since 1.0
201
	 *
202
	 * @param $definitions array of IParamDefinition
203
	 * @param $params array of IParam
204
	 * @param Options $options
205
	 *
206
	 * @throws Exception
207
	 */
208 65
	public function process( array &$definitions, array $params, Options $options ) {
209 65
		if ( $this->setCount == 0 ) {
210 1
			if ( $this->definition->isRequired() ) {
211
				// This should not occur, so throw an exception.
212
				throw new Exception( 'Attempted to validate a required parameter without first setting a value.' );
213
			}
214
			else {
215 1
				$this->setToDefault();
216
			}
217
		}
218
		else {
219 64
			$this->parseAndValidate( $options );
220
		}
221
222 65
		if ( !$this->hasFatalError() && ( $this->definition->shouldManipulateDefault() || !$this->wasSetToDefault() ) ) {
223 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...
224
		}
225 65
	}
226
227
	/**
228
	 * @since 1.0
229
	 *
230
	 * @param Options $options
231
	 *
232
	 * @return ValueParser
233
	 */
234 64
	public function getValueParser( Options $options ) {
235 64
		$parser = $this->definition->getValueParser();
236
237 64
		if ( get_class( $parser ) === NullParser::class ) {
238 64
			$parserType = $options->isStringlyTyped() ? 'string-parser' : 'typed-parser';
239
240
			// TODO: inject factory
241 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...
242
243 64
			if ( $parserClass !== NullParser::class ) {
244 50
				$parser = new $parserClass( new \ValueParsers\ParserOptions() );
245
			}
246
		}
247
248 64
		return $parser;
249
	}
250
251
	/**
252
	 * @since 1.0
253
	 *
254
	 * @param Options $options
255
	 */
256 64
	protected function parseAndValidate( Options $options ) {
257 64
		$parser = $this->getValueParser( $options );
258
259 64
		if ( $this->definition->isList() ) {
260
			$values = [];
261
262
			foreach ( $this->getValue() as $value ) {
263
				$parsedValue = $this->parseAndValidateValue( $parser, $value );
264
265
				if ( is_array( $parsedValue ) ) {
266
					$values[] = $parsedValue[0];
267
				}
268
			}
269
270
			$this->value = $values;
271
		}
272
		else {
273 64
			$parsedValue = $this->parseAndValidateValue( $parser, $this->getValue() );
274
275 64
			if ( is_array( $parsedValue ) ) {
276 64
				$this->value = $parsedValue[0];
277
			}
278
		}
279
280 64
		$this->setToDefaultIfNeeded();
281 64
	}
282
283
	/**
284
	 * Parses and validates the provided with with specified parser.
285
	 * The result is returned in an array on success. On fail, false is returned.
286
	 * The result is wrapped in an array since we need to be able to distinguish
287
	 * between the method returning false and the value being false.
288
	 *
289
	 * Parsing and validation errors get added to $this->errors.
290
	 *
291
	 * @since 1.0
292
	 *
293
	 * @param ValueParser $parser
294
	 * @param mixed $value
295
	 *
296
	 * @return array|bool
297
	 */
298 64
	protected function parseAndValidateValue( ValueParser $parser, $value ) {
299
		try {
300 64
			$value = $parser->parse( $value );
301
		}
302 7
		catch ( ParseException $parseException ) {
303 7
			$this->registerProcessingError( $parseException->getMessage() );
304 7
			return false;
305
		}
306
307 64
		if ( $value instanceof \DataValues\DataValue ) {
308 64
			$value = $value->getValue();
309
		}
310
311 64
		$this->validateValue( $value );
312
313 64
		return [ $value ];
314
	}
315
316
	/**
317
	 * @since 1.0
318
	 *
319
	 * @param string $message
320
	 */
321 21
	protected function registerProcessingError( $message ) {
322 21
		$this->errors[] = $this->newProcessingError( $message );
323 21
	}
324
325
	/**
326
	 * @since 1.0
327
	 *
328
	 * @param string $message
329
	 *
330
	 * @return ProcessingError
331
	 */
332 21
	protected function newProcessingError( $message ) {
333 21
		$severity = $this->isRequired() ? ProcessingError::SEVERITY_FATAL : ProcessingError::SEVERITY_NORMAL;
334 21
		return new ProcessingError( $message, $severity );
335
	}
336
337
	/**
338
	 * @since 1.0
339
	 *
340
	 * @param mixed $value
341
	 */
342 64
	protected function validateValue( $value ) {
343 64
		$validationCallback = $this->definition->getValidationCallback();
344
345 64
		if ( $validationCallback !== null && $validationCallback( $value ) !== true ) {
346 7
			$this->registerProcessingError( 'Validation callback failed' );
347
		}
348
		else {
349 64
			$validator = $this->definition->getValueValidator();
350 64
			if ( method_exists( $validator, 'setOptions' ) ) {
351 64
				$validator->setOptions( $this->definition->getOptions() );
352
			}
353 64
			$validationResult = $validator->validate( $value );
354
355 64
			if ( !$validationResult->isValid() ) {
356 16
				foreach ( $validationResult->getErrors() as $error ) {
357 16
					$this->registerProcessingError( $error->getText() );
358
				}
359
			}
360
		}
361 64
	}
362
363
	/**
364
	 * Sets the parameter value to the default if needed.
365
	 *
366
	 * @since 1.0
367
	 */
368 64
	protected function setToDefaultIfNeeded() {
369 64
		if ( $this->errors !== [] && !$this->hasFatalError() ) {
370 1
			$this->setToDefault();
371
		}
372 64
	}
373
374
	/**
375
	 * Returns the original use-provided name.
376
	 *
377
	 * @since 1.0
378
	 *
379
	 * @throws Exception
380
	 * @return string
381
	 */
382
	public function getOriginalName() {
383
		if ( $this->setCount == 0 ) {
384
			throw new Exception( 'No user input set to the parameter yet, so the original name does not exist' );
385
		}
386
		return $this->originalName;
387
	}
388
389
	/**
390
	 * Returns the original use-provided value.
391
	 *
392
	 * @since 1.0
393
	 *
394
	 * @throws Exception
395
	 * @return string
396
	 */
397
	public function getOriginalValue() {
398
		if ( $this->setCount == 0 ) {
399
			throw new Exception( 'No user input set to the parameter yet, so the original value does not exist' );
400
		}
401
		return $this->originalValue;
402
	}
403
404
	/**
405
	 * Returns all validation errors that occurred so far.
406
	 *
407
	 * @since 1.0
408
	 *
409
	 * @return ProcessingError[]
410
	 */
411 50
	public function getErrors() {
412 50
		return $this->errors;
413
	}
414
415
	/**
416
	 * Sets the parameter value to the default.
417
	 *
418
	 * @since 1.0
419
	 */
420 2
	protected function setToDefault() {
421 2
		$this->defaulted = true;
422 2
		$this->value = $this->definition->getDefault();
423 2
	}
424
425
	/**
426
	 * Gets if the parameter was set to it's default.
427
	 *
428
	 * @since 1.0
429
	 *
430
	 * @return boolean
431
	 */
432
	public function wasSetToDefault() {
433
		return $this->defaulted;
434
	}
435
436
	/**
437
	 * @return boolean
438
	 */
439 65
	public function hasFatalError() {
440 65
		foreach ( $this->errors as $error ) {
441 21
			if ( $error->isFatal() ) {
442 20
				return true;
443
			}
444
		}
445
446 57
		return false;
447
	}
448
449
	/**
450
	 * Returns the IParamDefinition this IParam was constructed from.
451
	 *
452
	 * @since 1.0
453
	 *
454
	 * @return IParamDefinition
455
	 */
456 64
	public function getDefinition() {
457 64
		return $this->definition;
458
	}
459
460
	/**
461
	 * Returns the parameters value.
462
	 *
463
	 * @since 1.0
464
	 *
465
	 * @return mixed
466
	 */
467 65
	public function &getValue() {
468 65
		return $this->value;
469
	}
470
471
	/**
472
	 * Returns if the parameter is required or not.
473
	 *
474
	 * @since 1.0
475
	 *
476
	 * @return boolean
477
	 */
478 21
	public function isRequired() {
479 21
		return $this->definition->isRequired();
480
	}
481
482
	/**
483
	 * Returns if the name of the parameter.
484
	 *
485
	 * @since 1.0
486
	 *
487
	 * @return boolean
488
	 */
489
	public function getName() {
490
		return $this->definition->getName();
491
	}
492
493
	/**
494
	 * Returns the parameter name aliases.
495
	 *
496
	 * @since 1.0
497
	 *
498
	 * @return string[]
499
	 */
500
	public function getAliases() {
501
		return $this->definition->getAliases();
502
	}
503
504
}