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
![]() |
|||
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 |