Passed
Push — master ( 295d69...f45702 )
by Jasper
02:00
created

DocumentParser::linkRelationships()   B

Complexity

Conditions 7
Paths 1

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.0084

Importance

Changes 0
Metric Value
cc 7
eloc 17
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 29
ccs 17
cts 18
cp 0.9444
crap 7.0084
rs 8.8333
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Swis\JsonApi\Client\Parsers;
6
7
use JsonException;
8
use Swis\JsonApi\Client\Collection;
9
use Swis\JsonApi\Client\CollectionDocument;
10
use Swis\JsonApi\Client\Document;
11
use Swis\JsonApi\Client\Exceptions\ValidationException;
12
use Swis\JsonApi\Client\Interfaces\DocumentInterface;
13
use Swis\JsonApi\Client\Interfaces\DocumentParserInterface;
14
use Swis\JsonApi\Client\Interfaces\ItemInterface;
15
use Swis\JsonApi\Client\Interfaces\ManyRelationInterface;
16
use Swis\JsonApi\Client\Interfaces\OneRelationInterface;
17
use Swis\JsonApi\Client\Interfaces\TypeMapperInterface;
18
use Swis\JsonApi\Client\ItemDocument;
19
use Swis\JsonApi\Client\TypeMapper;
20
21
class DocumentParser implements DocumentParserInterface
22
{
23
    private ItemParser $itemParser;
24
25
    private CollectionParser $collectionParser;
26
27
    private ErrorCollectionParser $errorCollectionParser;
28
29
    private LinksParser $linksParser;
30
31
    private JsonapiParser $jsonapiParser;
32
33
    private MetaParser $metaParser;
34
35 152
    public function __construct(
36
        ItemParser $itemParser,
37
        CollectionParser $collectionParser,
38
        ErrorCollectionParser $errorCollectionParser,
39
        LinksParser $linksParser,
40
        JsonapiParser $jsonapiParser,
41
        MetaParser $metaParser
42
    ) {
43 152
        $this->itemParser = $itemParser;
44 152
        $this->collectionParser = $collectionParser;
45 152
        $this->errorCollectionParser = $errorCollectionParser;
46 152
        $this->linksParser = $linksParser;
47 152
        $this->jsonapiParser = $jsonapiParser;
48 152
        $this->metaParser = $metaParser;
49 76
    }
50
51
    /**
52
     * @param \Swis\JsonApi\Client\Interfaces\TypeMapperInterface|null $typeMapper
53
     *
54
     * @return static
55
     */
56 152
    public static function create(TypeMapperInterface $typeMapper = null): self
57
    {
58 152
        $metaParser = new MetaParser();
59 152
        $linksParser = new LinksParser($metaParser);
60 152
        $itemParser = new ItemParser($typeMapper ?? new TypeMapper(), $linksParser, $metaParser);
61
62 152
        return new static(
63 76
            $itemParser,
64 152
            new CollectionParser($itemParser),
65 152
            new ErrorCollectionParser(
66 152
                new ErrorParser($linksParser, $metaParser)
67
            ),
68
            $linksParser,
69 152
            new JsonapiParser($metaParser),
70
            $metaParser
71
        );
72
    }
73
74
    /**
75
     * @param string $json
76
     *
77
     * @return \Swis\JsonApi\Client\Interfaces\DocumentInterface
78
     */
79 140
    public function parse(string $json): DocumentInterface
80
    {
81 140
        $data = $this->decodeJson($json);
82
83 136
        if (!is_object($data)) {
84 24
            throw new ValidationException(sprintf('Document MUST be an object, "%s" given.', gettype($data)));
85
        }
86 112
        if (!property_exists($data, 'data') && !property_exists($data, 'errors') && !property_exists($data, 'meta')) {
87 4
            throw new ValidationException('Document MUST contain at least one of the following properties: `data`, `errors`, `meta`.');
88
        }
89 108
        if (property_exists($data, 'data') && property_exists($data, 'errors')) {
90 4
            throw new ValidationException('The properties `data` and `errors` MUST NOT coexist in Document.');
91
        }
92 104
        if (!property_exists($data, 'data') && property_exists($data, 'included')) {
93 4
            throw new ValidationException('If Document does not contain a `data` property, the `included` property MUST NOT be present either.');
94
        }
95 100
        if (property_exists($data, 'data') && !is_object($data->data) && !is_array($data->data) && $data->data !== null) {
96 16
            throw new ValidationException(sprintf('Document property "data" MUST be null, an array or an object, "%s" given.', gettype($data->data)));
97
        }
98 84
        if (property_exists($data, 'included') && !is_array($data->included)) {
99 24
            throw new ValidationException(sprintf('Document property "included" MUST be an array, "%s" given.', gettype($data->included)));
100
        }
101
102 60
        $document = $this->getDocument($data);
103
104 56
        if (property_exists($data, 'links')) {
105 4
            $document->setLinks($this->linksParser->parse($data->links, LinksParser::SOURCE_DOCUMENT));
106
        }
107
108 56
        if (property_exists($data, 'errors')) {
109 4
            $document->setErrors($this->errorCollectionParser->parse($data->errors));
110
        }
111
112 56
        if (property_exists($data, 'meta')) {
113 8
            $document->setMeta($this->metaParser->parse($data->meta));
114
        }
115
116 56
        if (property_exists($data, 'jsonapi')) {
117 4
            $document->setJsonapi($this->jsonapiParser->parse($data->jsonapi));
118
        }
119
120 56
        return $document;
121
    }
122
123
    /**
124
     * @param string $json
125
     *
126
     * @return mixed
127
     */
128 140
    private function decodeJson(string $json)
129
    {
130
        try {
131 140
            return json_decode($json, false, 512, JSON_THROW_ON_ERROR);
132 4
        } catch (JsonException $exception) {
133 4
            throw new ValidationException(sprintf('Unable to parse JSON data: %s', $exception->getMessage()), 0, $exception);
134
        }
135
    }
136
137
    /**
138
     * @param mixed $data
139
     *
140
     * @return \Swis\JsonApi\Client\Interfaces\DocumentInterface
141
     */
142 60
    private function getDocument($data): DocumentInterface
143
    {
144 60
        if (!property_exists($data, 'data') || $data->data === null) {
145 8
            return new Document();
146
        }
147
148 52
        if (is_array($data->data)) {
149 28
            $document = (new CollectionDocument())
150 28
                ->setData($this->collectionParser->parse($data->data));
151
        } else {
152 24
            $document = (new ItemDocument())
153 24
                ->setData($this->itemParser->parse($data->data));
154
        }
155
156 52
        if (property_exists($data, 'included')) {
157 28
            $document->setIncluded($this->collectionParser->parse($data->included));
158
        }
159
160 52
        $allItems = Collection::wrap($document->getData())
0 ignored issues
show
Bug introduced by
$document->getData() of type Swis\JsonApi\Client\Interfaces\DataInterface is incompatible with the type iterable expected by parameter $value of Illuminate\Support\Collection::wrap(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
        $allItems = Collection::wrap(/** @scrutinizer ignore-type */ $document->getData())
Loading history...
161 52
            ->concat($document->getIncluded());
162
163 52
        $duplicateItems = $this->getDuplicateItems($allItems);
164
165 52
        if ($duplicateItems->isNotEmpty()) {
166 4
            throw new ValidationException(sprintf('Resources MUST be unique based on their `type` and `id`, %d duplicate(s) found.', $duplicateItems->count()));
167
        }
168
169 48
        $this->linkRelationships($allItems);
170
171 48
        return $document;
172
    }
173
174
    /**
175
     * @param \Swis\JsonApi\Client\Collection $items
176
     */
177 48
    private function linkRelationships(Collection $items): void
178
    {
179 48
        $keyedItems = $items->keyBy(fn (ItemInterface $item) => $this->getItemKey($item));
180
181 48
        $items->each(
182 48
            function (ItemInterface $item) use ($keyedItems) {
183 32
                foreach ($item->getRelations() as $name => $relation) {
184 20
                    if ($relation instanceof OneRelationInterface) {
185
                        /** @var \Swis\JsonApi\Client\Interfaces\ItemInterface|null $relatedItem */
186 12
                        $relatedItem = $relation->getData();
187
188 12
                        if ($relatedItem === null) {
189 4
                            continue;
190
                        }
191
192 8
                        $includedItem = $this->getItem($keyedItems, $relatedItem);
193 8
                        if ($includedItem !== null) {
194 8
                            $relation->setIncluded($includedItem);
195
                        }
196 8
                    } elseif ($relation instanceof ManyRelationInterface) {
197
                        /** @var \Swis\JsonApi\Client\Collection|null $relatedCollection */
198 8
                        $relatedCollection = $relation->getData();
199
200 8
                        if ($relatedCollection === null) {
201
                            continue;
202
                        }
203
204 8
                        $relation->setIncluded(
205 8
                            $relatedCollection->map(fn (ItemInterface $relatedItem) => $this->getItem($keyedItems, $relatedItem) ?? $relatedItem)
206
                        );
207
                    }
208
                }
209 24
            }
210
        );
211 24
    }
212
213
    /**
214
     * @param \Swis\JsonApi\Client\Collection               $included
215
     * @param \Swis\JsonApi\Client\Interfaces\ItemInterface $item
216
     *
217
     * @return \Swis\JsonApi\Client\Interfaces\ItemInterface|null
218
     */
219 12
    private function getItem(Collection $included, ItemInterface $item): ?ItemInterface
220
    {
221 12
        return $included->get($this->getItemKey($item));
222
    }
223
224
    /**
225
     * @param \Swis\JsonApi\Client\Interfaces\ItemInterface $item
226
     *
227
     * @return string
228
     */
229 36
    private function getItemKey(ItemInterface $item): string
230
    {
231 36
        return sprintf('%s:%s', $item->getType(), $item->getId());
232
    }
233
234
    /**
235
     * @param \Swis\JsonApi\Client\Collection $items
236
     *
237
     * @return \Swis\JsonApi\Client\Collection
238
     */
239 52
    private function getDuplicateItems(Collection $items): Collection
240
    {
241 52
        return $items->duplicates(fn (ItemInterface $item) => $this->getItemKey($item));
242
    }
243
}
244