Completed
Push — master ( 45c7cd...b1ef76 )
by David
16s queued 14s
created

FindObjectsFromRawSqlQueryFactory   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 495
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 45
eloc 228
c 5
b 0
f 0
dl 0
loc 495
rs 8.8

17 Methods

Rating   Name   Duplication   Size   Complexity  
A processParsedSelectQuery() 0 19 2
A __construct() 0 7 1
B generateGroupedSqlCount() 0 97 4
A generateParsedSqlCount() 0 15 4
A getTableGroupName() 0 4 1
A compute() 0 14 3
A getColumnDescriptors() 0 3 1
A getMagicSqlCount() 0 3 1
A processParsedUnionQuery() 0 29 2
C formatSelect() 0 91 14
A generateSimpleSqlCount() 0 41 3
A isDistinctQuery() 0 8 4
A getMagicSql() 0 3 1
A generateWrappedSqlCount() 0 26 1
A getMagicSqlSubQuery() 0 3 1
A sort() 0 3 1
A getSubQueryColumnDescriptors() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like FindObjectsFromRawSqlQueryFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FindObjectsFromRawSqlQueryFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\QueryFactory;
5
6
use Doctrine\DBAL\Platforms\MySqlPlatform;
7
use Doctrine\DBAL\Schema\Schema;
8
use TheCodingMachine\TDBM\TDBMException;
9
use TheCodingMachine\TDBM\TDBMService;
10
use PHPSQLParser\PHPSQLCreator;
11
use PHPSQLParser\PHPSQLParser;
12
use function array_merge;
13
14
/**
15
 * This class is in charge of formatting the SQL passed to findObjectsFromRawSql method.
16
 */
17
class FindObjectsFromRawSqlQueryFactory implements QueryFactory
18
{
19
    /**
20
     * @var array[]
21
     */
22
    protected $columnDescriptors;
23
    /**
24
     * @var Schema
25
     */
26
    private $schema;
27
    /**
28
     * @var string
29
     */
30
    private $processedSql;
31
    /**
32
     * @var string
33
     */
34
    private $processedSqlCount;
35
    /**
36
     * @var TDBMService
37
     */
38
    private $tdbmService;
39
    /**
40
     * @var string
41
     */
42
    private $mainTable;
43
44
    /**
45
     * FindObjectsFromRawSqlQueryFactory constructor.
46
     * @param TDBMService $tdbmService
47
     * @param Schema $schema
48
     * @param string $mainTable
49
     * @param string $sql
50
     * @param string $sqlCount
51
     */
52
    public function __construct(TDBMService $tdbmService, Schema $schema, string $mainTable, string $sql, string $sqlCount = null)
53
    {
54
        $this->tdbmService = $tdbmService;
55
        $this->schema = $schema;
56
        $this->mainTable = $mainTable;
57
58
        [$this->processedSql, $this->processedSqlCount, $this->columnDescriptors] = $this->compute($sql, $sqlCount);
59
    }
60
61
    public function sort($orderBy): void
62
    {
63
        throw new TDBMException('sort not supported for raw sql queries');
64
    }
65
66
    public function getMagicSql(): string
67
    {
68
        return $this->processedSql;
69
    }
70
71
    public function getMagicSqlCount(): string
72
    {
73
        return $this->processedSqlCount;
74
    }
75
76
    public function getColumnDescriptors(): array
77
    {
78
        return $this->columnDescriptors;
79
    }
80
81
    /**
82
     * @param string $sql
83
     * @param null|string $sqlCount
84
     * @return mixed[] An array of 3 elements: [$processedSql, $processedSqlCount, $columnDescriptors]
85
     * @throws TDBMException
86
     */
87
    private function compute(string $sql, ?string $sqlCount): array
88
    {
89
        $parser = new PHPSQLParser();
90
        $parsedSql = $parser->parse($sql);
91
92
        if (isset($parsedSql['SELECT'])) {
93
            [$processedSql, $processedSqlCount, $columnDescriptors] = $this->processParsedSelectQuery($parsedSql, $sqlCount);
94
        } elseif (isset($parsedSql['UNION'])) {
95
            [$processedSql, $processedSqlCount, $columnDescriptors] = $this->processParsedUnionQuery($parsedSql, $sqlCount);
96
        } else {
97
            throw new TDBMException('Unable to analyze query "'.$sql.'"');
98
        }
99
100
        return [$processedSql, $processedSqlCount, $columnDescriptors];
101
    }
102
103
    /**
104
     * @param mixed[] $parsedSql
105
     * @param null|string $sqlCount
106
     * @return mixed[] An array of 3 elements: [$processedSql, $processedSqlCount, $columnDescriptors]
107
     * @throws \PHPSQLParser\exceptions\UnsupportedFeatureException
108
     */
109
    private function processParsedUnionQuery(array $parsedSql, ?string $sqlCount): array
110
    {
111
        $selects = $parsedSql['UNION'];
112
113
        $parsedSqlList = [];
114
        $columnDescriptors = [];
115
116
        foreach ($selects as $select) {
117
            [$selectProcessedSql, $selectProcessedCountSql, $columnDescriptors] = $this->processParsedSelectQuery($select, '');
118
119
            // Let's reparse the returned SQL (not the most efficient way of doing things)
120
            $parser = new PHPSQLParser();
121
            $parsedSql = $parser->parse($selectProcessedSql);
122
123
            $parsedSqlList[] = $parsedSql;
124
        }
125
126
        // Let's rebuild the UNION query
127
        $query = ['UNION' => $parsedSqlList];
128
129
        // The count is the SUM of the count of the UNIONs
130
        $countQuery = $this->generateWrappedSqlCount($query);
131
132
        $generator = new PHPSQLCreator();
133
134
        $processedSql = $generator->create($query);
135
        $processedSqlCount = $generator->create($countQuery);
136
137
        return [$processedSql, $sqlCount ?? $processedSqlCount, $columnDescriptors];
138
    }
139
140
    /**
141
     * @param mixed[] $parsedSql
142
     * @param null|string $sqlCount
143
     * @return mixed[] An array of 3 elements: [$processedSql, $processedSqlCount, $columnDescriptors]
144
     */
145
    private function processParsedSelectQuery(array $parsedSql, ?string $sqlCount): array
146
    {
147
        // 1: let's reformat the SELECT and construct our columns
148
        list($select, $countSelect, $columnDescriptors) = $this->formatSelect($parsedSql['SELECT']);
149
        $generator = new PHPSQLCreator();
150
        $parsedSql['SELECT'] = $select;
151
        $processedSql = $generator->create($parsedSql);
152
153
        // 2: let's compute the count query if needed
154
        if ($sqlCount === null) {
155
            $parsedCountSql = $parsedSql;
156
            $parsedCountSql['SELECT'] = $countSelect;
157
            $parsedSqlCount = $this->generateParsedSqlCount($parsedCountSql);
158
            $processedSqlCount = $generator->create($parsedSqlCount);
159
        } else {
160
            $processedSqlCount = $sqlCount;
161
        }
162
163
        return [$processedSql, $processedSqlCount, $columnDescriptors];
164
    }
165
166
    /**
167
     * @param mixed[] $baseSelect
168
     * @return mixed[] An array of 2 elements: [$formattedSelect, $columnDescriptors]
169
     * @throws TDBMException
170
     * @throws \Doctrine\DBAL\Schema\SchemaException
171
     */
172
    private function formatSelect(array $baseSelect): array
173
    {
174
        $relatedTables = $this->tdbmService->_getRelatedTablesByInheritance($this->mainTable);
175
        $tableGroup = $this->getTableGroupName($relatedTables);
176
177
        $connection = $this->tdbmService->getConnection();
178
        $formattedSelect = [];
179
        $formattedCountSelect = [];
180
        $columnDescriptors = [];
181
        $fetchedTables = [];
182
183
        foreach ($baseSelect as $entry) {
184
            if ($entry['expr_type'] !== 'colref') {
185
                $formattedSelect[] = $entry;
186
                $formattedCountSelect[] = $entry;
187
                continue;
188
            }
189
190
            $noQuotes = $entry['no_quotes'];
191
            if ($noQuotes['delim'] !== '.' || count($noQuotes['parts']) !== 2) {
192
                $formattedSelect[] = $entry;
193
                $formattedCountSelect[] = $entry;
194
                continue;
195
            }
196
197
            $tableName = $noQuotes['parts'][0];
198
            if (!in_array($tableName, $relatedTables)) {
199
                $formattedSelect[] = $entry;
200
                $formattedCountSelect[] = $entry;
201
                continue;
202
            }
203
204
            $columnName = $noQuotes['parts'][1];
205
            if ($columnName !== '*') {
206
                $formattedSelect[] = $entry;
207
                $formattedCountSelect[] = $entry;
208
                continue;
209
            }
210
211
            $table = $this->schema->getTable($tableName);
212
            $pkColumns = $table->getPrimaryKeyColumns();
213
            foreach ($table->getColumns() as $column) {
214
                $columnName = $column->getName();
215
                $alias = AbstractQueryFactory::getColumnAlias($tableName, $columnName);
216
                $astColumn = [
217
                    'expr_type' => 'colref',
218
                    'base_expr' => $connection->quoteIdentifier($tableName) . '.' . $connection->quoteIdentifier($columnName),
219
                    'no_quotes' => [
220
                        'delim' => '.',
221
                        'parts' => [
222
                            $tableName,
223
                            $columnName
224
                        ]
225
                    ],
226
                    'alias' => [
227
                        'as' => true,
228
                        'name' => $connection->quoteIdentifier($alias),
229
                    ]
230
                ];
231
                $formattedSelect[] = $astColumn;
232
                if (in_array($columnName, $pkColumns)) {
233
                    $formattedCountSelect[] = $astColumn;
234
                }
235
                $columnDescriptors[$alias] = [
236
                    'as' => $alias,
237
                    'table' => $tableName,
238
                    'column' => $columnName,
239
                    'type' => $column->getType(),
240
                    'tableGroup' => $tableGroup,
241
                ];
242
            }
243
            $fetchedTables[] = $tableName;
244
        }
245
246
        $missingTables = array_diff($relatedTables, $fetchedTables);
247
        if (!empty($missingTables)) {
248
            throw new TDBMException('Missing tables '.implode(', ', $missingTables).' in SELECT statement');
249
        }
250
251
        for ($i = 0; $i < count($formattedSelect) - 1; $i++) {
252
            if (!isset($formattedSelect[$i]['delim'])) {
253
                $formattedSelect[$i]['delim'] = ',';
254
            }
255
        }
256
257
        for ($i = 0; $i < count($formattedCountSelect) - 1; $i++) {
258
            if (!isset($formattedCountSelect[$i]['delim'])) {
259
                $formattedCountSelect[$i]['delim'] = ',';
260
            }
261
        }
262
        return [$formattedSelect, $formattedCountSelect, $columnDescriptors];
263
    }
264
265
    /**
266
     * @param mixed[] $parsedSql
267
     * @return mixed[]
268
     */
269
    private function generateParsedSqlCount(array $parsedSql): array
270
    {
271
        if (isset($parsedSql['ORDER'])) {
272
            unset($parsedSql['ORDER']);
273
        }
274
275
        if (!isset($parsedSql['GROUP'])) {
276
            // most simple case:no GROUP BY in query
277
            return $this->generateSimpleSqlCount($parsedSql);
278
        } elseif (!isset($parsedSql['HAVING'])) {
279
            // GROUP BY without HAVING statement: let's COUNT the DISTINCT grouped columns
280
            return $this->generateGroupedSqlCount($parsedSql);
281
        } else {
282
            // GROUP BY with a HAVING statement: we'll have to wrap the query
283
            return $this->generateWrappedSqlCount($parsedSql);
284
        }
285
    }
286
287
    /**
288
     * @param mixed[] $parsedSql The AST of the SQL query
289
     * @return mixed[] An AST representing the matching COUNT query
290
     */
291
    private function generateSimpleSqlCount(array $parsedSql): array
292
    {
293
        // If the query is a DISTINCT, we need to deal with the count.
294
295
296
        // We need to count on the same columns: COUNT(DISTINCT country.id, country.label) ....
297
        // but we need to remove the "alias" bit.
298
299
        if ($this->isDistinctQuery($parsedSql)) {
300
            // Only MySQL can do DISTINCT counts.
301
            // Other databases should wrap the query
302
            if (!$this->tdbmService->getConnection()->getSchemaManager()->getDatabasePlatform() instanceof MySqlPlatform) {
303
                return $this->generateWrappedSqlCount($parsedSql);
304
            }
305
306
            $countSubExpr = array_map(function (array $item) {
307
                unset($item['alias']);
308
                return $item;
309
            }, $parsedSql['SELECT']);
310
        } else {
311
            $countSubExpr = [
312
                [
313
                'expr_type' => 'colref',
314
                'base_expr' => '*',
315
                'sub_tree' => false
316
                ]
317
            ];
318
        }
319
320
        $parsedSql['SELECT'] = [[
321
            'expr_type' => 'aggregate_function',
322
            'alias' => [
323
                'as' => true,
324
                'name' => 'cnt',
325
            ],
326
            'base_expr' => 'COUNT',
327
            'sub_tree' => $countSubExpr,
328
            'delim' => false,
329
        ]];
330
331
        return $parsedSql;
332
    }
333
334
    /**
335
     * @param mixed[] $parsedSql AST to analyze
336
     * @return bool
337
     */
338
    private function isDistinctQuery(array $parsedSql): bool
339
    {
340
        foreach ($parsedSql['SELECT'] as $item) {
341
            if ($item['expr_type'] === 'reserved' && $item['base_expr'] === 'DISTINCT') {
342
                return true;
343
            }
344
        }
345
        return false;
346
    }
347
348
    /**
349
     * @param mixed[] $parsedSql The AST of the SQL query
350
     * @return mixed[] An AST representing the matching COUNT query
351
     */
352
    private function generateGroupedSqlCount(array $parsedSql): array
353
    {
354
        $group = $parsedSql['GROUP'];
355
        unset($parsedSql['GROUP']);
356
357
        // Count(DISTINCT ...) on multiple columns is only valid in MySQL (unsupported on Pgsql or Oracle). For those, we need to do a subquery.
358
        if (count($group) === 1 || $this->tdbmService->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
359
            $parsedSql['SELECT'] = [[
360
                'expr_type' => 'aggregate_function',
361
                'alias' => [
362
                    'as' => true,
363
                    'name' => 'cnt',
364
                ],
365
                'base_expr' => 'COUNT',
366
                'sub_tree' => array_merge([[
367
                    'expr_type' => 'reserved',
368
                    'base_expr' => 'DISTINCT',
369
                    'delim' => ','
370
                ]], $group),
371
                'delim' => false,
372
            ]];
373
        } else {
374
            $innerColumns = [[
375
                'expr_type' => 'reserved',
376
                'base_expr' => 'DISTINCT',
377
                'delim' => ' '
378
            ]];
379
            foreach ($group as $item) {
380
                $item['delim'] = ',';
381
                $innerColumns[] = $item;
382
            }
383
            $innerColumns[count($innerColumns)-1]['delim'] = false;
384
            $parsedSql['SELECT'] = $innerColumns;
385
386
            $parsedSql = [
387
                'SELECT' =>
388
                    [
389
                        0 =>
390
                            [
391
                                'expr_type' => 'aggregate_function',
392
                                'alias' =>
393
                                    [
394
                                        'as' => true,
395
                                        'name' => 'cnt',
396
                                        'base_expr' => 'AS cnt',
397
                                        'no_quotes' =>
398
                                            [
399
                                                'delim' => false,
400
                                                'parts' =>
401
                                                    [
402
                                                        0 => 'cnt',
403
                                                    ],
404
                                            ],
405
                                    ],
406
                                'base_expr' => 'COUNT',
407
                                'sub_tree' =>
408
                                    [
409
                                        0 =>
410
                                            [
411
                                                'expr_type' => 'colref',
412
                                                'base_expr' => '*',
413
                                                'sub_tree' => false,
414
                                            ],
415
                                    ],
416
                                'delim' => false,
417
                            ],
418
                    ],
419
                'FROM' =>
420
                    [
421
                        0 =>
422
                            [
423
                                'expr_type' => 'subquery',
424
                                'alias' =>
425
                                    [
426
                                        'as' => false,
427
                                        'name' => 'subquery',
428
                                        'no_quotes' =>
429
                                            [
430
                                                'delim' => false,
431
                                                'parts' =>
432
                                                    [
433
                                                        0 => 'subquery',
434
                                                    ],
435
                                            ],
436
                                        'base_expr' => 'subquery',
437
                                    ],
438
                                'hints' => false,
439
                                'join_type' => 'JOIN',
440
                                'ref_type' => false,
441
                                'ref_clause' => false,
442
                                //'base_expr' => 'SELECT id FROM country',
443
                                'sub_tree' => $parsedSql
444
                            ],
445
                    ],
446
                ];
447
        }
448
        return $parsedSql;
449
    }
450
451
    /**
452
     * @param mixed[] $parsedSql The AST of the SQL query
453
     * @return mixed[] An AST representing the matching COUNT query
454
     */
455
    private function generateWrappedSqlCount(array $parsedSql): array
456
    {
457
        return [
458
            'SELECT' => [[
459
                'expr_type' => 'aggregate_function',
460
                'alias' => [
461
                    'as' => true,
462
                    'name' => 'cnt',
463
                ],
464
                'base_expr' => 'COUNT',
465
                'sub_tree' => [
466
                    [
467
                        'expr_type' => 'colref',
468
                        'base_expr' => '*',
469
                        'sub_tree' => false
470
                    ]
471
                ],
472
                'delim' => false,
473
            ]],
474
            'FROM' => [[
475
                'expr_type' => 'subquery',
476
                'alias' => [
477
                    'as' => true,
478
                    'name' => '____query'
479
                ],
480
                'sub_tree' => $parsedSql,
481
            ]]
482
        ];
483
    }
484
485
    /**
486
     * @param string[] $relatedTables
487
     * @return string
488
     */
489
    protected function getTableGroupName(array $relatedTables): string
490
    {
491
        sort($relatedTables);
492
        return implode('_``_', $relatedTables);
493
    }
494
495
    /**
496
     * Returns a sub-query to be used in another query.
497
     * A sub-query is similar to a query except it returns only the primary keys of the table (to be used as filters)
498
     *
499
     * @return string
500
     */
501
    public function getMagicSqlSubQuery(): string
502
    {
503
        throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
504
    }
505
506
    /**
507
     * @return array<int, array{table: string, column: string}> An array of column descriptors.
508
     */
509
    public function getSubQueryColumnDescriptors(): array
510
    {
511
        throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
512
    }
513
}
514