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

EntityRepository::__call()   C

Complexity

Conditions 11
Paths 43

Size

Total Lines 87
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 11.1367

Importance

Changes 5
Bugs 1 Features 1
Metric Value
cc 11
eloc 57
c 5
b 1
f 1
nc 43
nop 2
dl 0
loc 87
ccs 43
cts 48
cp 0.8958
crap 11.1367
rs 6.7915

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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