Completed
Push — master ( 1357ca...aa0a82 )
by Jasper
15s queued 11s
created

DocumentParser::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 1
dl 0
loc 15
ccs 10
cts 10
cp 1
crap 1
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
namespace Swis\JsonApi\Client\Parsers;
4
5
use Swis\JsonApi\Client\Collection;
6
use Swis\JsonApi\Client\CollectionDocument;
7
use Swis\JsonApi\Client\Document;
8
use Swis\JsonApi\Client\Exceptions\ValidationException;
9
use Swis\JsonApi\Client\Interfaces\DocumentInterface;
10
use Swis\JsonApi\Client\Interfaces\DocumentParserInterface;
11
use Swis\JsonApi\Client\Interfaces\ItemInterface;
12
use Swis\JsonApi\Client\Interfaces\ManyRelationInterface;
13
use Swis\JsonApi\Client\Interfaces\OneRelationInterface;
14
use Swis\JsonApi\Client\Interfaces\TypeMapperInterface;
15
use Swis\JsonApi\Client\ItemDocument;
16
use Swis\JsonApi\Client\TypeMapper;
17
18
class DocumentParser implements DocumentParserInterface
19
{
20
    /**
21
     * @var \Swis\JsonApi\Client\Parsers\ItemParser
22
     */
23
    private $itemParser;
24
25
    /**
26
     * @var \Swis\JsonApi\Client\Parsers\CollectionParser
27
     */
28
    private $collectionParser;
29
30
    /**
31
     * @var \Swis\JsonApi\Client\Parsers\ErrorCollectionParser
32
     */
33
    private $errorCollectionParser;
34
35
    /**
36
     * @var \Swis\JsonApi\Client\Parsers\LinksParser
37
     */
38
    private $linksParser;
39
40
    /**
41
     * @var \Swis\JsonApi\Client\Parsers\JsonapiParser
42
     */
43
    private $jsonapiParser;
44
45
    /**
46
     * @var \Swis\JsonApi\Client\Parsers\MetaParser
47
     */
48
    private $metaParser;
49
50
    /**
51
     * @param \Swis\JsonApi\Client\Parsers\ItemParser            $itemParser
52
     * @param \Swis\JsonApi\Client\Parsers\CollectionParser      $collectionParser
53
     * @param \Swis\JsonApi\Client\Parsers\ErrorCollectionParser $errorCollectionParser
54
     * @param \Swis\JsonApi\Client\Parsers\LinksParser           $linksParser
55
     * @param \Swis\JsonApi\Client\Parsers\JsonapiParser         $jsonapiParser
56 306
     * @param \Swis\JsonApi\Client\Parsers\MetaParser            $metaParser
57
     */
58
    public function __construct(
59
        ItemParser $itemParser,
60
        CollectionParser $collectionParser,
61
        ErrorCollectionParser $errorCollectionParser,
62
        LinksParser $linksParser,
63
        JsonapiParser $jsonapiParser,
64 306
        MetaParser $metaParser
65 306
    ) {
66 306
        $this->itemParser = $itemParser;
67 306
        $this->collectionParser = $collectionParser;
68 306
        $this->errorCollectionParser = $errorCollectionParser;
69 306
        $this->linksParser = $linksParser;
70 306
        $this->jsonapiParser = $jsonapiParser;
71
        $this->metaParser = $metaParser;
72
    }
73
74
    /**
75
     * @param \Swis\JsonApi\Client\Interfaces\TypeMapperInterface|null $typeMapper
76
     *
77 306
     * @return static
78
     */
79 306
    public static function create(TypeMapperInterface $typeMapper = null): self
80
    {
81 297
        $metaParser = new MetaParser();
82 54
        $linksParser = new LinksParser($metaParser);
83
        $itemParser = new ItemParser($typeMapper ?? new TypeMapper(), $linksParser, $metaParser);
84 243
85 9
        return new static(
86
            $itemParser,
87 234
            new CollectionParser($itemParser),
88 9
            new ErrorCollectionParser(
89
                new ErrorParser($linksParser, $metaParser)
90 225
            ),
91 9
            $linksParser,
92
            new JsonapiParser($metaParser),
93 216
            $metaParser
94 36
        );
95
    }
96 180
97 54
    /**
98
     * @param string $json
99
     *
100 126
     * @return \Swis\JsonApi\Client\Interfaces\DocumentInterface
101
     */
102 117
    public function parse(string $json): DocumentInterface
103 9
    {
104
        $data = $this->decodeJson($json);
105
106 117
        if (!is_object($data)) {
107 9
            throw new ValidationException(sprintf('Document MUST be an object, "%s" given.', gettype($data)));
108
        }
109
        if (!property_exists($data, 'data') && !property_exists($data, 'errors') && !property_exists($data, 'meta')) {
110 117
            throw new ValidationException('Document MUST contain at least one of the following properties: `data`, `errors`, `meta`.');
111 18
        }
112
        if (property_exists($data, 'data') && property_exists($data, 'errors')) {
113
            throw new ValidationException('The properties `data` and `errors` MUST NOT coexist in Document.');
114 117
        }
115 9
        if (!property_exists($data, 'data') && property_exists($data, 'included')) {
116
            throw new ValidationException('If Document does not contain a `data` property, the `included` property MUST NOT be present either.');
117
        }
118 117
        if (property_exists($data, 'data') && !is_object($data->data) && !is_array($data->data) && $data->data !== null) {
119
            throw new ValidationException(sprintf('Document property "data" MUST be null, an array or an object, "%s" given.', gettype($data->data)));
120
        }
121
        if (property_exists($data, 'included') && !is_array($data->included)) {
122
            throw new ValidationException(sprintf('Document property "included" MUST be an array, "%s" given.', gettype($data->included)));
123
        }
124
125
        $document = $this->getDocument($data);
126 306
127
        if (property_exists($data, 'links')) {
128 306
            $document->setLinks($this->linksParser->parse($data->links, LinksParser::SOURCE_DOCUMENT));
129
        }
130 306
131 9
        if (property_exists($data, 'errors')) {
132
            $document->setErrors($this->errorCollectionParser->parse($data->errors));
133
        }
134 297
135
        if (property_exists($data, 'meta')) {
136
            $document->setMeta($this->metaParser->parse($data->meta));
137
        }
138
139
        if (property_exists($data, 'jsonapi')) {
140
            $document->setJsonapi($this->jsonapiParser->parse($data->jsonapi));
141
        }
142 126
143
        return $document;
144 126
    }
145 18
146
    /**
147
     * @param string $json
148 108
     *
149 63
     * @return mixed
150 63
     */
151
    private function decodeJson(string $json)
152 45
    {
153 45
        $data = json_decode($json, false);
154
155
        if (json_last_error() !== JSON_ERROR_NONE) {
156 108
            throw new ValidationException(sprintf('Unable to parse JSON data: %s', json_last_error_msg()), json_last_error());
157 54
        }
158
159
        return $data;
160 108
    }
161 108
162
    /**
163 108
     * @param mixed $data
164
     *
165 108
     * @return \Swis\JsonApi\Client\Interfaces\DocumentInterface
166 9
     */
167
    private function getDocument($data): DocumentInterface
168
    {
169 99
        if (!property_exists($data, 'data') || $data->data === null) {
170
            return new Document();
171 99
        }
172
173
        if (is_array($data->data)) {
174
            $document = (new CollectionDocument())
175
                ->setData($this->collectionParser->parse($data->data));
176
        } else {
177 99
            $document = (new ItemDocument())
178
                ->setData($this->itemParser->parse($data->data));
179 99
        }
180 22
181 63
        if (property_exists($data, 'included')) {
182 99
            $document->setIncluded($this->collectionParser->parse($data->included));
183
        }
184
185 99
        $allItems = Collection::wrap($document->getData())
186 22
            ->concat($document->getIncluded());
187 63
188 36
        $duplicateItems = $this->getDuplicateItems($allItems);
189
190 18
        if ($duplicateItems->isNotEmpty()) {
191
            throw new ValidationException(sprintf('Resources MUST be unique based on their `type` and `id`, %d duplicate(s) found.', $duplicateItems->count()));
192 18
        }
193 9
194
        $this->linkRelationships($allItems);
195
196 9
        return $document;
197 9
    }
198 9
199
    /**
200 18
     * @param \Swis\JsonApi\Client\Collection $items
201
     */
202 18
    private function linkRelationships(Collection $items): void
203
    {
204
        $keyedItems = $items->keyBy(
205 18
            function (ItemInterface $item) {
206 9
                return $this->getItemKey($item);
207 9
            }
208 11
        );
209
210
        $items->each(
211
            function (ItemInterface $item) use ($keyedItems) {
212
                foreach ($item->getRelations() as $name => $relation) {
213 99
                    if ($relation instanceof OneRelationInterface) {
214
                        /** @var \Swis\JsonApi\Client\Interfaces\ItemInterface|null $relatedItem */
215 99
                        $relatedItem = $relation->getIncluded();
216
217
                        if ($relatedItem === null) {
218
                            continue;
219
                        }
220
221
                        $includedItem = $this->getItem($keyedItems, $relatedItem);
222
                        if ($includedItem !== null) {
223 18
                            $relation->associate($includedItem);
224
                        }
225 18
                    } elseif ($relation instanceof ManyRelationInterface) {
226
                        /** @var \Swis\JsonApi\Client\Collection $relatedCollection */
227
                        $relatedCollection = $relation->getIncluded();
228
229
                        /** @var \Swis\JsonApi\Client\Interfaces\ItemInterface $relatedItem */
230
                        foreach ($relatedCollection as $key => $relatedItem) {
231
                            $includedItem = $this->getItem($keyedItems, $relatedItem);
232
                            if ($includedItem !== null) {
233 72
                                $relatedCollection->put($key, $includedItem);
234
                            }
235 72
                        }
236
                    }
237
                }
238
            }
239
        );
240
    }
241
242
    /**
243 84
     * @param \Swis\JsonApi\Client\Collection               $included
244
     * @param \Swis\JsonApi\Client\Interfaces\ItemInterface $item
245 24
     *
246 72
     * @return \Swis\JsonApi\Client\Interfaces\ItemInterface|null
247 108
     */
248
    private function getItem(Collection $included, ItemInterface $item): ?ItemInterface
249
    {
250 108
        return $included->get($this->getItemKey($item));
251 60
    }
252
253
    /**
254
     * @param \Swis\JsonApi\Client\Interfaces\ItemInterface $item
255
     *
256
     * @return string
257
     */
258
    private function getItemKey(ItemInterface $item): string
259 48
    {
260
        return sprintf('%s:%s', $item->getType(), $item->getId());
261 48
    }
262
263 24
    /**
264 32
     * @param \Swis\JsonApi\Client\Collection $items
265 48
     *
266
     * @return \Swis\JsonApi\Client\Collection
267 48
     */
268
    private function getDuplicateItems(Collection $items): Collection
269 48
    {
270 32
        $valueRetriever = function (ItemInterface $item) {
271 32
            return $this->getItemKey($item);
272
        };
273 11
274
        // Collection->duplicates was introduced in Laravel 5.8
275
        if (method_exists($items, 'duplicates')) {
276
            return $items->duplicates($valueRetriever);
277 48
        }
278
279
        /*
280
         * Duplicates code copied, and simplified for our use case, from Laravel 6.
281
         *
282
         * @see https://github.com/laravel/framework/blob/v6.1.0/src/Illuminate/Support/Collection.php#L275
283
         */
284
        $values = $items->map($valueRetriever);
285
286
        $uniqueValues = $values->unique();
287
288
        $compare = static function ($a, $b) {
289
            return $a === $b;
290
        };
291
292
        $duplicates = new Collection();
293
294
        foreach ($values as $key => $value) {
295
            if ($uniqueValues->isNotEmpty() && $compare($value, $uniqueValues->first())) {
296
                $uniqueValues->shift();
297
            } else {
298
                $duplicates[$key] = $value;
299
            }
300
        }
301
302
        return $duplicates;
303
    }
304
}
305