Completed
Branch constraint-not-found (d3184e)
by Matt
02:45
created

Validator::__construct()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.25
Metric Value
cc 4
eloc 9
nc 5
nop 3
dl 0
loc 16
ccs 9
cts 12
cp 0.75
crap 4.25
rs 9.2
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 = 10;
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 72
    public function __construct($data, $schema, $ruleSet = null)
71
    {
72 72
        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 72
        if ($schema instanceof Reference) {
79 14
            $schema = $schema->resolve();
80 14
        }
81
82 72
        $this->data    = $data;
83 72
        $this->schema  = $schema;
84 72
        $this->ruleSet = $ruleSet ?: new DraftFour();
85 72
    }
86
87
    /**
88
     * @return boolean
89
     */
90 72
    public function fails()
91
    {
92 72
        return !$this->passes();
93
    }
94
95
    /**
96
     * @return boolean
97
     */
98 72
    public function passes()
99
    {
100 72
        return empty($this->errors());
101
    }
102
103
    /**
104
     * Get a collection of errors.
105
     *
106
     * @return ValidationError[]
107
     */
108 72
    public function errors()
109
    {
110 72
        $this->validate();
111
112 70
        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 38
    public function setMaxDepth($maxDepth)
123
    {
124 38
        $this->maxDepth = $maxDepth;
125
126 38
        return $this;
127
    }
128
129
    /**
130
     * Register a custom format validation extension.
131
     *
132
     * @param string          $format
133
     * @param FormatExtension $extension
134
     */
135 2
    public function registerFormatExtension($format, FormatExtension $extension)
136
    {
137 2
        $this->formatExtensions[$format] = $extension;
138 2
    }
139
140
    /**
141
     * @internal
142
     * @param int $depth
143
     * @return $this
144
     */
145 38
    public function setDepth($depth)
146
    {
147 38
        $this->depth = $depth;
148
149 38
        return $this;
150
    }
151
152
    /**
153
     * @internal
154
     * @return string
155
     */
156 72
    public function getPointer()
157
    {
158 72
        return $this->pointer;
159
    }
160
161
    /**
162
     * @internal
163
     * @param string $pointer
164
     * @return $this
165
     */
166 38
    public function setPointer($pointer)
167
    {
168 38
        $this->pointer = $pointer;
169
170 38
        return $this;
171
    }
172
173
    /**
174
     * Create a new sub-validator.
175
     *
176
     * @param mixed  $data
177
     * @param object $schema
178
     * @param string $pointer
179
     * @return Validator
180
     */
181 38
    public function makeSubSchemaValidator($data, $schema, $pointer)
182
    {
183 38
        return (new Validator($data, $schema))
184 38
            ->setPointer($pointer)
185 38
            ->setMaxDepth($this->maxDepth)
186 38
            ->setDepth($this->depth + 1);
187
    }
188
189
    /**
190
     * Validate the data and collect the errors.
191
     */
192 72
    private function validate()
193
    {
194 72
        if ($this->hasValidated) {
195 24
            return;
196
        }
197
198 72
        $this->checkDepth();
199
200 72
        foreach ($this->schema as $rule => $parameter) {
201 72
            $errors = $this->validateRule($rule, $parameter);
202 70
            $this->mergeErrors($errors);
203 70
        }
204
205 70
        $this->hasValidated = true;
206 70
    }
207
208
    /**
209
     * Keep track of how many levels deep we have validated.
210
     * This is to prevent a really deeply nested JSON
211
     * structure from causing the validator to continue
212
     * validating for an incredibly long time.
213
     *
214
     * @throws \League\JsonGuard\Exceptions\MaximumDepthExceededException
215
     */
216 72
    private function checkDepth()
217
    {
218 72
        if ($this->depth > $this->maxDepth) {
219 4
            throw new MaximumDepthExceededException();
220
        }
221 72
    }
222
223
    /**
224
     * Validate the data using the given rule and parameter.
225
     *
226
     * @param string $rule
227
     * @param mixed $parameter
228
     * @return null|ValidationError|ValidationError[]
229
     */
230 72
    private function validateRule($rule, $parameter)
231
    {
232 72
        if (!$this->ruleSet->has($rule)) {
233 14
            return null;
234
        }
235
236 72
        if ($this->isCustomFormatExtension($rule, $parameter)) {
237 2
            return $this->validateCustomFormat($parameter);
238
        }
239
240 70
        $constraint = $this->ruleSet->getConstraint($rule);
241
242 70
        return $this->invokeConstraint($constraint, $parameter);
243
    }
244
245
    /**
246
     * Invoke the given constraint and return the validation errors.
247
     *
248
     * @param \League\JsonGuard\Constraints\Constraint $constraint
249
     * @param mixed                                    $parameter
250
     *
251
     * @return \League\JsonGuard\ValidationError|\League\JsonGuard\ValidationError[]|null
252
     */
253 70
    private function invokeConstraint(Constraint $constraint, $parameter)
254
    {
255 70
        if ($constraint instanceof PropertyConstraint) {
256 58
            return $constraint::validate($this->data, $parameter, $this->getPointer());
257 44
        } elseif ($constraint instanceof ParentSchemaAwarePropertyConstraint) {
258 18
            return $constraint::validate($this->data, $this->schema, $parameter, $this->getPointer());
259 38
        } elseif ($constraint instanceof ContainerInstanceConstraint) {
260 38
            return $constraint::validate($this->data, $parameter, $this, $this->getPointer());
261 16
        } elseif ($constraint instanceof ParentSchemaAwareContainerInstanceConstraint) {
262 16
            return $constraint::validate($this->data, $this->schema, $parameter, $this, $this->getPointer());
263
        }
264
265
        throw new \InvalidArgumentException('Invalid constraint.');
266
    }
267
268
    /**
269
     * Determine if a rule has a custom format extension registered.
270
     *
271
     * @param string $rule
272
     * @param mixed $parameter
273
     *
274
     * @return bool
275
     */
276 72
    private function isCustomFormatExtension($rule, $parameter)
277
    {
278 72
        return $rule === 'format' && isset($this->formatExtensions[$parameter]);
279
    }
280
281
    /**
282
     * Call a custom format extension to validate the data.
283
     *
284
     * @param string $format
285
     *
286
     * @return ValidationError|null
287
     */
288 2
    private function validateCustomFormat($format)
289
    {
290
        /** @var FormatExtension $extension */
291 2
        $extension = $this->formatExtensions[$format];
292
293 2
        return $extension->validate($this->data, $this->getPointer());
294
    }
295
296
    /**
297
     * Merge the errors with our error collection.
298
     *
299
     * @param ValidationError[]|ValidationError|null $errors
300
     */
301 70
    private function mergeErrors($errors)
302
    {
303 70
        if (is_null($errors)) {
304 68
            return;
305
        }
306
307 70
        if (is_array($errors)) {
308 32
            $this->errors = array_merge($this->errors, $errors);
309 32
            return;
310
        }
311
312 66
        $this->errors[] = $errors;
313 66
    }
314
}
315