Passed
Pull Request — master (#5)
by Alex
02:23
created

EntityRepository   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 115
c 2
b 0
f 0
dl 0
loc 363
rs 9.2
wmc 40

20 Methods

Rating   Name   Duplication   Size   Complexity  
A findOneBy() 0 10 2
A findOneById() 0 3 1
A findAll() 0 3 1
A __construct() 0 6 1
A find() 0 10 2
A getClassName() 0 3 1
A getSingleResultOrNull() 0 12 3
A getSingleArrayResultOrNull() 0 18 3
A rollbackTransaction() 0 3 1
A saveCollection() 0 10 2
A clear() 0 10 2
A refresh() 0 13 2
A beginTransaction() 0 9 2
A delete() 0 13 2
A save() 0 10 2
A deleteCollection() 0 10 2
A findBy() 0 27 5
A commitTransaction() 0 9 2
A executeQuery() 0 10 2
A getSingleScalarResult() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like EntityRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityRepository, and based on these observations, apply Extract Interface, too.

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
     * @param int $limit
109
     * @param int $offset
110
     *
111
     * @return iterable<int, Entity>
112
     *
113
     * @throws EntityRepositoryException
114
     */
115
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): iterable
116
    {
117
        $options = [];
118
119
        try {
120
            if (null !== $orderBy) {
121
                $options[QueryServiceOption::ORDER_BY] = $orderBy;
122
            }
123
124
            if (null !== $limit) {
125
                $options[QueryServiceOption::MAX_RESULTS] = $limit;
126
            }
127
128
            if (null !== $offset) {
129
                $options[QueryServiceOption::FIRST_RESULT] = $offset;
130
            }
131
132
            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<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...
133
        } catch (QueryServiceException $e) {
134
            $errorMessage = sprintf('Unable to return a collection of type \'%s\'', $this->entityName);
135
136
            $this->logger->error(
137
                $errorMessage,
138
                ['exception' => $e, 'criteria' => $criteria, 'options' => $options]
139
            );
140
141
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
142
        }
143
    }
144
145
    /**
146
     * @param Entity $entity
147
     * @param array<mixed> $options
148
     *
149
     * @return Entity
150
     *
151
     * @throws EntityRepositoryException
152
     */
153
    public function save(EntityInterface $entity, array $options = []): EntityInterface
154
    {
155
        try {
156
            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...
157
        } catch (PersistenceException $e) {
158
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
159
160
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
161
162
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
163
        }
164
    }
165
166
    /**
167
     * @param iterable<Entity> $collection
168
     * @param array<mixed> $options
169
     *
170
     * @return iterable<Entity>
171
     *
172
     * @throws EntityRepositoryException
173
     */
174
    public function saveCollection(iterable $collection, array $options = []): iterable
175
    {
176
        try {
177
            return $this->persistService->saveCollection($collection, $options);
178
        } catch (PersistenceException $e) {
179
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
180
181
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
182
183
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
184
        }
185
    }
186
187
    /**
188
     * @param Entity $entity
189
     * @param array<mixed> $options
190
     *
191
     * @throws EntityRepositoryException
192
     */
193
    public function delete(EntityInterface $entity, array $options = []): bool
194
    {
195
        try {
196
            return $this->persistService->delete($entity, $options);
197
        } catch (\Exception $e) {
198
            $errorMessage = sprintf(
199
                'Unable to delete entity of type \'%s\'',
200
                $this->entityName
201
            );
202
203
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
204
205
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
206
        }
207
    }
208
209
    /**
210
     * @param iterable<int, Entity> $collection
211
     * @param array<mixed> $options
212
     *
213
     * @throws EntityRepositoryException
214
     */
215
    public function deleteCollection(iterable $collection, array $options = []): int
216
    {
217
        try {
218
            return $this->persistService->deleteCollection($collection, $options);
219
        } catch (PersistenceException $e) {
220
            $errorMessage = sprintf('Unable to delete entity collection of type \'%s\'', $this->entityName);
221
222
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
223
224
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
225
        }
226
    }
227
228
    /**
229
     * @throws EntityRepositoryException
230
     */
231
    public function clear(): void
232
    {
233
        try {
234
            $this->persistService->clear();
235
        } catch (PersistenceException $e) {
236
            $errorMessage = sprintf('Unable to clear entity of type \'%s\'', $this->entityName);
237
238
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
239
240
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
241
        }
242
    }
243
244
    /**
245
     * @param Entity $entity
246
     *
247
     * @throws EntityRepositoryException
248
     */
249
    public function refresh(EntityInterface $entity): void
250
    {
251
        try {
252
            $this->persistService->refresh($entity);
253
        } catch (PersistenceException $e) {
254
            $errorMessage = sprintf('Unable to refresh entity of type \'%s\'', $this->entityName);
255
256
            $this->logger->error(
257
                $errorMessage,
258
                ['exception' => $e, 'entity_name' => $this->entityName, 'id' => $entity->getId()]
259
            );
260
261
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
262
        }
263
    }
264
265
    /**
266
     * @throws EntityRepositoryException
267
     */
268
    public function beginTransaction(): void
269
    {
270
        try {
271
            $this->persistService->beginTransaction();
272
        } catch (\Exception $e) {
273
            throw new EntityRepositoryException(
274
                sprintf('Failed to start transaction for entity \'%s\'', $this->entityName),
275
                $e->getCode(),
276
                $e
277
            );
278
        }
279
    }
280
281
    /**
282
     * @throws EntityRepositoryException
283
     */
284
    public function commitTransaction(): void
285
    {
286
        try {
287
            $this->persistService->commitTransaction();
288
        } catch (\Exception $e) {
289
            throw new EntityRepositoryException(
290
                sprintf('Failed to commit transaction for entity \'%s\'', $this->entityName),
291
                $e->getCode(),
292
                $e
293
            );
294
        }
295
    }
296
297
    public function rollbackTransaction(): void
298
    {
299
        $this->persistService->rollbackTransaction();
300
    }
301
302
    /**
303
     * @param array<mixed> $options
304
     *
305
     * @throws EntityRepositoryException
306
     */
307
    protected function executeQuery(QueryBuilder|AbstractQuery $query, array $options = []): mixed
308
    {
309
        try {
310
            return $this->queryService->execute($query, $options);
311
        } catch (QueryServiceException $e) {
312
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
313
314
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
315
316
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
317
        }
318
    }
319
320
    /**
321
     * @param array<string, mixed> $options
322
     *
323
     * @return Entity|null
324
     *
325
     * @throws EntityRepositoryException
326
     */
327
    protected function getSingleResultOrNull(AbstractQuery|QueryBuilder $query, array $options = []): ?EntityInterface
328
    {
329
        try {
330
            $entity = $this->queryService->getSingleResultOrNull($query, $options);
331
332
            return ($entity instanceof EntityInterface) ? $entity : null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $entity instanceo...erface ? $entity : null also could return the type Arp\Entity\EntityInterface which is incompatible with the documented return type Arp\LaminasDoctrine\Repository\Entity|null.
Loading history...
333
        } catch (QueryServiceException $e) {
334
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
335
336
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
337
338
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
339
        }
340
    }
341
342
    /**
343
     * @param array<mixed> $options
344
     *
345
     * @return array<mixed>|null
346
     *
347
     * @throws EntityRepositoryException
348
     */
349
    protected function getSingleArrayResultOrNull(AbstractQuery|QueryBuilder $query, array $options = []): ?array
350
    {
351
        $options = array_replace_recursive(
352
            $options,
353
            [QueryServiceOption::HYDRATION_MODE => AbstractQuery::HYDRATE_ARRAY]
354
        );
355
356
        try {
357
            $result = $this->queryService->getSingleResultOrNull($query, $options);
358
        } catch (QueryServiceException $e) {
359
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
360
361
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
362
363
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
364
        }
365
366
        return is_array($result) ? $result : null;
367
    }
368
369
    /**
370
     * @param array<string, mixed> $options
371
     *
372
     * @return int|string|float|bool|null
373
     *
374
     * @throws EntityRepositoryException
375
     */
376
    protected function getSingleScalarResult(AbstractQuery|QueryBuilder $query, array $options = []): mixed
377
    {
378
        try {
379
            return $this->queryService->getSingleScalarResult($query, $options);
380
        } catch (QueryServiceException $e) {
381
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
382
383
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
384
385
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
386
        }
387
    }
388
}
389