Completed
Push — develop ( 323919...323bfc )
by Neomerx
15:34 queued 11:51
created

Schema::getProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php declare (strict_types = 1);
2
3
namespace Limoncello\Flute\Schema;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Limoncello\Contracts\Application\ModelInterface;
22
use Limoncello\Contracts\Data\ModelSchemaInfoInterface;
23
use Limoncello\Contracts\Data\RelationshipTypes;
24
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
25
use Limoncello\Flute\Contracts\Schema\JsonSchemasInterface;
26
use Limoncello\Flute\Contracts\Schema\SchemaInterface;
27
use Limoncello\Flute\Contracts\Validation\JsonApiQueryParserInterface;
28
use Neomerx\JsonApi\Contracts\Factories\FactoryInterface;
29
use Neomerx\JsonApi\Contracts\Schema\DocumentInterface;
30
use Neomerx\JsonApi\Contracts\Schema\LinkInterface;
31
use Neomerx\JsonApi\Schema\BaseSchema;
32
use Neomerx\JsonApi\Schema\Identifier;
33
use function array_key_exists;
34
use function assert;
35
use function http_build_query;
36
use function property_exists;
37
38
/**
39
 * @package Limoncello\Flute
40
 */
41
abstract class Schema extends BaseSchema implements SchemaInterface
42
{
43
    /**
44
     * @var ModelSchemaInfoInterface
45
     */
46
    private $modelSchemas;
47
48
    /**
49
     * @var JsonSchemasInterface
50
     */
51
    private $jsonSchemas;
52
53
    /**
54
     * @var array|null
55
     */
56
    private $attributesMapping;
57
58
    /**
59
     * @var array|null
60
     */
61
    private $relationshipsMapping;
62
63
    /**
64
     * @param FactoryInterface         $factory
65
     * @param JsonSchemasInterface     $jsonSchemas
66
     * @param ModelSchemaInfoInterface $modelSchemas
67
     */
68 28
    public function __construct(
69
        FactoryInterface $factory,
70
        JsonSchemasInterface $jsonSchemas,
71
        ModelSchemaInfoInterface $modelSchemas
72
    ) {
73 28
        assert(empty(static::TYPE) === false);
74 28
        assert(empty(static::MODEL) === false);
75
76 28
        parent::__construct($factory);
77
78 28
        $this->modelSchemas = $modelSchemas;
79 28
        $this->jsonSchemas  = $jsonSchemas;
80
81 28
        $this->attributesMapping    = null;
82 28
        $this->relationshipsMapping = null;
83
    }
84
85
    /**
86
     * @inheritdoc
87
     */
88 19
    public function getType(): string
89
    {
90 19
        return static::TYPE;
91
    }
92
93
    /**
94
     * @inheritdoc
95
     */
96 14
    public static function getAttributeMapping(string $jsonName): string
97
    {
98 14
        return static::getMappings()[static::SCHEMA_ATTRIBUTES][$jsonName];
99
    }
100
101
    /**
102
     * @inheritdoc
103
     */
104 12
    public static function getRelationshipMapping(string $jsonName): string
105
    {
106 12
        return static::getMappings()[static::SCHEMA_RELATIONSHIPS][$jsonName];
107
    }
108
109
    /**
110
     * @inheritdoc
111
     */
112 21
    public static function hasAttributeMapping(string $jsonName): bool
113
    {
114 21
        $mappings = static::getMappings();
115
116
        return
117 21
            array_key_exists(static::SCHEMA_ATTRIBUTES, $mappings) === true &&
118 21
            array_key_exists($jsonName, $mappings[static::SCHEMA_ATTRIBUTES]) === true;
119
    }
120
121
    /**
122
     * @inheritdoc
123
     */
124 15
    public static function hasRelationshipMapping(string $jsonName): bool
125
    {
126 15
        $mappings = static::getMappings();
127
128
        return
129 15
            array_key_exists(static::SCHEMA_RELATIONSHIPS, $mappings) === true &&
130 15
            array_key_exists($jsonName, $mappings[static::SCHEMA_RELATIONSHIPS]) === true;
131
    }
132
133
    /**
134
     * @inheritdoc
135
     */
136 16
    public function getAttributes($model): iterable
137
    {
138 16
        foreach ($this->getAttributesMapping() as $jsonAttrName => $modelAttrName) {
139 16
            if ($this->hasProperty($model, $modelAttrName) === true) {
140 16
                yield $jsonAttrName => $this->getProperty($model, $modelAttrName);
141
            }
142
        }
143
    }
144
145
    /**
146
     * @inheritdoc
147
     *
148
     * @SuppressWarnings(PHPMD.ElseExpression)
149
     */
150 19
    public function getRelationships($model): iterable
151
    {
152 19
        assert($model instanceof ModelInterface);
153
154 19
        foreach ($this->getRelationshipsMapping() as $jsonRelName => [$modelRelName, $belongsToFkName, $reverseType]) {
155
            // if model has relationship data then use it
156 19
            if ($this->hasProperty($model, $modelRelName) === true) {
157 4
                yield $jsonRelName => $this->createRelationshipRepresentationFromData(
158 4
                    $model,
159 4
                    $modelRelName,
0 ignored issues
show
Bug introduced by
The variable $modelRelName does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
160 4
                    $jsonRelName
161
                );
162 4
                continue;
163
            }
164
165
            // if relationship is `belongs-to` and has that ID we can add relationship as identifier
166 18
            if ($belongsToFkName !== null && $this->hasProperty($model, $belongsToFkName) === true) {
167 15
                $reverseIndex = $this->getProperty($model, $belongsToFkName);
0 ignored issues
show
Bug introduced by
The variable $belongsToFkName does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
168 15
                $identifier   = $reverseIndex === null ?
169 15
                    null : new Identifier((string)$reverseIndex, $reverseType, false, null);
0 ignored issues
show
Bug introduced by
The variable $reverseType does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
170
171
                yield $jsonRelName => [
172 15
                    static::RELATIONSHIP_DATA       => $identifier,
173 15
                    static::RELATIONSHIP_LINKS_SELF => $this->isAddSelfLinkInRelationshipWithData($jsonRelName),
174
                ];
175 15
                continue;
176
            }
177
178
            // if we are here it's nothing left but show relationship as a link
179 18
            yield $jsonRelName => [static::RELATIONSHIP_LINKS_SELF => true];
180
        }
181
    }
182
183
    /**
184
     * @inheritdoc
185
     */
186 17
    public function isAddSelfLinkInRelationshipWithData(string $relationshipName): bool
187
    {
188 17
        return false;
189
    }
190
191
    /**
192
     * @return ModelSchemaInfoInterface
193
     */
194 19
    protected function getModelSchemas(): ModelSchemaInfoInterface
195
    {
196 19
        return $this->modelSchemas;
197
    }
198
199
    /**
200
     * @return JsonSchemasInterface
201
     */
202 17
    protected function getJsonSchemas(): JsonSchemasInterface
203
    {
204 17
        return $this->jsonSchemas;
205
    }
206
207
    /**
208
     * @param ModelInterface $model
209
     * @param string         $modelRelName
210
     * @param string         $jsonRelName
211
     *
212
     * @return array
213
     *
214
     * @SuppressWarnings(PHPMD.ElseExpression)
215
     */
216 4
    protected function createRelationshipRepresentationFromData(
217
        ModelInterface $model,
218
        string $modelRelName,
219
        string $jsonRelName
220
    ): array {
221 4
        assert($this->hasProperty($model, $modelRelName) === true);
222 4
        $relationshipData = $this->getProperty($model, $modelRelName);
223 4
        $isPaginatedData  = $relationshipData instanceof PaginatedDataInterface;
224
225 4
        $description = [static::RELATIONSHIP_LINKS_SELF => $this->isAddSelfLinkInRelationshipWithData($jsonRelName)];
226
227 4
        if ($isPaginatedData === false) {
228 2
            $description[static::RELATIONSHIP_DATA] = $relationshipData;
229
230 2
            return $description;
231
        }
232
233 3
        assert($relationshipData instanceof PaginatedDataInterface);
234
235 3
        $description[static::RELATIONSHIP_DATA] = $relationshipData->getData();
236
237 3
        if ($relationshipData->hasMoreItems() === false) {
238 2
            return $description;
239
        }
240
241
        // if we are here then relationship contains paginated data, so we have to add pagination links
242 2
        $offset    = $relationshipData->getOffset();
243 2
        $limit     = $relationshipData->getLimit();
244 2
        $urlPrefix = $this->getRelationshipSelfSubUrl($model, $jsonRelName) . '?';
245
        $buildLink = function (int $offset, int $limit) use ($urlPrefix) : LinkInterface {
246
            $paramsWithPaging = [
247 2
                JsonApiQueryParserInterface::PARAM_PAGING_OFFSET => $offset,
248 2
                JsonApiQueryParserInterface::PARAM_PAGING_LIMIT  => $limit,
249
            ];
250
251 2
            $subUrl = $urlPrefix . http_build_query($paramsWithPaging);
252
253 2
            return $this->getFactory()->createLink(true, $subUrl, false);
254 2
        };
255
256 2
        $nextOffset = $offset + $limit;
257 2
        $nextLimit  = $limit;
258 2
        if ($offset <= 0) {
259 1
            $description[static::RELATIONSHIP_LINKS] = [
260 1
                DocumentInterface::KEYWORD_NEXT => $buildLink($nextOffset, $nextLimit),
261
            ];
262
        } else {
263 1
            $prevOffset = $offset - $limit;
264 1
            if ($prevOffset < 0) {
265
                // set offset 0 and decrease limit
266 1
                $prevLimit  = $limit + $prevOffset;
267 1
                $prevOffset = 0;
268
            } else {
269 1
                $prevLimit = $limit;
270
            }
271 1
            $description[static::RELATIONSHIP_LINKS] = [
272 1
                DocumentInterface::KEYWORD_PREV => $buildLink($prevOffset, $prevLimit),
273 1
                DocumentInterface::KEYWORD_NEXT => $buildLink($nextOffset, $nextLimit),
274
            ];
275
        }
276
277 2
        return $description;
278
    }
279
280
    /**
281
     * @param ModelInterface $model
282
     * @param string         $name
283
     *
284
     * @return bool
285
     */
286 19
    protected function hasProperty(ModelInterface $model, string $name): bool
287
    {
288 19
        $hasRelationship = property_exists($model, $name) || isset($model->{$name});
289
290 19
        return $hasRelationship;
291
    }
292
293
    /**
294
     * @param ModelInterface $model
295
     * @param string         $name
296
     *
297
     * @return mixed
298
     */
299 18
    protected function getProperty(ModelInterface $model, string $name)
300
    {
301 18
        assert($this->hasProperty($model, $name));
302
303 18
        return $model->{$name};
304
    }
305
306
    /**
307
     * @return array
308
     */
309 16
    private function getAttributesMapping(): array
310
    {
311 16
        if ($this->attributesMapping !== null) {
312 10
            return $this->attributesMapping;
313
        }
314
315 16
        $attributesMapping = static::getMappings()[static::SCHEMA_ATTRIBUTES] ?? [];
316
317
        // `id` is a `special` attribute and cannot be included in JSON API resource
318 16
        unset($attributesMapping[static::RESOURCE_ID]);
319
320 16
        $this->attributesMapping = $attributesMapping;
321
322 16
        return $this->attributesMapping;
323
    }
324
325
    /**
326
     * @return array
327
     */
328 19
    private function getRelationshipsMapping(): array
329
    {
330 19
        if ($this->relationshipsMapping !== null) {
331 12
            return $this->relationshipsMapping;
332
        }
333
334 19
        $relationshipsMapping = [];
335 19
        foreach (static::getMappings()[static::SCHEMA_RELATIONSHIPS] ?? [] as $jsonRelName => $modelRelName) {
336 19
            $belongsToFkName = null;
337 19
            $reverseJsonType = null;
338
339 19
            $relType = $this->getModelSchemas()->getRelationshipType(static::MODEL, $modelRelName);
340 19
            if ($relType === RelationshipTypes::BELONGS_TO) {
341 17
                $belongsToFkName = $this->getModelSchemas()->getForeignKey(static::MODEL, $modelRelName);
342 17
                $reverseSchema   = $this->getJsonSchemas()
343 17
                    ->getRelationshipSchema(static::class, $jsonRelName);
344 17
                $reverseJsonType = $reverseSchema->getType();
345
            }
346
347 19
            $relationshipsMapping[$jsonRelName] = [$modelRelName, $belongsToFkName, $reverseJsonType];
348
        }
349
350 19
        $this->relationshipsMapping = $relationshipsMapping;
351
352 19
        return $this->relationshipsMapping;
353
    }
354
355
    /**
356
     * @param ModelInterface $model
357
     * @param string         $jsonRelName
358
     *
359
     * @return string
360
     */
361 2
    private function getRelationshipSelfSubUrl(ModelInterface $model, string $jsonRelName): string
362
    {
363 2
        return $this->getSelfSubUrl($model) . '/' . DocumentInterface::KEYWORD_RELATIONSHIPS . '/' . $jsonRelName;
364
    }
365
}
366