1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the JVal package. |
5
|
|
|
* |
6
|
|
|
* For the full copyright and license information, please view the LICENSE |
7
|
|
|
* file that was distributed with this source code. |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
namespace JVal; |
11
|
|
|
|
12
|
|
|
use stdClass; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Implements the three steps needed to perform a JSON Schema validation, |
16
|
|
|
* i.e. distinct methods to recursively:. |
17
|
|
|
* |
18
|
|
|
* 1) resolve JSON pointer references within schema |
19
|
|
|
* 2) normalize and validate the syntax of the schema |
20
|
|
|
* 3) validate a given instance against it |
21
|
|
|
*/ |
22
|
|
|
class Walker |
23
|
|
|
{ |
24
|
|
|
/** |
25
|
|
|
* @var Registry |
26
|
|
|
*/ |
27
|
|
|
private $registry; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var Resolver |
31
|
|
|
*/ |
32
|
|
|
private $resolver; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var stdClass[] |
36
|
|
|
*/ |
37
|
|
|
private $parsedSchemas = []; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var stdClass[] |
41
|
|
|
*/ |
42
|
|
|
private $resolvedSchemas = []; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var Constraint[][] |
46
|
|
|
*/ |
47
|
|
|
private $constraintsCache = []; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Constructor. |
51
|
|
|
* |
52
|
|
|
* @param Registry $registry |
53
|
|
|
* @param Resolver $resolver |
54
|
|
|
*/ |
55
|
485 |
|
public function __construct(Registry $registry, Resolver $resolver) |
56
|
|
|
{ |
57
|
485 |
|
$this->registry = $registry; |
58
|
485 |
|
$this->resolver = $resolver; |
59
|
485 |
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Recursively resolve JSON pointer references within a given schema. |
63
|
|
|
* |
64
|
|
|
* @param stdClass $schema The schema to resolve |
65
|
|
|
* @param Uri $uri The URI of the schema |
66
|
|
|
* |
67
|
|
|
* @return stdClass |
68
|
|
|
*/ |
69
|
299 |
|
public function resolveReferences(stdClass $schema, Uri $uri) |
70
|
|
|
{ |
71
|
299 |
|
$this->resolver->initialize($schema, $uri); |
72
|
|
|
|
73
|
299 |
|
return $this->doResolveReferences($schema, $uri); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @param stdClass $schema |
78
|
|
|
* @param Uri $uri |
79
|
|
|
* |
80
|
|
|
* @return stdClass |
81
|
|
|
*/ |
82
|
299 |
|
private function doResolveReferences(stdClass $schema, Uri $uri) |
83
|
|
|
{ |
84
|
299 |
|
if ($this->isLooping($schema, $this->resolvedSchemas)) { |
85
|
20 |
|
return $schema; |
86
|
|
|
} |
87
|
|
|
|
88
|
299 |
|
$inScope = false; |
89
|
299 |
|
if (property_exists($schema, 'id') && is_string($schema->id)) { |
90
|
7 |
|
$this->resolver->enter(new Uri($schema->id)); |
91
|
7 |
|
$inScope = true; |
92
|
7 |
|
} |
93
|
|
|
|
94
|
299 |
|
if (property_exists($schema, '$ref')) { |
95
|
22 |
|
$resolved = $this->resolver->resolve($schema); |
96
|
22 |
|
$this->resolver->enter($resolved[0], $resolved[1]); |
97
|
22 |
|
$schema = $this->doResolveReferences($resolved[1], $resolved[0]); |
98
|
22 |
|
$this->resolver->leave(); |
99
|
22 |
|
} else { |
100
|
299 |
|
foreach ($schema as $property => $value) { |
101
|
299 |
|
if (is_object($value)) { |
102
|
114 |
|
$schema->{$property} = $this->doResolveReferences($value, $uri); |
103
|
299 |
|
} elseif (is_array($value)) { |
104
|
75 |
|
foreach ($value as $index => $element) { |
105
|
73 |
|
if (is_object($element)) { |
106
|
44 |
|
$schema->{$property}[$index] = $this->doResolveReferences($element, $uri); |
107
|
44 |
|
} |
108
|
75 |
|
} |
109
|
75 |
|
} |
110
|
299 |
|
} |
111
|
|
|
} |
112
|
|
|
|
113
|
299 |
|
if ($inScope) { |
114
|
7 |
|
$this->resolver->leave(); |
115
|
7 |
|
} |
116
|
|
|
|
117
|
299 |
|
return $schema; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Recursively normalizes a given schema and validates its syntax. |
122
|
|
|
* |
123
|
|
|
* @param stdClass $schema |
124
|
|
|
* @param Context $context |
125
|
|
|
* |
126
|
|
|
* @return stdClass |
127
|
|
|
*/ |
128
|
357 |
|
public function parseSchema(stdClass $schema, Context $context) |
129
|
|
|
{ |
130
|
357 |
|
if ($this->isLooping($schema, $this->parsedSchemas)) { |
131
|
12 |
|
return $schema; |
132
|
|
|
} |
133
|
|
|
|
134
|
357 |
|
if (isset($schema->{'$schema'})) { |
135
|
4 |
|
$context->setVersion($schema->{'$schema'}); |
136
|
4 |
|
} |
137
|
|
|
|
138
|
357 |
|
$constraints = $this->registry->getConstraints($context->getVersion()); |
139
|
|
|
|
140
|
357 |
View Code Duplication |
foreach ($constraints as $constraint) { |
|
|
|
|
141
|
357 |
|
foreach ($constraint->keywords() as $keyword) { |
142
|
357 |
|
if (property_exists($schema, $keyword)) { |
143
|
333 |
|
$constraint->normalize($schema, $context, $this); |
144
|
333 |
|
break; |
145
|
|
|
} |
146
|
357 |
|
} |
147
|
357 |
|
} |
148
|
|
|
|
149
|
357 |
|
return $schema; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Validates an instance against a given schema, populating a context |
154
|
|
|
* with encountered violations. |
155
|
|
|
* |
156
|
|
|
* @param mixed $instance |
157
|
|
|
* @param stdClass $schema |
158
|
|
|
* @param Context $context |
159
|
|
|
*/ |
160
|
348 |
|
public function applyConstraints($instance, stdClass $schema, Context $context) |
161
|
|
|
{ |
162
|
348 |
|
if (isset($schema->{'$schema'})) { |
163
|
4 |
|
$context->setVersion($schema->{'$schema'}); |
164
|
4 |
|
} |
165
|
|
|
|
166
|
348 |
|
$cacheKey = gettype($instance).spl_object_hash($schema); |
167
|
348 |
|
$constraints = & $this->constraintsCache[$cacheKey]; |
168
|
|
|
|
169
|
348 |
|
if ($constraints === null) { |
170
|
348 |
|
$version = $context->getVersion(); |
171
|
348 |
|
$instanceType = Types::getPrimitiveTypeOf($instance); |
172
|
348 |
|
$constraintsForType = $this->registry->getConstraintsForType($version, $instanceType); |
173
|
|
|
|
174
|
348 |
|
$constraints = []; |
175
|
348 |
View Code Duplication |
foreach ($constraintsForType as $constraint) { |
|
|
|
|
176
|
348 |
|
foreach ($constraint->keywords() as $keyword) { |
177
|
348 |
|
if (property_exists($schema, $keyword)) { |
178
|
307 |
|
$constraints[] = $constraint; |
179
|
307 |
|
break; |
180
|
|
|
} |
181
|
348 |
|
} |
182
|
348 |
|
} |
183
|
348 |
|
} |
184
|
|
|
|
185
|
348 |
|
foreach ($constraints as $constraint) { |
186
|
307 |
|
$constraint->apply($instance, $schema, $context, $this); |
187
|
348 |
|
} |
188
|
348 |
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Checks if given schema has been already visited. |
192
|
|
|
* |
193
|
|
|
* @param stdClass $schema |
194
|
|
|
* @param array $stack |
195
|
|
|
* |
196
|
|
|
* @return bool |
197
|
|
|
*/ |
198
|
363 |
|
private function isLooping(stdClass $schema, array &$stack) |
199
|
|
|
{ |
200
|
363 |
|
$schemaHash = spl_object_hash($schema); |
201
|
363 |
|
if (isset($stack[$schemaHash])) { |
202
|
20 |
|
return true; |
203
|
|
|
} |
204
|
|
|
|
205
|
363 |
|
$stack[$schemaHash] = true; |
206
|
363 |
|
return false; |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.