Passed
Pull Request — master (#29)
by no
04:45
created

Param   C

Complexity

Total Complexity 68

Size/Duplication

Total Lines 488
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 74.22%

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 11
dl 0
loc 488
ccs 95
cts 128
cp 0.7422
rs 5.6756
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A setUserValue() 0 16 3
A setValue() 0 3 1
D cleanValue() 0 38 18
B process() 0 18 6
A getValueParser() 0 16 4
B 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 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
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
	 * The definition of the parameter.
92
	 *
93
	 * @since 1.0
94
	 *
95
	 * @var ParamDefinition
96
	 */
97
	protected $definition;
98
99
	/**
100
	 * Constructor.
101
	 *
102
	 * @since 1.0
103
	 *
104
	 * @param IParamDefinition $definition
105
	 */
106 65
	public function __construct( IParamDefinition $definition ) {
107 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...
108 65
	}
109
110
	/**
111
	 * Sets and cleans the original value and name.
112
	 * @see IParam::setUserValue
113
	 *
114
	 * @since 1.0
115
	 *
116
	 * @param string $paramName
117
	 * @param string $paramValue
118
	 * @param Options $options
119
	 *
120
	 * @return boolean
121
	 */
122 64
	public function setUserValue( $paramName, $paramValue, Options $options ) {
123 64
		if ( $this->setCount > 0 && !$options->acceptOverriding() ) {
124
			// TODO
125
			return false;
126
		}
127
		else {
128 64
			$this->originalName = $paramName;
129 64
			$this->originalValue = $paramValue;
130
131 64
			$this->cleanValue( $options );
132
133 64
			$this->setCount++;
134
135 64
			return true;
136
		}
137
	}
138
139
	/**
140
	 * Sets the value.
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
	 * TODO: the per-parameter lowercaseing and trimming here needs some thought
154
	 *
155
	 * @since 1.0
156
	 *
157
	 * @param Options $options
158
	 */
159 64
	protected function cleanValue( Options $options ) {
160 64
		$this->value = $this->originalValue;
161
162 64
		$trim = $this->getDefinition()->trimDuringClean();
163
164 64
		if ( $trim === true || ( is_null( $trim ) && $options->trimValues() ) ) {
165 64
			if ( is_string( $this->value ) ) {
166 64
				$this->value = trim( $this->value );
167
			}
168
		}
169
170 64
		if ( $this->definition->isList() ) {
171
			$this->value = explode( $this->definition->getDelimiter(), $this->value );
172
173
			if ( $trim === true || ( is_null( $trim ) && $options->trimValues() ) ) {
174
				foreach ( $this->value as &$element ) {
175
					if ( is_string( $element ) ) {
176
						$element = trim( $element );
177
					}
178
				}
179
			}
180
		}
181
182 64
		$definitionOptions = $this->definition->getOptions();
183
184 64
		if ( $options->lowercaseValues() || ( array_key_exists( 'tolower', $definitionOptions ) && $definitionOptions['tolower'] ) ) {
185
			if ( $this->definition->isList() ) {
186
				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...
187
					if ( is_string( $element ) ) {
188
						$element = strtolower( $element );
189
					}
190
				}
191
			}
192
			elseif ( is_string( $this->value ) ) {
193
				$this->value = strtolower( $this->value );
194
			}
195
		}
196 64
	}
197
198
	/**
199
	 * Parameter processing entry point.
200
	 * Processes the parameter. This includes parsing, validation and additional formatting.
201
	 *
202
	 * @since 1.0
203
	 *
204
	 * @param $definitions array of IParamDefinition
205
	 * @param $params array of IParam
206
	 * @param Options $options
207
	 *
208
	 * @throws Exception
209
	 */
210 65
	public function process( array &$definitions, array $params, Options $options ) {
211 65
		if ( $this->setCount == 0 ) {
212 1
			if ( $this->definition->isRequired() ) {
213
				// This should not occur, so throw an exception.
214
				throw new Exception( 'Attempted to validate a required parameter without first setting a value.' );
215
			}
216
			else {
217 1
				$this->setToDefault();
218
			}
219
		}
220
		else {
221 64
			$this->parseAndValidate( $options );
222
		}
223
224 65
		if ( !$this->hasFatalError() && ( $this->definition->shouldManipulateDefault() || !$this->wasSetToDefault() ) ) {
225 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...
226
		}
227 65
	}
228
229
	/**
230
	 * @since 1.0
231
	 *
232
	 * @param Options $options
233
	 *
234
	 * @return ValueParser
235
	 */
236 64
	public function getValueParser( Options $options ) {
237 64
		$parser = $this->definition->getValueParser();
238
239 64
		if ( get_class( $parser ) === NullParser::class ) {
240 64
			$parserType = $options->isStringlyTyped() ? 'string-parser' : 'typed-parser';
241
242
			// TODO: inject factory
243 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...
244
245 64
			if ( $parserClass !== NullParser::class ) {
246 50
				$parser = new $parserClass( new \ValueParsers\ParserOptions() );
247
			}
248
		}
249
250 64
		return $parser;
251
	}
252
253
	/**
254
	 * @since 1.0
255
	 *
256
	 * @param Options $options
257
	 */
258 64
	protected function parseAndValidate( Options $options ) {
259 64
		$parser = $this->getValueParser( $options );
260
261 64
		if ( $this->definition->isList() ) {
262
			$values = [];
263
264
			foreach ( $this->getValue() as $value ) {
265
				$parsedValue = $this->parseAndValidateValue( $parser, $value );
266
267
				if ( is_array( $parsedValue ) ) {
268
					$values[] = $parsedValue[0];
269
				}
270
			}
271
272
			$this->value = $values;
273
		}
274
		else {
275 64
			$parsedValue = $this->parseAndValidateValue( $parser, $this->getValue() );
276
277 64
			if ( is_array( $parsedValue ) ) {
278 64
				$this->value = $parsedValue[0];
279
			}
280
		}
281
282 64
		$this->setToDefaultIfNeeded();
283 64
	}
284
285
	/**
286
	 * Parses and validates the provided with with specified parser.
287
	 * The result is returned in an array on success. On fail, false is returned.
288
	 * The result is wrapped in an array since we need to be able to distinguish
289
	 * between the method returning false and the value being false.
290
	 *
291
	 * Parsing and validation errors get added to $this->errors.
292
	 *
293
	 * @since 1.0
294
	 *
295
	 * @param ValueParser $parser
296
	 * @param mixed $value
297
	 *
298
	 * @return array|bool
299
	 */
300 64
	protected function parseAndValidateValue( ValueParser $parser, $value ) {
301
		try {
302 64
			$value = $parser->parse( $value );
303
		}
304 7
		catch ( ParseException $parseException ) {
305 7
			$this->registerProcessingError( $parseException->getMessage() );
306 7
			return false;
307
		}
308
309 64
		if ( $value instanceof \DataValues\DataValue ) {
310 64
			$value = $value->getValue();
311
		}
312
313 64
		$this->validateValue( $value );
314
315 64
		return [ $value ];
316
	}
317
318
	/**
319
	 * @since 1.0
320
	 *
321
	 * @param string $message
322
	 */
323 21
	protected function registerProcessingError( $message ) {
324 21
		$this->errors[] = $this->newProcessingError( $message );
325 21
	}
326
327
	/**
328
	 * @since 1.0
329
	 *
330
	 * @param string $message
331
	 *
332
	 * @return ProcessingError
333
	 */
334 21
	protected function newProcessingError( $message ) {
335 21
		$severity = $this->isRequired() ? ProcessingError::SEVERITY_FATAL : ProcessingError::SEVERITY_NORMAL;
336 21
		return new ProcessingError( $message, $severity );
337
	}
338
339
	/**
340
	 * @since 1.0
341
	 *
342
	 * @param mixed $value
343
	 */
344 64
	protected function validateValue( $value ) {
345 64
		$validationCallback = $this->definition->getValidationCallback();
346
347 64
		if ( $validationCallback !== null && $validationCallback( $value ) !== true ) {
348 7
			$this->registerProcessingError( 'Validation callback failed' );
349
		}
350
		else {
351 64
			$validator = $this->definition->getValueValidator();
352 64
			if ( method_exists( $validator, 'setOptions' ) ) {
353 64
				$validator->setOptions( $this->definition->getOptions() );
354
			}
355 64
			$validationResult = $validator->validate( $value );
356
357 64
			if ( !$validationResult->isValid() ) {
358 16
				foreach ( $validationResult->getErrors() as $error ) {
359 16
					$this->registerProcessingError( $error->getText() );
360
				}
361
			}
362
		}
363 64
	}
364
365
	/**
366
	 * Sets the parameter value to the default if needed.
367
	 *
368
	 * @since 1.0
369
	 */
370 64
	protected function setToDefaultIfNeeded() {
371 64
		if ( $this->errors !== [] && !$this->hasFatalError() ) {
372 1
			$this->setToDefault();
373
		}
374 64
	}
375
376
	/**
377
	 * Returns the original use-provided name.
378
	 *
379
	 * @since 1.0
380
	 *
381
	 * @throws Exception
382
	 * @return string
383
	 */
384
	public function getOriginalName() {
385
		if ( $this->setCount == 0 ) {
386
			throw new Exception( 'No user input set to the parameter yet, so the original name does not exist' );
387
		}
388
		return $this->originalName;
389
	}
390
391
	/**
392
	 * Returns the original use-provided value.
393
	 *
394
	 * @since 1.0
395
	 *
396
	 * @throws Exception
397
	 * @return string
398
	 */
399
	public function getOriginalValue() {
400
		if ( $this->setCount == 0 ) {
401
			throw new Exception( 'No user input set to the parameter yet, so the original value does not exist' );
402
		}
403
		return $this->originalValue;
404
	}
405
406
	/**
407
	 * Returns all validation errors that occurred so far.
408
	 *
409
	 * @since 1.0
410
	 *
411
	 * @return ProcessingError[]
412
	 */
413 50
	public function getErrors() {
414 50
		return $this->errors;
415
	}
416
417
	/**
418
	 * Sets the parameter value to the default.
419
	 *
420
	 * @since 1.0
421
	 */
422 2
	protected function setToDefault() {
423 2
		$this->defaulted = true;
424 2
		$this->value = $this->definition->getDefault();
425 2
	}
426
427
	/**
428
	 * Gets if the parameter was set to it's default.
429
	 *
430
	 * @since 1.0
431
	 *
432
	 * @return boolean
433
	 */
434
	public function wasSetToDefault() {
435
		return $this->defaulted;
436
	}
437
438
	/**
439
	 * Returns false when there are no fatal errors or an ProcessingError when one is found.
440
	 *
441
	 * @return mixed false or ProcessingError
442
	 */
443 65
	public function hasFatalError() {
444 65
		foreach ( $this->errors as $error ) {
445 21
			if ( $error->isFatal() ) {
446 20
				return true;
447
			}
448
		}
449
450 57
		return false;
451
	}
452
453
	/**
454
	 * Returns the IParamDefinition this IParam was constructed from.
455
	 *
456
	 * @since 1.0
457
	 *
458
	 * @return IParamDefinition
459
	 */
460 64
	public function getDefinition() {
461 64
		return $this->definition;
462
	}
463
464
	/**
465
	 * Returns the parameters value.
466
	 *
467
	 * @since 1.0
468
	 *
469
	 * @return mixed
470
	 */
471 65
	public function &getValue() {
472 65
		return $this->value;
473
	}
474
475
	/**
476
	 * Returns if the parameter is required or not.
477
	 *
478
	 * @since 1.0
479
	 *
480
	 * @return boolean
481
	 */
482 21
	public function isRequired() {
483 21
		return $this->definition->isRequired();
484
	}
485
486
	/**
487
	 * Returns if the name of the parameter.
488
	 *
489
	 * @since 1.0
490
	 *
491
	 * @return boolean
492
	 */
493
	public function getName() {
494
		return $this->definition->getName();
495
	}
496
497
	/**
498
	 * Returns the parameter name aliases.
499
	 *
500
	 * @since 1.0
501
	 *
502
	 * @return array
503
	 */
504
	public function getAliases() {
505
		return $this->definition->getAliases();
506
	}
507
508
}