generateGroupedSqlCount()   B
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 97
Code Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 61
c 2
b 0
f 0
dl 0
loc 97
rs 8.8509
cc 4
nc 2
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace TheCodingMachine\TDBM\QueryFactory;
6
7
use Doctrine\DBAL\Platforms\MySQLPlatform;
8
use Doctrine\DBAL\Schema\Schema;
9
use PHPSQLParser\builders\OrderByBuilder;
10
use PHPSQLParser\builders\SelectStatementBuilder;
11
use TheCodingMachine\TDBM\TDBMException;
12
use TheCodingMachine\TDBM\TDBMService;
13
use PHPSQLParser\PHPSQLCreator;
14
use PHPSQLParser\PHPSQLParser;
15
16
use function array_merge;
17
18
/**
19
 * This class is in charge of formatting the SQL passed to findObjectsFromRawSql method.
20
 */
21
class FindObjectsFromRawSqlQueryFactory implements QueryFactory
22
{
23
    /**
24
     * @var array[]
25
     */
26
    protected $columnDescriptors;
27
    /**
28
     * @var Schema
29
     */
30
    private $schema;
31
    /**
32
     * @var string
33
     */
34
    private $processedSql;
35
    /**
36
     * @var string
37
     */
38
    private $processedSqlCount;
39
    /**
40
     * @var TDBMService
41
     */
42
    private $tdbmService;
43
    /**
44
     * @var string
45
     */
46
    private $mainTable;
47
48
    /**
49
     * FindObjectsFromRawSqlQueryFactory constructor.
50
     * @param TDBMService $tdbmService
51
     * @param Schema $schema
52
     * @param string $mainTable
53
     * @param string $sql
54
     * @param string $sqlCount
55
     */
56
    public function __construct(TDBMService $tdbmService, Schema $schema, string $mainTable, string $sql, string $sqlCount = null)
57
    {
58
        $this->tdbmService = $tdbmService;
59
        $this->schema = $schema;
60
        $this->mainTable = $mainTable;
61
62
        [$this->processedSql, $this->processedSqlCount, $this->columnDescriptors] = $this->compute($sql, $sqlCount);
63
    }
64
65
    public function sort($orderBy): void
66
    {
67
        throw new TDBMException('sort not supported for raw sql queries');
68
    }
69
70
    public function getMagicSql(): string
71
    {
72
        return $this->processedSql;
73
    }
74
75
    public function getMagicSqlCount(): string
76
    {
77
        return $this->processedSqlCount;
78
    }
79
80
    public function getColumnDescriptors(): array
81
    {
82
        return $this->columnDescriptors;
83
    }
84
85
    /**
86
     * @param string $sql
87
     * @param null|string $sqlCount
88
     * @return mixed[] An array of 3 elements: [$processedSql, $processedSqlCount, $columnDescriptors]
89
     * @throws TDBMException
90
     */
91
    private function compute(string $sql, ?string $sqlCount): array
92
    {
93
        $parser = new PHPSQLParser();
94
        $parsedSql = $parser->parse($sql);
95
96
        if (isset($parsedSql['SELECT'])) {
97
            [$processedSql, $processedSqlCount, $columnDescriptors] = $this->processParsedSelectQuery($parsedSql, $sqlCount);
98
        } elseif (isset($parsedSql['UNION'])) {
99
            [$processedSql, $processedSqlCount, $columnDescriptors] = $this->processParsedUnionQuery($parsedSql, $sqlCount);
100
        } else {
101
            throw new TDBMException('Unable to analyze query "'.$sql.'"');
102
        }
103
104
        return [$processedSql, $processedSqlCount, $columnDescriptors];
105
    }
106
107
    /**
108
     * @param mixed[] $parsedSql
109
     * @param null|string $sqlCount
110
     * @return mixed[] An array of 3 elements: [$processedSql, $processedSqlCount, $columnDescriptors]
111
     * @throws \PHPSQLParser\exceptions\UnsupportedFeatureException|\PHPSQLParser\exceptions\UnableToCreateSQLException
112
     */
113
    private function processParsedUnionQuery(array $parsedSql, ?string $sqlCount): array
114
    {
115
        $selects = $parsedSql['UNION'];
116
117
        $parsedSqlList = [];
118
        $columnDescriptors = [];
119
120
        foreach ($selects as $select) {
121
            [$selectProcessedSql, $selectProcessedCountSql, $columnDescriptors] = $this->processParsedSelectQuery($select, '');
122
123
            // Let's reparse the returned SQL (not the most efficient way of doing things)
124
            $parser = new PHPSQLParser();
125
            $parsedSelectSql = $parser->parse($selectProcessedSql);
126
127
            $parsedSqlList[] = $parsedSelectSql;
128
        }
129
130
        // Let's rebuild the UNION query
131
        $query = ['UNION' => $parsedSqlList];
132
133
        // The count is the SUM of the count of the UNIONs
134
        $countQuery = $this->generateWrappedSqlCount($query);
135
136
        $generator = new PHPSQLCreator();
137
138
        // Replaced the default generator by our own to add parenthesis around each SELECT
139
        $processedSql = $this->buildUnion($query);
140
        $processedSqlCount = $generator->create($countQuery);
141
142
        // Let's add the ORDER BY if any
143
        if (isset($parsedSql['0']['ORDER'])) {
144
            $orderByBuilder = new OrderByBuilder();
145
            $processedSql .= " " . $orderByBuilder->build($parsedSql['0']['ORDER']);
146
        }
147
148
        return [$processedSql, $sqlCount ?? $processedSqlCount, $columnDescriptors];
149
    }
150
151
    /**
152
     * @param mixed[] $parsed
153
     */
154
    private function buildUnion(array $parsed): string
155
    {
156
        $selectBuilder = new SelectStatementBuilder();
157
158
        return implode(' UNION ', array_map(function ($clause) use ($selectBuilder) {
159
            return '(' . $selectBuilder->build($clause) . ')';
160
        }, $parsed['UNION']));
161
    }
162
163
    /**
164
     * @param mixed[] $parsedSql
165
     * @param null|string $sqlCount
166
     * @return mixed[] An array of 3 elements: [$processedSql, $processedSqlCount, $columnDescriptors]
167
     */
168
    private function processParsedSelectQuery(array $parsedSql, ?string $sqlCount): array
169
    {
170
        // 1: let's reformat the SELECT and construct our columns
171
        list($select, $countSelect, $columnDescriptors) = $this->formatSelect($parsedSql['SELECT']);
172
        $generator = new PHPSQLCreator();
173
        $parsedSql['SELECT'] = $select;
174
        $processedSql = $generator->create($parsedSql);
175
176
        // 2: let's compute the count query if needed
177
        if ($sqlCount === null) {
178
            $parsedCountSql = $parsedSql;
179
            $parsedCountSql['SELECT'] = $countSelect;
180
            $parsedSqlCount = $this->generateParsedSqlCount($parsedCountSql);
181
            $processedSqlCount = $generator->create($parsedSqlCount);
182
        } else {
183
            $processedSqlCount = $sqlCount;
184
        }
185
186
        return [$processedSql, $processedSqlCount, $columnDescriptors];
187
    }
188
189
    /**
190
     * @param mixed[] $baseSelect
191
     * @return mixed[] An array of 2 elements: [$formattedSelect, $columnDescriptors]
192
     * @throws TDBMException
193
     * @throws \Doctrine\DBAL\Schema\SchemaException
194
     */
195
    private function formatSelect(array $baseSelect): array
196
    {
197
        $relatedTables = $this->tdbmService->_getRelatedTablesByInheritance($this->mainTable);
198
        $tableGroup = $this->getTableGroupName($relatedTables);
199
200
        $connection = $this->tdbmService->getConnection();
201
        $formattedSelect = [];
202
        $formattedCountSelect = [];
203
        $columnDescriptors = [];
204
        $fetchedTables = [];
205
206
        foreach ($baseSelect as $entry) {
207
            if ($entry['expr_type'] !== 'colref') {
208
                $formattedSelect[] = $entry;
209
                $formattedCountSelect[] = $entry;
210
                continue;
211
            }
212
213
            $noQuotes = $entry['no_quotes'];
214
            if ($noQuotes['delim'] !== '.' || count($noQuotes['parts']) !== 2) {
215
                $formattedSelect[] = $entry;
216
                $formattedCountSelect[] = $entry;
217
                continue;
218
            }
219
220
            $tableName = $noQuotes['parts'][0];
221
            if (!in_array($tableName, $relatedTables)) {
222
                $formattedSelect[] = $entry;
223
                $formattedCountSelect[] = $entry;
224
                continue;
225
            }
226
227
            $columnName = $noQuotes['parts'][1];
228
            if ($columnName !== '*') {
229
                $formattedSelect[] = $entry;
230
                $formattedCountSelect[] = $entry;
231
                continue;
232
            }
233
234
            $table = $this->schema->getTable($tableName);
235
            $pkColumns = $table->getPrimaryKeyColumns();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Schema\Table::getPrimaryKeyColumns() has been deprecated: Use {@see getPrimaryKey()} and {@see Index::getColumns()} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

235
            $pkColumns = /** @scrutinizer ignore-deprecated */ $table->getPrimaryKeyColumns();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
236
            foreach ($table->getColumns() as $column) {
237
                $columnName = $column->getName();
238
                $alias = AbstractQueryFactory::getColumnAlias($tableName, $columnName);
239
                $astColumn = [
240
                    'expr_type' => 'colref',
241
                    'base_expr' => $connection->quoteIdentifier($tableName) . '.' . $connection->quoteIdentifier($columnName),
242
                    'no_quotes' => [
243
                        'delim' => '.',
244
                        'parts' => [
245
                            $tableName,
246
                            $columnName
247
                        ]
248
                    ],
249
                    'alias' => [
250
                        'as' => true,
251
                        'name' => $connection->quoteIdentifier($alias),
252
                    ]
253
                ];
254
                $formattedSelect[] = $astColumn;
255
                if (array_key_exists($columnName, $pkColumns)) {
256
                    $formattedCountSelect[] = $astColumn;
257
                }
258
                $columnDescriptors[$alias] = [
259
                    'as' => $alias,
260
                    'table' => $tableName,
261
                    'column' => $columnName,
262
                    'type' => $column->getType(),
263
                    'tableGroup' => $tableGroup,
264
                ];
265
            }
266
            $fetchedTables[] = $tableName;
267
        }
268
269
        $missingTables = array_diff($relatedTables, $fetchedTables);
270
        if (!empty($missingTables)) {
271
            throw new TDBMException('Missing tables '.implode(', ', $missingTables).' in SELECT statement');
272
        }
273
274
        for ($i = 0; $i < count($formattedSelect) - 1; $i++) {
275
            if (!isset($formattedSelect[$i]['delim'])) {
276
                $formattedSelect[$i]['delim'] = ',';
277
            }
278
        }
279
280
        for ($i = 0; $i < count($formattedCountSelect) - 1; $i++) {
281
            if (!isset($formattedCountSelect[$i]['delim'])) {
282
                $formattedCountSelect[$i]['delim'] = ',';
283
            }
284
        }
285
        return [$formattedSelect, $formattedCountSelect, $columnDescriptors];
286
    }
287
288
    /**
289
     * @param mixed[] $parsedSql
290
     * @return mixed[]
291
     */
292
    private function generateParsedSqlCount(array $parsedSql): array
293
    {
294
        if (isset($parsedSql['ORDER'])) {
295
            unset($parsedSql['ORDER']);
296
        }
297
298
        if (!isset($parsedSql['GROUP'])) {
299
            // most simple case:no GROUP BY in query
300
            return $this->generateSimpleSqlCount($parsedSql);
301
        } elseif (!isset($parsedSql['HAVING'])) {
302
            // GROUP BY without HAVING statement: let's COUNT the DISTINCT grouped columns
303
            return $this->generateGroupedSqlCount($parsedSql);
304
        } else {
305
            // GROUP BY with a HAVING statement: we'll have to wrap the query
306
            return $this->generateWrappedSqlCount($parsedSql);
307
        }
308
    }
309
310
    /**
311
     * @param mixed[] $parsedSql The AST of the SQL query
312
     * @return mixed[] An AST representing the matching COUNT query
313
     */
314
    private function generateSimpleSqlCount(array $parsedSql): array
315
    {
316
        // If the query is a DISTINCT, we need to deal with the count.
317
318
319
        // We need to count on the same columns: COUNT(DISTINCT country.id, country.label) ....
320
        // but we need to remove the "alias" bit.
321
322
        if ($this->isDistinctQuery($parsedSql)) {
323
            // Only MySQL can do DISTINCT counts.
324
            // Other databases should wrap the query
325
            if (!$this->tdbmService->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
326
                return $this->generateWrappedSqlCount($parsedSql);
327
            }
328
329
            $countSubExpr = array_map(function (array $item) {
330
                unset($item['alias']);
331
                return $item;
332
            }, $parsedSql['SELECT']);
333
        } else {
334
            $countSubExpr = [
335
                [
336
                'expr_type' => 'colref',
337
                'base_expr' => '*',
338
                'sub_tree' => false
339
                ]
340
            ];
341
        }
342
343
        $parsedSql['SELECT'] = [[
344
            'expr_type' => 'aggregate_function',
345
            'alias' => [
346
                'as' => true,
347
                'name' => 'cnt',
348
            ],
349
            'base_expr' => 'COUNT',
350
            'sub_tree' => $countSubExpr,
351
            'delim' => false,
352
        ]];
353
354
        return $parsedSql;
355
    }
356
357
    /**
358
     * @param mixed[] $parsedSql AST to analyze
359
     * @return bool
360
     */
361
    private function isDistinctQuery(array $parsedSql): bool
362
    {
363
        foreach ($parsedSql['SELECT'] as $item) {
364
            if ($item['expr_type'] === 'reserved' && $item['base_expr'] === 'DISTINCT') {
365
                return true;
366
            }
367
        }
368
        return false;
369
    }
370
371
    /**
372
     * @param mixed[] $parsedSql The AST of the SQL query
373
     * @return mixed[] An AST representing the matching COUNT query
374
     */
375
    private function generateGroupedSqlCount(array $parsedSql): array
376
    {
377
        $group = $parsedSql['GROUP'];
378
        unset($parsedSql['GROUP']);
379
380
        // Count(DISTINCT ...) on multiple columns is only valid in MySQL (unsupported on Pgsql or Oracle). For those, we need to do a subquery.
381
        if (count($group) === 1 || $this->tdbmService->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
382
            $parsedSql['SELECT'] = [[
383
                'expr_type' => 'aggregate_function',
384
                'alias' => [
385
                    'as' => true,
386
                    'name' => 'cnt',
387
                ],
388
                'base_expr' => 'COUNT',
389
                'sub_tree' => array_merge([[
390
                    'expr_type' => 'reserved',
391
                    'base_expr' => 'DISTINCT',
392
                    'delim' => ','
393
                ]], $group),
394
                'delim' => false,
395
            ]];
396
        } else {
397
            $innerColumns = [[
398
                'expr_type' => 'reserved',
399
                'base_expr' => 'DISTINCT',
400
                'delim' => ' '
401
            ]];
402
            foreach ($group as $item) {
403
                $item['delim'] = ',';
404
                $innerColumns[] = $item;
405
            }
406
            $innerColumns[count($innerColumns) - 1]['delim'] = false;
407
            $parsedSql['SELECT'] = $innerColumns;
408
409
            $parsedSql = [
410
                'SELECT' =>
411
                    [
412
                        0 =>
413
                            [
414
                                'expr_type' => 'aggregate_function',
415
                                'alias' =>
416
                                    [
417
                                        'as' => true,
418
                                        'name' => 'cnt',
419
                                        'base_expr' => 'AS cnt',
420
                                        'no_quotes' =>
421
                                            [
422
                                                'delim' => false,
423
                                                'parts' =>
424
                                                    [
425
                                                        0 => 'cnt',
426
                                                    ],
427
                                            ],
428
                                    ],
429
                                'base_expr' => 'COUNT',
430
                                'sub_tree' =>
431
                                    [
432
                                        0 =>
433
                                            [
434
                                                'expr_type' => 'colref',
435
                                                'base_expr' => '*',
436
                                                'sub_tree' => false,
437
                                            ],
438
                                    ],
439
                                'delim' => false,
440
                            ],
441
                    ],
442
                'FROM' =>
443
                    [
444
                        0 =>
445
                            [
446
                                'expr_type' => 'subquery',
447
                                'alias' =>
448
                                    [
449
                                        'as' => false,
450
                                        'name' => 'subquery',
451
                                        'no_quotes' =>
452
                                            [
453
                                                'delim' => false,
454
                                                'parts' =>
455
                                                    [
456
                                                        0 => 'subquery',
457
                                                    ],
458
                                            ],
459
                                        'base_expr' => 'subquery',
460
                                    ],
461
                                'hints' => false,
462
                                'join_type' => 'JOIN',
463
                                'ref_type' => false,
464
                                'ref_clause' => false,
465
                                //'base_expr' => 'SELECT id FROM country',
466
                                'sub_tree' => $parsedSql
467
                            ],
468
                    ],
469
                ];
470
        }
471
        return $parsedSql;
472
    }
473
474
    /**
475
     * @param mixed[] $parsedSql The AST of the SQL query
476
     * @return mixed[] An AST representing the matching COUNT query
477
     */
478
    private function generateWrappedSqlCount(array $parsedSql): array
479
    {
480
        return [
481
            'SELECT' => [[
482
                'expr_type' => 'aggregate_function',
483
                'alias' => [
484
                    'as' => true,
485
                    'name' => 'cnt',
486
                ],
487
                'base_expr' => 'COUNT',
488
                'sub_tree' => [
489
                    [
490
                        'expr_type' => 'colref',
491
                        'base_expr' => '*',
492
                        'sub_tree' => false
493
                    ]
494
                ],
495
                'delim' => false,
496
            ]],
497
            'FROM' => [[
498
                'expr_type' => 'subquery',
499
                'alias' => [
500
                    'as' => true,
501
                    'name' => '____query'
502
                ],
503
                'sub_tree' => $parsedSql,
504
            ]]
505
        ];
506
    }
507
508
    /**
509
     * @param string[] $relatedTables
510
     * @return string
511
     */
512
    protected function getTableGroupName(array $relatedTables): string
513
    {
514
        sort($relatedTables);
515
        return implode('_``_', $relatedTables);
516
    }
517
518
    /**
519
     * Returns a sub-query to be used in another query.
520
     * A sub-query is similar to a query except it returns only the primary keys of the table (to be used as filters)
521
     *
522
     * @return string
523
     */
524
    public function getMagicSqlSubQuery(): string
525
    {
526
        throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
527
    }
528
529
    /**
530
     * @return array<int, array{table: string, column: string}> An array of column descriptors.
531
     */
532
    public function getSubQueryColumnDescriptors(): array
533
    {
534
        throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
535
    }
536
}
537