EntityRepository::find()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

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