Passed
Pull Request — master (#8)
by Alex
07:09
created

AbstractEntityRepository::resolveQuery()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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