Passed
Pull Request — master (#4)
by Alex
08:39
created

EntityRepository::saveCollection()   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\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
 * @extends EntityRepositoryInterface<EntityInterface>
23
 *
24
 * @author   Alex Patterson <[email protected]>
25
 * @package  Arp\LaminasDoctrine\Repository
26
 */
27
abstract class EntityRepository implements EntityRepositoryInterface, TransactionServiceInterface
28
{
29
    /**
30
     * @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...
31
     */
32
    protected string $entityName;
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         $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<TEntity>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<TEntity> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<TEntity>.
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 TEntity|null
0 ignored issues
show
Bug introduced by
The type Arp\LaminasDoctrine\Repository\TEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
74
     *
75
     * @throws EntityRepositoryException
76
     */
77
    public function find($id): ?EntityInterface
78
    {
79
        try {
80
            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\TEntity|null.
Loading history...
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
     * @return TEntity|null
92
     *
93
     * @throws EntityRepositoryException
94
     */
95
    public function findOneById($id): ?EntityInterface
96
    {
97
        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\TEntity|null.
Loading history...
98
    }
99
100
    /**
101
     * Return a single entity instance matching the provided $criteria.
102
     *
103
     * @param array<mixed> $criteria The entity filter criteria.
104
     *
105
     * @return TEntity|null
106
     *
107
     * @throws EntityRepositoryException
108
     */
109
    public function findOneBy(array $criteria): ?EntityInterface
110
    {
111
        try {
112
            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\TEntity|null.
Loading history...
113
        } catch (QueryServiceException $e) {
114
            $errorMessage = sprintf('Unable to find entity of type \'%s\'', $this->entityName);
115
116
            $this->logger->error($errorMessage, ['exception' => $e, 'criteria' => $criteria]);
117
118
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
119
        }
120
    }
121
122
    /**
123
     * Return all the entities within the collection.
124
     *
125
     * @return TEntity[]|iterable<int, TEntity>
126
     *
127
     * @throws EntityRepositoryException
128
     */
129
    public function findAll(): iterable
130
    {
131
        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...
132
    }
133
134
    /**
135
     * Return a collection of entities that match the provided $criteria.
136
     *
137
     * @param array<mixed>      $criteria
138
     * @param array<mixed>|null $orderBy
139
     * @param int|null          $limit
140
     * @param int|null          $offset
141
     *
142
     * @return TEntity[]|iterable
143
     *
144
     * @throws EntityRepositoryException
145
     */
146
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): iterable
147
    {
148
        $options = [];
149
150
        try {
151
            if (null !== $orderBy) {
152
                $options[QueryServiceOption::ORDER_BY] = $orderBy;
153
            }
154
155
            if (null !== $limit) {
156
                $options[QueryServiceOption::MAX_RESULTS] = $limit;
157
            }
158
159
            if (null !== $offset) {
160
                $options[QueryServiceOption::FIRST_RESULT] = $offset;
161
            }
162
163
            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...
164
        } catch (QueryServiceException $e) {
165
            $errorMessage = sprintf('Unable to return a collection of type \'%s\'', $this->entityName);
166
167
            $this->logger->error(
168
                $errorMessage,
169
                ['exception' => $e, 'criteria' => $criteria, 'options' => $options]
170
            );
171
172
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
173
        }
174
    }
175
176
    /**
177
     * Save a single entity instance
178
     *
179
     * @param TEntity      $entity
180
     * @param array<mixed> $options
181
     *
182
     * @return TEntity
183
     *
184
     * @throws EntityRepositoryException
185
     */
186
    public function save(EntityInterface $entity, array $options = []): EntityInterface
187
    {
188
        try {
189
            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\TEntity.
Loading history...
190
        } catch (PersistenceException $e) {
191
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
192
193
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
194
195
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
196
        }
197
    }
198
199
    /**
200
     * Save a collection of entities in a single transaction
201
     *
202
     * @param iterable<TEntity> $collection The collection of entities that should be saved.
203
     * @param array<mixed>      $options    the optional save options.
204
     *
205
     * @return iterable<TEntity>
206
     *
207
     * @throws EntityRepositoryException If the save cannot be completed
208
     */
209
    public function saveCollection(iterable $collection, array $options = []): iterable
210
    {
211
        try {
212
            return $this->persistService->saveCollection($collection, $options);
213
        } catch (PersistenceException $e) {
214
            $errorMessage = sprintf('Unable to save entity of type \'%s\'', $this->entityName);
215
216
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
217
218
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
219
        }
220
    }
221
222
    /**
223
     * @param TEntity|string|int|mixed $entity
224
     * @param array<mixed>             $options
225
     *
226
     * @return bool
227
     *
228
     * @throws EntityRepositoryException
229
     */
230
    public function delete($entity, array $options = []): bool
231
    {
232
        if (is_string($entity) || is_int($entity)) {
233
            $id = $entity;
234
            $entity = $this->find($id);
235
236
            if (null === $entity) {
237
                $errorMessage = sprintf(
238
                    'Unable to delete entity \'%s::%s\': The entity could not be found',
239
                    $this->entityName,
240
                    $id
241
                );
242
243
                $this->logger->error($errorMessage);
244
245
                throw new EntityNotFoundException($errorMessage);
246
            }
247
        } elseif (!$entity instanceof EntityInterface) {
248
            $errorMessage = sprintf(
249
                'The \'entity\' argument must be a \'string\' or an object of type \'%s\'; '
250
                . '\'%s\' provided in \'%s::%s\'',
251
                EntityInterface::class,
252
                (is_object($entity) ? get_class($entity) : gettype($entity)),
253
                static::class,
254
                __FUNCTION__
255
            );
256
257
            $this->logger->error($errorMessage);
258
259
            throw new EntityRepositoryException($errorMessage);
260
        }
261
262
263
        try {
264
            return $this->persistService->delete($entity, $options);
265
        } catch (\Exception $e) {
266
            $errorMessage = sprintf(
267
                'Unable to delete entity of type \'%s\'',
268
                $this->entityName
269
            );
270
271
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
272
273
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
274
        }
275
    }
276
277
    /**
278
     * Perform a deletion of a collection of entities
279
     *
280
     * @param iterable<TEntity> $collection
281
     * @param array<mixed>      $options
282
     *
283
     * @return int
284
     *
285
     * @throws EntityRepositoryException
286
     */
287
    public function deleteCollection(iterable $collection, array $options = []): int
288
    {
289
        try {
290
            return $this->persistService->deleteCollection($collection, $options);
291
        } catch (PersistenceException $e) {
292
            $errorMessage = sprintf('Unable to delete entity collection of type \'%s\'', $this->entityName);
293
294
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
295
296
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
297
        }
298
    }
299
300
    /**
301
     * @throws EntityRepositoryException
302
     */
303
    public function clear(): void
304
    {
305
        try {
306
            $this->persistService->clear();
307
        } catch (PersistenceException $e) {
308
            $errorMessage = sprintf('Unable to clear entity of type \'%s\'', $this->entityName);
309
310
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
311
312
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
313
        }
314
    }
315
316
    /**
317
     * @param TEntity $entity
318
     *
319
     * @throws EntityRepositoryException
320
     */
321
    public function refresh(EntityInterface $entity): void
322
    {
323
        try {
324
            $this->persistService->refresh($entity);
325
        } catch (PersistenceException $e) {
326
            $errorMessage = sprintf('Unable to refresh entity of type \'%s\'', $this->entityName);
327
328
            $this->logger->error(
329
                $errorMessage,
330
                ['exception' => $e, 'entity_name' => $this->entityName, 'id' => $entity->getId()]
331
            );
332
333
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
334
        }
335
    }
336
337
    /**
338
     * @throws EntityRepositoryException
339
     */
340
    public function beginTransaction(): void
341
    {
342
        try {
343
            $this->persistService->beginTransaction();
344
        } catch (\Exception $e) {
345
            throw new EntityRepositoryException(
346
                sprintf('Failed to start transaction for entity \'%s\'', $this->entityName),
347
                $e->getCode(),
348
                $e
349
            );
350
        }
351
    }
352
353
    /**
354
     * @throws EntityRepositoryException
355
     */
356
    public function commitTransaction(): void
357
    {
358
        try {
359
            $this->persistService->commitTransaction();
360
        } catch (\Exception $e) {
361
            throw new EntityRepositoryException(
362
                sprintf('Failed to commit transaction for entity \'%s\'', $this->entityName),
363
                $e->getCode(),
364
                $e
365
            );
366
        }
367
    }
368
369
    public function rollbackTransaction(): void
370
    {
371
        $this->persistService->rollbackTransaction();
372
    }
373
374
    /**
375
     * Execute query builder or query instance and return the results.
376
     *
377
     * @param object|QueryBuilder|AbstractQuery $query
378
     * @param array<mixed>                      $options
379
     *
380
     * @return TEntity[]|iterable<int, TEntity|array>
381
     *
382
     * @throws EntityRepositoryException
383
     */
384
    protected function executeQuery(object $query, array $options = [])
385
    {
386
        try {
387
            return $this->queryService->execute($query, $options);
388
        } catch (QueryServiceException $e) {
389
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
390
391
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
392
393
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
394
        }
395
    }
396
397
    /**
398
     * Return a single entity instance. NULL will be returned if the result set contains 0 or more than 1 result.
399
     *
400
     * Optionally control the object hydration with QueryServiceOption::HYDRATE_MODE.
401
     *
402
     * @param object|AbstractQuery|QueryBuilder $query
403
     * @param array<string, mixed>              $options
404
     *
405
     * @return array<mixed>|TEntity|null
406
     *
407
     * @throws EntityRepositoryException
408
     */
409
    protected function getSingleResultOrNull(object $query, array $options = [])
410
    {
411
        try {
412
            return $this->queryService->getSingleResultOrNull($query, $options);
413
        } catch (QueryServiceException $e) {
414
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
415
416
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
417
418
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
419
        }
420
    }
421
422
    /**
423
     * Return a result set containing a single array result. NULL will be returned if the result set
424
     * contains 0 or more than 1 result.
425
     *
426
     * @param object       $query
427
     * @param array<mixed> $options
428
     *
429
     * @return array<mixed>|null
430
     *
431
     * @throws EntityRepositoryException
432
     */
433
    protected function getSingleArrayResultOrNull(object $query, array $options = []): ?array
434
    {
435
        $result = $this->getSingleResultOrNull(
436
            $query,
437
            array_replace_recursive(
438
                $options,
439
                [QueryServiceOption::HYDRATION_MODE => AbstractQuery::HYDRATE_ARRAY]
440
            )
441
        );
442
443
        return is_array($result) ? $result : null;
444
    }
445
446
    /**
447
     * @param object|AbstractQuery|QueryBuilder $query
448
     * @param array<string, mixed>              $options
449
     *
450
     * @return int|string|float|bool|null
451
     *
452
     * @throws EntityRepositoryException
453
     */
454
    protected function getSingleScalarResult(object $query, array $options = [])
455
    {
456
        try {
457
            return $this->queryService->getSingleScalarResult($query, $options);
458
        } catch (QueryServiceException $e) {
459
            $errorMessage = sprintf('Failed to perform query for entity type \'%s\'', $this->entityName);
460
461
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
462
463
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
464
        }
465
    }
466
}
467