Issues (6)

src/Query/QueryService.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\DoctrineEntityRepository\Query;
6
7
use Arp\DoctrineEntityRepository\Constant\QueryServiceOption;
8
use Arp\DoctrineEntityRepository\Query\Exception\QueryServiceException;
9
use Arp\Entity\EntityInterface;
10
use Doctrine\ORM\AbstractQuery;
11
use Doctrine\ORM\EntityManagerInterface;
12
use Doctrine\ORM\Query;
13
use Doctrine\ORM\QueryBuilder;
14
use Doctrine\ORM\TransactionRequiredException;
15
use Psr\Log\LoggerInterface;
16
17
/**
18
 * @author  Alex Patterson <[email protected]>
19
 * @package Arp\DoctrineEntityRepository\Query
20
 */
21
class QueryService implements QueryServiceInterface
22
{
23
    /**
24
     * @var class-string
25
     */
26
    protected string $entityName;
27
28
    /**
29
     * @var EntityManagerInterface
30
     */
31
    protected EntityManagerInterface $entityManager;
32
33
    /**
34
     * @var LoggerInterface
35
     */
36
    protected LoggerInterface $logger;
37
38
    /**
39
     * @param class-string           $entityName
40
     * @param EntityManagerInterface $entityManager
41
     * @param LoggerInterface        $logger
42
     */
43
    public function __construct(string $entityName, EntityManagerInterface $entityManager, LoggerInterface $logger)
44
    {
45
        $this->entityName = $entityName;
46
        $this->entityManager = $entityManager;
47
        $this->logger = $logger;
48
    }
49
50
    /**
51
     * @return string
52
     */
53
    public function getEntityName(): string
54
    {
55
        return $this->entityName;
56
    }
57
58
    /**
59
     * @param object|AbstractQuery|QueryBuilder $queryOrBuilder
60
     * @param array<string, mixed>              $options
61
     *
62
     * @return EntityInterface|null|array<mixed>
63
     *
64
     * @throws QueryServiceException
65
     */
66
    public function getSingleResultOrNull(object $queryOrBuilder, array $options = [])
67
    {
68
        $result = $this->execute($queryOrBuilder, $options);
69
70
        if (empty($result)) {
71
            return null;
72
        }
73
74
        if (!is_array($result)) {
75
            return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type integer which is incompatible with the documented return type Arp\Entity\EntityInterfa...array<mixed,mixed>|null.
Loading history...
76
        }
77
78
        if (count($result) > 1) {
79
            return null;
80
        }
81
82
        return array_shift($result);
83
    }
84
85
    /**
86
     * @param object|AbstractQuery|QueryBuilder $queryOrBuilder
87
     * @param array<string, mixed>              $options
88
     *
89
     * @return int|mixed|string
90
     *
91
     * @throws QueryServiceException
92
     */
93
    public function getSingleScalarResult(object $queryOrBuilder, array $options = [])
94
    {
95
        try {
96
            return $this->getQuery($queryOrBuilder, $options)->getSingleScalarResult();
97
        } catch (QueryServiceException $e) {
98
            throw $e;
99
        } catch (\Exception $e) {
100
            $message = sprintf(
101
                'An error occurred while loading fetching a single scalar result: %s',
102
                $e->getMessage()
103
            );
104
105
            $this->logger->error($message, ['exception' => $e]);
106
107
            throw new QueryServiceException($message, $e->getCode(), $e);
108
        }
109
    }
110
111
    /**
112
     * Construct and execute the query.
113
     *
114
     * @param object|AbstractQuery|QueryBuilder $queryOrBuilder
115
     * @param array<string, mixed>              $options
116
     *
117
     * @return mixed
118
     *
119
     * @throws QueryServiceException
120
     */
121
    public function execute(object $queryOrBuilder, array $options = [])
122
    {
123
        try {
124
            return $this->getQuery($queryOrBuilder, $options)->execute();
125
        } catch (QueryServiceException $e) {
126
            throw $e;
127
        } catch (\Exception $e) {
128
            $message = sprintf('Failed to execute query : %s', $e->getMessage());
129
130
            $this->logger->error($message, ['exception' => $e]);
131
132
            throw new QueryServiceException($message, $e->getCode(), $e);
133
        }
134
    }
135
136
    /**
137
     * Find a single entity matching the provided identity.
138
     *
139
     * @param mixed                $id      The identity of the entity to match.
140
     * @param array<string, mixed> $options The optional query options.
141
     *
142
     * @return EntityInterface|null
143
     *
144
     * @throws QueryServiceException
145
     */
146
    public function findOneById($id, array $options = []): ?EntityInterface
147
    {
148
        return $this->findOne(compact('id'), $options);
149
    }
150
151
    /**
152
     * Find a single entity matching the provided criteria.
153
     *
154
     * @param array<string, mixed> $criteria The search criteria that should be matched on.
155
     * @param array<string, mixed> $options  The optional query options.
156
     *
157
     * @return EntityInterface|null
158
     *
159
     * @throws QueryServiceException
160
     */
161
    public function findOne(array $criteria, array $options = []): ?EntityInterface
162
    {
163
        try {
164
            $persist = $this->entityManager->getUnitOfWork()->getEntityPersister($this->entityName);
165
166
            $entity = $persist->load(
167
                $criteria,
168
                $options[QueryServiceOption::ENTITY] ?? null,
169
                $options[QueryServiceOption::ASSOCIATION] ?? null,
170
                $options[QueryServiceOption::HINTS] ?? [],
171
                $options[QueryServiceOption::LOCK_MODE] ?? null,
172
                1,
173
                $options[QueryServiceOption::ORDER_BY] ?? null
174
            );
175
176
            return ($entity instanceof EntityInterface) ? $entity : null;
177
        } catch (\Exception $e) {
178
            $message = sprintf('Failed to execute \'findOne\' query: %s', $e->getMessage());
179
180
            $this->logger->error($message, ['exception' => $e, 'criteria' => $criteria, 'options' => $options]);
181
182
            throw new QueryServiceException($message, $e->getCode(), $e);
183
        }
184
    }
185
186
    /**
187
     * Find a collection of entities that match the provided criteria.
188
     *
189
     * @param array<string, mixed> $criteria The search criteria that should be matched on.
190
     * @param array<string, mixed> $options  The optional query options.
191
     *
192
     * @return iterable<EntityInterface>
193
     *
194
     * @throws QueryServiceException
195
     */
196
    public function findMany(array $criteria, array $options = []): iterable
197
    {
198
        try {
199
            $persister = $this->entityManager->getUnitOfWork()->getEntityPersister($this->entityName);
200
201
            return $persister->loadAll(
202
                $criteria,
203
                $options[QueryServiceOption::ORDER_BY] ?? null,
204
                $options[QueryServiceOption::MAX_RESULTS] ?? null,
205
                $options[QueryServiceOption::FIRST_RESULT] ?? null
206
            );
207
        } catch (\Exception $e) {
208
            $message = sprintf('Failed to execute \'findMany\' query: %s', $e->getMessage());
209
210
            $this->logger->error($message, ['exception' => $e, 'criteria' => $criteria, 'options' => $options]);
211
212
            throw new QueryServiceException($message, $e->getCode(), $e);
213
        }
214
    }
215
216
    /**
217
     * Return the result set count.
218
     *
219
     * @param array<string, mixed> $criteria
220
     *
221
     * @return int
222
     */
223
    public function count(array $criteria): int
224
    {
225
        $unitOfWork = $this->entityManager->getUnitOfWork();
226
227
        return $unitOfWork->getEntityPersister($this->entityName)->count($criteria);
228
    }
229
230
    /**
231
     * Set the query builder options.
232
     *
233
     * @param QueryBuilder         $queryBuilder The query builder to update.
234
     * @param array<string, mixed> $options      The query builder options to set.
235
     *
236
     * @return QueryBuilder
237
     */
238
    protected function prepareQueryBuilder(QueryBuilder $queryBuilder, array $options = []): QueryBuilder
239
    {
240
        if (isset($options[QueryServiceOption::FIRST_RESULT])) {
241
            $queryBuilder->setFirstResult($options[QueryServiceOption::FIRST_RESULT]);
242
        }
243
244
        if (isset($options[QueryServiceOption::MAX_RESULTS])) {
245
            $queryBuilder->setMaxResults($options[QueryServiceOption::MAX_RESULTS]);
246
        }
247
248
        if (isset($options[QueryServiceOption::ORDER_BY]) && is_array($options[QueryServiceOption::ORDER_BY])) {
249
            foreach ($options[QueryServiceOption::ORDER_BY] as $fieldName => $orderDirection) {
250
                $queryBuilder->addOrderBy(
251
                    $fieldName,
252
                    ('DESC' === strtoupper($orderDirection) ? 'DESC' : 'ASC')
253
                );
254
            }
255
        }
256
257
        return $queryBuilder;
258
    }
259
260
    /**
261
     * Prepare the provided query by setting the $options.
262
     *
263
     * @param AbstractQuery        $query
264
     * @param array<string, mixed> $options
265
     *
266
     * @return AbstractQuery
267
     *
268
     * @throws QueryServiceException
269
     */
270
    protected function prepareQuery(AbstractQuery $query, array $options = []): AbstractQuery
271
    {
272
        if (isset($options['params'])) {
273
            $query->setParameters($options['params']);
274
        }
275
276
        if (isset($options[QueryServiceOption::HYDRATION_MODE])) {
277
            $query->setHydrationMode($options[QueryServiceOption::HYDRATION_MODE]);
278
        }
279
280
        if (isset($options['result_set_mapping'])) {
281
            $query->setResultSetMapping($options['result_set_mapping']);
282
        }
283
284
        if (isset($options[QueryServiceOption::HINTS]) && is_array($options[QueryServiceOption::HINTS])) {
285
            foreach ($options[QueryServiceOption::HINTS] as $hint => $hintValue) {
286
                $query->setHint($hint, $hintValue);
287
            }
288
        }
289
290
        if ($query instanceof Query) {
291
            if (!empty($options[QueryServiceOption::DQL])) {
292
                $query->setDQL($options[QueryServiceOption::DQL]);
293
            }
294
295
            if (isset($options[QueryServiceOption::LOCK_MODE])) {
296
                try {
297
                    $query->setLockMode($options[QueryServiceOption::LOCK_MODE]);
298
                } catch (TransactionRequiredException $e) {
299
                    throw new QueryServiceException($e->getMessage(), $e->getCode(), $e);
300
                }
301
            }
302
        }
303
304
        return $query;
305
    }
306
307
    /**
308
     * Return a new query builder instance.
309
     *
310
     * @param string|null $alias The optional query builder alias.
311
     *
312
     * @return QueryBuilder
313
     */
314
    public function createQueryBuilder(string $alias = null): QueryBuilder
315
    {
316
        $queryBuilder = $this->entityManager->createQueryBuilder();
317
318
        if (null !== $alias) {
319
            $queryBuilder->select($alias)->from($this->entityName, $alias);
320
        }
321
322
        return $queryBuilder;
323
    }
324
325
    /**
326
     * Resolve the ORM Query instance for a QueryBuilder and set the optional $options
327
     *
328
     * @param object|AbstractQuery|QueryBuilder $queryOrBuilder
329
     * @param array<mixed>               $options
330
     *
331
     * @return AbstractQuery
332
     *
333
     * @throws QueryServiceException
334
     */
335
    private function getQuery(object $queryOrBuilder, array $options = []): AbstractQuery
336
    {
337
        if (!$queryOrBuilder instanceof AbstractQuery && !$queryOrBuilder instanceof QueryBuilder) {
338
            throw new QueryServiceException(
339
                sprintf(
340
                    'The queryOrBuilder argument must be an object of type '
341
                    . '\'%s\' or \'%s\'; \'%s\' provided in \'%s\'.',
342
                    AbstractQuery::class,
343
                    QueryBuilder::class,
344
                    get_class($queryOrBuilder),
345
                    __METHOD__
346
                )
347
            );
348
        }
349
350
        if ($queryOrBuilder instanceof QueryBuilder) {
351
            $query = $this->prepareQueryBuilder($queryOrBuilder, $options)->getQuery();
352
        } else {
353
            $query = $queryOrBuilder;
354
        }
355
356
        return $this->prepareQuery($query, $options);
357
    }
358
}
359