Completed
Push — master ( 0208de...026a8e )
by Vincent
02:43 queued 12s
created

ValidateStructure::validateTopLevelLinksMember()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
nc 2
nop 3
dl 0
loc 8
ccs 6
cts 6
cp 1
crap 2
rs 10
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\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::META, $json)) {
49 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

49
            $this->/** @scrutinizer ignore-call */ 
50
                   validateMetaObject($json[Members::META], $strict);
Loading history...
50
        }
51
52 39
        if (\array_key_exists(Members::ERRORS, $json)) {
53 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

53
            $this->/** @scrutinizer ignore-call */ 
54
                   validateErrorsObject($json[Members::ERRORS], $strict);
Loading history...
54
        }
55
56 36
        if (\array_key_exists(Members::JSONAPI, $json)) {
57 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

57
            $this->/** @scrutinizer ignore-call */ 
58
                   validateJsonapiObject($json[Members::JSONAPI], $strict);
Loading history...
58
        }
59
60 33
        if (\array_key_exists(Members::LINKS, $json)) {
61 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

61
            /** @scrutinizer ignore-call */ 
62
            $withPagination = $this->canBePaginated($json);
Loading history...
62 6
            $this->validateTopLevelLinksMember($json[Members::LINKS], $withPagination, $strict);
63
        }
64 30
    }
65
66
    /**
67
     * Asserts that a json document has valid top-level structure.
68
     *
69
     * It will do the following checks :
70
     * 1) asserts that the json document contains at least one of the following top-level members :
71
     * "data", "meta" or "errors" (@see containsAtLeastOneMember).
72
     * 2) asserts that the members "data" and "errors" does not coexist in the same document.
73
     * 3) asserts that the json document contains only the following members :
74
     * "data", "errors", "meta", "jsonapi", "links", "included" (@see containsOnlyAllowedMembers).
75
     * 4) if the json document does not contain a top-level "data" member, the "included" member must not
76
     * be present either.
77
78
     * @param array $json
79
     *
80
     * @return void
81
     * @throws \VGirol\JsonApiStructure\Exception\ValidationException
82
     */
83 177
    public function validateTopLevelMembers(array $json)
84
    {
85 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

85
        /** @scrutinizer ignore-call */ 
86
        $expected = $this->getRule('Document.AtLeast');
Loading history...
86 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

86
        $this->/** @scrutinizer ignore-call */ 
87
               containsAtLeastOneMember(
Loading history...
87 177
            $expected,
88 59
            $json,
89 177
            \sprintf(Messages::DOCUMENT_TOP_LEVEL_MEMBERS, implode('", "', $expected)),
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::DOCUMENT_DOCUMENT_TOP_LEVEL_MEMBERS_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::DOCUMENT_DOCUMENT_TOP_LEVEL_MEMBERS_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::DOCUMENT_DOCUMENT_TOP_LEVEL_MEMBERS_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)) {
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)) {
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::DOCUMENT_NO_DUPLICATE_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
        $constraint = new ContainsAtLeastOne($expected);
304
305 36
        return $constraint->handle($resource);
306
    }
307
308
    /**
309
     * Get all the resource identifier objects (resource linkage) presents in a collection of resource.
310
     *
311
     * @param array $data
312
     *
313
     * @return array
314
     */
315 12
    private function getAllResourceIdentifierObjects($data): array
316
    {
317 12
        $arr = [];
318 12
        if (\count($data) == 0) {
319
            return $arr;
320
        }
321 12
        if (!$this->isArrayOfObjects($data)) {
322 6
            $data = [$data];
323
        }
324 12
        foreach ($data as $obj) {
325 12
            if (!\array_key_exists(Members::RELATIONSHIPS, $obj)) {
326 12
                continue;
327
            }
328 12
            foreach ($obj[Members::RELATIONSHIPS] as $relationship) {
329 12
                if (!\array_key_exists(Members::DATA, $relationship)) {
330 3
                    continue;
331
                }
332 12
                $arr = \array_merge(
333 12
                    $arr,
334 12
                    $this->isArrayOfObjects($relationship[Members::DATA]) ?
335 12
                        $relationship[Members::DATA] : [$relationship[Members::DATA]]
336
                );
337
            }
338
        }
339
340 12
        return $arr;
341
    }
342
343
    /**
344
     * Checks if a resource is present in a given array.
345
     *
346
     * @param array $needle
347
     * @param array $arr
348
     *
349
     * @return bool
350
     */
351 12
    private function existsInArray($needle, $arr): bool
352
    {
353 12
        foreach ($arr as $resIdentifier) {
354 12
            $test = $resIdentifier[Members::TYPE] === $needle[Members::TYPE]
355 12
                && $resIdentifier[Members::ID] === $needle[Members::ID];
356 12
            if ($test) {
357 12
                return true;
358
            }
359
        }
360
361 3
        return false;
362
    }
363
}
364