Passed
Pull Request — master (#8)
by Alex
08:00
created

AbstractEntityRepository::resolveQuery()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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