Passed
Push — master ( 433542...84cc78 )
by Jeroen De
02:07
created

Param   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 497
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 74.63%

Importance

Changes 0
Metric Value
wmc 70
lcom 1
cbo 11
dl 0
loc 497
ccs 100
cts 134
cp 0.7463
rs 2.8
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A setUserValue() 0 16 3
A setValue() 0 3 1
D cleanValue() 0 39 18
A process() 0 18 6
A getValueParser() 0 16 4
A parseAndValidate() 0 26 5
A parseAndValidateValue() 0 17 3
A registerProcessingError() 0 3 1
A newProcessingError() 0 4 2
B validateValue() 0 20 6
A setToDefaultIfNeeded() 0 5 2
A shouldSetToDefault() 0 11 3
A getOriginalName() 0 6 2
A getOriginalValue() 0 6 2
A getErrors() 0 3 1
A setToDefault() 0 4 1
A wasSetToDefault() 0 3 1
A hasFatalError() 0 9 3
A getDefinition() 0 3 1
A getValue() 0 3 1
A isRequired() 0 3 1
A getName() 0 3 1
A getAliases() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Param often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Param, and based on these observations, apply Extract Interface, too.

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 jeroendedauw
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 Jeroen De Dauw
$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
169 64
		if ( $this->definition->isList() ) {
170
			$this->value = explode( $this->definition->getDelimiter(), $this->value );
171
172
			if ( $trim === true || ( is_null( $trim ) && $options->trimValues() ) ) {
173
				foreach ( $this->value as &$element ) {
174
					if ( is_string( $element ) ) {
175
						$element = trim( $element );
176
					}
177
				}
178
			}
179
		}
180
181 64
		$definitionOptions = $this->definition->getOptions();
182
183 64
		if ( $options->lowercaseValues() || ( array_key_exists( 'tolower', $definitionOptions ) && $definitionOptions['tolower'] ) ) {
184
			if ( $this->definition->isList() ) {
185
				foreach ( $this->value as &$element ) {
0 ignored issues
show
Bug introduced by jeroendedauw
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...
186
					if ( is_string( $element ) ) {
187
						$element = strtolower( $element );
188
					}
189
				}
190
			}
191
			elseif ( is_string( $this->value ) ) {
192
				$this->value = strtolower( $this->value );
193
			}
194
		}
195 64
	}
196
197
	/**
198
	 * Parameter processing entry point.
199
	 * Processes the parameter. This includes parsing, validation and additional formatting.
200
	 *
201
	 * @since 1.0
202
	 *
203
	 * @param $definitions array of IParamDefinition
204
	 * @param $params array of IParam
205
	 * @param Options $options
206
	 *
207
	 * @throws Exception
208
	 */
209 65
	public function process( array &$definitions, array $params, Options $options ) {
210 65
		if ( $this->setCount == 0 ) {
211 1
			if ( $this->definition->isRequired() ) {
212
				// This should not occur, so throw an exception.
213
				throw new Exception( 'Attempted to validate a required parameter without first setting a value.' );
214
			}
215
			else {
216 1
				$this->setToDefault();
217
			}
218
		}
219
		else {
220 64
			$this->parseAndValidate( $options );
221
		}
222
223 65
		if ( !$this->hasFatalError() && ( $this->definition->shouldManipulateDefault() || !$this->wasSetToDefault() ) ) {
224 57
			$this->definition->format( $this, $definitions, $params );
0 ignored issues
show
Deprecated Code introduced by jeroendedauw
The method ParamProcessor\ParamDefinition::format() has been deprecated.

This method has been deprecated.

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