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

QueryService::getSingleResultOrNull()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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