Passed
Pull Request — master (#5)
by Alex
12:40
created

EntityRepository   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 114
c 2
b 0
f 0
dl 0
loc 359
rs 9.28
wmc 39

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getSingleResultOrNull() 0 10 2
A getSingleArrayResultOrNull() 0 18 3
A rollbackTransaction() 0 3 1
A saveCollection() 0 10 2
A clear() 0 10 2
A findOneBy() 0 10 2
A findOneById() 0 3 1
A refresh() 0 13 2
A findAll() 0 3 1
A beginTransaction() 0 9 2
A delete() 0 13 2
A save() 0 10 2
A deleteCollection() 0 10 2
A __construct() 0 6 1
A find() 0 10 2
A findBy() 0 27 5
A commitTransaction() 0 9 2
A executeQuery() 0 10 2
A getSingleScalarResult() 0 10 2
A getClassName() 0 3 1
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\EntityRepositoryException;
9
use Arp\LaminasDoctrine\Repository\Persistence\Exception\PersistenceException;
10
use Arp\LaminasDoctrine\Repository\Persistence\PersistServiceInterface;
11
use Arp\LaminasDoctrine\Repository\Persistence\TransactionServiceInterface;
12
use Arp\LaminasDoctrine\Repository\Query\Exception\QueryServiceException;
13
use Arp\LaminasDoctrine\Repository\Query\QueryServiceInterface;
14
use Arp\LaminasDoctrine\Repository\Query\QueryServiceOption;
15
use Doctrine\ORM\AbstractQuery;
16
use Doctrine\ORM\QueryBuilder;
17
use Psr\Log\LoggerInterface;
18
19
/**
20
 * @template Entity of EntityInterface
21
 * @implements EntityRepositoryInterface<EntityInterface>
22
 */
23
class EntityRepository implements EntityRepositoryInterface, TransactionServiceInterface
24
{
25
    /**
26
     * @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...
27
     * @param QueryServiceInterface<Entity> $queryService
28
     * @param PersistServiceInterface<Entity> $persistService
29
     * @param LoggerInterface $logger
30
     */
31
    public function __construct(
32
        private readonly string $entityName,
33
        private readonly QueryServiceInterface $queryService,
34
        private readonly PersistServiceInterface $persistService,
35
        private readonly LoggerInterface $logger
36
    ) {
37
    }
38
39
    /**
40
     * @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...
41
     */
42
    public function getClassName(): string
43
    {
44
        return $this->entityName;
45
    }
46
47
    /**
48
     * @return Entity|null
49
     *
50
     * @throws EntityRepositoryException
51
     */
52
    public function find($id): ?EntityInterface
53
    {
54
        try {
55
            return $this->queryService->findOneById($id);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->queryService->findOneById($id) also could return the type Arp\Entity\EntityInterface which is incompatible with the documented return type Arp\LaminasDoctrine\Repository\Entity|null.
Loading history...
56
        } catch (QueryServiceException $e) {
57
            $errorMessage = sprintf('Unable to find entity of type \'%s\'', $this->entityName);
58
59
            $this->logger->error($errorMessage, ['exception' => $e, 'id' => $id]);
60
61
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
62
        }
63
    }
64
65
    /**
66
     * @return Entity|null
67
     *
68
     * @throws EntityRepositoryException
69
     */
70
    public function findOneById(int $id): ?EntityInterface
71
    {
72
        return $this->find($id);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->find($id) also could return the type Arp\Entity\EntityInterface which is incompatible with the documented return type Arp\LaminasDoctrine\Repository\Entity|null.
Loading history...
73
    }
74
75
    /**
76
     * @param array<mixed> $criteria The entity filter criteria.
77
     *
78
     * @return Entity|null
79
     *
80
     * @throws EntityRepositoryException
81
     */
82
    public function findOneBy(array $criteria): ?EntityInterface
83
    {
84
        try {
85
            return $this->queryService->findOne($criteria);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->queryService->findOne($criteria) also could return the type Arp\Entity\EntityInterface which is incompatible with the documented return type Arp\LaminasDoctrine\Repository\Entity|null.
Loading history...
86
        } catch (QueryServiceException $e) {
87
            $errorMessage = sprintf('Unable to find entity of type \'%s\'', $this->entityName);
88
89
            $this->logger->error($errorMessage, ['exception' => $e, 'criteria' => $criteria]);
90
91
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
92
        }
93
    }
94
95
    /**
96
     * @return iterable<int, Entity>
97
     *
98
     * @throws EntityRepositoryException
99
     */
100
    public function findAll(): iterable
101
    {
102
        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...
103
    }
104
105
    /**
106
     * @param array<mixed> $criteria
107
     * @param array<mixed>|null $orderBy
108
     *
109
     * @return iterable<int, Entity>
110
     *
111
     * @throws EntityRepositoryException
112
     */
113
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): iterable
114
    {
115
        $options = [];
116
117
        try {
118
            if (null !== $orderBy) {
119
                $options[QueryServiceOption::ORDER_BY] = $orderBy;
120
            }
121
122
            if (null !== $limit) {
123
                $options[QueryServiceOption::MAX_RESULTS] = $limit;
124
            }
125
126
            if (null !== $offset) {
127
                $options[QueryServiceOption::FIRST_RESULT] = $offset;
128
            }
129
130
            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...
131
        } catch (QueryServiceException $e) {
132
            $errorMessage = sprintf('Unable to return a collection of type \'%s\'', $this->entityName);
133
134
            $this->logger->error(
135
                $errorMessage,
136
                ['exception' => $e, 'criteria' => $criteria, 'options' => $options]
137
            );
138
139
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
140
        }
141
    }
142
143
    /**
144
     * @param Entity $entity
145
     * @param array<mixed> $options
146
     *
147
     * @return Entity
148
     *
149
     * @throws EntityRepositoryException
150
     */
151
    public function save(EntityInterface $entity, array $options = []): EntityInterface
152
    {
153
        try {
154
            return $this->persistService->save($entity, $options);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->persistSer...save($entity, $options) returns the type Arp\Entity\EntityInterface which is incompatible with the documented return type Arp\LaminasDoctrine\Repository\Entity.
Loading history...
155
        } catch (PersistenceException $e) {
156
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
157
158
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
159
160
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
161
        }
162
    }
163
164
    /**
165
     * @param iterable<Entity> $collection
166
     * @param array<mixed> $options
167
     *
168
     * @return iterable<Entity>
169
     *
170
     * @throws EntityRepositoryException
171
     */
172
    public function saveCollection(iterable $collection, array $options = []): iterable
173
    {
174
        try {
175
            return $this->persistService->saveCollection($collection, $options);
176
        } catch (PersistenceException $e) {
177
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
178
179
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
180
181
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
182
        }
183
    }
184
185
    /**
186
     * @param Entity $entity
187
     * @param array<mixed> $options
188
     *
189
     * @throws EntityRepositoryException
190
     */
191
    public function delete(EntityInterface $entity, array $options = []): bool
192
    {
193
        try {
194
            return $this->persistService->delete($entity, $options);
195
        } catch (\Exception $e) {
196
            $errorMessage = sprintf(
197
                'Unable to delete entity of type \'%s\'',
198
                $this->entityName
199
            );
200
201
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
202
203
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
204
        }
205
    }
206
207
    /**
208
     * @param iterable<int, Entity> $collection
209
     * @param array<mixed> $options
210
     *
211
     * @throws EntityRepositoryException
212
     */
213
    public function deleteCollection(iterable $collection, array $options = []): int
214
    {
215
        try {
216
            return $this->persistService->deleteCollection($collection, $options);
217
        } catch (PersistenceException $e) {
218
            $errorMessage = sprintf('Unable to delete entity collection of type \'%s\'', $this->entityName);
219
220
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
221
222
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
223
        }
224
    }
225
226
    /**
227
     * @throws EntityRepositoryException
228
     */
229
    public function clear(): void
230
    {
231
        try {
232
            $this->persistService->clear();
233
        } catch (PersistenceException $e) {
234
            $errorMessage = sprintf('Unable to clear entity of type \'%s\'', $this->entityName);
235
236
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
237
238
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
239
        }
240
    }
241
242
    /**
243
     * @param Entity $entity
244
     *
245
     * @throws EntityRepositoryException
246
     */
247
    public function refresh(EntityInterface $entity): void
248
    {
249
        try {
250
            $this->persistService->refresh($entity);
251
        } catch (PersistenceException $e) {
252
            $errorMessage = sprintf('Unable to refresh entity of type \'%s\'', $this->entityName);
253
254
            $this->logger->error(
255
                $errorMessage,
256
                ['exception' => $e, 'entity_name' => $this->entityName, 'id' => $entity->getId()]
257
            );
258
259
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
260
        }
261
    }
262
263
    /**
264
     * @throws EntityRepositoryException
265
     */
266
    public function beginTransaction(): void
267
    {
268
        try {
269
            $this->persistService->beginTransaction();
270
        } catch (\Exception $e) {
271
            throw new EntityRepositoryException(
272
                sprintf('Failed to start transaction for entity \'%s\'', $this->entityName),
273
                $e->getCode(),
274
                $e
275
            );
276
        }
277
    }
278
279
    /**
280
     * @throws EntityRepositoryException
281
     */
282
    public function commitTransaction(): void
283
    {
284
        try {
285
            $this->persistService->commitTransaction();
286
        } catch (\Exception $e) {
287
            throw new EntityRepositoryException(
288
                sprintf('Failed to commit transaction for entity \'%s\'', $this->entityName),
289
                $e->getCode(),
290
                $e
291
            );
292
        }
293
    }
294
295
    public function rollbackTransaction(): void
296
    {
297
        $this->persistService->rollbackTransaction();
298
    }
299
300
    /**
301
     * @param array<mixed> $options
302
     *
303
     * @throws EntityRepositoryException
304
     */
305
    protected function executeQuery(QueryBuilder|AbstractQuery $query, array $options = []): mixed
306
    {
307
        try {
308
            return $this->queryService->execute($query, $options);
309
        } catch (QueryServiceException $e) {
310
            $errorMessage = sprintf('Failed to perform query for entity 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 array<string, mixed> $options
320
     *
321
     * @return Entity|null
322
     *
323
     * @throws EntityRepositoryException
324
     */
325
    protected function getSingleResultOrNull(AbstractQuery|QueryBuilder $query, array $options = []): ?EntityInterface
326
    {
327
        try {
328
            return $this->queryService->getSingleResultOrNull($query, $options);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->queryServi...rNull($query, $options) could return the type array which is incompatible with the type-hinted return Arp\Entity\EntityInterface|null. Consider adding an additional type-check to rule them out.
Loading history...
329
        } catch (QueryServiceException $e) {
330
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
331
332
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
333
334
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
335
        }
336
    }
337
338
    /**
339
     * @param array<mixed> $options
340
     *
341
     * @return array<mixed>|null
342
     *
343
     * @throws EntityRepositoryException
344
     */
345
    protected function getSingleArrayResultOrNull(AbstractQuery|QueryBuilder $query, array $options = []): ?array
346
    {
347
        $options = array_replace_recursive(
348
            $options,
349
            [QueryServiceOption::HYDRATION_MODE => AbstractQuery::HYDRATE_ARRAY]
350
        );
351
352
        try {
353
            $result = $this->queryService->getSingleResultOrNull($query, $options);
354
        } catch (QueryServiceException $e) {
355
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
356
357
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
358
359
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
360
        }
361
362
        return is_array($result) ? $result : null;
363
    }
364
365
    /**
366
     * @param array<string, mixed> $options
367
     *
368
     * @return int|string|float|bool|null
369
     *
370
     * @throws EntityRepositoryException
371
     */
372
    protected function getSingleScalarResult(AbstractQuery|QueryBuilder $query, array $options = []): mixed
373
    {
374
        try {
375
            return $this->queryService->getSingleScalarResult($query, $options);
376
        } catch (QueryServiceException $e) {
377
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
378
379
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
380
381
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
382
        }
383
    }
384
}
385