Completed
Push — master ( cc21f4...3eeaf5 )
by Julien
06:41 queued 03:06
created

EntityRepository::normalizeCacheKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

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