Completed
Pull Request — master (#88)
by Julien
02:57
created

EntityRepository::findAll()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5.025

Importance

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