SchemaValidator::recursiveProcess()   F
last analyzed

Complexity

Conditions 62
Paths > 20000

Size

Total Lines 180
Code Lines 95

Duplication

Lines 17
Ratio 9.44 %

Importance

Changes 0
Metric Value
dl 17
loc 180
rs 2
c 0
b 0
f 0
cc 62
eloc 95
nc 63459
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Guzzle\Service\Description;
4
5
use Guzzle\Common\ToArrayInterface;
6
7
/**
8
 * Default parameter validator
9
 */
10
class SchemaValidator implements ValidatorInterface
11
{
12
    /** @var self Cache instance of the object */
13
    protected static $instance;
14
15
    /** @var bool Whether or not integers are converted to strings when an integer is received for a string input */
16
    protected $castIntegerToStringType;
17
18
    /** @var array Errors encountered while validating */
19
    protected $errors;
20
21
    /**
22
     * @return self
23
     * @codeCoverageIgnore
24
     */
25
    public static function getInstance()
26
    {
27
        if (!self::$instance) {
28
            self::$instance = new self();
29
        }
30
31
        return self::$instance;
32
    }
33
34
    /**
35
     * @param bool $castIntegerToStringType Set to true to convert integers into strings when a required type is a
36
     *                                      string and the input value is an integer. Defaults to true.
37
     */
38
    public function __construct($castIntegerToStringType = true)
39
    {
40
        $this->castIntegerToStringType = $castIntegerToStringType;
41
    }
42
43
    public function validate(Parameter $param, &$value)
44
    {
45
        $this->errors = array();
46
        $this->recursiveProcess($param, $value);
47
48
        if (empty($this->errors)) {
49
            return true;
50
        } else {
51
            sort($this->errors);
52
            return false;
53
        }
54
    }
55
56
    /**
57
     * Get the errors encountered while validating
58
     *
59
     * @return array
60
     */
61
    public function getErrors()
62
    {
63
        return $this->errors ?: array();
64
    }
65
66
    /**
67
     * Recursively validate a parameter
68
     *
69
     * @param Parameter $param  API parameter being validated
70
     * @param mixed     $value  Value to validate and validate. The value may change during this validate.
71
     * @param string    $path   Current validation path (used for error reporting)
72
     * @param int       $depth  Current depth in the validation validate
73
     *
74
     * @return bool Returns true if valid, or false if invalid
75
     */
76
    protected function recursiveProcess(Parameter $param, &$value, $path = '', $depth = 0)
77
    {
78
        // Update the value by adding default or static values
79
        $value = $param->getValue($value);
80
81
        $required = $param->getRequired();
82
        // if the value is null and the parameter is not required or is static, then skip any further recursion
83
        if ((null === $value && !$required) || $param->getStatic()) {
84
            return true;
85
        }
86
87
        $type = $param->getType();
88
        // Attempt to limit the number of times is_array is called by tracking if the value is an array
89
        $valueIsArray = is_array($value);
90
        // If a name is set then update the path so that validation messages are more helpful
91
        if ($name = $param->getName()) {
92
            $path .= "[{$name}]";
93
        }
94
95
        if ($type == 'object') {
96
97
            // Objects are either associative arrays, ToArrayInterface, or some other object
98
            if ($param->getInstanceOf()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $param->getInstanceOf() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
99
                $instance = $param->getInstanceOf();
100
                if (!($value instanceof $instance)) {
101
                    $this->errors[] = "{$path} must be an instance of {$instance}";
102
                    return false;
103
                }
104
            }
105
106
            // Determine whether or not this "value" has properties and should be traversed
107
            $traverse = $temporaryValue = false;
108
109
            // Convert the value to an array
110
            if (!$valueIsArray && $value instanceof ToArrayInterface) {
111
                $value = $value->toArray();
112
            }
113
114
            if ($valueIsArray) {
115
                // Ensure that the array is associative and not numerically indexed
116
                if (isset($value[0])) {
117
                    $this->errors[] = "{$path} must be an array of properties. Got a numerically indexed array.";
118
                    return false;
119
                }
120
                $traverse = true;
121
            } elseif ($value === null) {
122
                // Attempt to let the contents be built up by default values if possible
123
                $value = array();
124
                $temporaryValue = $valueIsArray = $traverse = true;
0 ignored issues
show
Unused Code introduced by
$valueIsArray is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
125
            }
126
127
            if ($traverse) {
128
129
                if ($properties = $param->getProperties()) {
130
                    // if properties were found, the validate each property of the value
131
                    foreach ($properties as $property) {
132
                        $name = $property->getName();
133
                        if (isset($value[$name])) {
134
                            $this->recursiveProcess($property, $value[$name], $path, $depth + 1);
135
                        } else {
136
                            $current = null;
137
                            $this->recursiveProcess($property, $current, $path, $depth + 1);
138
                            // Only set the value if it was populated with something
139
                            if (null !== $current) {
140
                                $value[$name] = $current;
141
                            }
142
                        }
143
                    }
144
                }
145
146
                $additional = $param->getAdditionalProperties();
147
                if ($additional !== true) {
148
                    // If additional properties were found, then validate each against the additionalProperties attr.
149
                    $keys = array_keys($value);
150
                    // Determine the keys that were specified that were not listed in the properties of the schema
151
                    $diff = array_diff($keys, array_keys($properties));
152
                    if (!empty($diff)) {
153
                        // Determine which keys are not in the properties
154
                        if ($additional instanceOf Parameter) {
155
                            foreach ($diff as $key) {
156
                                $this->recursiveProcess($additional, $value[$key], "{$path}[{$key}]", $depth);
157
                            }
158
                        } else {
159
                            // if additionalProperties is set to false and there are additionalProperties in the values, then fail
160
                            foreach ($diff as $prop) {
161
                                $this->errors[] = sprintf('%s[%s] is not an allowed property', $path, $prop);
162
                            }
163
                        }
164
                    }
165
                }
166
167
                // A temporary value will be used to traverse elements that have no corresponding input value.
168
                // This allows nested required parameters with default values to bubble up into the input.
169
                // Here we check if we used a temp value and nothing bubbled up, then we need to remote the value.
170
                if ($temporaryValue && empty($value)) {
171
                    $value = null;
172
                    $valueIsArray = false;
0 ignored issues
show
Unused Code introduced by
$valueIsArray is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
173
                }
174
            }
175
176
        } elseif ($type == 'array' && $valueIsArray && $param->getItems()) {
177
            foreach ($value as $i => &$item) {
178
                // Validate each item in an array against the items attribute of the schema
179
                $this->recursiveProcess($param->getItems(), $item, $path . "[{$i}]", $depth + 1);
180
            }
181
        }
182
183
        // If the value is required and the type is not null, then there is an error if the value is not set
184
        if ($required && $value === null && $type != 'null') {
185
            $message = "{$path} is " . ($param->getType() ? ('a required ' . implode(' or ', (array) $param->getType())) : 'required');
186
            if ($param->getDescription()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $param->getDescription() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
187
                $message .= ': ' . $param->getDescription();
188
            }
189
            $this->errors[] = $message;
190
            return false;
191
        }
192
193
        // Validate that the type is correct. If the type is string but an integer was passed, the class can be
194
        // instructed to cast the integer to a string to pass validation. This is the default behavior.
195
        if ($type && (!$type = $this->determineType($type, $value))) {
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type array; however, Guzzle\Service\Descripti...idator::determineType() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
196
            if ($this->castIntegerToStringType && $param->getType() == 'string' && is_integer($value)) {
197
                $value = (string) $value;
198
            } else {
199
                $this->errors[] = "{$path} must be of type " . implode(' or ', (array) $param->getType());
200
            }
201
        }
202
203
        // Perform type specific validation for strings, arrays, and integers
204
        if ($type == 'string') {
205
206
            // Strings can have enums which are a list of predefined values
207
            if (($enum = $param->getEnum()) && !in_array($value, $enum)) {
208
                $this->errors[] = "{$path} must be one of " . implode(' or ', array_map(function ($s) {
209
                    return '"' . addslashes($s) . '"';
210
                }, $enum));
211
            }
212
            // Strings can have a regex pattern that the value must match
213
            if (($pattern  = $param->getPattern()) && !preg_match($pattern, $value)) {
214
                $this->errors[] = "{$path} must match the following regular expression: {$pattern}";
215
            }
216
217
            $strLen = null;
218 View Code Duplication
            if ($min = $param->getMinLength()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
                $strLen = strlen($value);
220
                if ($strLen < $min) {
221
                    $this->errors[] = "{$path} length must be greater than or equal to {$min}";
222
                }
223
            }
224 View Code Duplication
            if ($max = $param->getMaxLength()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
225
                if (($strLen ?: strlen($value)) > $max) {
226
                    $this->errors[] = "{$path} length must be less than or equal to {$max}";
227
                }
228
            }
229
230
        } elseif ($type == 'array') {
231
232
            $size = null;
233 View Code Duplication
            if ($min = $param->getMinItems()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
234
                $size = count($value);
235
                if ($size < $min) {
236
                    $this->errors[] = "{$path} must contain {$min} or more elements";
237
                }
238
            }
239
            if ($max = $param->getMaxItems()) {
240
                if (($size ?: count($value)) > $max) {
241
                    $this->errors[] = "{$path} must contain {$max} or fewer elements";
242
                }
243
            }
244
245
        } elseif ($type == 'integer' || $type == 'number' || $type == 'numeric') {
246
            if (($min = $param->getMinimum()) && $value < $min) {
247
                $this->errors[] = "{$path} must be greater than or equal to {$min}";
248
            }
249
            if (($max = $param->getMaximum()) && $value > $max) {
250
                $this->errors[] = "{$path} must be less than or equal to {$max}";
251
            }
252
        }
253
254
        return empty($this->errors);
255
    }
256
257
    /**
258
     * From the allowable types, determine the type that the variable matches
259
     *
260
     * @param string $type  Parameter type
261
     * @param mixed  $value Value to determine the type
262
     *
263
     * @return string|bool Returns the matching type on
264
     */
265
    protected function determineType($type, $value)
266
    {
267
        foreach ((array) $type as $t) {
268
            if ($t == 'string' && (is_string($value) || (is_object($value) && method_exists($value, '__toString')))) {
269
                return 'string';
270
            } elseif ($t == 'object' && (is_array($value) || is_object($value))) {
271
                return 'object';
272
            } elseif ($t == 'array' && is_array($value)) {
273
                return 'array';
274
            } elseif ($t == 'integer' && is_integer($value)) {
275
                return 'integer';
276
            } elseif ($t == 'boolean' && is_bool($value)) {
277
                return 'boolean';
278
            } elseif ($t == 'number' && is_numeric($value)) {
279
                return 'number';
280
            } elseif ($t == 'numeric' && is_numeric($value)) {
281
                return 'numeric';
282
            } elseif ($t == 'null' && !$value) {
283
                return 'null';
284
            } elseif ($t == 'any') {
285
                return 'any';
286
            }
287
        }
288
289
        return false;
290
    }
291
}
292