Completed
Push — master ( ef46bd...45441d )
by Jeroen De
02:58
created

Param::wasSetToDefault()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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 80
	public function __construct( IParamDefinition $definition ) {
79 80
		$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 80
	}
81
82
	/**
83
	 * Sets and cleans the original value and name.
84
	 */
85 78
	public function setUserValue( string $paramName, $paramValue, Options $options ): bool {
86 78
		if ( $this->setCount > 0 && !$options->acceptOverriding() ) {
87
			// TODO
88
			return false;
89
		}
90
91 78
		$this->originalName = $paramName;
92 78
		$this->originalValue = $paramValue;
93
94 78
		$this->cleanValue( $options );
95
96 78
		$this->setCount++;
97
98 78
		return true;
99
	}
100
101
	/**
102
	 * @param mixed $value
103
	 */
104 63
	public function setValue( $value ) {
105 63
		$this->value = $value;
106 63
	}
107
108
	/**
109
	 * Sets the $value to a cleaned value of $originalValue.
110
	 */
111 78
	protected function cleanValue( Options $options ) {
112 78
		if ( $this->definition->isList() ) {
113
			$this->value = explode( $this->definition->getDelimiter(), $this->originalValue );
114
		}
115
		else {
116 78
			$this->value = $this->originalValue;
117
		}
118
119 78
		if ( $this->shouldTrim( $options ) ) {
120 78
			$this->trimValue();
121
		}
122
123 78
		if ( $this->shouldLowercase( $options ) ) {
124
			$this->lowercaseValue();
125
		}
126 78
	}
127
128 78
	private function shouldTrim( Options $options ): bool {
129 78
		$trim = $this->definition->trimDuringClean();
130
131 78
		if ( $trim === true ) {
132
			return true;
133
		}
134
135 78
		return is_null( $trim ) && $options->trimValues();
136
	}
137
138 78
	private function trimValue() {
139 78
		if ( is_string( $this->value ) ) {
140 78
			$this->value = trim( $this->value );
141
		}
142 54
		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 78
	}
150
151 78
	private function shouldLowercase( Options $options ): bool {
152 78
		if ( $options->lowercaseValues() ) {
153
			return true;
154
		}
155
156 78
		$definitionOptions = $this->definition->getOptions();
157
158 78
		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 79
	public function process( array &$definitions, array $params, Options $options ) {
185 79
		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 78
			$this->parseAndValidate( $options );
196
		}
197
198 79
		if ( !$this->hasFatalError() && ( $this->definition->shouldManipulateDefault() || !$this->wasSetToDefault() ) ) {
199 63
			$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 79
	}
202
203 78
	public function getValueParser( Options $options ): ValueParser {
204 78
		$parser = $this->definition->getValueParser();
205
206 78
		if ( !( $parser instanceof NullParser ) ) {
207
			return $parser;
208
		}
209
210
		// TODO: inject factory
211 78
		$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 78
		$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 78
		return new $parserClass( new \ValueParsers\ParserOptions() );
216
	}
217
218 78
	protected function parseAndValidate( Options $options ) {
219 78
		$parser = $this->getValueParser( $options );
220
221 78
		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 78
			$parsedValue = $this->parseAndValidateValue( $parser, $this->getValue() );
236
237 78
			if ( is_array( $parsedValue ) ) {
238 78
				$this->value = $parsedValue[0];
239
			}
240
		}
241
242 78
		$this->setToDefaultIfNeeded();
243 78
	}
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 78
	protected function parseAndValidateValue( ValueParser $parser, $value ) {
261
		try {
262 78
			$value = $parser->parse( $value );
263
		}
264 7
		catch ( ParseException $parseException ) {
265 7
			$this->registerProcessingError( $parseException->getMessage() );
266 7
			return false;
267
		}
268
269 78
		if ( $value instanceof DataValue ) {
270 78
			$value = $value->getValue();
271
		}
272
273 78
		$this->validateValue( $value );
274
275 78
		return [ $value ];
276
	}
277
278 29
	protected function registerProcessingError( string $message ) {
279 29
		$this->errors[] = $this->newProcessingError( $message );
280 29
	}
281
282 29
	protected function newProcessingError( string $message ): ProcessingError {
283 29
		$severity = $this->isRequired() ? ProcessingError::SEVERITY_FATAL : ProcessingError::SEVERITY_NORMAL;
284 29
		return new ProcessingError( $message, $severity );
285
	}
286
287
	/**
288
	 * @param mixed $value
289
	 */
290 78
	protected function validateValue( $value ) {
291 78
		$validationCallback = $this->definition->getValidationCallback();
292
293 78
		if ( $validationCallback !== null && $validationCallback( $value ) !== true ) {
294 7
			$this->registerProcessingError( 'Validation callback failed' );
295
		}
296
		else {
297 78
			$validator = $this->definition->getValueValidator();
298 78
			if ( method_exists( $validator, 'setOptions' ) ) {
299 78
				$validator->setOptions( $this->definition->getOptions() );
300
			}
301 78
			$validationResult = $validator->validate( $value );
302
303 78
			if ( !$validationResult->isValid() ) {
304 24
				foreach ( $validationResult->getErrors() as $error ) {
305 24
					$this->registerProcessingError( $error->getText() );
306
				}
307
			}
308
		}
309 78
	}
310
311
	/**
312
	 * Sets the parameter value to the default if needed.
313
	 */
314 78
	protected function setToDefaultIfNeeded() {
315 78
		if ( $this->shouldSetToDefault() ) {
316 1
			$this->setToDefault();
317
		}
318 78
	}
319
320 78
	private function shouldSetToDefault(): bool {
321 78
		if ( $this->hasFatalError() ) {
322 28
			return false;
323
		}
324
325 62
		if ( $this->definition->isList() ) {
326
			return $this->errors !== [] && $this->value === [];
327
		}
328
329 62
		return $this->errors !== [];
330
	}
331
332
	/**
333
	 * Returns the original use-provided name.
334
	 *
335
	 * @throws Exception
336
	 * @return string
337
	 */
338 14
	public function getOriginalName(): string {
339 14
		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 14
		return $this->originalName;
343
	}
344
345
	/**
346
	 * Returns the original use-provided value.
347
	 *
348
	 * @throws Exception
349
	 * @return mixed
350
	 */
351 14
	public function getOriginalValue() {
352 14
		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 14
		return $this->originalValue;
356
	}
357
358
	/**
359
	 * Returns all validation errors that occurred so far.
360
	 *
361
	 * @return ProcessingError[]
362
	 */
363 64
	public function getErrors(): array {
364 64
		return $this->errors;
365
	}
366
367
	/**
368
	 * Sets the parameter value to the default.
369
	 */
370 2
	protected function setToDefault() {
371 2
		$this->defaulted = true;
372 2
		$this->value = $this->definition->getDefault();
373 2
	}
374
375 14
	public function wasSetToDefault(): bool {
376 14
		return $this->defaulted;
377
	}
378
379 79
	public function hasFatalError(): bool {
380 79
		foreach ( $this->errors as $error ) {
381 29
			if ( $error->isFatal() ) {
382 28
				return true;
383
			}
384
		}
385
386 63
		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 79
	public function &getValue() {
400 79
		return $this->value;
401
	}
402
403 30
	public function isRequired(): bool {
404 30
		return $this->definition->isRequired();
405
	}
406
407 14
	public function getName(): string {
408 14
		return $this->definition->getName();
409
	}
410
411
	/**
412
	 * @return string[]
413
	 */
414
	public function getAliases(): array {
415
		return $this->definition->getAliases();
416
	}
417
418
}