Completed
Push — master ( de431b...4a1f54 )
by Jasper
05:03
created

DocumentParser::getItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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