Completed
Pull Request — master (#30)
by Matt
13:30
created

Validator::setDepth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace League\JsonGuard;
4
5
use League\JsonGuard\Constraints\Constraint;
6
use League\JsonGuard\Constraints\ContainerInstanceConstraint;
7
use League\JsonGuard\Constraints\ParentSchemaAwareContainerInstanceConstraint;
8
use League\JsonGuard\Constraints\ParentSchemaAwarePropertyConstraint;
9
use League\JsonGuard\Constraints\PropertyConstraint;
10
use League\JsonGuard\Exceptions\MaximumDepthExceededException;
11
use League\JsonGuard\RuleSets\DraftFour;
12
13
class Validator implements SubSchemaValidatorFactory
14
{
15
    /**
16
     * @var array
17
     */
18
    private $errors = [];
19
20
    /**
21
     * @var mixed
22
     */
23
    private $data;
24
25
    /**
26
     * @var object
27
     */
28
    private $schema;
29
30
    /**
31
     * @var string
32
     */
33
    private $pointer = '';
34
35
    /**
36
     * The maximum depth the validator should recurse into $data
37
     * before throwing an exception.
38
     *
39
     * @var int
40
     */
41
    private $maxDepth = 50;
42
43
    /**
44
     * The depth the validator has reached in the data.
45
     *
46
     * @var int
47
     */
48
    private $depth = 0;
49
50
    /**
51
     * @var \League\JsonGuard\FormatExtension[]
52
     */
53
    private $formatExtensions = [];
54
55
    /**
56
     * @var \League\JsonGuard\RuleSet
57
     */
58
    private $ruleSet;
59
60
    /**
61
     * @var bool
62
     */
63
    private $hasValidated;
64
65
    /**
66
     * @param mixed        $data
67
     * @param object       $schema
68
     * @param RuleSet|null $ruleSet
69
     */
70 78
    public function __construct($data, $schema, $ruleSet = null)
71
    {
72 78
        if (!is_object($schema)) {
73
            throw new \InvalidArgumentException(
74
                sprintf('The schema should be an object from a json_decode call, got "%s"', gettype($schema))
75
            );
76
        }
77
78 78
        if ($schema instanceof Reference) {
79 14
            $schema = $schema->resolve();
80 14
        }
81
82 78
        $this->data    = $data;
83 78
        $this->schema  = $schema;
84 78
        $this->ruleSet = $ruleSet ?: new DraftFour();
85 78
    }
86
87
    /**
88
     * @return boolean
89
     */
90 76
    public function fails()
91
    {
92 76
        return !$this->passes();
93
    }
94
95
    /**
96
     * @return boolean
97
     */
98 76
    public function passes()
99
    {
100 76
        return empty($this->errors());
101
    }
102
103
    /**
104
     * Get a collection of errors.
105
     *
106
     * @return ValidationError[]
107
     */
108 76
    public function errors()
109
    {
110 76
        $this->validate();
111
112 74
        return $this->errors;
113
    }
114
115
    /**
116
     * Set the maximum allowed depth data will be validated until.
117
     * If the data exceeds the stack depth an exception is thrown.
118
     *
119
     * @param int $maxDepth
120
     *
121
     * @return $this
122 42
     */
123
    public function setMaxDepth($maxDepth)
124 42
    {
125
        $this->maxDepth = $maxDepth;
126 42
127
        return $this;
128
    }
129
130
    /**
131
     * Register a custom format validation extension.
132
     *
133
     * @param string          $format
134
     * @param FormatExtension $extension
135 4
     */
136
    public function registerFormatExtension($format, FormatExtension $extension)
137 4
    {
138 4
        $this->formatExtensions[$format] = $extension;
139
    }
140
141
    /**
142
     * @internal
143
     * @return string
144
     */
145 42
    public function getPointer()
146
    {
147 42
        return $this->pointer;
148
    }
149 42
150
    /**
151
     * Create a new sub-validator.
152
     *
153
     * @param mixed  $data
154
     * @param object $schema
155
     * @param string $pointer
156
     *
157 42
     * @return Validator
158
     */
159 42
    public function makeSubSchemaValidator($data, $schema, $pointer)
160
    {
161 42
        $validator = new Validator($data, $schema, $this->ruleSet);
162
163
        $validator->pointer          = $pointer;
164
        $validator->maxDepth         = $this->maxDepth;
165
        $validator->formatExtensions = $this->formatExtensions;
166
        $validator->depth            = $this->depth + 1;
167
168 76
        return $validator;
169
    }
170 76
171
    /**
172
     * Validate the data and collect the errors.
173
     */
174
    private function validate()
175
    {
176
        if ($this->hasValidated) {
177
            return;
178 42
        }
179
180 42
        $this->checkDepth();
181
182 42
        foreach ($this->schema as $rule => $parameter) {
183
            $errors = $this->validateRule($rule, $parameter);
184
            $this->mergeErrors($errors);
185
        }
186
187
        $this->hasValidated = true;
188
    }
189
190
    /**
191
     * Keep track of how many levels deep we have validated.
192
     * This is to prevent a really deeply nested JSON
193 42
     * structure from causing the validator to continue
194
     * validating for an incredibly long time.
195 42
     *
196 42
     * @throws \League\JsonGuard\Exceptions\MaximumDepthExceededException
197 42
     */
198 42
    private function checkDepth()
199 42
    {
200
        if ($this->depth > $this->maxDepth) {
201
            throw new MaximumDepthExceededException();
202
        }
203
    }
204
205 76
    /**
206
     * Validate the data using the given rule and parameter.
207 76
     *
208 28
     * @param string $rule
209
     * @param mixed  $parameter
210
     *
211 76
     * @return null|ValidationError|ValidationError[]
212
     */
213 76
    private function validateRule($rule, $parameter)
214 76
    {
215 74
        if (!$this->ruleSet->has($rule)) {
216 74
            return null;
217
        }
218 74
219 74
        if ($this->isCustomFormatExtension($rule, $parameter)) {
220
            return $this->validateCustomFormat($parameter);
221
        }
222
223
        $constraint = $this->ruleSet->getConstraint($rule);
224
225
        return $this->invokeConstraint($constraint, $parameter);
226
    }
227
228
    /**
229 76
     * Invoke the given constraint and return the validation errors.
230
     *
231 76
     * @param \League\JsonGuard\Constraints\Constraint $constraint
232 4
     * @param mixed                                    $parameter
233
     *
234 76
     * @return \League\JsonGuard\ValidationError|\League\JsonGuard\ValidationError[]|null
235
     */
236
    private function invokeConstraint(Constraint $constraint, $parameter)
237
    {
238
        if ($constraint instanceof PropertyConstraint) {
239
            return $constraint::validate($this->data, $parameter, $this->getPointer());
240
        } elseif ($constraint instanceof ParentSchemaAwarePropertyConstraint) {
241
            return $constraint::validate($this->data, $this->schema, $parameter, $this->getPointer());
242
        } elseif ($constraint instanceof ContainerInstanceConstraint) {
243 76
            return $constraint::validate($this->data, $parameter, $this, $this->getPointer());
244
        } elseif ($constraint instanceof ParentSchemaAwareContainerInstanceConstraint) {
245 76
            return $constraint::validate($this->data, $this->schema, $parameter, $this, $this->getPointer());
246 16
        }
247
248
        throw new \InvalidArgumentException('Invalid constraint.');
249 76
    }
250 4
251
    /**
252
     * Determine if a rule has a custom format extension registered.
253 74
     *
254
     * @param string $rule
255 74
     * @param mixed  $parameter
256
     *
257
     * @return bool
258
     */
259
    private function isCustomFormatExtension($rule, $parameter)
260
    {
261
        return $rule === 'format' && isset($this->formatExtensions[$parameter]);
262
    }
263
264
    /**
265
     * Call a custom format extension to validate the data.
266 74
     *
267
     * @param string $format
268 74
     *
269 62
     * @return ValidationError|null
270 48
     */
271 20
    private function validateCustomFormat($format)
272 42
    {
273 42
        /** @var FormatExtension $extension */
274 16
        $extension = $this->formatExtensions[$format];
275 16
276
        return $extension->validate($this->data, $this->getPointer());
277
    }
278
279
    /**
280
     * Merge the errors with our error collection.
281
     *
282
     * @param ValidationError[]|ValidationError|null $errors
283
     */
284
    private function mergeErrors($errors)
285
    {
286
        if (is_null($errors)) {
287
            return;
288
        }
289 76
290
        if (is_array($errors)) {
291 76
            $this->errors = array_merge($this->errors, $errors);
292
293
            return;
294
        }
295
296
        $this->errors[] = $errors;
297
    }
298
}
299