Completed
Pull Request — master (#93)
by Julien
05:06 queued 02:16
created

EntityRepository::update()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 3.0059

Importance

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