Completed
Pull Request — master (#4)
by Alex
03:14 queued 03:14
created

EntityRepository::delete()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 45
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 29
c 1
b 0
f 0
dl 0
loc 45
rs 8.5226
cc 7
nc 6
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\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
 * @author  Alex Patterson <[email protected]>
21
 * @package Arp\LaminasDoctrine\Repository
22
 */
23
abstract class EntityRepository implements EntityRepositoryInterface
24
{
25
    /**
26
     * @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...
27
     */
28
    protected string $entityName;
29
30
    /**
31
     * @var QueryServiceInterface
32
     */
33
    protected QueryServiceInterface $queryService;
34
35
    /**
36
     * @var PersistServiceInterface
37
     */
38
    protected PersistServiceInterface $persistService;
39
40
    /**
41
     * @var LoggerInterface
42
     */
43
    protected LoggerInterface $logger;
44
45
    /**
46
     * @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...
47
     * @param QueryServiceInterface         $queryService
48
     * @param PersistServiceInterface       $persistService
49
     * @param LoggerInterface               $logger
50
     */
51
    public function __construct(
52
        string $entityName,
53
        QueryServiceInterface $queryService,
54
        PersistServiceInterface $persistService,
55
        LoggerInterface $logger
56
    ) {
57
        $this->entityName = $entityName;
58
        $this->queryService = $queryService;
59
        $this->persistService = $persistService;
60
        $this->logger = $logger;
61
    }
62
63
    /**
64
     * Return the fully qualified class name of the mapped entity instance.
65
     *
66
     * @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...
67
     */
68
    public function getClassName(): string
69
    {
70
        return $this->entityName;
71
    }
72
73
    /**
74
     * Return a single entity instance matching the provided $id.
75
     *
76
     * @param string|int $id
77
     *
78
     * @return EntityInterface|null
79
     *
80
     * @throws EntityRepositoryException
81
     */
82
    public function find($id): ?EntityInterface
83
    {
84
        try {
85
            return $this->queryService->findOneById($id);
86
        } catch (QueryServiceException $e) {
87
            $errorMessage = sprintf('Unable to find entity of type \'%s\': %s', $this->entityName, $e->getMessage());
88
89
            $this->logger->error($errorMessage, ['exception' => $e, 'id' => $id]);
90
91
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
92
        }
93
    }
94
95
    /**
96
     * Return a single entity instance matching the provided $criteria.
97
     *
98
     * @param array<mixed> $criteria The entity filter criteria.
99
     *
100
     * @return EntityInterface|null
101
     *
102
     * @throws EntityRepositoryException
103
     */
104
    public function findOneBy(array $criteria): ?EntityInterface
105
    {
106
        try {
107
            return $this->queryService->findOne($criteria);
108
        } catch (QueryServiceException $e) {
109
            $errorMessage = sprintf('Unable to find entity of type \'%s\': %s', $this->entityName, $e->getMessage());
110
111
            $this->logger->error($errorMessage, ['exception' => $e, 'criteria' => $criteria]);
112
113
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
114
        }
115
    }
116
117
    /**
118
     * Return all the entities within the collection.
119
     *
120
     * @return EntityInterface[]|iterable<int, EntityInterface>
121
     *
122
     * @throws EntityRepositoryException
123
     */
124
    public function findAll(): iterable
125
    {
126
        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...
127
    }
128
129
    /**
130
     * Return a collection of entities that match the provided $criteria.
131
     *
132
     * @param array<mixed>      $criteria
133
     * @param array<mixed>|null $orderBy
134
     * @param int|null          $limit
135
     * @param int|null          $offset
136
     *
137
     * @return EntityInterface[]|iterable
138
     *
139
     * @throws EntityRepositoryException
140
     */
141
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): iterable
142
    {
143
        $options = [];
144
145
        try {
146
            if (null !== $orderBy) {
147
                $options[QueryServiceOption::ORDER_BY] = $orderBy;
148
            }
149
150
            if (null !== $limit) {
151
                $options[QueryServiceOption::MAX_RESULTS] = $limit;
152
            }
153
154
            if (null !== $offset) {
155
                $options[QueryServiceOption::FIRST_RESULT] = $offset;
156
            }
157
158
            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...
159
        } catch (QueryServiceException $e) {
160
            $errorMessage = sprintf(
161
                'Unable to return a collection of type \'%s\': %s',
162
                $this->entityName,
163
                $e->getMessage()
164
            );
165
166
            $this->logger->error(
167
                $errorMessage,
168
                ['exception' => $e, 'criteria' => $criteria, 'options' => $options]
169
            );
170
171
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
172
        }
173
    }
174
175
    /**
176
     * Save a single entity instance
177
     *
178
     * @param EntityInterface $entity
179
     * @param array<mixed>    $options
180
     *
181
     * @return EntityInterface
182
     *
183
     * @throws EntityRepositoryException
184
     */
185
    public function save(EntityInterface $entity, array $options = []): EntityInterface
186
    {
187
        try {
188
            return $this->persistService->save($entity, $options);
189
        } catch (PersistenceException $e) {
190
            $errorMessage = sprintf('Unable to save entity of type \'%s\': %s', $this->entityName, $e->getMessage());
191
192
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
193
194
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
195
        }
196
    }
197
198
    /**
199
     * Save a collection of entities in a single transaction
200
     *
201
     * @param iterable<EntityInterface> $collection The collection of entities that should be saved.
202
     * @param array<mixed>              $options    the optional save options.
203
     *
204
     * @return iterable<EntityInterface>
205
     *
206
     * @throws EntityRepositoryException If the save cannot be completed
207
     */
208
    public function saveCollection(iterable $collection, array $options = []): iterable
209
    {
210
        try {
211
            return $this->persistService->saveCollection($collection, $options);
212
        } catch (PersistenceException $e) {
213
            $errorMessage = sprintf('Unable to save entity of type \'%s\': %s', $this->entityName, $e->getMessage());
214
215
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
216
217
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
218
        }
219
    }
220
221
    /**
222
     * @param EntityInterface|string|int|mixed $entity
223
     * @param array<mixed>                     $options
224
     *
225
     * @return bool
226
     *
227
     * @throws EntityRepositoryException
228
     */
229
    public function delete($entity, array $options = []): bool
230
    {
231
        if (is_string($entity) || is_int($entity)) {
232
            $id = $entity;
233
            $entity = $this->find($id);
234
235
            if (null === $entity) {
236
                $errorMessage = sprintf(
237
                    'Unable to delete entity \'%s::%s\': The entity could not be found',
238
                    $this->entityName,
239
                    $id
240
                );
241
242
                $this->logger->error($errorMessage);
243
244
                throw new EntityNotFoundException($errorMessage);
245
            }
246
        } elseif (!$entity instanceof EntityInterface) {
247
            $errorMessage = sprintf(
248
                'The \'entity\' argument must be a \'string\' or an object of type \'%s\'; '
249
                . '\'%s\' provided in \'%s::%s\'',
250
                EntityInterface::class,
251
                (is_object($entity) ? get_class($entity) : gettype($entity)),
252
                static::class,
253
                __FUNCTION__
254
            );
255
256
            $this->logger->error($errorMessage);
257
258
            throw new EntityRepositoryException($errorMessage);
259
        }
260
261
262
        try {
263
            return $this->persistService->delete($entity, $options);
264
        } catch (\Exception $e) {
265
            $errorMessage = sprintf(
266
                'Unable to delete entity of type \'%s\': %s',
267
                $this->entityName,
268
                $e->getMessage()
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<EntityInterface> $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(
293
                'Unable to delete entity collection of type \'%s\': %s',
294
                $this->entityName,
295
                $e->getMessage()
296
            );
297
298
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
299
300
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
301
        }
302
    }
303
304
    /**
305
     * @throws EntityRepositoryException
306
     */
307
    public function clear(): void
308
    {
309
        try {
310
            $this->persistService->clear();
311
        } catch (PersistenceException $e) {
312
            $errorMessage = sprintf(
313
                'Unable to clear entity of type \'%s\': %s',
314
                $this->entityName,
315
                $e->getMessage()
316
            );
317
318
            $this->logger->error($errorMessage, ['exception' => $e, 'entity_name' => $this->entityName]);
319
320
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
321
        }
322
    }
323
324
    /**
325
     * @param EntityInterface $entity
326
     *
327
     * @throws EntityRepositoryException
328
     */
329
    public function refresh(EntityInterface $entity): void
330
    {
331
        try {
332
            $this->persistService->refresh($entity);
333
        } catch (PersistenceException $e) {
334
            $errorMessage = sprintf(
335
                'Unable to refresh entity of type \'%s\': %s',
336
                $this->entityName,
337
                $e->getMessage()
338
            );
339
340
            $this->logger->error(
341
                $errorMessage,
342
                ['exception' => $e, 'entity_name' => $this->entityName, 'id' => $entity->getId()]
343
            );
344
345
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
346
        }
347
    }
348
349
    /**
350
     * Execute query builder or query instance and return the results.
351
     *
352
     * @param object|QueryBuilder|AbstractQuery $query
353
     * @param array<mixed>                      $options
354
     *
355
     * @return EntityInterface[]|iterable
356
     *
357
     * @throws EntityRepositoryException
358
     */
359
    protected function executeQuery(object $query, array $options = [])
360
    {
361
        try {
362
            return $this->queryService->execute($query, $options);
363
        } catch (QueryServiceException $e) {
364
            $errorMessage = sprintf(
365
                'Failed to perform query for entity type \'%s\': %s',
366
                $this->entityName,
367
                $e->getMessage()
368
            );
369
370
            $this->logger->error($errorMessage, ['exception' => $e]);
371
372
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
373
        }
374
    }
375
376
    /**
377
     * Return a single entity instance. NULL will be returned if the result set contains 0 or more than 1 result.
378
     *
379
     * Optionally control the object hydration with QueryServiceOption::HYDRATE_MODE.
380
     *
381
     * @param object|AbstractQuery|QueryBuilder $query
382
     * @param array<string, mixed>              $options
383
     *
384
     * @return array<mixed>|EntityInterface|null
385
     *
386
     * @throws EntityRepositoryException
387
     */
388
    protected function getSingleResultOrNull(object $query, array $options = [])
389
    {
390
        try {
391
            return $this->queryService->getSingleResultOrNull($query, $options);
392
        } catch (QueryServiceException $e) {
393
            $errorMessage = sprintf(
394
                'Failed to perform query for entity type \'%s\': %s',
395
                $this->entityName,
396
                $e->getMessage()
397
            );
398
399
            $this->logger->error(
400
                $errorMessage,
401
                ['exception' => $e, 'entity_name' => $this->entityName]
402
            );
403
404
            throw new EntityRepositoryException($errorMessage, $e->getCode(), $e);
405
        }
406
    }
407
408
    /**
409
     * Return a result set containing a single array result. NULL will be returned if the result set
410
     * contains 0 or more than 1 result.
411
     *
412
     * @param object       $query
413
     * @param array<mixed> $options
414
     *
415
     * @return array<mixed>|null
416
     *
417
     * @throws EntityRepositoryException
418
     */
419
    protected function getSingleArrayResultOrNull(object $query, array $options = []): ?array
420
    {
421
        $result = $this->getSingleResultOrNull(
422
            $query,
423
            array_replace_recursive(
424
                $options,
425
                [QueryServiceOption::HYDRATION_MODE => AbstractQuery::HYDRATE_ARRAY]
426
            )
427
        );
428
429
        return is_array($result) ? $result : null;
430
    }
431
}
432