Completed
Push — develop ( de7f4b...5c2193 )
by Neomerx
09:05
created

Schema   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 70.8%

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 3
dl 0
loc 322
ccs 80
cts 113
cp 0.708
rs 8.2608
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getAttributeMapping() 0 4 1
A getRelationshipMapping() 0 4 1
A getModelSchemes() 0 4 1
A getRelationshipLinkRepresentation() 0 7 1
A getExcludesFromDefaultShowSelfLinkInRelationships() 0 4 1
A getExcludesFromDefaultShowRelatedLinkInRelationships() 0 4 1
A isShowSelfLinkInRelationships() 0 4 1
A isShowRelatedLinkInRelationships() 0 4 1
A hasRelationship() 0 6 1
A showSelfInRelationship() 0 8 2
A showRelatedInRelationship() 0 8 2
A hasAttributeMapping() 0 8 2
A hasRelationshipMapping() 0 8 2
A getAttributes() 0 17 4
C getRelationships() 0 74 12
B getRelationshipDescription() 0 30 3
A getRelationshipLinks() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like Schema 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Schema, and based on these observations, apply Extract Interface, too.

1
<?php namespace Limoncello\Flute\Schema;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
20
use Limoncello\Contracts\Data\RelationshipTypes;
21
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
22
use Limoncello\Flute\Contracts\Schema\SchemaInterface;
23
use Limoncello\Flute\Contracts\Validation\JsonApiQueryValidatingParserInterface;
24
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
25
use Neomerx\JsonApi\Contracts\Document\LinkInterface;
26
use Neomerx\JsonApi\Contracts\Factories\FactoryInterface;
27
use Neomerx\JsonApi\Schema\BaseSchema;
28
29
/**
30
 * @package Limoncello\Flute
31
 */
32
abstract class Schema extends BaseSchema implements SchemaInterface
33
{
34
    /**
35
     * @var ModelSchemeInfoInterface
36
     */
37
    private $modelSchemes;
38
39
    /**
40
     * @param FactoryInterface         $factory
41
     * @param ModelSchemeInfoInterface $modelSchemes
42
     */
43 24
    public function __construct(FactoryInterface $factory, ModelSchemeInfoInterface $modelSchemes)
44
    {
45
        /** @noinspection PhpUndefinedFieldInspection */
46 24
        $this->resourceType = static::TYPE;
47
48 24
        parent::__construct($factory);
49
50 24
        $this->modelSchemes = $modelSchemes;
51
    }
52
53
    /**
54
     * @inheritdoc
55
     */
56 12
    public static function getAttributeMapping(string $jsonName): string
57
    {
58 12
        return static::getMappings()[static::SCHEMA_ATTRIBUTES][$jsonName];
59
    }
60
61
    /**
62
     * @inheritdoc
63
     */
64 9
    public static function getRelationshipMapping(string $jsonName): string
65
    {
66 9
        return static::getMappings()[static::SCHEMA_RELATIONSHIPS][$jsonName];
67
    }
68
69
    /**
70
     * @inheritdoc
71
     */
72 18
    public static function hasAttributeMapping(string $jsonName): bool
73
    {
74 18
        $mappings = static::getMappings();
75
76
        return
77 18
            array_key_exists(static::SCHEMA_ATTRIBUTES, $mappings) === true &&
78 18
            array_key_exists($jsonName, $mappings[static::SCHEMA_ATTRIBUTES]) === true;
79
    }
80
81
    /**
82
     * @inheritdoc
83
     */
84 12
    public static function hasRelationshipMapping(string $jsonName): bool
85
    {
86 12
        $mappings = static::getMappings();
87
88
        return
89 12
            array_key_exists(static::SCHEMA_RELATIONSHIPS, $mappings) === true &&
90 12
            array_key_exists($jsonName, $mappings[static::SCHEMA_RELATIONSHIPS]) === true;
91
    }
92
93
    /**
94
     * @inheritdoc
95
     */
96
    public function getAttributes($model, array $fieldKeysFilter = null): ?array
97
    {
98
        $attributes = [];
99
        $mappings   = static::getMappings();
100
        if (array_key_exists(static::SCHEMA_ATTRIBUTES, $mappings) === true) {
101
            $attrMappings = $mappings[static::SCHEMA_ATTRIBUTES];
102
103
            // `id` is a `special` attribute and cannot be included in JSON API resource
104
            unset($attrMappings[static::RESOURCE_ID]);
105
106
            foreach ($attrMappings as $jsonAttrName => $modelAttrName) {
107
                $attributes[$jsonAttrName] = isset($model->{$modelAttrName}) === true ? $model->{$modelAttrName} : null;
108
            }
109
        }
110
111
        return $attributes;
112
    }
113
114
    /** @noinspection PhpMissingParentCallCommonInspection
115
     * @inheritdoc
116
     *
117
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
118
     * @SuppressWarnings(PHPMD.ElseExpression)
119
     */
120 1
    public function getRelationships($model, bool $isPrimary, array $includeRelationships): ?array
121
    {
122 1
        $modelClass    = get_class($model);
123 1
        $relationships = [];
124 1
        $mappings      = static::getMappings();
125 1
        if (array_key_exists(static::SCHEMA_RELATIONSHIPS, $mappings) === true) {
126 1
            $relMappings = $mappings[static::SCHEMA_RELATIONSHIPS];
127 1
            foreach ($relMappings as $jsonRelName => $modelRelName) {
128 1
                $isRelToBeIncluded = array_key_exists($jsonRelName, $includeRelationships) === true;
129
130 1
                $hasRelData = $this->hasRelationship($model, $modelRelName);
131 1
                $relType    = $this->getModelSchemes()->getRelationshipType($modelClass, $modelRelName);
132
133 1
                $isShowAsLink = false;
134
135
                // there is a case for `to-1` relationship when we can return identity resource (type + id)
136 1
                if ($relType === RelationshipTypes::BELONGS_TO) {
137 1
                    if ($isRelToBeIncluded === true && $hasRelData === true) {
138
                        $relationships[$jsonRelName] = [static::DATA => $model->{$modelRelName}];
139
                        continue;
140
                    } else {
141 1
                        $schema = $this->getModelSchemes();
142
143 1
                        $class  = get_class($model);
144 1
                        $fkName = $schema->getForeignKey($class, $modelRelName);
145
146 1
                        $isShowAsLink = property_exists($model, $fkName) === false;
147
148 1
                        if ($isShowAsLink === false) {
149
                            // show as identity (type, id)
150
151 1
                            $identity       = null;
152 1
                            $reversePkValue = $model->{$fkName};
153 1
                            if ($reversePkValue !== null) {
154
                                $reverseClass  = $schema->getReverseModelClass($class, $modelRelName);
155
                                $reversePkName = $schema->getPrimaryKey($reverseClass);
156
157
                                $identity                   = new $reverseClass;
158
                                $identity->{$reversePkName} = $reversePkValue;
159
                            }
160
161 1
                            $relationships[$jsonRelName] = [
162 1
                                static::DATA  => $identity,
163 1
                                static::LINKS => $this->getRelationshipLinks($model, $jsonRelName),
164
                            ];
165 1
                            continue;
166
                        }
167
168
                        // the relationship will be shown as a link
169
                    }
170
                }
171
172
                // if our storage do not have any data for this relationship or relationship would not
173
                // be included we return it as link
174 1
                if ($hasRelData === false ||
175 1
                    ($isRelToBeIncluded === false && $relType !== RelationshipTypes::BELONGS_TO)
176
                ) {
177 1
                    $isShowAsLink = true;
178
                }
179
180 1
                if ($isShowAsLink === true) {
181 1
                    $relationships[$jsonRelName] = $this->getRelationshipLinkRepresentation($model, $jsonRelName);
182 1
                    continue;
183
                }
184
185
                // if we are here this is a `to-Many` relationship and we have to show data and got the data
186
187
                $relUri                      = $this->getRelationshipSelfUrl($model, $jsonRelName);
188
                $relationships[$jsonRelName] = $this->getRelationshipDescription($model->{$modelRelName}, $relUri);
189
            }
190
        }
191
192 1
        return $relationships;
193
    }
194
195
    /**
196
     * @return ModelSchemeInfoInterface
197
     */
198 1
    protected function getModelSchemes(): ModelSchemeInfoInterface
199
    {
200 1
        return $this->modelSchemes;
201
    }
202
203
    /**
204
     * @param PaginatedDataInterface $data
205
     * @param string                 $uri
206
     *
207
     * @return array
208
     */
209
    protected function getRelationshipDescription(PaginatedDataInterface $data, string $uri): array
210
    {
211
        if ($data->hasMoreItems() === false) {
212
            return [static::DATA => $data->getData()];
213
        }
214
215
        $buildUrl = function ($offset) use ($data, $uri) {
216
            $paramsWithPaging = [
217
                JsonApiQueryValidatingParserInterface::PARAM_PAGING_OFFSET => $offset,
218
                JsonApiQueryValidatingParserInterface::PARAM_PAGING_LIMIT  => $data->getLimit(),
219
            ];
220
            $fullUrl          = $uri . '?' . http_build_query($paramsWithPaging);
221
222
            return $fullUrl;
223
        };
224
225
        $links = [];
226
227
        // It looks like relationship can only hold first data rows so we might need `next` link but never `prev`
228
229
        if ($data->hasMoreItems() === true) {
230
            $offset                                 = $data->getOffset() + $data->getLimit();
231
            $links[DocumentInterface::KEYWORD_NEXT] = $this->createLink($buildUrl($offset));
232
        }
233
234
        return [
235
            static::DATA  => $data->getData(),
236
            static::LINKS => $links,
237
        ];
238
    }
239
240
    /**
241
     * @param mixed  $model
242
     * @param string $jsonRelationship
243
     *
244
     * @return array
245
     */
246 1
    protected function getRelationshipLinkRepresentation($model, string $jsonRelationship): array
247
    {
248
        return [
249 1
            static::LINKS     => $this->getRelationshipLinks($model, $jsonRelationship),
250 1
            static::SHOW_DATA => false,
251
        ];
252
    }
253
254
    /**
255
     * @param mixed  $model
256
     * @param string $jsonRelationship
257
     *
258
     * @return array
259
     */
260 1
    protected function getRelationshipLinks($model, string $jsonRelationship): array
261
    {
262 1
        $links = [];
263 1
        if ($this->showSelfInRelationship($jsonRelationship) === true) {
264 1
            $links[LinkInterface::SELF] = $this->getRelationshipSelfLink($model, $jsonRelationship);
265
        }
266 1
        if ($this->showRelatedInRelationship($jsonRelationship) === true) {
267
            $links[LinkInterface::RELATED] = $this->getRelationshipRelatedLink($model, $jsonRelationship);
268
        }
269
270 1
        return $links;
271
    }
272
273
    /**
274
     * Gets excludes from default 'show `self` link in relationships' rule.
275
     *
276
     * @return array Should be in ['jsonRelationship' => true] format.
277
     */
278 1
    protected function getExcludesFromDefaultShowSelfLinkInRelationships(): array
279
    {
280 1
        return [];
281
    }
282
283
    /**
284
     * Gets excludes from default 'show `related` link in relationships' rule.
285
     *
286
     * @return array Should be in ['jsonRelationship' => true] format.
287
     */
288 1
    protected function getExcludesFromDefaultShowRelatedLinkInRelationships(): array
289
    {
290 1
        return [];
291
    }
292
293
    /**
294
     * If `self` link should be shown in relationships by default.
295
     *
296
     * @return bool
297
     */
298 1
    protected function isShowSelfLinkInRelationships(): bool
299
    {
300 1
        return true;
301
    }
302
303
    /**
304
     * If `related` link should be shown in relationships by default.
305
     *
306
     * @return bool
307
     */
308 1
    protected function isShowRelatedLinkInRelationships(): bool
309
    {
310 1
        return true;
311
    }
312
313
    /**
314
     * @param mixed  $model
315
     * @param string $name
316
     *
317
     * @return bool
318
     */
319 1
    private function hasRelationship($model, string $name): bool
320
    {
321 1
        $hasRelationship = property_exists($model, $name);
322
323 1
        return $hasRelationship;
324
    }
325
326
    /**
327
     * @param string $jsonRelationship
328
     *
329
     * @return bool
330
     */
331 1
    private function showSelfInRelationship(string $jsonRelationship): bool
332
    {
333 1
        $default = $this->isShowSelfLinkInRelationships();
334 1
        $result  = isset($this->getExcludesFromDefaultShowSelfLinkInRelationships()[$jsonRelationship]) === true ?
335 1
            !$default : $default;
336
337 1
        return $result;
338
    }
339
340
    /**
341
     * @param string $jsonRelationship
342
     *
343
     * @return bool
344
     */
345 1
    private function showRelatedInRelationship(string $jsonRelationship): bool
346
    {
347 1
        $default = $this->isShowRelatedLinkInRelationships();
348 1
        $result  = isset($this->getExcludesFromDefaultShowRelatedLinkInRelationships()[$jsonRelationship]) === true ?
349 1
            !$default : $default;
350
351 1
        return $result;
352
    }
353
}
354