1 | <?php |
||||||
2 | |||||||
3 | declare(strict_types=1); |
||||||
4 | |||||||
5 | namespace VGirol\JsonApiStructure\Concern; |
||||||
6 | |||||||
7 | use VGirol\JsonApiConstant\Members; |
||||||
8 | use VGirol\JsonApiStructure\Constraint\ContainsAtLeastOne; |
||||||
9 | use VGirol\JsonApiStructure\Messages; |
||||||
10 | |||||||
11 | /** |
||||||
12 | * Assertions relating to the jsonapi object |
||||||
13 | */ |
||||||
14 | trait ValidateStructure |
||||||
15 | { |
||||||
16 | /** |
||||||
17 | * Asserts that a json document has valid structure. |
||||||
18 | * |
||||||
19 | * It will do the following checks : |
||||||
20 | * 1) checks top-level members (@see hasValidTopLevelMembers) |
||||||
21 | * |
||||||
22 | * Optionaly, if presents, it will checks : |
||||||
23 | * 2) primary data (@see validatePrimaryData) |
||||||
24 | * 3) errors object (@see validateErrorsObject) |
||||||
25 | * 4) meta object (@see validateMetaObject) |
||||||
26 | * 5) jsonapi object (@see validateJsonapiObject) |
||||||
27 | * 6) top-level links object (@see validateTopLevelLinksMember) |
||||||
28 | * 7) included object (@see validateIncludedCollection) |
||||||
29 | * |
||||||
30 | * @param array $json |
||||||
31 | * @param boolean $strict If true, unsafe characters are not allowed when checking members name. |
||||||
32 | * |
||||||
33 | * @return void |
||||||
34 | * @throws \VGirol\JsonApiStructure\Exception\ValidationException |
||||||
35 | */ |
||||||
36 | 162 | public function validateStructure(array $json, bool $strict) |
|||||
37 | { |
||||||
38 | 162 | $this->validateTopLevelMembers($json); |
|||||
39 | |||||||
40 | 141 | if (\array_key_exists(Members::DATA, $json)) { |
|||||
41 | 135 | $this->validatePrimaryData($json[Members::DATA], $strict); |
|||||
42 | |||||||
43 | 39 | if (\array_key_exists(Members::INCLUDED, $json)) { |
|||||
44 | 6 | $this->validateIncludedCollection($json[Members::INCLUDED], $json[Members::DATA], $strict); |
|||||
45 | } |
||||||
46 | } |
||||||
47 | |||||||
48 | 42 | if (\array_key_exists(Members::LINKS, $json)) { |
|||||
49 | 6 | $withPagination = $this->canBePaginated($json); |
|||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
50 | 6 | $this->validateTopLevelLinksMember($json[Members::LINKS], $withPagination, $strict); |
|||||
51 | } |
||||||
52 | |||||||
53 | $tests = [ |
||||||
54 | 39 | Members::META => 'validateMetaObject', |
|||||
55 | Members::ERRORS => 'validateErrorsObject', |
||||||
56 | Members::JSONAPI => 'validateJsonapiObject' |
||||||
57 | ]; |
||||||
58 | 39 | foreach ($tests as $member => $func) { |
|||||
59 | 39 | if (\array_key_exists($member, $json)) { |
|||||
60 | 15 | \call_user_func_array([$this, $func], [$json[$member], $strict]); |
|||||
61 | } |
||||||
62 | } |
||||||
63 | 30 | } |
|||||
64 | |||||||
65 | /** |
||||||
66 | * Asserts that a json document has valid top-level structure. |
||||||
67 | * |
||||||
68 | * It will do the following checks : |
||||||
69 | * 1) asserts that the json document contains at least one of the following top-level members : |
||||||
70 | * "data", "meta" or "errors" (@see containsAtLeastOneMember). |
||||||
71 | * 2) asserts that the members "data" and "errors" does not coexist in the same document. |
||||||
72 | * 3) asserts that the json document contains only the following members : |
||||||
73 | * "data", "errors", "meta", "jsonapi", "links", "included" (@see containsOnlyAllowedMembers). |
||||||
74 | * 4) if the json document does not contain a top-level "data" member, the "included" member must not |
||||||
75 | * be present either. |
||||||
76 | |||||||
77 | * @param array $json |
||||||
78 | * |
||||||
79 | * @return void |
||||||
80 | * @throws \VGirol\JsonApiStructure\Exception\ValidationException |
||||||
81 | */ |
||||||
82 | 177 | public function validateTopLevelMembers(array $json) |
|||||
83 | { |
||||||
84 | 177 | $expected = $this->getRule('Document.AtLeast'); |
|||||
0 ignored issues
–
show
It seems like
getRule() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
85 | 177 | $this->containsAtLeastOneMember( |
|||||
0 ignored issues
–
show
It seems like
containsAtLeastOneMember() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
86 | 177 | $expected, |
|||||
87 | $json, |
||||||
88 | 177 | \sprintf(Messages::DOCUMENT_TOP_LEVEL_MEMBERS, implode('", "', $expected)), |
|||||
89 | 177 | 403 |
|||||
90 | ); |
||||||
91 | |||||||
92 | 174 | if (\array_key_exists(Members::DATA, $json) && \array_key_exists(Members::ERRORS, $json)) { |
|||||
93 | 3 | $this->throw(Messages::DOCUMENT_DOCUMENT_TOP_LEVEL_MEMBERS_DATA_AND_ERROR, 403); |
|||||
0 ignored issues
–
show
It seems like
throw() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
94 | } |
||||||
95 | |||||||
96 | 171 | $allowed = $this->getRule('Document.Allowed'); |
|||||
97 | 171 | $this->containsOnlyAllowedMembers($allowed, $json); |
|||||
0 ignored issues
–
show
It seems like
containsOnlyAllowedMembers() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
98 | |||||||
99 | 165 | if (!\array_key_exists(Members::DATA, $json)) { |
|||||
100 | 27 | if (\array_key_exists(Members::INCLUDED, $json)) { |
|||||
101 | 3 | $this->throw(Messages::DOCUMENT_DOCUMENT_TOP_LEVEL_MEMBERS_DATA_AND_INCLUDED, 403); |
|||||
102 | } |
||||||
103 | 24 | if (!$this->isAutomatic() && $this->dataIsRequired()) { |
|||||
0 ignored issues
–
show
It seems like
dataIsRequired() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() It seems like
isAutomatic() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
104 | 18 | $this->throw(Messages::REQUEST_ERROR_NO_DATA_MEMBER, 403); |
|||||
105 | } |
||||||
106 | } |
||||||
107 | 144 | } |
|||||
108 | |||||||
109 | /** |
||||||
110 | * Asserts a json fragment is a valid primary data object. |
||||||
111 | * |
||||||
112 | * It will do the following checks : |
||||||
113 | * 1) asserts that the primary data is either an object, an array of objects or the `null` value. |
||||||
114 | * 2) if the primary data is not null, checks if it is a valid single resource or a valid resource collection |
||||||
115 | * (@see validateResourceObject or @see validateResourceIdentifierObject). |
||||||
116 | * |
||||||
117 | * @param array|null $json |
||||||
118 | * @param boolean $strict If true, unsafe characters are not allowed when checking members name. |
||||||
119 | * |
||||||
120 | * @return void |
||||||
121 | * @throws \VGirol\JsonApiStructure\Exception\ValidationException |
||||||
122 | */ |
||||||
123 | 135 | public function validatePrimaryData($json, bool $strict): void |
|||||
124 | { |
||||||
125 | 135 | if ($json === null) { |
|||||
126 | 18 | if (!$this->isAutomatic() && !($this->isRelationshipRoute() && $this->isToOne())) { |
|||||
0 ignored issues
–
show
It seems like
isToOne() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() It seems like
isRelationshipRoute() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
127 | 15 | $this->throw(Messages::REQUEST_ERROR_DATA_MEMBER_NULL, 403); |
|||||
128 | } |
||||||
129 | 3 | return; |
|||||
130 | } |
||||||
131 | |||||||
132 | 117 | if (!\is_array($json)) { |
|||||
0 ignored issues
–
show
|
|||||||
133 | 18 | $this->throw(sprintf(Messages::REQUEST_ERROR_DATA_MEMBER_NOT_ARRAY, gettype($json)), 403); |
|||||
134 | } |
||||||
135 | |||||||
136 | 99 | if (\count($json) == 0) { |
|||||
137 | 18 | if (!$this->isAutomatic() && !($this->isRelationshipRoute() && $this->isToMany()) |
|||||
0 ignored issues
–
show
It seems like
isToMany() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
138 | 18 | || ($this->isRelationshipRoute() && $this->isToMany() && ($this->isPost() || $this->isDelete()))) { |
|||||
0 ignored issues
–
show
It seems like
isPost() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() It seems like
isDelete() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
139 | 15 | $this->throw( |
|||||
140 | 15 | $this->isCollection() ? Messages::REQUEST_ERROR_DATA_MEMBER_NOT_COLLECTION : |
|||||
0 ignored issues
–
show
It seems like
isCollection() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
141 | 15 | Messages::REQUEST_ERROR_DATA_MEMBER_NOT_SINGLE, |
|||||
142 | 15 | 403 |
|||||
143 | ); |
||||||
144 | } |
||||||
145 | 3 | return; |
|||||
146 | } |
||||||
147 | |||||||
148 | 81 | if ($this->isArrayOfObjects($json)) { |
|||||
0 ignored issues
–
show
It seems like
isArrayOfObjects() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
149 | 30 | if (!$this->isAutomatic() && $this->isSingle()) { |
|||||
0 ignored issues
–
show
It seems like
isSingle() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
150 | 9 | $this->throw(Messages::REQUEST_ERROR_DATA_MEMBER_NOT_SINGLE, 403); |
|||||
151 | } |
||||||
152 | |||||||
153 | // Resource collection (Resource Objects or Resource Identifier Objects) |
||||||
154 | 21 | $this->validatePrimaryCollection($json, true, $strict); |
|||||
155 | |||||||
156 | 12 | return; |
|||||
157 | } |
||||||
158 | |||||||
159 | 51 | if (!$this->isAutomatic() && $this->isCollection()) { |
|||||
160 | 9 | $this->throw(Messages::REQUEST_ERROR_DATA_MEMBER_NOT_COLLECTION, 403); |
|||||
161 | } |
||||||
162 | |||||||
163 | // Single Resource (Resource Object or Resource Identifier Object) |
||||||
164 | 42 | $this->validatePrimarySingle($json, $strict); |
|||||
165 | 21 | } |
|||||
166 | |||||||
167 | /** |
||||||
168 | * Asserts that a json fragment is a valid top-level links member. |
||||||
169 | * |
||||||
170 | * It will do the following checks : |
||||||
171 | * 1) asserts that the top-level "links" member contains only the following allowed members : |
||||||
172 | * "self", "related" and optionaly pagination links (@see validateLinksObject). |
||||||
173 | * |
||||||
174 | * @param array $json |
||||||
175 | * @param boolean $withPagination |
||||||
176 | * @param boolean $strict If true, unsafe characters are not allowed when checking members name. |
||||||
177 | * |
||||||
178 | * @return void |
||||||
179 | * @throws \VGirol\JsonApiStructure\Exception\ValidationException |
||||||
180 | */ |
||||||
181 | 12 | public function validateTopLevelLinksMember($json, bool $withPagination, bool $strict): void |
|||||
182 | { |
||||||
183 | 12 | $this->canBePaginated($json); |
|||||
184 | 12 | $allowed = $this->getRule('Document.LinksObject.Allowed'); |
|||||
185 | 12 | if ($withPagination) { |
|||||
186 | 9 | $allowed = array_merge($allowed, $this->getRule('LinksObject.Pagination')); |
|||||
187 | } |
||||||
188 | 12 | $this->validateLinksObject($json, $allowed, $strict); |
|||||
0 ignored issues
–
show
It seems like
validateLinksObject() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
189 | 6 | } |
|||||
190 | |||||||
191 | /** |
||||||
192 | * Asserts that a collection of included resources is valid. |
||||||
193 | * |
||||||
194 | * It will do the following checks : |
||||||
195 | * 1) asserts that it is an array of objects (@see isArrayOfObjects). |
||||||
196 | * 2) asserts that each resource of the collection is valid (@see validateResourceObject). |
||||||
197 | * 3) asserts that each resource in the collection corresponds to an existing resource linkage |
||||||
198 | * present in either primary data, primary data relationships or another included resource. |
||||||
199 | * 4) asserts that each resource in the collection is unique (i.e. each couple id-type is unique). |
||||||
200 | * |
||||||
201 | * @param array $included The included top-level member of the json document. |
||||||
202 | * @param array $data The primary data of the json document. |
||||||
203 | * @param boolean $strict If true, unsafe characters are not allowed when checking members name. |
||||||
204 | * |
||||||
205 | * @return void |
||||||
206 | * @throws \VGirol\JsonApiStructure\Exception\ValidationException |
||||||
207 | */ |
||||||
208 | 21 | public function validateIncludedCollection($included, $data, bool $strict): void |
|||||
209 | { |
||||||
210 | 21 | $this->validateResourceObjectCollection($included, $strict); |
|||||
0 ignored issues
–
show
It seems like
validateResourceObjectCollection() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
211 | |||||||
212 | 12 | $resIdentifiers = \array_merge( |
|||||
213 | 12 | $this->getAllResourceIdentifierObjects($data), |
|||||
214 | 12 | $this->getAllResourceIdentifierObjects($included) |
|||||
215 | ); |
||||||
216 | |||||||
217 | 12 | $present = []; |
|||||
218 | 12 | foreach ($included as $inc) { |
|||||
219 | 12 | if (!$this->existsInArray($inc, $resIdentifiers)) { |
|||||
220 | 3 | $this->throw(Messages::INCLUDED_RESOURCE_NOT_LINKED, 403); |
|||||
221 | } |
||||||
222 | |||||||
223 | 12 | if (!\array_key_exists($inc[Members::TYPE], $present)) { |
|||||
224 | 12 | $present[$inc[Members::TYPE]] = []; |
|||||
225 | } |
||||||
226 | 12 | if (\in_array($inc[Members::ID], $present[$inc[Members::TYPE]])) { |
|||||
227 | 3 | $this->throw(Messages::DOCUMENT_NO_DUPLICATE_RESOURCE, 403); |
|||||
228 | } |
||||||
229 | |||||||
230 | 12 | \array_push($present[$inc[Members::TYPE]], $inc[Members::ID]); |
|||||
231 | } |
||||||
232 | 6 | } |
|||||
233 | |||||||
234 | /** |
||||||
235 | * Asserts that a collection of resource object is valid. |
||||||
236 | * |
||||||
237 | * @param array $list |
||||||
238 | * @param boolean $checkType If true, asserts that all resources of the collection are of same type |
||||||
239 | * @param boolean $strict If true, excludes not safe characters when checking members name |
||||||
240 | * |
||||||
241 | * @return void |
||||||
242 | * @throws \VGirol\JsonApiStructure\Exception\ValidationException |
||||||
243 | */ |
||||||
244 | 21 | private function validatePrimaryCollection($list, bool $checkType, bool $strict): void |
|||||
245 | { |
||||||
246 | 21 | $isResourceObject = null; |
|||||
247 | 21 | foreach ($list as $resource) { |
|||||
248 | 21 | if ($checkType) { |
|||||
249 | // Assert that all resources of the collection are of same type. |
||||||
250 | 21 | if ($isResourceObject === null) { |
|||||
251 | 21 | $isResourceObject = $this->dataIsResourceObject($resource); |
|||||
252 | } |
||||||
253 | |||||||
254 | 21 | if ($isResourceObject !== $this->dataIsResourceObject($resource)) { |
|||||
255 | $this->throw(Messages::PRIMARY_DATA_SAME_TYPE, 403); |
||||||
256 | } |
||||||
257 | } |
||||||
258 | |||||||
259 | // Check the resource |
||||||
260 | 21 | $this->validatePrimarySingle($resource, $strict); |
|||||
261 | } |
||||||
262 | 12 | } |
|||||
263 | |||||||
264 | /** |
||||||
265 | * Assert that a single resource object is valid. |
||||||
266 | * |
||||||
267 | * @param array $resource |
||||||
268 | * @param boolean $strict If true, excludes not safe characters when checking members name |
||||||
269 | * |
||||||
270 | * @return void |
||||||
271 | * @throws \VGirol\JsonApiStructure\Exception\ValidationException |
||||||
272 | */ |
||||||
273 | 63 | private function validatePrimarySingle($resource, bool $strict): void |
|||||
274 | { |
||||||
275 | 63 | $isResourceObject = $this->isAutomatic() ? |
|||||
276 | 18 | $this->dataIsResourceObject($resource) : |
|||||
277 | 63 | !$this->isRelationshipRoute(); |
|||||
278 | 63 | if ($isResourceObject) { |
|||||
279 | 39 | $this->validateResourceObject($resource, $strict); |
|||||
0 ignored issues
–
show
It seems like
validateResourceObject() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
280 | |||||||
281 | 21 | return; |
|||||
282 | } |
||||||
283 | |||||||
284 | 24 | $this->validateResourceIdentifierObject($resource, $strict); |
|||||
0 ignored issues
–
show
It seems like
validateResourceIdentifierObject() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
285 | 12 | } |
|||||
286 | |||||||
287 | /** |
||||||
288 | * Checks if a given json fragment is a resource object. |
||||||
289 | * |
||||||
290 | * @param array $resource |
||||||
291 | * |
||||||
292 | * @return bool |
||||||
293 | */ |
||||||
294 | 36 | private function dataIsResourceObject($resource): bool |
|||||
295 | { |
||||||
296 | $expected = [ |
||||||
297 | 36 | Members::ATTRIBUTES, |
|||||
298 | Members::RELATIONSHIPS, |
||||||
299 | Members::LINKS |
||||||
300 | ]; |
||||||
301 | |||||||
302 | 36 | $constraint = new ContainsAtLeastOne($expected); |
|||||
303 | |||||||
304 | 36 | return $constraint->handle($resource); |
|||||
305 | } |
||||||
306 | |||||||
307 | /** |
||||||
308 | * Get all the resource identifier objects (resource linkage) presents in a collection of resource. |
||||||
309 | * |
||||||
310 | * @param array $data |
||||||
311 | * |
||||||
312 | * @return array |
||||||
313 | */ |
||||||
314 | 12 | private function getAllResourceIdentifierObjects($data): array |
|||||
315 | { |
||||||
316 | 12 | $arr = []; |
|||||
317 | 12 | if (\count($data) == 0) { |
|||||
318 | return $arr; |
||||||
319 | } |
||||||
320 | 12 | if (!$this->isArrayOfObjects($data)) { |
|||||
321 | 6 | $data = [$data]; |
|||||
322 | } |
||||||
323 | 12 | foreach ($data as $obj) { |
|||||
324 | 12 | if (!\array_key_exists(Members::RELATIONSHIPS, $obj)) { |
|||||
325 | 12 | continue; |
|||||
326 | } |
||||||
327 | 12 | foreach ($obj[Members::RELATIONSHIPS] as $relationship) { |
|||||
328 | 12 | if (!\array_key_exists(Members::DATA, $relationship)) { |
|||||
329 | 3 | continue; |
|||||
330 | } |
||||||
331 | 12 | $arr = \array_merge( |
|||||
332 | 12 | $arr, |
|||||
333 | 12 | $this->isArrayOfObjects($relationship[Members::DATA]) ? |
|||||
334 | 12 | $relationship[Members::DATA] : [$relationship[Members::DATA]] |
|||||
335 | ); |
||||||
336 | } |
||||||
337 | } |
||||||
338 | |||||||
339 | 12 | return $arr; |
|||||
340 | } |
||||||
341 | |||||||
342 | /** |
||||||
343 | * Checks if a resource is present in a given array. |
||||||
344 | * |
||||||
345 | * @param array $needle |
||||||
346 | * @param array $arr |
||||||
347 | * |
||||||
348 | * @return bool |
||||||
349 | */ |
||||||
350 | 12 | private function existsInArray($needle, $arr): bool |
|||||
351 | { |
||||||
352 | 12 | foreach ($arr as $resIdentifier) { |
|||||
353 | 12 | $test = $resIdentifier[Members::TYPE] === $needle[Members::TYPE] |
|||||
354 | 12 | && $resIdentifier[Members::ID] === $needle[Members::ID]; |
|||||
355 | 12 | if ($test) { |
|||||
356 | 12 | return true; |
|||||
357 | } |
||||||
358 | } |
||||||
359 | |||||||
360 | 3 | return false; |
|||||
361 | } |
||||||
362 | } |
||||||
363 |