Completed
Push — master ( a0be20...c3e525 )
by Matt
02:18
created

Validator::invokeConstraint()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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