mapado /
rest-client-sdk
| 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
Loading history...
|
|||
| 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 |