Completed
Push — master ( 96d573...f9f049 )
by Ehsan
07:54
created

SchemaValidator::getErrors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
            if ($min = $param->getMinLength()) {
219
                $strLen = strlen($value);
220
                if ($strLen < $min) {
221
                    $this->errors[] = "{$path} length must be greater than or equal to {$min}";
222
                }
223
            }
224
            if ($max = $param->getMaxLength()) {
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
            if ($min = $param->getMinItems()) {
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