Param::hasFatalError()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 3
1
<?php
2
3
namespace ParamProcessor\PackagePrivate;
4
5
use DataValues\DataValue;
6
use Exception;
7
use ParamProcessor\IParam;
8
use ParamProcessor\IParamDefinition;
9
use ParamProcessor\Options;
10
use ParamProcessor\ParamDefinition;
11
use ParamProcessor\ParamDefinitionFactory;
12
use ParamProcessor\ProcessingError;
13
use ValueParsers\NullParser;
14
use ValueParsers\ParseException;
15
use ValueParsers\ValueParser;
16
17
/**
18
 * Package private!
19
 *
20
 * Parameter class, representing the "instance" of a parameter.
21
 * Holds a ParamDefinition, user provided input (name & value) and processing state.
22
 *
23
 * @licence GNU GPL v2+
24
 * @author Jeroen De Dauw < [email protected] >
25
 */
26
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...
27
28
	/**
29
	 * Indicates whether parameters not found in the criteria list
30
	 * should be stored in case they are not accepted. The default is false.
31
	 *
32
	 * @deprecated since 1.7
33
	 */
34
	public static $accumulateParameterErrors = false;
35
36
	/**
37
	 * The original parameter name as provided by the user. This can be the
38
	 * main name or an alias.
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
	 * @var string
47
	 */
48
	protected $originalValue;
49
50
	/**
51
	 * @var mixed
52
	 */
53
	protected $value;
54
55
	/**
56
	 * Keeps track of how many times the parameter has been set by the user.
57
	 * This is used to detect overrides and for figuring out a parameter is missing.
58
	 * @var integer
59
	 */
60
	protected $setCount = 0;
61
62
	/**
63
	 * @var ProcessingError[]
64
	 */
65
	protected $errors = [];
66
67
	/**
68
	 * Indicates if the parameter was set to it's default.
69
	 * @var boolean
70
	 */
71
	protected $defaulted = false;
72
73
	/**
74
	 * @var ParamDefinition
75
	 */
76
	protected $definition;
77
78 107
	public function __construct( IParamDefinition $definition ) {
79 107
		$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...
80 107
	}
81
82
	/**
83
	 * Sets and cleans the original value and name.
84
	 */
85 105
	public function setUserValue( string $paramName, $paramValue, Options $options ): bool {
86 105
		if ( $this->setCount > 0 && !$options->acceptOverriding() ) {
87
			// TODO
88
			return false;
89
		}
90
91 105
		$this->originalName = $paramName;
92 105
		$this->originalValue = $paramValue;
93
94 105
		$this->cleanValue( $options );
95
96 105
		$this->setCount++;
97
98 105
		return true;
99
	}
100
101
	/**
102
	 * @param mixed $value
103
	 */
104 93
	public function setValue( $value ) {
105 93
		$this->value = $value;
106 93
	}
107
108
	/**
109
	 * Sets the $value to a cleaned value of $originalValue.
110
	 */
111 105
	protected function cleanValue( Options $options ) {
112 105
		if ( $this->definition->isList() ) {
113
			$this->value = explode( $this->definition->getDelimiter(), $this->originalValue );
114
		}
115
		else {
116 105
			$this->value = $this->originalValue;
117
		}
118
119 105
		if ( $this->shouldTrim( $options ) ) {
120 105
			$this->trimValue();
121
		}
122
123 105
		if ( $this->shouldLowercase( $options ) ) {
124
			$this->lowercaseValue();
125
		}
126 105
	}
127
128 105
	private function shouldTrim( Options $options ): bool {
129 105
		$trim = $this->definition->trimDuringClean();
130
131 105
		if ( $trim === true ) {
132
			return true;
133
		}
134
135 105
		return is_null( $trim ) && $options->trimValues();
136
	}
137
138 105
	private function trimValue() {
139 105
		if ( is_string( $this->value ) ) {
140 99
			$this->value = trim( $this->value );
141
		}
142 58
		elseif ( $this->definition->isList() ) {
143
			foreach ( $this->value as &$element ) {
0 ignored issues
show
Bug introduced by
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...
144
				if ( is_string( $element ) ) {
145
					$element = trim( $element );
146
				}
147
			}
148
		}
149 105
	}
150
151 105
	private function shouldLowercase( Options $options ): bool {
152 105
		if ( $options->lowercaseValues() ) {
153
			return true;
154
		}
155
156 105
		$definitionOptions = $this->definition->getOptions();
157
158 105
		return array_key_exists( 'tolower', $definitionOptions ) && $definitionOptions['tolower'];
159
	}
160
161
	private function lowercaseValue() {
162
		if ( $this->definition->isList() ) {
163
			foreach ( $this->value as &$element ) {
164
				if ( is_string( $element ) ) {
165
					$element = strtolower( $element );
166
				}
167
			}
168
		}
169
		elseif ( is_string( $this->value ) ) {
170
			$this->value = strtolower( $this->value );
171
		}
172
	}
173
174
	/**
175
	 * Parameter processing entry point.
176
	 * Processes the parameter. This includes parsing, validation and additional formatting.
177
	 *
178
	 * @param ParamDefinition[] $definitions
179
	 * @param Param[] $params
180
	 * @param Options $options
181
	 *
182
	 * @throws Exception
183
	 */
184 105
	public function process( array &$definitions, array $params, Options $options ) {
185 105
		if ( $this->setCount == 0 ) {
186 1
			if ( $this->definition->isRequired() ) {
187
				// This should not occur, so throw an exception.
188
				throw new Exception( 'Attempted to validate a required parameter without first setting a value.' );
189
			}
190
			else {
191 1
				$this->setToDefault();
192
			}
193
		}
194
		else {
195 104
			$this->parseAndValidate( $options );
196
		}
197
198 105
		if ( !$this->hasFatalError() && ( $this->definition->shouldManipulateDefault() || !$this->wasSetToDefault() ) ) {
199 93
			$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...
200
		}
201 105
	}
202
203 104
	public function getValueParser( Options $options ): ValueParser {
204 104
		$parser = $this->definition->getValueParser();
205
206 104
		if ( !( $parser instanceof NullParser ) ) {
207
			return $parser;
208
		}
209
210
		// TODO: inject factory
211 104
		$type = ParamDefinitionFactory::singleton()->getType( $this->definition->getType() );
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...
212
213 104
		$parserClass = $options->isStringlyTyped() ? $type->getStringParserClass() : $type->getTypedParserClass();
0 ignored issues
show
Deprecated Code introduced by
The method ParamProcessor\Options::isStringlyTyped() has been deprecated with message: since 1.7

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...
214
215 104
		return new $parserClass( new \ValueParsers\ParserOptions( $this->definition->getOptions() ) );
216
	}
217
218 104
	protected function parseAndValidate( Options $options ) {
219 104
		$parser = $this->getValueParser( $options );
220
221 104
		if ( $this->definition->isList() ) {
222
			$values = [];
223
224
			foreach ( $this->getValue() as $value ) {
225
				$parsedValue = $this->parseAndValidateValue( $parser, $value );
226
227
				if ( is_array( $parsedValue ) ) {
228
					$values[] = $parsedValue[0];
229
				}
230
			}
231
232
			$this->value = $values;
233
		}
234
		else {
235 104
			$parsedValue = $this->parseAndValidateValue( $parser, $this->getValue() );
236
237 104
			if ( is_array( $parsedValue ) ) {
238 99
				$this->value = $parsedValue[0];
239
			}
240
		}
241
242 104
		$this->setToDefaultIfNeeded();
243 104
	}
244
245
	/**
246
	 * Parses and validates the provided with with specified parser.
247
	 * The result is returned in an array on success. On fail, false is returned.
248
	 * The result is wrapped in an array since we need to be able to distinguish
249
	 * between the method returning false and the value being false.
250
	 *
251
	 * Parsing and validation errors get added to $this->errors.
252
	 *
253
	 * @since 1.0
254
	 *
255
	 * @param ValueParser $parser
256
	 * @param mixed $value
257
	 *
258
	 * @return array|bool
259
	 */
260 104
	protected function parseAndValidateValue( ValueParser $parser, $value ) {
261
		try {
262 104
			$value = $parser->parse( $value );
263
		}
264 13
		catch ( ParseException $parseException ) {
265 13
			$this->registerProcessingError( $parseException->getMessage() );
266 13
			return false;
267
		}
268
269 99
		if ( $value instanceof DataValue ) {
270 58
			$value = $value->getValue();
271
		}
272
273 99
		$this->validateValue( $value );
274
275 99
		return [ $value ];
276
	}
277
278 35
	protected function registerProcessingError( string $message ) {
279 35
		$this->errors[] = $this->newProcessingError( $message );
280 35
	}
281
282 35
	protected function newProcessingError( string $message ): ProcessingError {
283 35
		$severity = $this->isRequired() ? ProcessingError::SEVERITY_FATAL : ProcessingError::SEVERITY_NORMAL;
284 35
		return new ProcessingError( $message, $severity );
285
	}
286
287
	/**
288
	 * @param mixed $value
289
	 */
290 99
	protected function validateValue( $value ) {
291 99
		$validationCallback = $this->definition->getValidationCallback();
292
293 99
		if ( $validationCallback !== null && $validationCallback( $value ) !== true ) {
294 7
			$this->registerProcessingError( 'Validation callback failed' );
295
		}
296
		else {
297 99
			$validator = $this->definition->getValueValidator();
298 99
			if ( method_exists( $validator, 'setOptions' ) ) {
299 99
				$validator->setOptions( $this->definition->getOptions() );
300
			}
301 99
			$validationResult = $validator->validate( $value );
302
303 99
			if ( !$validationResult->isValid() ) {
304 24
				foreach ( $validationResult->getErrors() as $error ) {
305 24
					$this->registerProcessingError( $error->getText() );
306
				}
307
			}
308
		}
309 99
	}
310
311
	/**
312
	 * Sets the parameter value to the default if needed.
313
	 */
314 104
	protected function setToDefaultIfNeeded() {
315 104
		if ( $this->shouldSetToDefault() ) {
316 15
			$this->setToDefault();
317
		}
318 104
	}
319
320 104
	private function shouldSetToDefault(): bool {
321 104
		if ( $this->hasFatalError() ) {
322 20
			return false;
323
		}
324
325 92
		if ( $this->definition->isList() ) {
326
			return $this->errors !== [] && $this->value === [];
327
		}
328
329 92
		return $this->errors !== [];
330
	}
331
332
	/**
333
	 * Returns the original use-provided name.
334
	 *
335
	 * @throws Exception
336
	 * @return string
337
	 */
338 36
	public function getOriginalName(): string {
339 36
		if ( $this->setCount == 0 ) {
340
			throw new Exception( 'No user input set to the parameter yet, so the original name does not exist' );
341
		}
342 36
		return $this->originalName;
343
	}
344
345
	/**
346
	 * Returns the original use-provided value.
347
	 *
348
	 * @throws Exception
349
	 * @return mixed
350
	 */
351 36
	public function getOriginalValue() {
352 36
		if ( $this->setCount == 0 ) {
353
			throw new Exception( 'No user input set to the parameter yet, so the original value does not exist' );
354
		}
355 36
		return $this->originalValue;
356
	}
357
358
	/**
359
	 * Returns all validation errors that occurred so far.
360
	 *
361
	 * @return ProcessingError[]
362
	 */
363 95
	public function getErrors(): array {
364 95
		return $this->errors;
365
	}
366
367
	/**
368
	 * Sets the parameter value to the default.
369
	 */
370 16
	protected function setToDefault() {
371 16
		$this->defaulted = true;
372 16
		$this->value = $this->definition->getDefault();
373 16
	}
374
375 49
	public function wasSetToDefault(): bool {
376 49
		return $this->defaulted;
377
	}
378
379 105
	public function hasFatalError(): bool {
380 105
		foreach ( $this->errors as $error ) {
381 35
			if ( $error->isFatal() ) {
382 20
				return true;
383
			}
384
		}
385
386 93
		return false;
387
	}
388
389
	/**
390
	 * Returns the ParamDefinition this Param was constructed from.
391
	 */
392
	public function getDefinition(): ParamDefinition {
393
		return $this->definition;
394
	}
395
396
	/**
397
	 * @return mixed
398
	 */
399 105
	public function &getValue() {
400 105
		return $this->value;
401
	}
402
403 36
	public function isRequired(): bool {
404 36
		return $this->definition->isRequired();
405
	}
406
407 49
	public function getName(): string {
408 49
		return $this->definition->getName();
409
	}
410
411
	/**
412
	 * @return string[]
413
	 */
414
	public function getAliases(): array {
415
		return $this->definition->getAliases();
416
	}
417
418
}