ValidateStructure   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Test Coverage

Coverage 98.37%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 114
c 2
b 0
f 0
dl 0
loc 347
ccs 121
cts 123
cp 0.9837
rs 3.52
wmc 61

10 Methods

Rating   Name   Duplication   Size   Complexity  
B getAllResourceIdentifierObjects() 0 26 8
A validateStructure() 0 25 6
D validatePrimaryData() 0 42 20
A validateIncludedCollection() 0 23 5
A validateTopLevelLinksMember() 0 8 2
A validatePrimaryCollection() 0 17 5
A validatePrimarySingle() 0 12 3
A dataIsResourceObject() 0 11 1
A existsInArray() 0 11 4
B validateTopLevelMembers() 0 23 7

How to fix   Complexity   

Complex Class

Complex classes like ValidateStructure often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ValidateStructure, and based on these observations, apply Extract Interface, too.

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
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

49
            /** @scrutinizer ignore-call */ 
50
            $withPagination = $this->canBePaginated($json);
Loading history...
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
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
            $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
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

93
            $this->/** @scrutinizer ignore-call */ 
94
                   throw(Messages::DOCUMENT_DOCUMENT_TOP_LEVEL_MEMBERS_DATA_AND_ERROR, 403);
Loading history...
94
        }
95
96 171
        $allowed = $this->getRule('Document.Allowed');
97 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

97
        $this->/** @scrutinizer ignore-call */ 
98
               containsOnlyAllowedMembers($allowed, $json);
Loading history...
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
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

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

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

126
            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

126
            if (!$this->isAutomatic() && !($this->/** @scrutinizer ignore-call */ isRelationshipRoute() && $this->isToOne())) {
Loading history...
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
introduced by
The condition is_array($json) is always true.
Loading history...
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
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

137
            if (!$this->isAutomatic() && !($this->isRelationshipRoute() && $this->/** @scrutinizer ignore-call */ isToMany())
Loading history...
138 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

138
                || ($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

138
                || ($this->isRelationshipRoute() && $this->isToMany() && ($this->isPost() || $this->/** @scrutinizer ignore-call */ isDelete()))) {
Loading history...
139 15
                $this->throw(
140 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

140
                    $this->/** @scrutinizer ignore-call */ 
141
                           isCollection() ?  Messages::REQUEST_ERROR_DATA_MEMBER_NOT_COLLECTION :
Loading history...
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
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

148
        if ($this->/** @scrutinizer ignore-call */ isArrayOfObjects($json)) {
Loading history...
149 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

149
            if (!$this->isAutomatic() && $this->/** @scrutinizer ignore-call */ isSingle()) {
Loading history...
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
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

188
        $this->/** @scrutinizer ignore-call */ 
189
               validateLinksObject($json, $allowed, $strict);
Loading history...
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
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

210
        $this->/** @scrutinizer ignore-call */ 
211
               validateResourceObjectCollection($included, $strict);
Loading history...
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
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

279
            $this->/** @scrutinizer ignore-call */ 
280
                   validateResourceObject($resource, $strict);
Loading history...
280
281 21
            return;
282
        }
283
284 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

284
        $this->/** @scrutinizer ignore-call */ 
285
               validateResourceIdentifierObject($resource, $strict);
Loading history...
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