Passed
Push — master ( 1ec89a...1818e9 )
by Vincent
06:54 queued 04:35
created

ValidateStructure::validateStructure()   B

Complexity

Conditions 7
Paths 48

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 14
nc 48
nop 2
dl 0
loc 27
ccs 15
cts 15
cp 1
crap 7
rs 8.8333
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace VGirol\JsonApiStructure\Concern;
6
7
use VGirol\JsonApiConstant\Members;
8
use VGirol\JsonApiStructure\Messages;
9
10
/**
11
 * Assertions relating to the jsonapi object
12
 */
13
trait ValidateStructure
14
{
15
    /**
16
     * Asserts that a json document has valid structure.
17
     *
18
     * It will do the following checks :
19
     * 1) checks top-level members (@see hasValidTopLevelMembers)
20
     *
21
     * Optionaly, if presents, it will checks :
22
     * 2) primary data (@see validatePrimaryData)
23
     * 3) errors object (@see validateErrorsObject)
24
     * 4) meta object (@see validateMetaObject)
25
     * 5) jsonapi object (@see validateJsonapiObject)
26
     * 6) top-level links object (@see validateTopLevelLinksMember)
27
     * 7) included object (@see validateIncludedCollection)
28
     *
29
     * @param array   $json
30
     * @param boolean $strict If true, unsafe characters are not allowed when checking members name.
31
     *
32
     * @return void
33
     * @throws \VGirol\JsonApiStructure\Exception\ValidationException
34
     */
35 162
    public function validateStructure(array $json, bool $strict)
36
    {
37 162
        $this->validateTopLevelMembers($json);
38
39 141
        if (\array_key_exists(Members::DATA, $json)) {
40 135
            $this->validatePrimaryData($json[Members::DATA], $strict);
41
42 39
            if (\array_key_exists(Members::INCLUDED, $json)) {
43 6
                $this->validateIncludedCollection($json[Members::INCLUDED], $json[Members::DATA], $strict);
44
            }
45
        }
46
47 42
        if (\array_key_exists(Members::META, $json)) {
48 6
            $this->validateMetaObject($json[Members::META], $strict);
0 ignored issues
show
Bug introduced by
It seems like validateMetaObject() 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 ignore-call  annotation

48
            $this->/** @scrutinizer ignore-call */ 
49
                   validateMetaObject($json[Members::META], $strict);
Loading history...
49
        }
50
51 39
        if (\array_key_exists(Members::ERRORS, $json)) {
52 6
            $this->validateErrorsObject($json[Members::ERRORS], $strict);
0 ignored issues
show
Bug introduced by
It seems like validateErrorsObject() 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 ignore-call  annotation

52
            $this->/** @scrutinizer ignore-call */ 
53
                   validateErrorsObject($json[Members::ERRORS], $strict);
Loading history...
53
        }
54
55 36
        if (\array_key_exists(Members::JSONAPI, $json)) {
56 6
            $this->validateJsonapiObject($json[Members::JSONAPI], $strict);
0 ignored issues
show
Bug introduced by
It seems like validateJsonapiObject() 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 ignore-call  annotation

56
            $this->/** @scrutinizer ignore-call */ 
57
                   validateJsonapiObject($json[Members::JSONAPI], $strict);
Loading history...
57
        }
58
59 33
        if (\array_key_exists(Members::LINKS, $json)) {
60 6
            $withPagination = $this->canBePaginated($json);
0 ignored issues
show
Bug introduced by
It seems like canBePaginated() 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 ignore-call  annotation

60
            /** @scrutinizer ignore-call */ 
61
            $withPagination = $this->canBePaginated($json);
Loading history...
61 6
            $this->validateTopLevelLinksMember($json[Members::LINKS], $withPagination, $strict);
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
Bug introduced by
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 ignore-call  annotation

84
        /** @scrutinizer ignore-call */ 
85
        $expected = $this->getRule('Document.AtLeast');
Loading history...
85 177
        $this->containsAtLeastOneMember(
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

85
        $this->/** @scrutinizer ignore-call */ 
86
               containsAtLeastOneMember(
Loading history...
86 177
            $expected,
87 59
            $json,
88 177
            \sprintf(Messages::TOP_LEVEL_MEMBERS, implode('", "', $expected)),
89 177
            false,
90 177
            403
91
        );
92
93 174
        if (\array_key_exists(Members::DATA, $json) && \array_key_exists(Members::ERRORS, $json)) {
94 3
            $this->throw(Messages::TOP_LEVEL_DATA_AND_ERROR, 403);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

94
            $this->/** @scrutinizer ignore-call */ 
95
                   throw(Messages::TOP_LEVEL_DATA_AND_ERROR, 403);
Loading history...
95
        }
96
97 171
        $allowed = $this->getRule('Document.Allowed');
98 171
        $this->containsOnlyAllowedMembers($allowed, $json);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

98
        $this->/** @scrutinizer ignore-call */ 
99
               containsOnlyAllowedMembers($allowed, $json);
Loading history...
99
100 165
        if (!\array_key_exists(Members::DATA, $json)) {
101 27
            if (\array_key_exists(Members::INCLUDED, $json)) {
102 3
                $this->throw(Messages::TOP_LEVEL_DATA_AND_INCLUDED, 403);
103
            }
104 24
            if (!$this->isAutomatic() && $this->dataIsRequired()) {
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

104
            if (!$this->/** @scrutinizer ignore-call */ isAutomatic() && $this->dataIsRequired()) {
Loading history...
Bug introduced by
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 ignore-call  annotation

104
            if (!$this->isAutomatic() && $this->/** @scrutinizer ignore-call */ dataIsRequired()) {
Loading history...
105 18
                $this->throw(Messages::REQUEST_ERROR_NO_DATA_MEMBER, 403);
106
            }
107
        }
108 144
    }
109
110
    /**
111
     * Asserts a json fragment is a valid primary data object.
112
     *
113
     * It will do the following checks :
114
     * 1) asserts that the primary data is either an object, an array of objects or the `null` value.
115
     * 2) if the primary data is not null, checks if it is a valid single resource or a valid resource collection
116
     * (@see validateResourceObject or @see validateResourceIdentifierObject).
117
     *
118
     * @param array|null $json
119
     * @param boolean    $strict If true, unsafe characters are not allowed when checking members name.
120
     *
121
     * @return void
122
     * @throws \VGirol\JsonApiStructure\Exception\ValidationException
123
     */
124 135
    public function validatePrimaryData($json, bool $strict): void
125
    {
126 135
        if ($json === null) {
127 18
            if (!$this->isAutomatic() && !($this->isRelationshipRoute() && $this->isToOne())) {
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

127
            if (!$this->isAutomatic() && !($this->isRelationshipRoute() && $this->/** @scrutinizer ignore-call */ isToOne())) {
Loading history...
Bug introduced by
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 ignore-call  annotation

127
            if (!$this->isAutomatic() && !($this->/** @scrutinizer ignore-call */ isRelationshipRoute() && $this->isToOne())) {
Loading history...
128 15
                $this->throw(Messages::REQUEST_ERROR_DATA_MEMBER_NULL, 403);
129
            }
130 3
            return;
131
        }
132
133 117
        if (!\is_array($json)) {
0 ignored issues
show
introduced by
The condition is_array($json) is always true.
Loading history...
134 18
            $this->throw(sprintf(Messages::REQUEST_ERROR_DATA_MEMBER_NOT_ARRAY, gettype($json)), 403);
135
        }
136
137 99
        if (\count($json) == 0) {
138 18
            if (!$this->isAutomatic() && !($this->isRelationshipRoute() && $this->isToMany())
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! $this->isAutomatic() ...() || $this->isDelete(), Probably Intended Meaning: ! $this->isAutomatic() &...) || $this->isDelete())
Loading history...
Bug introduced by
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 ignore-call  annotation

138
            if (!$this->isAutomatic() && !($this->isRelationshipRoute() && $this->/** @scrutinizer ignore-call */ isToMany())
Loading history...
139 18
                || ($this->isRelationshipRoute() && $this->isToMany() && ($this->isPost() || $this->isDelete()))) {
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

139
                || ($this->isRelationshipRoute() && $this->isToMany() && ($this->/** @scrutinizer ignore-call */ isPost() || $this->isDelete()))) {
Loading history...
Bug introduced by
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 ignore-call  annotation

139
                || ($this->isRelationshipRoute() && $this->isToMany() && ($this->isPost() || $this->/** @scrutinizer ignore-call */ isDelete()))) {
Loading history...
140 15
                $this->throw(
141 15
                    $this->isCollection() ?  Messages::REQUEST_ERROR_DATA_MEMBER_NOT_COLLECTION :
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

141
                    $this->/** @scrutinizer ignore-call */ 
142
                           isCollection() ?  Messages::REQUEST_ERROR_DATA_MEMBER_NOT_COLLECTION :
Loading history...
142 15
                        Messages::REQUEST_ERROR_DATA_MEMBER_NOT_SINGLE,
143 15
                    403
144
                );
145
            }
146 3
            return;
147
        }
148
149 81
        if ($this->isArrayOfObjects($json, true)) {
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

149
        if ($this->/** @scrutinizer ignore-call */ isArrayOfObjects($json, true)) {
Loading history...
150 30
            if (!$this->isAutomatic() && $this->isSingle()) {
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

150
            if (!$this->isAutomatic() && $this->/** @scrutinizer ignore-call */ isSingle()) {
Loading history...
151 9
                $this->throw(Messages::REQUEST_ERROR_DATA_MEMBER_NOT_SINGLE, 403);
152
            }
153
154
            // Resource collection (Resource Objects or Resource Identifier Objects)
155 21
            $this->validatePrimaryCollection($json, true, $strict);
156
157 12
            return;
158
        }
159
160 51
        if (!$this->isAutomatic() && $this->isCollection()) {
161 9
            $this->throw(Messages::REQUEST_ERROR_DATA_MEMBER_NOT_COLLECTION, 403);
162
        }
163
164
        // Single Resource (Resource Object or Resource Identifier Object)
165 42
        $this->validatePrimarySingle($json, $strict);
166 21
    }
167
168
    /**
169
     * Asserts that a json fragment is a valid top-level links member.
170
     *
171
     * It will do the following checks :
172
     * 1) asserts that the top-level "links" member contains only the following allowed members :
173
     * "self", "related" and optionaly pagination links (@see validateLinksObject).
174
     *
175
     * @param array   $json
176
     * @param boolean $withPagination
177
     * @param boolean $strict         If true, unsafe characters are not allowed when checking members name.
178
     *
179
     * @return void
180
     * @throws \VGirol\JsonApiStructure\Exception\ValidationException
181
     */
182 12
    public function validateTopLevelLinksMember($json, bool $withPagination, bool $strict): void
183
    {
184 12
        $this->canBePaginated($json);
185 12
        $allowed = $this->getRule('Document.LinksObject.Allowed');
186 12
        if ($withPagination) {
187 9
            $allowed = array_merge($allowed, $this->getRule('LinksObject.Pagination'));
188
        }
189 12
        $this->validateLinksObject($json, $allowed, $strict);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

189
        $this->/** @scrutinizer ignore-call */ 
190
               validateLinksObject($json, $allowed, $strict);
Loading history...
190 6
    }
191
192
    /**
193
     * Asserts that a collection of included resources is valid.
194
     *
195
     * It will do the following checks :
196
     * 1) asserts that it is an array of objects (@see isArrayOfObjects).
197
     * 2) asserts that each resource of the collection is valid (@see validateResourceObject).
198
     * 3) asserts that each resource in the collection corresponds to an existing resource linkage
199
     * present in either primary data, primary data relationships or another included resource.
200
     * 4) asserts that each resource in the collection is unique (i.e. each couple id-type is unique).
201
     *
202
     * @param array   $included The included top-level member of the json document.
203
     * @param array   $data     The primary data of the json document.
204
     * @param boolean $strict   If true, unsafe characters are not allowed when checking members name.
205
     *
206
     * @return void
207
     * @throws \VGirol\JsonApiStructure\Exception\ValidationException
208
     */
209 21
    public function validateIncludedCollection($included, $data, bool $strict): void
210
    {
211 21
        $this->validateResourceObjectCollection($included, $strict);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

211
        $this->/** @scrutinizer ignore-call */ 
212
               validateResourceObjectCollection($included, $strict);
Loading history...
212
213 12
        $resIdentifiers = \array_merge(
214 12
            $this->getAllResourceIdentifierObjects($data),
215 12
            $this->getAllResourceIdentifierObjects($included)
216
        );
217
218 12
        $present = [];
219 12
        foreach ($included as $inc) {
220 12
            if (!$this->existsInArray($inc, $resIdentifiers)) {
221 3
                $this->throw(Messages::INCLUDED_RESOURCE_NOT_LINKED, 403);
222
            }
223
224 12
            if (!\array_key_exists($inc[Members::TYPE], $present)) {
225 12
                $present[$inc[Members::TYPE]] = [];
226
            }
227 12
            if (\in_array($inc[Members::ID], $present[$inc[Members::TYPE]])) {
228 3
                $this->throw(Messages::COMPOUND_DOCUMENT_ONLY_ONE_RESOURCE, 403);
229
            }
230
231 12
            \array_push($present[$inc[Members::TYPE]], $inc[Members::ID]);
232
        }
233 6
    }
234
235
    /**
236
     * Asserts that a collection of resource object is valid.
237
     *
238
     * @param array   $list
239
     * @param boolean $checkType If true, asserts that all resources of the collection are of same type
240
     * @param boolean $strict    If true, excludes not safe characters when checking members name
241
     *
242
     * @return void
243
     * @throws \VGirol\JsonApiStructure\Exception\ValidationException
244
     */
245 21
    private function validatePrimaryCollection($list, bool $checkType, bool $strict): void
246
    {
247 21
        $isResourceObject = null;
248 21
        foreach ($list as $resource) {
249 21
            if ($checkType) {
250
                // Assert that all resources of the collection are of same type.
251 21
                if ($isResourceObject === null) {
252 21
                    $isResourceObject = $this->dataIsResourceObject($resource);
253
                }
254
255 21
                if ($isResourceObject !== $this->dataIsResourceObject($resource)) {
256
                    $this->throw(Messages::PRIMARY_DATA_SAME_TYPE, 403);
257
                }
258
            }
259
260
            // Check the resource
261 21
            $this->validatePrimarySingle($resource, $strict);
262
        }
263 12
    }
264
265
    /**
266
     * Assert that a single resource object is valid.
267
     *
268
     * @param array   $resource
269
     * @param boolean $strict   If true, excludes not safe characters when checking members name
270
     *
271
     * @return void
272
     * @throws \VGirol\JsonApiStructure\Exception\ValidationException
273
     */
274 63
    private function validatePrimarySingle($resource, bool $strict): void
275
    {
276 63
        $isResourceObject = $this->isAutomatic() ?
277 18
            $this->dataIsResourceObject($resource) :
278 63
            !$this->isRelationshipRoute();
279 63
        if ($isResourceObject) {
280 39
            $this->validateResourceObject($resource, $strict);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

280
            $this->/** @scrutinizer ignore-call */ 
281
                   validateResourceObject($resource, $strict);
Loading history...
281
282 21
            return;
283
        }
284
285 24
        $this->validateResourceIdentifierObject($resource, $strict);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

285
        $this->/** @scrutinizer ignore-call */ 
286
               validateResourceIdentifierObject($resource, $strict);
Loading history...
286 12
    }
287
288
    /**
289
     * Checks if a given json fragment is a resource object.
290
     *
291
     * @param array $resource
292
     *
293
     * @return bool
294
     */
295 36
    private function dataIsResourceObject($resource): bool
296
    {
297
        $expected = [
298 36
            Members::ATTRIBUTES,
299
            Members::RELATIONSHIPS,
300
            Members::LINKS
301
        ];
302
303 36
        return $this->containsAtLeastOneMember($expected, $resource, '', true);
304
    }
305
306
    /**
307
     * Get all the resource identifier objects (resource linkage) presents in a collection of resource.
308
     *
309
     * @param array $data
310
     *
311
     * @return array
312
     */
313 12
    private function getAllResourceIdentifierObjects($data): array
314
    {
315 12
        $arr = [];
316 12
        if (\count($data) == 0) {
317
            return $arr;
318
        }
319 12
        if (!$this->isArrayOfObjects($data, true)) {
320 6
            $data = [$data];
321
        }
322 12
        foreach ($data as $obj) {
323 12
            if (!\array_key_exists(Members::RELATIONSHIPS, $obj)) {
324 12
                continue;
325
            }
326 12
            foreach ($obj[Members::RELATIONSHIPS] as $relationship) {
327 12
                if (!\array_key_exists(Members::DATA, $relationship)) {
328 3
                    continue;
329
                }
330 12
                $arr = \array_merge(
331 12
                    $arr,
332 12
                    $this->isArrayOfObjects($relationship[Members::DATA], true) ?
333 12
                        $relationship[Members::DATA] : [$relationship[Members::DATA]]
334
                );
335
            }
336
        }
337
338 12
        return $arr;
339
    }
340
341
    /**
342
     * Checks if a resource is present in a given array.
343
     *
344
     * @param array $needle
345
     * @param array $arr
346
     *
347
     * @return bool
348
     */
349 12
    private function existsInArray($needle, $arr): bool
350
    {
351 12
        foreach ($arr as $resIdentifier) {
352 12
            $test = $resIdentifier[Members::TYPE] === $needle[Members::TYPE]
353 12
                && $resIdentifier[Members::ID] === $needle[Members::ID];
354 12
            if ($test) {
355 12
                return true;
356
            }
357
        }
358
359 3
        return false;
360
    }
361
}
362