testExecuteWillThrowQueryServiceExceptionIfProvidedInvalidQuery()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 20
rs 9.8333
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArpTest\DoctrineEntityRepository\Query;
6
7
use Arp\DoctrineEntityRepository\Constant\QueryServiceOption;
8
use Arp\DoctrineEntityRepository\Query\Exception\QueryServiceException;
9
use Arp\DoctrineEntityRepository\Query\QueryService;
10
use Arp\DoctrineEntityRepository\Query\QueryServiceInterface;
11
use Arp\Entity\EntityInterface;
12
use Doctrine\ORM\AbstractQuery;
13
use Doctrine\ORM\EntityManagerInterface;
14
use Doctrine\ORM\Query;
15
use Doctrine\ORM\QueryBuilder;
16
use PHPUnit\Framework\MockObject\MockObject;
17
use PHPUnit\Framework\TestCase;
18
use Psr\Log\LoggerInterface;
19
20
/**
21
 * @covers  \Arp\DoctrineEntityRepository\Query\QueryService
22
 *
23
 * @author  Alex Patterson <[email protected]>
24
 * @package ArpTest\DoctrineEntityRepository\Query
25
 */
26
final class QueryServiceTest extends TestCase
27
{
28
    /**
29
     * @var class-string
30
     */
31
    private string $entityName;
32
33
    /**
34
     * @var EntityManagerInterface&MockObject
35
     */
36
    private $entityManager;
37
38
    /**
39
     * @var LoggerInterface&MockObject
40
     */
41
    private $logger;
42
43
    /**
44
     * Setup the test case dependencies.
45
     */
46
    public function setUp(): void
47
    {
48
        $this->entityName = EntityInterface::class;
49
50
        $this->entityManager = $this->getMockForAbstractClass(EntityManagerInterface::class);
51
52
        $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
53
    }
54
55
    /**
56
     * Assert that the QueryService implements QueryServiceInterface
57
     */
58
    public function testImplementsQueryServiceInterface(): void
59
    {
60
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
61
62
        $this->assertInstanceOf(QueryServiceInterface::class, $queryService);
63
    }
64
65
    /**
66
     * Assert getEntityName() will return the entity class name
67
     */
68
    public function testGetEntityName(): void
69
    {
70
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
71
72
        $this->assertSame($this->entityName, $queryService->getEntityName());
73
    }
74
75
    /**
76
     * Assert that a query builder is returned without an alias
77
     */
78
    public function testCreateQueryBuilderWillReturnQueryBuilderWithoutAlias(): void
79
    {
80
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
81
82
        /** @var QueryBuilder&MockObject $queryBuilder */
83
        $queryBuilder = $this->createMock(QueryBuilder::class);
84
85
        $this->entityManager->expects($this->once())
86
            ->method('createQueryBuilder')
87
            ->willReturn($queryBuilder);
88
89
        $queryBuilder->expects($this->never())->method('select');
90
        $queryBuilder->expects($this->never())->method('from');
91
92
        $queryService->createQueryBuilder();
93
    }
94
95
    /**
96
     * Assert that a query builder is returned with the provided $alias
97
     */
98
    public function testCreateQueryBuilderWillReturnQueryBuilderWithAlias(): void
99
    {
100
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
101
102
        $alias = 'foo';
103
104
        /** @var QueryBuilder&MockObject $queryBuilder */
105
        $queryBuilder = $this->createMock(QueryBuilder::class);
106
107
        $this->entityManager->expects($this->once())
108
            ->method('createQueryBuilder')
109
            ->willReturn($queryBuilder);
110
111
        $queryBuilder->expects($this->once())
112
            ->method('select')
113
            ->with($alias)
114
            ->willReturn($queryBuilder);
115
116
        $queryBuilder->expects($this->once())
117
            ->method('from')
118
            ->with($this->entityName, $alias);
119
120
        $queryService->createQueryBuilder($alias);
121
    }
122
123
    /**
124
     * Assert calls to getSingleResultOrNull() will return NULL for an empty result set
125
     *
126
     * @throws QueryServiceException
127
     */
128
    public function testGetSingleResultWillReturnNullForEmptyResultSet(): void
129
    {
130
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
131
132
        /** @var QueryBuilder&MockObject $queryBuilder */
133
        $queryBuilder = $this->createMock(QueryBuilder::class);
134
135
        /** @var AbstractQuery&MockObject $query */
136
        $query = $this->createMock(AbstractQuery::class);
137
138
        $queryBuilder->expects($this->once())
139
            ->method('getQuery')
140
            ->willReturn($query);
141
142
        $resultSet = []; // Empty result set will cause our NULL result
143
144
        $query->expects($this->once())
145
            ->method('execute')
146
            ->willReturn($resultSet);
147
148
        $this->assertNull($queryService->getSingleResultOrNull($queryBuilder));
149
    }
150
151
    /**
152
     * Assert calls to getSingleResultOrNull() will return the results for non-array values
153
     *
154
     * @dataProvider getGetSingleResultWillReturnResultForNonArrayResultSetData
155
     *
156
     * @param mixed        $resultSet
157
     * @param array<mixed> $options
158
     *
159
     * @throws QueryServiceException
160
     */
161
    public function testGetSingleResultWillReturnResultForNonArrayResultSet($resultSet, array $options = []): void
162
    {
163
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
164
165
        /** @var QueryBuilder&MockObject $queryBuilder */
166
        $queryBuilder = $this->createMock(QueryBuilder::class);
167
168
        /** @var AbstractQuery&MockObject $query */
169
        $query = $this->createMock(AbstractQuery::class);
170
171
        $queryBuilder->expects($this->once())
172
            ->method('getQuery')
173
            ->willReturn($query);
174
175
        $query->expects($this->once())
176
            ->method('execute')
177
            ->willReturn($resultSet);
178
179
        $this->assertSame($resultSet, $queryService->getSingleResultOrNull($queryBuilder, $options));
180
    }
181
182
    /**
183
     * @return array<mixed>
184
     */
185
    public function getGetSingleResultWillReturnResultForNonArrayResultSetData(): array
186
    {
187
        return [
188
            [true],
189
            [new \stdClass()],
190
            [$this->createMock(EntityInterface::class)],
191
        ];
192
    }
193
194
    /**
195
     * Assert calls to getSingleResultOrNull() will return NULL is the result set returned from the query is
196
     * greater than one
197
     *
198
     * @param array<mixed> $options
199
     *
200
     * @throws QueryServiceException
201
     */
202
    public function testGetSingleResultWillReturnNullForResultsWithMoreThanOneRecord(array $options = []): void
203
    {
204
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
205
206
        /** @var QueryBuilder&MockObject $queryBuilder */
207
        $queryBuilder = $this->createMock(QueryBuilder::class);
208
209
        /** @var AbstractQuery&MockObject $query */
210
        $query = $this->createMock(AbstractQuery::class);
211
212
        $queryBuilder->expects($this->once())
213
            ->method('getQuery')
214
            ->willReturn($query);
215
216
        /** @var array<EntityInterface&MockObject> $resultSet */
217
        $resultSet = [
218
            $this->createMock(EntityInterface::class),
219
            $this->createMock(EntityInterface::class),
220
            $this->createMock(EntityInterface::class),
221
        ];
222
223
        $query->expects($this->once())
224
            ->method('execute')
225
            ->willReturn($resultSet);
226
227
        $this->assertNull($queryService->getSingleResultOrNull($queryBuilder, $options));
228
    }
229
230
    /**
231
     * Assert just a single record is returned from getSingleResultOrNull()
232
     *
233
     * @param array<mixed> $options
234
     *
235
     * @dataProvider getGetSingleResultWillReturnSingleResultData
236
     *
237
     * @throws QueryServiceException
238
     */
239
    public function testGetSingleResultWillReturnSingleResult(array $options = []): void
240
    {
241
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
242
243
        /** @var QueryBuilder&MockObject $queryBuilder */
244
        $queryBuilder = $this->createMock(QueryBuilder::class);
245
246
        if (array_key_exists(QueryServiceOption::FIRST_RESULT, $options)) {
247
            $queryBuilder->expects($this->once())
248
                ->method('setFirstResult')
249
                ->with($options[QueryServiceOption::FIRST_RESULT]);
250
        }
251
252
        if (array_key_exists(QueryServiceOption::MAX_RESULTS, $options)) {
253
            $queryBuilder->expects($this->once())
254
                ->method('setMaxResults')
255
                ->with($options[QueryServiceOption::MAX_RESULTS]);
256
        }
257
258
        if (
259
            array_key_exists(QueryServiceOption::ORDER_BY, $options)
260
            && is_array($options[QueryServiceOption::ORDER_BY])
261
        ) {
262
            $addOrderByArgs = [];
263
            foreach ($options[QueryServiceOption::ORDER_BY] as $fieldName => $orderDirection) {
264
                $addOrderByArgs[] = [$fieldName, $orderDirection];
265
            }
266
267
            $queryBuilder->expects($this->exactly(count($addOrderByArgs)))
268
                ->method('addOrderBy')
269
                ->withConsecutive(...$addOrderByArgs);
270
        }
271
272
        /** @var AbstractQuery&MockObject $query */
273
        $query = $this->createMock(AbstractQuery::class);
274
275
        $queryBuilder->expects($this->once())
276
            ->method('getQuery')
277
            ->willReturn($query);
278
279
        /** @var array<EntityInterface&MockObject> $resultSet */
280
        $resultSet = [
281
            $this->createMock(EntityInterface::class),
282
        ];
283
284
        $query->expects($this->once())
285
            ->method('execute')
286
            ->willReturn($resultSet);
287
288
        $this->assertSame($resultSet[0], $queryService->getSingleResultOrNull($queryBuilder, $options));
289
    }
290
291
    /**
292
     * @return array<mixed>
293
     */
294
    public function getGetSingleResultWillReturnSingleResultData(): array
295
    {
296
        return [
297
            [
298
                [
299
                    QueryServiceOption::FIRST_RESULT => 12,
300
                ],
301
            ],
302
303
            [
304
                [
305
                    QueryServiceOption::MAX_RESULTS => 100,
306
                ],
307
            ],
308
309
            [
310
                [
311
                    QueryServiceOption::MAX_RESULTS => 10,
312
                    QueryServiceOption::ORDER_BY    => [
313
                        'foo' => 'ASC',
314
                        'bar' => 'DESC',
315
                    ],
316
                ],
317
            ],
318
        ];
319
    }
320
321
    /**
322
     * Assert that if we provide an invalid query object to execute() a QueryServiceException will be thrown
323
     *
324
     * @throws QueryServiceException
325
     */
326
    public function testExecuteWillThrowQueryServiceExceptionIfProvidedInvalidQuery(): void
327
    {
328
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
329
330
        $invalidQuery = new \stdClass();
331
332
        $this->expectException(QueryServiceException::class);
333
        $this->expectExceptionMessage(
334
            sprintf(
335
                'The queryOrBuilder argument must be an object of type '
336
                . '\'%s\' or \'%s\'; \'%s\' provided in \'%s::%s\'.',
337
                AbstractQuery::class,
338
                QueryBuilder::class,
339
                get_class($invalidQuery),
340
                QueryService::class,
341
                'getQuery'
342
            )
343
        );
344
345
        $queryService->execute($invalidQuery);
346
    }
347
348
    /**
349
     * Assert that a query object provided to execute will be prepared with the provided options and then executed.
350
     *
351
     * @param array<mixed> $options The optional query options to assert get set on the query when being prepared.
352
     *
353
     * @dataProvider getExecuteWillPrepareAndExecuteQueryData
354
     * @throws QueryServiceException
355
     */
356
    public function testExecuteWillPrepareAndExecuteQuery(array $options = []): void
357
    {
358
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
359
360
        /** @var AbstractQuery&MockObject $query */
361
        $query = $this->createMock(AbstractQuery::class);
362
363
        if (array_key_exists('params', $options)) {
364
            $query->expects($this->once())
365
                ->method('setParameters')
366
                ->with($options['params']);
367
        }
368
369
        if (array_key_exists(QueryServiceOption::HYDRATION_MODE, $options)) {
370
            $query->expects($this->once())
371
                ->method('setHydrationMode')
372
                ->with($options[QueryServiceOption::HYDRATION_MODE]);
373
        }
374
375
        if (array_key_exists('hydration_cache_profile', $options)) {
376
            $query->expects($this->once())
377
                ->method('setHydrationCacheProfile')
378
                ->with($options['hydration_cache_profile']);
379
        }
380
381
        if (array_key_exists('result_set_mapping', $options)) {
382
            $query->expects($this->once())
383
                ->method('setResultSetMapping')
384
                ->with($options['result_set_mapping']);
385
        }
386
387
        if (!empty($options[QueryServiceOption::DQL]) && $query instanceof AbstractQuery) {
388
            $query->expects($this->once())
389
                ->method('setDQL')
390
                ->with($options[QueryServiceOption::DQL]);
391
        }
392
393
        $query->expects($this->once())->method('execute')->willReturn([]);
394
395
        $queryService->execute($query, $options);
396
    }
397
398
    /**
399
     * @return array<mixed>
400
     */
401
    public function getExecuteWillPrepareAndExecuteQueryData(): array
402
    {
403
        return [
404
            // Empty options
405
            [
406
                [],
407
            ],
408
409
            // Set parameters
410
            [
411
                [
412
                    'params' => [
413
                        'foo' => 'bar',
414
                        'baz' => 123,
415
                    ],
416
                ],
417
            ],
418
419
            // Hydration Mode
420
            [
421
                [
422
                    QueryServiceOption::HYDRATION_MODE => Query::HYDRATE_ARRAY,
423
                ],
424
            ],
425
        ];
426
    }
427
428
    /**
429
     * Assert that if an exception is raised when calling execute() that the exception is caught, logged and rethrown
430
     * as a QueryServiceException.
431
     *
432
     * @covers \Arp\DoctrineEntityRepository\Query\QueryService::execute
433
     * @covers \Arp\DoctrineEntityRepository\Query\QueryService::prepareQuery
434
     *
435
     * @throws QueryServiceException
436
     */
437
    public function testExecuteWillCatchAndThrowExceptionsASQueryServiceException(): void
438
    {
439
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
440
441
        /** @var AbstractQuery&MockObject $query */
442
        $query = $this->createMock(AbstractQuery::class);
443
444
        $exceptionCode = 1234;
445
        $exceptionMessage = 'This is a example exception message';
446
        $exception = new \Exception($exceptionMessage, $exceptionCode);
447
448
        $query->expects($this->once())
449
            ->method('execute')
450
            ->willThrowException($exception);
451
452
        $errorMessage = sprintf('Failed to execute query : %s', $exceptionMessage);
453
454
        $this->logger->expects($this->once())
455
            ->method('error')
456
            ->with($errorMessage, compact('exception'));
457
458
        $this->expectException(QueryServiceException::class);
459
        $this->expectExceptionMessage($errorMessage);
460
        $this->expectExceptionCode($exceptionCode);
461
462
        $queryService->execute($query);
463
    }
464
465
    /**
466
     * Assert that a valid query provided to execute() will be executed and the result set returned.
467
     *
468
     * @covers \Arp\DoctrineEntityRepository\Query\QueryService::execute
469
     * @covers \Arp\DoctrineEntityRepository\Query\QueryService::prepareQuery
470
     *
471
     * @throws QueryServiceException
472
     */
473
    public function testExecuteQueryWillReturnResultSet(): void
474
    {
475
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
476
477
        /** @var AbstractQuery&MockObject $query */
478
        $query = $this->createMock(AbstractQuery::class);
479
480
        /** @var EntityInterface[]&MockObject[] $resultSet */
481
        $resultSet = [
482
            $this->getMockForAbstractClass(EntityInterface::class),
483
            $this->getMockForAbstractClass(EntityInterface::class),
484
            $this->getMockForAbstractClass(EntityInterface::class),
485
        ];
486
487
        $query->expects($this->once())
488
            ->method('execute')
489
            ->willReturn($resultSet);
490
491
        $this->assertSame($resultSet, $queryService->execute($query));
492
    }
493
494
    /**
495
     * Assert that a valid query provided to execute() will be executed and the result set returned.
496
     *
497
     * @covers \Arp\DoctrineEntityRepository\Query\QueryService::execute
498
     * @covers \Arp\DoctrineEntityRepository\Query\QueryService::prepareQuery
499
     *
500
     * @throws QueryServiceException
501
     */
502
    public function testExecuteQueryBuilderWillReturnResultSet(): void
503
    {
504
        $queryService = new QueryService($this->entityName, $this->entityManager, $this->logger);
505
506
        /** @var AbstractQuery&MockObject $query */
507
        $query = $this->createMock(AbstractQuery::class);
508
509
        /** @var EntityInterface[]&MockObject[] $resultSet */
510
        $resultSet = [
511
            $this->getMockForAbstractClass(EntityInterface::class),
512
            $this->getMockForAbstractClass(EntityInterface::class),
513
            $this->getMockForAbstractClass(EntityInterface::class),
514
        ];
515
516
        $query->expects($this->once())
517
            ->method('execute')
518
            ->willReturn($resultSet);
519
520
        $this->assertSame($resultSet, $queryService->execute($query));
521
    }
522
}
523