Completed
Pull Request — master (#25)
by Matt
05:48
created

Validator::mergeErrors()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 3
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
     * @return $this
121
     */
122 42
    public function setMaxDepth($maxDepth)
123
    {
124 42
        $this->maxDepth = $maxDepth;
125
126 42
        return $this;
127
    }
128
129
    /**
130
     * Register a custom format validation extension.
131
     *
132
     * @param string          $format
133
     * @param FormatExtension $extension
134
     */
135 4
    public function registerFormatExtension($format, FormatExtension $extension)
136
    {
137 4
        $this->formatExtensions[$format] = $extension;
138 4
    }
139
140
    /**
141
     * @internal
142
     * @param FormatExtension[] $formatExtensions
143
     * @return $this
144
     */
145 42
    public function setFormatExtensions(array $formatExtensions)
146
    {
147 42
        $this->formatExtensions = $formatExtensions;
148
149 42
        return $this;
150
    }
151
152
    /**
153
     * @internal
154
     * @param int $depth
155
     * @return $this
156
     */
157 42
    public function setDepth($depth)
158
    {
159 42
        $this->depth = $depth;
160
161 42
        return $this;
162
    }
163
164
    /**
165
     * @internal
166
     * @return string
167
     */
168 76
    public function getPointer()
169
    {
170 76
        return $this->pointer;
171
    }
172
173
    /**
174
     * @internal
175
     * @param string $pointer
176
     * @return $this
177
     */
178 42
    public function setPointer($pointer)
179
    {
180 42
        $this->pointer = $pointer;
181
182 42
        return $this;
183
    }
184
185
    /**
186
     * Create a new sub-validator.
187
     *
188
     * @param mixed  $data
189
     * @param object $schema
190
     * @param string $pointer
191
     * @return Validator
192
     */
193 42
    public function makeSubSchemaValidator($data, $schema, $pointer)
194
    {
195 42
        return (new Validator($data, $schema))
196 42
            ->setPointer($pointer)
197 42
            ->setMaxDepth($this->maxDepth)
198 42
            ->setFormatExtensions($this->formatExtensions)
199 42
            ->setDepth($this->depth + 1);
200
    }
201
202
    /**
203
     * Validate the data and collect the errors.
204
     */
205 76
    private function validate()
206
    {
207 76
        if ($this->hasValidated) {
208 28
            return;
209
        }
210
211 76
        $this->checkDepth();
212
213 76
        foreach ($this->schema as $rule => $parameter) {
214 76
            $errors = $this->validateRule($rule, $parameter);
215 74
            $this->mergeErrors($errors);
216 74
        }
217
218 74
        $this->hasValidated = true;
219 74
    }
220
221
    /**
222
     * Keep track of how many levels deep we have validated.
223
     * This is to prevent a really deeply nested JSON
224
     * structure from causing the validator to continue
225
     * validating for an incredibly long time.
226
     *
227
     * @throws \League\JsonGuard\Exceptions\MaximumDepthExceededException
228
     */
229 76
    private function checkDepth()
230
    {
231 76
        if ($this->depth > $this->maxDepth) {
232 4
            throw new MaximumDepthExceededException();
233
        }
234 76
    }
235
236
    /**
237
     * Validate the data using the given rule and parameter.
238
     *
239
     * @param string $rule
240
     * @param mixed $parameter
241
     * @return null|ValidationError|ValidationError[]
242
     */
243 76
    private function validateRule($rule, $parameter)
244
    {
245 76
        if (!$this->ruleSet->has($rule)) {
246 16
            return null;
247
        }
248
249 76
        if ($this->isCustomFormatExtension($rule, $parameter)) {
250 4
            return $this->validateCustomFormat($parameter);
251
        }
252
253 74
        $constraint = $this->ruleSet->getConstraint($rule);
254
255 74
        return $this->invokeConstraint($constraint, $parameter);
256
    }
257
258
    /**
259
     * Invoke the given constraint and return the validation errors.
260
     *
261
     * @param \League\JsonGuard\Constraints\Constraint $constraint
262
     * @param mixed                                    $parameter
263
     *
264
     * @return \League\JsonGuard\ValidationError|\League\JsonGuard\ValidationError[]|null
265
     */
266 74
    private function invokeConstraint(Constraint $constraint, $parameter)
267
    {
268 74
        if ($constraint instanceof PropertyConstraint) {
269 62
            return $constraint::validate($this->data, $parameter, $this->getPointer());
270 48
        } elseif ($constraint instanceof ParentSchemaAwarePropertyConstraint) {
271 20
            return $constraint::validate($this->data, $this->schema, $parameter, $this->getPointer());
272 42
        } elseif ($constraint instanceof ContainerInstanceConstraint) {
273 42
            return $constraint::validate($this->data, $parameter, $this, $this->getPointer());
274 16
        } elseif ($constraint instanceof ParentSchemaAwareContainerInstanceConstraint) {
275 16
            return $constraint::validate($this->data, $this->schema, $parameter, $this, $this->getPointer());
276
        }
277
278
        throw new \InvalidArgumentException('Invalid constraint.');
279
    }
280
281
    /**
282
     * Determine if a rule has a custom format extension registered.
283
     *
284
     * @param string $rule
285
     * @param mixed $parameter
286
     *
287
     * @return bool
288
     */
289 76
    private function isCustomFormatExtension($rule, $parameter)
290
    {
291 76
        return $rule === 'format' && isset($this->formatExtensions[$parameter]);
292
    }
293
294
    /**
295
     * Call a custom format extension to validate the data.
296
     *
297
     * @param string $format
298
     *
299
     * @return ValidationError|null
300
     */
301 4
    private function validateCustomFormat($format)
302
    {
303
        /** @var FormatExtension $extension */
304 4
        $extension = $this->formatExtensions[$format];
305
306 4
        return $extension->validate($this->data, $this->getPointer());
307
    }
308
309
    /**
310
     * Merge the errors with our error collection.
311
     *
312
     * @param ValidationError[]|ValidationError|null $errors
313
     */
314 74
    private function mergeErrors($errors)
315
    {
316 74
        if (is_null($errors)) {
317 72
            return;
318
        }
319
320 74
        if (is_array($errors)) {
321 36
            $this->errors = array_merge($this->errors, $errors);
322 36
            return;
323
        }
324
325 70
        $this->errors[] = $errors;
326 70
    }
327
}
328