Completed
Push — master ( 52a24b...0631b2 )
by Alex
16s queued 13s
created

EntityRepository::executeQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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