Passed
Push — master ( 01c95e...1b44dd )
by Julien
01:08
created

EntityRepository   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Test Coverage

Coverage 88.29%

Importance

Changes 0
Metric Value
eloc 218
dl 0
loc 476
ccs 181
cts 205
cp 0.8829
rs 7.92
c 0
b 0
f 0
wmc 51

16 Methods

Rating   Name   Duplication   Size   Complexity  
A remove() 0 7 1
A removeFromCache() 0 13 3
A find() 0 25 3
A persist() 0 49 4
A assertNotObject() 0 10 2
A assertArray() 0 10 3
C __call() 0 87 11
A findAll() 0 39 5
A saveToCache() 0 11 3
A fetchFromCache() 0 15 3
A getClassMetadata() 0 9 2
A update() 0 45 3
A addQueryParameter() 0 9 2
A normalizeCacheKey() 0 11 2
A convertQueryParameters() 0 19 3
A __construct() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like EntityRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityRepository, and based on these observations, apply Extract Interface, too.

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 1
                        $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 1
            $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 1
                $serializationContext
270
            );
271 1
            $newSerializedModel = $this->unitOfWork->getDirtyData(
272 1
                $newSerializedModel,
273 1
                $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->
291
                    entityName}. Maybe you have a custom hydrator returning null?"
292
            );
293
        }
294
295 1
        return $out;
296
    }
297
298
    public function persist(
299
        object $model,
300
        array $serializationContext = [],
301
        array $queryParams = []
302
    ): object {
303 1
        $mapping = $this->sdk->getMapping();
304 1
        $prefix = $mapping->getIdPrefix();
305 1
        $key = $mapping->getKeyFromModel($this->entityName);
306
307 1
        $path = empty($prefix) ? '/' . $key : $prefix . '/' . $key;
308
309 1
        $oldSerializedModel = $this->getClassMetadata()->getDefaultSerializedModel();
310 1
        $newSerializedModel = $this->sdk->getSerializer()->serialize(
311 1
            $model,
312 1
            $this->entityName,
313 1
            $serializationContext
314
        );
315
316 1
        $diff = $this->unitOfWork->getDirtyData(
317 1
            $newSerializedModel,
318 1
            $oldSerializedModel,
319 1
            $this->getClassMetadata()
320
        );
321
322 1
        $data = $this->restClient->post(
323 1
            $this->addQueryParameter($path, $queryParams),
324 1
            $diff
325
        );
326 1
        $data = $this->assertNotObject($data, __METHOD__);
327
328 1
        if (null === $data) {
0 ignored issues
show
introduced by
The condition null === $data is always false.
Loading history...
329
            throw new RestException(
330
                "No data found after sending a `POST` request to {$path}. Did the server returned a 4xx or 5xx status code?",
331
                $path
332
            );
333
        }
334
335 1
        $hydrator = $this->sdk->getModelHydrator();
336
337 1
        $out = $hydrator->hydrate($data, $this->entityName);
338
339 1
        if (null === $out) {
340
            throw new HydratorException(
341
                "Unable to convert data from POST request ({$path}) to an instance of {$this->
342
                    entityName}. Maybe you have a custom hydrator returning null?"
343
            );
344
        }
345
346 1
        return $out;
347
    }
348
349
    /**
350
     * @return object|false
351
     */
352
    protected function fetchFromCache(string $key)
353
    {
354 1
        $key = $this->normalizeCacheKey($key);
355 1
        $cacheItemPool = $this->sdk->getCacheItemPool();
356 1
        if ($cacheItemPool) {
357 1
            $cacheKey = $this->sdk->getCachePrefix() . $key;
358 1
            if ($cacheItemPool->hasItem($cacheKey)) {
359 1
                $cacheItem = $cacheItemPool->getItem($cacheKey);
360 1
                $cacheData = $cacheItem->get();
361
362 1
                return $cacheData;
363
            }
364
        }
365
366 1
        return false;
367
    }
368
369
    protected function saveToCache(string $key, ?object $value): void
370
    {
371 1
        $key = $this->normalizeCacheKey($key);
372 1
        $cacheItemPool = $this->sdk->getCacheItemPool();
373 1
        if ($cacheItemPool) {
374 1
            $cacheKey = $this->sdk->getCachePrefix() . $key;
375
376 1
            if (!$cacheItemPool->hasItem($cacheKey)) {
377 1
                $cacheItem = $cacheItemPool->getItem($cacheKey);
378 1
                $cacheItem->set($value);
379 1
                $cacheItemPool->save($cacheItem);
380
            }
381
        }
382 1
    }
383
384
    /**
385
     * remove from cache
386
     *
387
     * @return bool true if no cache or cache successfully cleared, false otherwise
388
     */
389
    protected function removeFromCache(string $key): bool
390
    {
391 1
        $key = $this->normalizeCacheKey($key);
392 1
        $cacheItemPool = $this->sdk->getCacheItemPool();
393 1
        if ($cacheItemPool) {
394 1
            $cacheKey = $this->sdk->getCachePrefix() . $key;
395
396 1
            if ($cacheItemPool->hasItem($cacheKey)) {
397 1
                return $cacheItemPool->deleteItem($cacheKey);
398
            }
399
        }
400
401 1
        return true;
402
    }
403
404
    protected function addQueryParameter(
405
        string $path,
406
        array $params = []
407
    ): string {
408 1
        if (empty($params)) {
409 1
            return $path;
410
        }
411
412 1
        return $path . '?' . http_build_query($params);
413
    }
414
415
    private function convertQueryParameters(array $queryParameters): array
416
    {
417 1
        $mapping = $this->sdk->getMapping();
418
419 1
        return array_map(function ($item) use ($mapping) {
420 1
            if (is_object($item)) {
421 1
                $classname = get_class($item);
422
423 1
                if ($mapping->hasClassMetadata($classname)) {
424 1
                    $idGetter = $mapping->getClassMetadata(
425 1
                        $classname
426 1
                    )->getIdGetter();
427
428 1
                    return $item->{$idGetter}();
429
                }
430
            }
431
432 1
            return $item;
433 1
        }, $queryParameters);
434
    }
435
436
    private function normalizeCacheKey(string $key): string
437
    {
438 1
        $out = preg_replace('~[\\/\{\}@:\(\)]~', '_', $key);
439
440 1
        if (null === $out) {
441
            throw new \RuntimeException(
442
                'Unable to normalize cache key. This should not happen.'
443
            );
444
        }
445
446 1
        return $out;
447
    }
448
449
    private function getClassMetadata(): ClassMetadata
450
    {
451 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...
452 1
            $this->classMetadataCache = $this->sdk->getMapping()->getClassMetadata(
453 1
                $this->entityName
454
            );
455
        }
456
457 1
        return $this->classMetadataCache;
458
    }
459
460
    /**
461
     * @var array|ResponseInterface|null
462
     */
463
    private function assertArray($data, string $methodName): array
464
    {
465 1
        if (is_array($data)) {
466 1
            return $data;
467
        }
468
469
        $type = null === $data ? 'null' : get_class($data);
470
471
        throw new UnexpectedTypeException(
472
            "Return of method {$methodName} should be an array. {$type} given."
473
        );
474
    }
475
476
    /**
477
     * @var array|ResponseInterface|null
478
     *
479
     * @return array|null
480
     */
481
    private function assertNotObject($data, string $methodName)
482
    {
483 1
        if (!is_object($data)) {
484 1
            return $data;
485
        }
486
487
        $type = get_class($data);
488
489
        throw new UnexpectedTypeException(
490
            "Return of method {$methodName} should be an array. {$type} given."
491
        );
492
    }
493
}
494