1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Mapado\RestClientSdk; |
||
6 | |||
7 | use Mapado\RestClientSdk\Collection\Collection; |
||
8 | use Mapado\RestClientSdk\Exception\HydratorException; |
||
9 | use Mapado\RestClientSdk\Exception\RestException; |
||
10 | use Mapado\RestClientSdk\Exception\SdkException; |
||
11 | use Mapado\RestClientSdk\Exception\UnexpectedTypeException; |
||
12 | use Mapado\RestClientSdk\Helper\ArrayHelper; |
||
13 | use Mapado\RestClientSdk\Mapping\ClassMetadata; |
||
14 | use Psr\Http\Message\ResponseInterface; |
||
15 | |||
16 | class EntityRepository |
||
17 | { |
||
18 | /** |
||
19 | * REST Client. |
||
20 | * |
||
21 | * @var RestClient |
||
22 | */ |
||
23 | protected $restClient; |
||
24 | |||
25 | /** |
||
26 | * SDK Client. |
||
27 | * |
||
28 | * @var SdkClient |
||
29 | */ |
||
30 | protected $sdk; |
||
31 | |||
32 | /** |
||
33 | * @var string |
||
34 | */ |
||
35 | protected $entityName; |
||
36 | |||
37 | /** |
||
38 | * classMetadataCache |
||
39 | * |
||
40 | * @var ClassMetadata |
||
41 | */ |
||
42 | private $classMetadataCache; |
||
43 | |||
44 | /** |
||
45 | * unitOfWork |
||
46 | * |
||
47 | * @var UnitOfWork |
||
48 | */ |
||
49 | private $unitOfWork; |
||
50 | |||
51 | /** |
||
52 | * EntityRepository constructor |
||
53 | * |
||
54 | * @param SdkClient $sdkClient The client to connect to the datasource with |
||
55 | * @param RestClient $restClient The client to process the http requests |
||
56 | * @param string $entityName The entity to work with |
||
57 | */ |
||
58 | public function __construct( |
||
59 | SdkClient $sdkClient, |
||
60 | RestClient $restClient, |
||
61 | UnitOfWork $unitOfWork, |
||
62 | string $entityName |
||
63 | ) { |
||
64 | 1 | $this->sdk = $sdkClient; |
|
65 | 1 | $this->restClient = $restClient; |
|
66 | 1 | $this->unitOfWork = $unitOfWork; |
|
67 | 1 | $this->entityName = $entityName; |
|
68 | 1 | } |
|
69 | |||
70 | /** |
||
71 | * Adds support for magic finders. |
||
72 | * |
||
73 | * @return array|object|null the found entity/entities |
||
74 | */ |
||
75 | public function __call(string $method, array $arguments) |
||
76 | { |
||
77 | switch (true) { |
||
78 | 1 | case 0 === mb_strpos($method, 'findBy'): |
|
79 | 1 | $fieldName = mb_strtolower(mb_substr($method, 6)); |
|
80 | 1 | $methodName = 'findBy'; |
|
81 | 1 | break; |
|
82 | |||
83 | 1 | case 0 === mb_strpos($method, 'findOneBy'): |
|
84 | 1 | $fieldName = mb_strtolower(mb_substr($method, 9)); |
|
85 | 1 | $methodName = 'findOneBy'; |
|
86 | 1 | break; |
|
87 | |||
88 | default: |
||
89 | throw new \BadMethodCallException('Undefined method \'' . $method . '\'. The method name must start with |
||
90 | either findBy or findOneBy!'); |
||
91 | } |
||
92 | |||
93 | 1 | if (empty($arguments)) { |
|
94 | throw new SdkException('You need to pass a parameter to ' . $method); |
||
95 | } |
||
96 | |||
97 | 1 | $mapping = $this->sdk->getMapping(); |
|
98 | 1 | $key = $mapping->getKeyFromModel($this->entityName); |
|
99 | 1 | $prefix = $mapping->getIdPrefix(); |
|
100 | 1 | $path = empty($prefix) ? '/' . $key : $prefix . '/' . $key; |
|
101 | |||
102 | 1 | if (!empty($fieldName)) { |
|
103 | 1 | $queryParams = [$fieldName => current($arguments)]; |
|
104 | } else { |
||
105 | 1 | $queryParams = current($arguments); |
|
106 | } |
||
107 | $path .= |
||
108 | 1 | '?' . http_build_query($this->convertQueryParameters($queryParams)); |
|
109 | |||
110 | // if entityList is found in cache, return it |
||
111 | 1 | $entityListFromCache = $this->fetchFromCache($path); |
|
112 | 1 | if (false !== $entityListFromCache) { |
|
113 | 1 | return $entityListFromCache; |
|
114 | } |
||
115 | |||
116 | 1 | $data = $this->restClient->get($path); |
|
117 | 1 | $hydrator = $this->sdk->getModelHydrator(); |
|
118 | |||
119 | 1 | if ('findOneBy' == $methodName) { |
|
120 | // If more results are found but one is requested return the first hit. |
||
121 | 1 | $collectionKey = $mapping->getConfig()['collectionKey']; |
|
122 | |||
123 | 1 | $data = $this->assertArray($data, $methodName); |
|
124 | 1 | $entityList = ArrayHelper::arrayGet($data, $collectionKey); |
|
125 | 1 | if (!empty($entityList)) { |
|
126 | 1 | $data = current($entityList); |
|
127 | 1 | $hydratedData = $hydrator->hydrate($data, $this->entityName); |
|
128 | |||
129 | 1 | $identifier = $hydratedData->{$this->getClassMetadata()->getIdGetter()}(); |
|
130 | 1 | if (null !== $hydratedData) { |
|
131 | 1 | $this->unitOfWork->registerClean( |
|
132 | 1 | $identifier, |
|
133 | $hydratedData |
||
134 | ); |
||
135 | } |
||
136 | 1 | $this->saveToCache($identifier, $hydratedData); |
|
137 | } else { |
||
138 | 1 | $hydratedData = null; |
|
139 | } |
||
140 | } else { |
||
141 | 1 | $data = $this->assertNotObject($data, $methodName); |
|
142 | 1 | $hydratedData = $hydrator->hydrateList($data, $this->entityName); |
|
143 | |||
144 | // then cache each entity from list |
||
145 | 1 | foreach ($hydratedData as $entity) { |
|
146 | 1 | $identifier = $entity->{$this->getClassMetadata()->getIdGetter()}(); |
|
147 | 1 | $this->saveToCache($identifier, $entity); |
|
148 | 1 | $this->unitOfWork->registerClean($identifier, $entity); |
|
149 | } |
||
150 | } |
||
151 | |||
152 | 1 | $this->saveToCache($path, $hydratedData); |
|
153 | |||
154 | 1 | return $hydratedData; |
|
155 | } |
||
156 | |||
157 | /** |
||
158 | * find - finds one item of the entity based on the @REST\Id field in the entity |
||
159 | * |
||
160 | * @param string|int|mixed $id id of the element to fetch |
||
161 | * @param array $queryParams query parameters to add to the query |
||
162 | */ |
||
163 | public function find($id, array $queryParams = []): ?object |
||
164 | { |
||
165 | 1 | $hydrator = $this->sdk->getModelHydrator(); |
|
166 | 1 | $id = $hydrator->convertId($id, $this->entityName); |
|
167 | |||
168 | 1 | $id = $this->addQueryParameter($id, $queryParams); |
|
169 | |||
170 | // if entity is found in cache, return it |
||
171 | 1 | $entityFromCache = $this->fetchFromCache($id); |
|
172 | 1 | if (false != $entityFromCache) { |
|
173 | 1 | return $entityFromCache; |
|
174 | } |
||
175 | |||
176 | 1 | $data = $this->restClient->get($id); |
|
177 | 1 | $data = $this->assertNotObject($data, __METHOD__); |
|
178 | |||
179 | 1 | $entity = $hydrator->hydrate($data, $this->entityName); |
|
180 | |||
181 | // cache entity |
||
182 | 1 | $this->saveToCache($id, $entity); |
|
183 | 1 | if (null !== $entity) { |
|
184 | 1 | $this->unitOfWork->registerClean($id, $entity); // another register clean will be made in the Serializer if the id different from the called uri |
|
185 | } |
||
186 | |||
187 | 1 | return $entity; |
|
188 | } |
||
189 | |||
190 | public function findAll(): Collection |
||
191 | { |
||
192 | 1 | $mapping = $this->sdk->getMapping(); |
|
193 | 1 | $key = $this->getClassMetadata()->getKey(); |
|
194 | 1 | $prefix = $mapping->getIdPrefix(); |
|
195 | 1 | $path = empty($prefix) ? '/' . $key : $prefix . '/' . $key; |
|
196 | |||
197 | 1 | $entityListFromCache = $this->fetchFromCache($path); |
|
198 | |||
199 | // if entityList is found in cache, return it |
||
200 | 1 | if (false !== $entityListFromCache) { |
|
201 | 1 | if (!$entityListFromCache instanceof Collection) { |
|
202 | throw new \RuntimeException('Entity list in cache should be an instance of ' . Collection::class . '. This should not happen.'); |
||
203 | } |
||
204 | |||
205 | 1 | return $entityListFromCache; |
|
206 | } |
||
207 | |||
208 | 1 | $data = $this->restClient->get($path); |
|
209 | 1 | $data = $this->assertNotObject($data, __METHOD__); |
|
210 | |||
211 | 1 | $hydrator = $this->sdk->getModelHydrator(); |
|
212 | 1 | $entityList = $hydrator->hydrateList($data, $this->entityName); |
|
213 | |||
214 | // cache entity list |
||
215 | 1 | $this->saveToCache($path, $entityList); |
|
216 | |||
217 | // then cache each entity from list |
||
218 | 1 | foreach ($entityList as $entity) { |
|
219 | 1 | $identifier = $entity->{$this->getClassMetadata()->getIdGetter()}(); |
|
220 | 1 | $this->unitOfWork->registerClean($identifier, $entity); |
|
221 | 1 | $this->saveToCache($identifier, $entity); |
|
222 | } |
||
223 | |||
224 | 1 | return $entityList; |
|
225 | } |
||
226 | |||
227 | /** |
||
228 | * remove entity |
||
229 | * |
||
230 | * @TODO STILL NEEDS TO BE CONVERTED TO ENTITY MODEL |
||
231 | */ |
||
232 | public function remove(object $model): void |
||
233 | { |
||
234 | 1 | $identifier = $model->{$this->getClassMetadata()->getIdGetter()}(); |
|
235 | 1 | $this->removeFromCache($identifier); |
|
236 | 1 | $this->unitOfWork->clear($identifier); |
|
237 | |||
238 | 1 | $this->restClient->delete($identifier); |
|
239 | 1 | } |
|
240 | |||
241 | public function update( |
||
242 | object $model, |
||
243 | array $serializationContext = [], |
||
244 | array $queryParams = [] |
||
245 | ): object { |
||
246 | 1 | $identifier = $model->{$this->getClassMetadata()->getIdGetter()}(); |
|
247 | 1 | $serializer = $this->sdk->getSerializer(); |
|
248 | 1 | $newSerializedModel = $serializer->serialize( |
|
249 | 1 | $model, |
|
250 | 1 | $this->entityName, |
|
251 | $serializationContext |
||
252 | ); |
||
253 | |||
254 | 1 | $oldModel = $this->unitOfWork->getDirtyEntity($identifier); |
|
255 | 1 | if ($oldModel) { |
|
256 | 1 | $oldSerializedModel = $serializer->serialize( |
|
257 | 1 | $oldModel, |
|
258 | 1 | $this->entityName, |
|
259 | $serializationContext |
||
260 | ); |
||
261 | 1 | $newSerializedModel = $this->unitOfWork->getDirtyData( |
|
262 | 1 | $newSerializedModel, |
|
263 | $oldSerializedModel, |
||
264 | 1 | $this->getClassMetadata() |
|
265 | ); |
||
266 | } |
||
267 | |||
268 | 1 | $path = $this->addQueryParameter($identifier, $queryParams); |
|
269 | |||
270 | 1 | $data = $this->restClient->put($path, $newSerializedModel); |
|
271 | 1 | $data = $this->assertArray($data, __METHOD__); |
|
272 | |||
273 | 1 | $this->removeFromCache($identifier); |
|
274 | // $this->unitOfWork->registerClean($identifier, $data); |
||
275 | 1 | $hydrator = $this->sdk->getModelHydrator(); |
|
276 | 1 | $out = $hydrator->hydrate($data, $this->entityName); |
|
277 | |||
278 | 1 | if (null === $out) { |
|
279 | throw new HydratorException("Unable to convert data from PUT request ({$path}) to an instance of {$this->entityName}. Maybe you have a custom hydrator returning null?"); |
||
280 | } |
||
281 | |||
282 | 1 | return $out; |
|
283 | } |
||
284 | |||
285 | public function persist( |
||
286 | object $model, |
||
287 | array $serializationContext = [], |
||
288 | array $queryParams = [] |
||
289 | ): object { |
||
290 | 1 | $mapping = $this->sdk->getMapping(); |
|
291 | 1 | $prefix = $mapping->getIdPrefix(); |
|
292 | 1 | $key = $mapping->getKeyFromModel($this->entityName); |
|
293 | |||
294 | 1 | $path = empty($prefix) ? '/' . $key : $prefix . '/' . $key; |
|
295 | |||
296 | 1 | $oldSerializedModel = $this->getClassMetadata()->getDefaultSerializedModel(); |
|
297 | 1 | $newSerializedModel = $this->sdk |
|
298 | 1 | ->getSerializer() |
|
299 | 1 | ->serialize($model, $this->entityName, $serializationContext); |
|
300 | |||
301 | 1 | $diff = $this->unitOfWork->getDirtyData( |
|
302 | 1 | $newSerializedModel, |
|
303 | $oldSerializedModel, |
||
304 | 1 | $this->getClassMetadata() |
|
305 | ); |
||
306 | |||
307 | 1 | $data = $this->restClient->post( |
|
308 | 1 | $this->addQueryParameter($path, $queryParams), |
|
309 | $diff |
||
310 | ); |
||
311 | 1 | $data = $this->assertNotObject($data, __METHOD__); |
|
312 | |||
313 | 1 | if (null === $data) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
314 | throw new RestException("No data found after sending a `POST` request to {$path}. Did the server returned a 4xx or 5xx status code?", $path); |
||
315 | } |
||
316 | |||
317 | 1 | $hydrator = $this->sdk->getModelHydrator(); |
|
318 | |||
319 | 1 | $out = $hydrator->hydrate($data, $this->entityName); |
|
320 | |||
321 | 1 | if (null === $out) { |
|
322 | throw new HydratorException("Unable to convert data from POST request ({$path}) to an instance of {$this->entityName}. Maybe you have a custom hydrator returning null?"); |
||
323 | } |
||
324 | |||
325 | 1 | return $out; |
|
326 | } |
||
327 | |||
328 | /** |
||
329 | * @return object|false |
||
330 | */ |
||
331 | protected function fetchFromCache(string $key) |
||
332 | { |
||
333 | 1 | $key = $this->normalizeCacheKey($key); |
|
334 | 1 | $cacheItemPool = $this->sdk->getCacheItemPool(); |
|
335 | 1 | if ($cacheItemPool) { |
|
336 | 1 | $cacheKey = $this->sdk->getCachePrefix() . $key; |
|
337 | 1 | if ($cacheItemPool->hasItem($cacheKey)) { |
|
338 | 1 | $cacheItem = $cacheItemPool->getItem($cacheKey); |
|
339 | 1 | $cacheData = $cacheItem->get(); |
|
340 | |||
341 | 1 | return $cacheData; |
|
342 | } |
||
343 | } |
||
344 | |||
345 | 1 | return false; |
|
346 | } |
||
347 | |||
348 | protected function saveToCache(string $key, ?object $value): void |
||
349 | { |
||
350 | 1 | $key = $this->normalizeCacheKey($key); |
|
351 | 1 | $cacheItemPool = $this->sdk->getCacheItemPool(); |
|
352 | 1 | if ($cacheItemPool) { |
|
353 | 1 | $cacheKey = $this->sdk->getCachePrefix() . $key; |
|
354 | |||
355 | 1 | if (!$cacheItemPool->hasItem($cacheKey)) { |
|
356 | 1 | $cacheItem = $cacheItemPool->getItem($cacheKey); |
|
357 | 1 | $cacheItem->set($value); |
|
358 | 1 | $cacheItemPool->save($cacheItem); |
|
359 | } |
||
360 | } |
||
361 | 1 | } |
|
362 | |||
363 | /** |
||
364 | * remove from cache |
||
365 | * |
||
366 | * @return bool true if no cache or cache successfully cleared, false otherwise |
||
367 | */ |
||
368 | protected function removeFromCache(string $key): bool |
||
369 | { |
||
370 | 1 | $key = $this->normalizeCacheKey($key); |
|
371 | 1 | $cacheItemPool = $this->sdk->getCacheItemPool(); |
|
372 | 1 | if ($cacheItemPool) { |
|
373 | 1 | $cacheKey = $this->sdk->getCachePrefix() . $key; |
|
374 | |||
375 | 1 | if ($cacheItemPool->hasItem($cacheKey)) { |
|
376 | 1 | return $cacheItemPool->deleteItem($cacheKey); |
|
377 | } |
||
378 | } |
||
379 | |||
380 | 1 | return true; |
|
381 | } |
||
382 | |||
383 | protected function addQueryParameter( |
||
384 | string $path, |
||
385 | array $params = [] |
||
386 | ): string { |
||
387 | 1 | if (empty($params)) { |
|
388 | 1 | return $path; |
|
389 | } |
||
390 | |||
391 | 1 | return $path . '?' . http_build_query($params); |
|
392 | } |
||
393 | |||
394 | private function convertQueryParameters(array $queryParameters): array |
||
395 | { |
||
396 | 1 | $mapping = $this->sdk->getMapping(); |
|
397 | |||
398 | 1 | return array_map(function ($item) use ($mapping) { |
|
399 | 1 | if (is_object($item)) { |
|
400 | 1 | $classname = get_class($item); |
|
401 | |||
402 | 1 | if ($mapping->hasClassMetadata($classname)) { |
|
403 | $idGetter = $mapping |
||
404 | 1 | ->getClassMetadata($classname) |
|
405 | 1 | ->getIdGetter(); |
|
406 | |||
407 | 1 | return $item->{$idGetter}(); |
|
408 | } |
||
409 | } |
||
410 | |||
411 | 1 | return $item; |
|
412 | 1 | }, $queryParameters); |
|
413 | } |
||
414 | |||
415 | private function normalizeCacheKey(string $key): string |
||
416 | { |
||
417 | 1 | $out = preg_replace('~[\\/\{\}@:\(\)]~', '_', $key); |
|
418 | |||
419 | 1 | if (null === $out) { |
|
420 | throw new \RuntimeException('Unable to normalize cache key. This should not happen.'); |
||
421 | } |
||
422 | |||
423 | 1 | return $out; |
|
424 | } |
||
425 | |||
426 | private function getClassMetadata(): ClassMetadata |
||
427 | { |
||
428 | 1 | if (!isset($this->classMetadata)) { |
|
0 ignored issues
–
show
|
|||
429 | 1 | $this->classMetadataCache = $this->sdk |
|
430 | 1 | ->getMapping() |
|
431 | 1 | ->getClassMetadata($this->entityName); |
|
432 | } |
||
433 | |||
434 | 1 | return $this->classMetadataCache; |
|
435 | } |
||
436 | |||
437 | /** |
||
438 | * @param array|ResponseInterface|null $data |
||
439 | */ |
||
440 | private function assertArray($data, string $methodName): array |
||
441 | { |
||
442 | 1 | if (is_array($data)) { |
|
443 | 1 | return $data; |
|
444 | } |
||
445 | |||
446 | $type = null === $data ? 'null' : get_class($data); |
||
447 | |||
448 | throw new UnexpectedTypeException("Return of method {$methodName} should be an array. {$type} given."); |
||
449 | } |
||
450 | |||
451 | /** |
||
452 | * @param array|ResponseInterface|null $data |
||
453 | * |
||
454 | * @return array|null |
||
455 | */ |
||
456 | private function assertNotObject($data, string $methodName) |
||
457 | { |
||
458 | 1 | if (!is_object($data)) { |
|
459 | 1 | return $data; |
|
460 | } |
||
461 | |||
462 | $type = get_class($data); |
||
463 | |||
464 | throw new UnexpectedTypeException("Return of method {$methodName} should be an array. {$type} given."); |
||
465 | } |
||
466 | } |
||
467 |