Passed
Pull Request — master (#97)
by Jasper
11:23
created

DocumentParser::getDuplicateItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

186
        $allItems = Collection::wrap(/** @scrutinizer ignore-type */ $document->getData())
Loading history...
187 52
            ->concat($document->getIncluded());
188
189 52
        $duplicateItems = $this->getDuplicateItems($allItems);
190
191 52
        if ($duplicateItems->isNotEmpty()) {
192 4
            throw new ValidationException(sprintf('Resources MUST be unique based on their `type` and `id`, %d duplicate(s) found.', $duplicateItems->count()));
193
        }
194
195 48
        $this->linkRelationships($allItems);
196
197 48
        return $document;
198
    }
199
200
    /**
201
     * @param \Swis\JsonApi\Client\Collection $items
202
     */
203 48
    private function linkRelationships(Collection $items): void
204
    {
205 48
        $keyedItems = $items->keyBy(
206 48
            function (ItemInterface $item) {
207 32
                return $this->getItemKey($item);
208 24
            }
209
        );
210
211 48
        $items->each(
212 48
            function (ItemInterface $item) use ($keyedItems) {
213 32
                foreach ($item->getRelations() as $name => $relation) {
214 20
                    if ($relation instanceof OneRelationInterface) {
215
                        /** @var \Swis\JsonApi\Client\Interfaces\ItemInterface|null $relatedItem */
216 12
                        $relatedItem = $relation->getData();
217
218 12
                        if ($relatedItem === null) {
219 4
                            continue;
220
                        }
221
222 8
                        $includedItem = $this->getItem($keyedItems, $relatedItem);
223 8
                        if ($includedItem !== null) {
224 8
                            $relation->setIncluded($includedItem);
225
                        }
226 8
                    } elseif ($relation instanceof ManyRelationInterface) {
227
                        /** @var \Swis\JsonApi\Client\Collection|null $relatedCollection */
228 8
                        $relatedCollection = $relation->getData();
229
230 8
                        if ($relatedCollection === null) {
231
                            continue;
232
                        }
233
234 8
                        $relation->setIncluded(
235 8
                            $relatedCollection->map(function (ItemInterface $relatedItem) use ($keyedItems) {
236 4
                                return $this->getItem($keyedItems, $relatedItem) ?? $relatedItem;
237 4
                            })
238
                        );
239
                    }
240
                }
241 24
            }
242
        );
243 24
    }
244
245
    /**
246
     * @param \Swis\JsonApi\Client\Collection               $included
247
     * @param \Swis\JsonApi\Client\Interfaces\ItemInterface $item
248
     *
249
     * @return \Swis\JsonApi\Client\Interfaces\ItemInterface|null
250
     */
251 12
    private function getItem(Collection $included, ItemInterface $item): ?ItemInterface
252
    {
253 12
        return $included->get($this->getItemKey($item));
254
    }
255
256
    /**
257
     * @param \Swis\JsonApi\Client\Interfaces\ItemInterface $item
258
     *
259
     * @return string
260
     */
261 36
    private function getItemKey(ItemInterface $item): string
262
    {
263 36
        return sprintf('%s:%s', $item->getType(), $item->getId());
264
    }
265
266
    /**
267
     * @param \Swis\JsonApi\Client\Collection $items
268
     *
269
     * @return \Swis\JsonApi\Client\Collection
270
     */
271 52
    private function getDuplicateItems(Collection $items): Collection
272
    {
273 52
        $valueRetriever = function (ItemInterface $item) {
274 36
            return $this->getItemKey($item);
275
        };
276
277 52
        return $items->duplicates($valueRetriever);
278
    }
279
}
280