DocumentParser::create()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1

Importance

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

148
        $allItems = Collection::wrap(/** @scrutinizer ignore-type */ $document->getData())
Loading history...
149 52
            ->concat($document->getIncluded());
150
151 52
        $duplicateItems = $this->getDuplicateItems($allItems);
152
153 52
        if ($duplicateItems->isNotEmpty()) {
154 4
            throw new ValidationException(sprintf('Resources MUST be unique based on their `type` and `id`, %d duplicate(s) found.', $duplicateItems->count()));
155
        }
156
157 48
        $this->linkRelationships($allItems);
158
159 48
        return $document;
160
    }
161
162 48
    private function linkRelationships(Collection $items): void
163
    {
164 48
        $keyedItems = $items->keyBy(fn (ItemInterface $item) => $this->getItemKey($item));
165
166 48
        $items->each(
167 48
            function (ItemInterface $item) use ($keyedItems) {
168 32
                foreach ($item->getRelations() as $name => $relation) {
169 20
                    if ($relation instanceof OneRelationInterface) {
170
                        /** @var \Swis\JsonApi\Client\Interfaces\ItemInterface|null $relatedItem */
171 12
                        $relatedItem = $relation->getData();
172
173 12
                        if ($relatedItem === null) {
174 4
                            continue;
175
                        }
176
177 8
                        $includedItem = $this->getItem($keyedItems, $relatedItem);
178 8
                        if ($includedItem !== null) {
179 8
                            $relation->setIncluded($includedItem);
180
                        }
181 8
                    } elseif ($relation instanceof ManyRelationInterface) {
182
                        /** @var \Swis\JsonApi\Client\Collection|null $relatedCollection */
183 8
                        $relatedCollection = $relation->getData();
184
185 8
                        if ($relatedCollection === null) {
186
                            continue;
187
                        }
188
189 8
                        $relation->setIncluded(
190 8
                            $relatedCollection->map(fn (ItemInterface $relatedItem) => $this->getItem($keyedItems, $relatedItem) ?? $relatedItem)
191 4
                        );
192
                    }
193
                }
194 48
            }
195 24
        );
196 24
    }
197
198 12
    private function getItem(Collection $included, ItemInterface $item): ?ItemInterface
199
    {
200 12
        return $included->get($this->getItemKey($item));
201
    }
202
203 36
    private function getItemKey(ItemInterface $item): string
204
    {
205 36
        return sprintf('%s:%s', $item->getType(), $item->getId());
206
    }
207
208 52
    private function getDuplicateItems(Collection $items): Collection
209
    {
210 52
        return $items->duplicates(fn (ItemInterface $item) => $this->getItemKey($item));
211
    }
212
}
213