BuildQueryTest::testFilterCriteria()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 4
eloc 17
c 2
b 2
f 0
nc 4
nop 3
dl 0
loc 29
rs 9.7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArpTest\DoctrineQueryFilter;
6
7
use Arp\DoctrineQueryFilter\Enum\FieldType;
8
use Arp\DoctrineQueryFilter\Enum\OrderByDirection;
9
use Arp\DoctrineQueryFilter\Exception\QueryFilterManagerException;
10
use Arp\DoctrineQueryFilter\Filter\FilterFactory;
11
use Arp\DoctrineQueryFilter\Metadata\Typecaster;
12
use Arp\DoctrineQueryFilter\Metadata\UniqidParamNameGenerator;
13
use Arp\DoctrineQueryFilter\QueryFilterManager;
14
use Arp\DoctrineQueryFilter\Sort\Field;
15
use Arp\DoctrineQueryFilter\Sort\SortFactory;
16
use Doctrine\ORM\EntityManager;
17
use Doctrine\ORM\Mapping\ClassMetadata;
18
use Doctrine\ORM\Query\Expr;
19
use Doctrine\ORM\Query\Parameter;
20
use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder;
21
use PHPUnit\Framework\MockObject\MockObject;
22
use PHPUnit\Framework\TestCase;
23
24
final class BuildQueryTest extends TestCase
25
{
26
    private QueryFilterManager $queryFilterManager;
27
28
    /**
29
     * @var array<string, array<string, mixed>>
30
     */
31
    private array $fieldMapping = [];
32
33
    /**
34
     * @var array<string, array<string, mixed>>
35
     */
36
    private array $associationMapping = [];
37
38
    public function setUp(): void
39
    {
40
        $this->fieldMapping = [
41
            'id' => [
42
                'type' => FieldType::INTEGER->value,
43
            ],
44
            'forename' => [
45
                'type' => FieldType::STRING->value,
46
            ],
47
            'username' => [
48
                'type' => FieldType::STRING->value,
49
            ],
50
            'age' => [
51
                'type' => FieldType::INTEGER->value,
52
            ],
53
            'enabled' => [
54
                'type' => FieldType::BOOLEAN->value,
55
            ],
56
            'createdDate' => [
57
                'type' => FieldType::DATETIME_MUTABLE->value,
58
            ]
59
        ];
60
61
        $this->queryFilterManager = new QueryFilterManager(
62
            new FilterFactory(new Typecaster(), new UniqidParamNameGenerator()),
63
            new SortFactory()
64
        );
65
    }
66
67
    /**
68
     * @dataProvider getFilterCriteriaData
69
     *
70
     * @throws QueryFilterManagerException
71
     */
72
    public function testFilterCriteria(array $criteria, string $expectedDql, array $expectedParams = []): void
73
    {
74
        $entityName = 'Customer';
75
        $queryAlias = 'c';
76
77
        $doctrineMetadata = $this->createDoctrineMetadata($entityName);
78
        $entityManager = $this->createEntityManager();
79
        $entityManager->method('getClassMetadata')
80
            ->with($entityName)
81
            ->willReturn($doctrineMetadata);
82
83
        $doctrineQueryBuilder = $this->createDoctrineQueryBuilder($entityManager)
84
            ->select($queryAlias)
85
            ->from($entityName, $queryAlias);
86
87
        $queryBuilder = $this->queryFilterManager->filter($doctrineQueryBuilder, $entityName, $criteria);
88
89
        $this->assertStringMatchesFormat($expectedDql, $queryBuilder->getDQL());
90
91
        if (!empty($expectedParams)) {
92
            /**
93
             * @var int $index
94
             * @var Parameter $parameter
95
             */
96
            foreach (array_values($queryBuilder->getParameters()->toArray()) as $index => $parameter) {
97
                if (!isset($expectedParams[$index])) {
98
                    $this->fail('Found unexpected parameter at index ' . $index);
99
                }
100
                $this->assertSame($expectedParams[$index], $parameter->getValue());
101
            }
102
        }
103
    }
104
105
    public function getFilterCriteriaData(): array
106
    {
107
        return [
108
            // Simple AND
109
            [
110
                [
111
                    'filters' => [
112
                        ['name' => 'eq', 'field' => 'forename', 'value' => 'Fred'],
113
                        ['name' => 'between', 'field' => 'age', 'from' => 18, 'to' => 30],
114
                    ],
115
                ],
116
                'SELECT c FROM Customer c WHERE c.forename = :%s AND (c.age BETWEEN :%s AND :%s)',
117
                ['Fred', 18, 30],
118
            ],
119
120
            // OR conditions
121
            [
122
                [
123
                    'filters' => [
124
                        ['name' => 'eq', 'field' => 'enabled', 'value' => true],
125
                        ['name' => 'or',
126
                            'conditions' => [
127
                                ['name' => 'eq', 'field' => 'username', 'value' => 'Fred'],
128
                                ['name' => 'eq', 'field' => 'username', 'value' => 'bob'],
129
                            ]
130
                        ],
131
                    ],
132
                ],
133
                'SELECT c FROM Customer c WHERE c.enabled = :%s AND (c.username = :%s OR c.username = :%s)',
134
                [true, 'Fred', 'bob',],
135
            ],
136
137
            // Sorting
138
            [
139
                [
140
                    'filters' => [
141
                        ['name' => 'eq', 'field' => 'id', 'value' => 123],
142
                    ],
143
                    'sort' => [
144
                        ['name' => Field::class, 'field' => 'id', 'direction' => OrderByDirection::DESC->value],
145
                        ['field' => 'createdDate']
146
                    ]
147
                ],
148
                'SELECT c FROM Customer c WHERE c.id = :%s ORDER BY c.id DESC, c.createdDate ASC',
149
                [123]
150
            ],
151
152
            // Like
153
            [
154
                [
155
                    'filters' => [
156
                        [
157
                            'name' => 'or',
158
                            'conditions' => [
159
                                ['name' => 'like', 'field' => 'forename', 'value' => '%Bob%'],
160
                                ['name' => 'gt', 'field' => 'age', 'value' => 18],
161
                            ]
162
                        ],
163
                        ['name' => 'eq', 'field' => 'enabled', 'value' => 1],
164
                    ]
165
                ],
166
                'SELECT c FROM Customer c WHERE (c.forename LIKE :%s OR c.age > :%s) AND c.enabled = :%s',
167
                ['%Bob%', 18, true],
168
            ]
169
        ];
170
    }
171
172
    /**
173
     * @param string $name
174
     *
175
     * @return ClassMetadata<object>
176
     */
177
    private function createDoctrineMetadata(string $name): ClassMetadata
178
    {
179
        $doctrineMetadata = $this->createMock(ClassMetadata::class);
180
181
        $doctrineMetadata->method('getName')
182
            ->willReturn($name);
183
184
        $doctrineMetadata->method('hasField')
185
            ->willReturnCallback(fn (string $fieldName) => isset($this->fieldMapping[$fieldName]));
186
187
        $doctrineMetadata->method('hasAssociation')
188
            ->willReturnCallback(fn (string $fieldName) => isset($this->associationMapping[$fieldName]));
189
190
        $doctrineMetadata->method('getFieldMapping')
191
            ->willReturnCallback(fn (string $fieldName) => $this->fieldMapping[$fieldName] ?? []);
192
193
        return $doctrineMetadata;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $doctrineMetadata returns the type PHPUnit\Framework\MockObject\MockObject which is incompatible with the type-hinted return Doctrine\ORM\Mapping\ClassMetadata.
Loading history...
194
    }
195
196
    private function createDoctrineQueryBuilder(EntityManager $entityManager): DoctrineQueryBuilder
197
    {
198
        return new DoctrineQueryBuilder($entityManager);
199
    }
200
201
    private function createEntityManager(): EntityManager&MockObject
202
    {
203
        $entityManager = $this->createMock(EntityManager::class);
204
205
        $entityManager->method('createQueryBuilder')
206
            ->willReturnCallback(fn () => $this->createDoctrineQueryBuilder($entityManager));
207
208
        $entityManager->method('getExpressionBuilder')
209
            ->willReturnCallback(static fn () => new Expr());
210
211
        return $entityManager;
212
    }
213
}
214