Passed
Pull Request — master (#8)
by Alex
03:52
created

AbstractEntityRepository::getSingleResultOrNull()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 19
ccs 0
cts 12
cp 0
rs 9.8666
cc 2
nc 2
nop 2
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\DoctrineEntityRepository;
6
7
use Arp\DoctrineEntityRepository\Constant\HydrateMode;
8
use Arp\DoctrineEntityRepository\Constant\QueryServiceOption;
9
use Arp\DoctrineEntityRepository\Exception\EntityNotFoundException;
10
use Arp\DoctrineEntityRepository\Exception\EntityRepositoryException;
11
use Arp\DoctrineEntityRepository\Persistence\Exception\PersistenceException;
12
use Arp\DoctrineEntityRepository\Persistence\PersistServiceInterface;
13
use Arp\DoctrineEntityRepository\Query\Exception\QueryServiceException;
14
use Arp\DoctrineEntityRepository\Query\QueryServiceInterface;
15
use Arp\Entity\EntityInterface;
16
use Doctrine\ORM\AbstractQuery;
17
use Doctrine\ORM\QueryBuilder;
18
use Psr\Log\LoggerInterface;
19
20
/**
21
 * @author  Alex Patterson <[email protected]>
22
 * @package Arp\DoctrineEntityRepository
23
 */
24
abstract class AbstractEntityRepository implements EntityRepositoryInterface
25
{
26
    /**
27
     * @var string
28
     */
29
    protected string $entityName;
30
31
    /**
32
     * @var QueryServiceInterface
33
     */
34
    protected QueryServiceInterface $queryService;
35
36
    /**
37
     * @var PersistServiceInterface
38
     */
39
    protected PersistServiceInterface $persistService;
40
41
    /**
42
     * @var LoggerInterface
43
     */
44
    protected LoggerInterface $logger;
45
46
    /**
47
     * @param string                  $entityName
48
     * @param QueryServiceInterface   $queryService
49
     * @param PersistServiceInterface $persistService
50
     * @param LoggerInterface         $logger
51
     */
52 34
    public function __construct(
53
        string $entityName,
54
        QueryServiceInterface $queryService,
55
        PersistServiceInterface $persistService,
56
        LoggerInterface $logger
57
    ) {
58 34
        $this->entityName = $entityName;
59 34
        $this->queryService = $queryService;
60 34
        $this->persistService = $persistService;
61 34
        $this->logger = $logger;
62 34
    }
63
64
    /**
65
     * Return the fully qualified class name of the mapped entity instance.
66
     *
67
     * @return string
68
     */
69 1
    public function getClassName(): string
70
    {
71 1
        return $this->entityName;
72
    }
73
74
    /**
75
     * Return a single entity instance matching the provided $id.
76
     *
77
     * @param string|int $id
78
     *
79
     * @return EntityInterface|null
80
     *
81
     * @throws EntityRepositoryException
82
     */
83 5
    public function find($id): ?EntityInterface
84
    {
85
        try {
86 5
            return $this->queryService->findOneById($id);
87 1
        } catch (QueryServiceException $e) {
88 1
            $errorMessage = sprintf('Unable to find entity of type \'%s\': %s', $this->entityName, $e->getMessage());
89
90 1
            $this->logger->error($errorMessage, ['exception' => $e, 'id' => $id]);
91
92 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
93
        }
94
    }
95
96
    /**
97
     * Return a single entity instance matching the provided $criteria.
98
     *
99
     * @param array<mixed> $criteria The entity filter criteria.
100
     *
101
     * @return EntityInterface|null
102
     *
103
     * @throws EntityRepositoryException
104
     */
105 3
    public function findOneBy(array $criteria): ?EntityInterface
106
    {
107
        try {
108 3
            return $this->queryService->findOne($criteria);
109 1
        } catch (QueryServiceException $e) {
110 1
            $errorMessage = sprintf('Unable to find entity of type \'%s\': %s', $this->entityName, $e->getMessage());
111
112 1
            $this->logger->error($errorMessage, ['exception' => $e, 'criteria' => $criteria]);
113
114 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
115
        }
116
    }
117
118
    /**
119
     * Return all of the entities within the collection.
120
     *
121
     * @return EntityInterface[]|iterable
122
     *
123
     * @throws EntityRepositoryException
124
     */
125 1
    public function findAll(): iterable
126
    {
127 1
        return $this->findBy([]);
128
    }
129
130
    /**
131
     * Return a collection of entities that match the provided $criteria.
132
     *
133
     * @param array<mixed>      $criteria
134
     * @param array<mixed>|null $orderBy
135
     * @param int|null          $limit
136
     * @param int|null          $offset
137
     *
138
     * @return EntityInterface[]|iterable
139
     *
140
     * @throws EntityRepositoryException
141
     */
142 7
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): iterable
143
    {
144 7
        $options = [];
145
146
        try {
147 7
            if (null !== $orderBy) {
148 2
                $options[QueryServiceOption::ORDER_BY] = $orderBy;
149
            }
150
151 7
            if (null !== $limit) {
152 2
                $options[QueryServiceOption::LIMIT] = $limit;
153
            }
154
155 7
            if (null !== $offset) {
156 2
                $options[QueryServiceOption::OFFSET] = $offset;
157
            }
158
159 7
            return $this->queryService->findMany($criteria, $options);
160 1
        } catch (QueryServiceException $e) {
161 1
            $errorMessage = sprintf(
162 1
                'Unable to return a collection of type \'%s\': %s',
163 1
                $this->entityName,
164 1
                $e->getMessage()
165
            );
166
167 1
            $this->logger->error(
168 1
                $errorMessage,
169 1
                ['exception' => $e, 'criteria' => $criteria, 'options' => $options]
170
            );
171
172 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
173
        }
174
    }
175
176
    /**
177
     * Save a single entity instance
178
     *
179
     * @param EntityInterface $entity
180
     * @param array<mixed>    $options
181
     *
182
     * @return EntityInterface
183
     *
184
     * @throws EntityRepositoryException
185
     */
186 2
    public function save(EntityInterface $entity, array $options = []): EntityInterface
187
    {
188
        try {
189 2
            return $this->persistService->save($entity, $options);
190 1
        } catch (PersistenceException $e) {
191 1
            $errorMessage = sprintf('Unable to save entity of type \'%s\': %s', $this->entityName, $e->getMessage());
192
193 1
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
194
195 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
196
        }
197
    }
198
199
    /**
200
     * Save a collection of entities in a single transaction
201
     *
202
     * @param iterable<EntityInterface> $collection The collection of entities that should be saved.
203
     * @param array<mixed>              $options    the optional save options.
204
     *
205
     * @return iterable<EntityInterface>
206
     *
207
     * @throws EntityRepositoryException If the save cannot be completed
208
     */
209 2
    public function saveCollection(iterable $collection, array $options = []): iterable
210
    {
211
        try {
212 2
            return $this->persistService->saveCollection($collection, $options);
213 1
        } catch (PersistenceException $e) {
214 1
            $errorMessage = sprintf('Unable to save entity of type \'%s\': %s', $this->entityName, $e->getMessage());
215
216 1
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
217
218 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
219
        }
220
    }
221
222
    /**
223
     * @param EntityInterface|string|int|mixed $entity
224
     * @param array<mixed>                     $options
225
     *
226
     * @return bool
227
     *
228
     * @throws EntityRepositoryException
229
     */
230 7
    public function delete($entity, array $options = []): bool
231
    {
232 7
        if (is_string($entity) || is_int($entity)) {
233 2
            $id = $entity;
234 2
            $entity = $this->find($id);
235
236 2
            if (null === $entity) {
237 1
                $errorMessage = sprintf(
238 1
                    'Unable to delete entity \'%s::%s\': The entity could not be found',
239 1
                    $this->entityName,
240
                    $id
241
                );
242
243 1
                $this->logger->error($errorMessage);
244
245 2
                throw new EntityNotFoundException($errorMessage);
246
            }
247 5
        } elseif (!$entity instanceof EntityInterface) {
248 3
            $errorMessage = sprintf(
249
                'The \'entity\' argument must be a \'string\' or an object of type \'%s\'; '
250 3
                . '\'%s\' provided in \'%s::%s\'',
251 3
                EntityInterface::class,
252 3
                (is_object($entity) ? get_class($entity) : gettype($entity)),
253 3
                static::class,
254 3
                __FUNCTION__
255
            );
256
257 3
            $this->logger->error($errorMessage);
258
259 3
            throw new EntityRepositoryException($errorMessage);
260
        }
261
262
263
        try {
264 3
            return $this->persistService->delete($entity, $options);
265 1
        } catch (\Exception $e) {
266 1
            $errorMessage = sprintf(
267 1
                'Unable to delete entity of type \'%s\': %s',
268 1
                $this->entityName,
269 1
                $e->getMessage()
270
            );
271
272 1
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
273
274 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
275
        }
276
    }
277
278
    /**
279
     * Perform a deletion of a collection of entities
280
     *
281
     * @param iterable<EntityInterface> $collection
282
     * @param array<mixed>              $options
283
     *
284
     * @return int
285
     *
286
     * @throws EntityRepositoryException
287
     */
288 2
    public function deleteCollection(iterable $collection, array $options = []): int
289
    {
290
        try {
291 2
            return $this->persistService->deleteCollection($collection, $options);
292 1
        } catch (PersistenceException $e) {
293 1
            $errorMessage = sprintf(
294 1
                'Unable to delete entity collection of type \'%s\': %s',
295 1
                $this->entityName,
296 1
                $e->getMessage()
297
            );
298
299 1
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
300
301 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
302
        }
303
    }
304
305
    /**
306
     * @throws EntityRepositoryException
307
     */
308 2
    public function clear(): void
309
    {
310
        try {
311 2
            $this->persistService->clear();
312 1
        } catch (PersistenceException $e) {
313 1
            $errorMessage = sprintf(
314 1
                'Unable to clear entity of type \'%s\': %s',
315 1
                $this->entityName,
316 1
                $e->getMessage()
317
            );
318
319 1
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
320
321 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
322
        }
323 1
    }
324
325
    /**
326
     * @param EntityInterface $entity
327
     *
328
     * @throws EntityRepositoryException
329
     */
330 2
    public function refresh(EntityInterface $entity): void
331
    {
332
        try {
333 2
            $this->persistService->refresh($entity);
334 1
        } catch (PersistenceException $e) {
335 1
            $errorMessage = sprintf(
336 1
                'Unable to refresh entity of type \'%s\': %s',
337 1
                $this->entityName,
338 1
                $e->getMessage()
339
            );
340
341 1
            $this->logger->error(
342 1
                $errorMessage,
343 1
                ['exception' => $e, 'entity_name' => $this->entityName, 'id' => $entity->getId()]
344
            );
345
346 1
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
347
        }
348 1
    }
349
350
    /**
351
     * Execute query builder or query instance and return the results.
352
     *
353
     * @param object       $query
354
     * @param array<mixed> $options
355
     *
356
     * @return EntityInterface[]|iterable
357
     *
358
     * @throws EntityRepositoryException
359
     */
360
    protected function executeQuery(object $query, array $options = [])
361
    {
362
        $query = $this->resolveQuery($query);
363
364
        try {
365
            return $this->queryService->execute($query, $options);
366
        } catch (QueryServiceException $e) {
367
            $errorMessage = sprintf(
368
                'Failed to perform query for entity type \'%s\': %s',
369
                $this->entityName,
370
                $e->getMessage()
371
            );
372
373
            $this->logger->error(
374
                $errorMessage,
375
                ['exception' => $e, 'entity_name' => $this->entityName, 'sql' => $query->getSQL()]
376
            );
377
378
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
379
        }
380
    }
381
382
    /**
383
     * Return a single entity instance. NULL will be returned if the result set contains 0 or more than 1 result.
384
     *
385
     * Optionally control the object hydration with QueryServiceOption::HYDRATE_MODE.
386
     *
387
     * @param object       $query
388
     * @param array<mixed> $options
389
     *
390
     * @return array<mixed>|EntityInterface|null
391
     *
392
     * @throws EntityRepositoryException
393
     */
394
    protected function getSingleResultOrNull(object $query, array $options = [])
395
    {
396
        $query = $this->resolveQuery($query);
397
398
        try {
399
            return $this->queryService->getSingleResultOrNull($query, $options);
400
        } catch (QueryServiceException $e) {
401
            $errorMessage = sprintf(
402
                'Failed to perform query for entity type \'%s\': %s',
403
                $this->entityName,
404
                $e->getMessage()
405
            );
406
407
            $this->logger->error(
408
                $errorMessage,
409
                ['exception' => $e, 'entity_name' => $this->entityName, 'sql' => $query->getSQL()]
410
            );
411
412
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
413
        }
414
    }
415
416
    /**
417
     * Return a result set containing a single array result. NULL will be returned if the result set
418
     * contains 0 or more than 1 result.
419
     *
420
     * @param object       $query
421
     * @param array<mixed> $options
422
     *
423
     * @return array<mixed>|null
424
     *
425
     * @throws EntityRepositoryException
426
     */
427
    protected function getSingleArrayResultOrNull(object $query, array $options = []): ?array
428
    {
429
        $options = array_replace_recursive(
430
            $options,
431
            [
432
                QueryServiceOption::HYDRATION_MODE => HydrateMode::ARRAY,
433
            ]
434
        );
435
436
        $result = $this->getSingleResultOrNull($query, $options);
437
438
        return is_array($result) ? $result : null;
439
    }
440
441
    /**
442
     * Resolve the Doctrine query object from a possible QueryBuilder instance.
443
     *
444
     * @param object $query
445
     *
446
     * @return AbstractQuery
447
     *
448
     * @throws EntityRepositoryException
449
     */
450
    private function resolveQuery(object $query): AbstractQuery
451
    {
452
        if ($query instanceof QueryBuilder) {
453
            $query = $query->getQuery();
454
        }
455
456
        if (!$query instanceof AbstractQuery) {
457
            throw new EntityRepositoryException(
458
                sprintf(
459
                    'The \'query\' argument must be an object of type \'%s\' or \'%s\'; \'%s\' provided in \'%s\'',
460
                    QueryBuilder::class,
461
                    AbstractQuery::class,
462
                    get_class($query),
463
                    __METHOD__
464
                )
465
            );
466
        }
467
468
        return $query;
469
    }
470
}
471