Passed
Pull Request — master (#4)
by Alex
02:54
created

EntityRepository::getClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\LaminasDoctrine\Repository;
6
7
use Arp\Entity\EntityInterface;
8
use Arp\LaminasDoctrine\Repository\Exception\EntityNotFoundException;
9
use Arp\LaminasDoctrine\Repository\Exception\EntityRepositoryException;
10
use Arp\LaminasDoctrine\Repository\Persistence\Exception\PersistenceException;
11
use Arp\LaminasDoctrine\Repository\Persistence\PersistServiceInterface;
12
use Arp\LaminasDoctrine\Repository\Persistence\TransactionServiceInterface;
13
use Arp\LaminasDoctrine\Repository\Query\Exception\QueryServiceException;
14
use Arp\LaminasDoctrine\Repository\Query\QueryServiceInterface;
15
use Arp\LaminasDoctrine\Repository\Query\QueryServiceOption;
16
use Doctrine\ORM\AbstractQuery;
17
use Doctrine\ORM\QueryBuilder;
18
use Psr\Log\LoggerInterface;
19
20
/**
21
 * @template TEntity as EntityInterface
22
 * @implements EntityRepositoryInterface<EntityInterface>
23
 */
24
class EntityRepository implements EntityRepositoryInterface, TransactionServiceInterface
25
{
26
    /**
27
     * @var class-string<EntityInterface>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<EntityInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<EntityInterface>.
Loading history...
28
     */
29
    protected string $entityName;
30
31
    /**
32
     * @var QueryServiceInterface<EntityInterface>
33
     */
34
    protected QueryServiceInterface $queryService;
35
36
    protected PersistServiceInterface $persistService;
37
38
    protected LoggerInterface $logger;
39
40
    /**
41
     * @param class-string<EntityInterface>          $entityName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<EntityInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<EntityInterface>.
Loading history...
42
     * @param QueryServiceInterface<EntityInterface> $queryService
43
     * @param PersistServiceInterface                $persistService
44
     * @param LoggerInterface                        $logger
45
     */
46
    public function __construct(
47
        string $entityName,
48
        QueryServiceInterface $queryService,
49
        PersistServiceInterface $persistService,
50
        LoggerInterface $logger
51
    ) {
52
        $this->entityName = $entityName;
53
        $this->queryService = $queryService;
54
        $this->persistService = $persistService;
55
        $this->logger = $logger;
56
    }
57
58
    /**
59
     * Return the fully qualified class name of the mapped entity instance.
60
     *
61
     * @return class-string<EntityInterface>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<EntityInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<EntityInterface>.
Loading history...
62
     */
63
    public function getClassName(): string
64
    {
65
        return $this->entityName;
66
    }
67
68
    /**
69
     * Return a single entity instance matching the provided $id.
70
     *
71
     * @param string|int $id
72
     *
73
     * @return EntityInterface|null
74
     *
75
     * @throws EntityRepositoryException
76
     */
77
    public function find($id): ?EntityInterface
78
    {
79
        try {
80
            return $this->queryService->findOneById($id);
81
        } catch (QueryServiceException $e) {
82
            $errorMessage = sprintf('Unable to find entity of type \'%s\'', $this->entityName);
83
84
            $this->logger->error($errorMessage, ['exception' => $e, 'id' => $id]);
85
86
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
87
        }
88
    }
89
90
    /**
91
     * @param string|int $id
92
     *
93
     * @return EntityInterface|null
94
     *
95
     * @throws EntityRepositoryException
96
     */
97
    public function findOneById($id): ?EntityInterface
98
    {
99
        return $this->find($id);
100
    }
101
102
    /**
103
     * Return a single entity instance matching the provided $criteria.
104
     *
105
     * @param array<mixed> $criteria The entity filter criteria.
106
     *
107
     * @return EntityInterface|null
108
     *
109
     * @throws EntityRepositoryException
110
     */
111
    public function findOneBy(array $criteria): ?EntityInterface
112
    {
113
        try {
114
            return $this->queryService->findOne($criteria);
115
        } catch (QueryServiceException $e) {
116
            $errorMessage = sprintf('Unable to find entity of type \'%s\'', $this->entityName);
117
118
            $this->logger->error($errorMessage, ['exception' => $e, 'criteria' => $criteria]);
119
120
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
121
        }
122
    }
123
124
    /**
125
     * Return all the entities within the collection.
126
     *
127
     * @return iterable<int, EntityInterface>
128
     *
129
     * @throws EntityRepositoryException
130
     */
131
    public function findAll(): iterable
132
    {
133
        return $this->findBy([]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->findBy(array()) returns the type iterable which is incompatible with the return type mandated by Doctrine\Persistence\ObjectRepository::findAll() of array<integer,object>.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
134
    }
135
136
    /**
137
     * Return a collection of entities that match the provided $criteria.
138
     *
139
     * @param array<mixed>      $criteria
140
     * @param array<mixed>|null $orderBy
141
     * @param int|null          $limit
142
     * @param int|null          $offset
143
     *
144
     * @return EntityInterface[]|iterable
145
     *
146
     * @throws EntityRepositoryException
147
     */
148
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): iterable
149
    {
150
        $options = [];
151
152
        try {
153
            if (null !== $orderBy) {
154
                $options[QueryServiceOption::ORDER_BY] = $orderBy;
155
            }
156
157
            if (null !== $limit) {
158
                $options[QueryServiceOption::MAX_RESULTS] = $limit;
159
            }
160
161
            if (null !== $offset) {
162
                $options[QueryServiceOption::FIRST_RESULT] = $offset;
163
            }
164
165
            return $this->queryService->findMany($criteria, $options);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->queryServi...ny($criteria, $options) returns the type iterable which is incompatible with the return type mandated by Doctrine\Persistence\ObjectRepository::findBy() of array<mixed,object>.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
166
        } catch (QueryServiceException $e) {
167
            $errorMessage = sprintf('Unable to return a collection of type \'%s\'', $this->entityName);
168
169
            $this->logger->error(
170
                $errorMessage,
171
                ['exception' => $e, 'criteria' => $criteria, 'options' => $options]
172
            );
173
174
            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<mixed>    $options
183
     *
184
     * @return EntityInterface
185
     *
186
     * @throws EntityRepositoryException
187
     */
188
    public function save(EntityInterface $entity, array $options = []): EntityInterface
189
    {
190
        try {
191
            return $this->persistService->save($entity, $options);
192
        } catch (PersistenceException $e) {
193
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
194
195
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
196
197
            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<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
        try {
214
            return $this->persistService->saveCollection($collection, $options);
215
        } catch (PersistenceException $e) {
216
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
217
218
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
219
220
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
221
        }
222
    }
223
224
    /**
225
     * @param EntityInterface|string|int|mixed $entity
226
     * @param array<mixed>                     $options
227
     *
228
     * @return bool
229
     *
230
     * @throws EntityRepositoryException
231
     */
232
    public function delete($entity, array $options = []): bool
233
    {
234
        if (is_string($entity) || is_int($entity)) {
235
            $id = $entity;
236
            $entity = $this->find($id);
237
238
            if (null === $entity) {
239
                $errorMessage = sprintf(
240
                    'Unable to delete entity \'%s::%s\': The entity could not be found',
241
                    $this->entityName,
242
                    $id
243
                );
244
245
                $this->logger->error($errorMessage);
246
247
                throw new EntityNotFoundException($errorMessage);
248
            }
249
        } elseif (!$entity instanceof EntityInterface) {
250
            $errorMessage = sprintf(
251
                'The \'entity\' argument must be a \'string\' or an object of type \'%s\'; '
252
                . '\'%s\' provided in \'%s::%s\'',
253
                EntityInterface::class,
254
                (is_object($entity) ? get_class($entity) : gettype($entity)),
255
                static::class,
256
                __FUNCTION__
257
            );
258
259
            $this->logger->error($errorMessage);
260
261
            throw new EntityRepositoryException($errorMessage);
262
        }
263
264
265
        try {
266
            return $this->persistService->delete($entity, $options);
267
        } catch (\Exception $e) {
268
            $errorMessage = sprintf(
269
                'Unable to delete entity of type \'%s\'',
270
                $this->entityName
271
            );
272
273
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
274
275
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
276
        }
277
    }
278
279
    /**
280
     * Perform a deletion of a collection of entities
281
     *
282
     * @param iterable<int, EntityInterface> $collection
283
     * @param array<mixed>                   $options
284
     *
285
     * @return int
286
     *
287
     * @throws EntityRepositoryException
288
     */
289
    public function deleteCollection(iterable $collection, array $options = []): int
290
    {
291
        try {
292
            return $this->persistService->deleteCollection($collection, $options);
293
        } catch (PersistenceException $e) {
294
            $errorMessage = sprintf('Unable to delete entity collection of type \'%s\'', $this->entityName);
295
296
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
297
298
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
299
        }
300
    }
301
302
    /**
303
     * @throws EntityRepositoryException
304
     */
305
    public function clear(): void
306
    {
307
        try {
308
            $this->persistService->clear();
309
        } catch (PersistenceException $e) {
310
            $errorMessage = sprintf('Unable to clear entity of type \'%s\'', $this->entityName);
311
312
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
313
314
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
315
        }
316
    }
317
318
    /**
319
     * @param EntityInterface $entity
320
     *
321
     * @throws EntityRepositoryException
322
     */
323
    public function refresh(EntityInterface $entity): void
324
    {
325
        try {
326
            $this->persistService->refresh($entity);
327
        } catch (PersistenceException $e) {
328
            $errorMessage = sprintf('Unable to refresh entity of type \'%s\'', $this->entityName);
329
330
            $this->logger->error(
331
                $errorMessage,
332
                ['exception' => $e, 'entity_name' => $this->entityName, 'id' => $entity->getId()]
333
            );
334
335
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
336
        }
337
    }
338
339
    /**
340
     * @throws EntityRepositoryException
341
     */
342
    public function beginTransaction(): void
343
    {
344
        try {
345
            $this->persistService->beginTransaction();
346
        } catch (\Exception $e) {
347
            throw new EntityRepositoryException(
348
                sprintf('Failed to start transaction for entity \'%s\'', $this->entityName),
349
                $e->getCode(),
350
                $e
351
            );
352
        }
353
    }
354
355
    /**
356
     * @throws EntityRepositoryException
357
     */
358
    public function commitTransaction(): void
359
    {
360
        try {
361
            $this->persistService->commitTransaction();
362
        } catch (\Exception $e) {
363
            throw new EntityRepositoryException(
364
                sprintf('Failed to commit transaction for entity \'%s\'', $this->entityName),
365
                $e->getCode(),
366
                $e
367
            );
368
        }
369
    }
370
371
    public function rollbackTransaction(): void
372
    {
373
        $this->persistService->rollbackTransaction();
374
    }
375
376
    /**
377
     * Execute query builder or query instance and return the results.
378
     *
379
     * @param object|QueryBuilder|AbstractQuery $query
380
     * @param array<mixed>                      $options
381
     *
382
     * @return iterable<int, EntityInterface|array>|mixed
383
     *
384
     * @throws EntityRepositoryException
385
     */
386
    protected function executeQuery(object $query, array $options = [])
387
    {
388
        try {
389
            return $this->queryService->execute($query, $options);
390
        } catch (QueryServiceException $e) {
391
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
392
393
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
394
395
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
396
        }
397
    }
398
399
    /**
400
     * Return a single entity instance. NULL will be returned if the result set contains 0 or more than 1 result.
401
     *
402
     * Optionally control the object hydration with QueryServiceOption::HYDRATE_MODE.
403
     *
404
     * @param object|AbstractQuery|QueryBuilder $query
405
     * @param array<string, mixed>              $options
406
     *
407
     * @return array<mixed>|EntityInterface|null
408
     *
409
     * @throws EntityRepositoryException
410
     */
411
    protected function getSingleResultOrNull(object $query, array $options = [])
412
    {
413
        try {
414
            return $this->queryService->getSingleResultOrNull($query, $options);
415
        } catch (QueryServiceException $e) {
416
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
417
418
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
419
420
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
421
        }
422
    }
423
424
    /**
425
     * Return a result set containing a single array result. NULL will be returned if the result set
426
     * contains 0 or more than 1 result.
427
     *
428
     * @param object       $query
429
     * @param array<mixed> $options
430
     *
431
     * @return array<mixed>|null
432
     *
433
     * @throws EntityRepositoryException
434
     */
435
    protected function getSingleArrayResultOrNull(object $query, array $options = []): ?array
436
    {
437
        $result = $this->getSingleResultOrNull(
438
            $query,
439
            array_replace_recursive(
440
                $options,
441
                [QueryServiceOption::HYDRATION_MODE => AbstractQuery::HYDRATE_ARRAY]
442
            )
443
        );
444
445
        return is_array($result) ? $result : null;
446
    }
447
448
    /**
449
     * @param object|AbstractQuery|QueryBuilder $query
450
     * @param array<string, mixed>              $options
451
     *
452
     * @return int|string|float|bool|null
453
     *
454
     * @throws EntityRepositoryException
455
     */
456
    protected function getSingleScalarResult(object $query, array $options = [])
457
    {
458
        try {
459
            return $this->queryService->getSingleScalarResult($query, $options);
460
        } catch (QueryServiceException $e) {
461
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
462
463
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
464
465
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
466
        }
467
    }
468
}
469