Passed
Pull Request — master (#4)
by Alex
02:54
created

QueryService::getQuery()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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