AbstractQueryFactory::getColumnsList()   F
last analyzed

Complexity

Conditions 20
Paths 372

Size

Total Lines 119
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 73
c 0
b 0
f 0
dl 0
loc 119
rs 1.1833
cc 20
nc 372
nop 4

How to fix   Long Method    Complexity   

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 TheCodingMachine\TDBM\OrderByAnalyzer;
10
use TheCodingMachine\TDBM\TDBMInvalidArgumentException;
11
use TheCodingMachine\TDBM\TDBMService;
12
use TheCodingMachine\TDBM\UncheckedOrderBy;
13
14
use function array_unique;
15
use function in_array;
16
17
abstract class AbstractQueryFactory implements QueryFactory
18
{
19
    /**
20
     * @var TDBMService
21
     */
22
    protected $tdbmService;
23
24
    /**
25
     * @var Schema
26
     */
27
    protected $schema;
28
29
    /**
30
     * @var OrderByAnalyzer
31
     */
32
    protected $orderByAnalyzer;
33
34
    /**
35
     * @var string|UncheckedOrderBy|null
36
     */
37
    protected $orderBy;
38
39
    /**
40
     * @var string|null
41
     */
42
    protected $magicSql;
43
    /**
44
     * @var string|null
45
     */
46
    protected $magicSqlCount;
47
    /**
48
     * @var string|null
49
     */
50
    protected $magicSqlSubQuery;
51
    /** @var array<string, array<string, mixed>>|null */
52
    protected $columnDescList;
53
    /** @var array<int, array{table: string, column: string}>|null */
54
    protected $subQueryColumnDescList;
55
    /** @var string */
56
    protected $mainTable;
57
58
    /**
59
     * @param TDBMService $tdbmService
60
     * @param Schema $schema
61
     * @param OrderByAnalyzer $orderByAnalyzer
62
     * @param string|UncheckedOrderBy|null $orderBy
63
     */
64
    public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, string $mainTable, $orderBy)
65
    {
66
        $this->tdbmService = $tdbmService;
67
        $this->schema = $schema;
68
        $this->orderByAnalyzer = $orderByAnalyzer;
69
        $this->orderBy = $orderBy;
70
        $this->mainTable = $mainTable;
71
    }
72
73
    /**
74
     * Returns the column list that must be fetched for the SQL request.
75
     *
76
     * Note: MySQL dictates that ORDER BYed columns should appear in the SELECT clause.
77
     *
78
     * @param string $mainTable
79
     * @param string[] $additionalTablesFetch
80
     * @param string|UncheckedOrderBy|null $orderBy
81
     *
82
     * @param bool $canAddAdditionalTablesFetch Set to true if the function can add additional tables to fetch (so if the factory generates its own FROM clause)
83
     * @return mixed[] A 3 elements array: [$columnDescList, $columnsList, $reconstructedOrderBy]
84
     */
85
    protected function getColumnsList(string $mainTable, array $additionalTablesFetch = array(), $orderBy = null, bool $canAddAdditionalTablesFetch = false): array
86
    {
87
        // From the table name and the additional tables we want to fetch, let's build a list of all tables
88
        // that must be part of the select columns.
89
90
        $connection = $this->tdbmService->getConnection();
91
92
        $tableGroups = [];
93
        $allFetchedTables = $this->tdbmService->_getRelatedTablesByInheritance($mainTable);
94
        $tableGroupName = $this->getTableGroupName($allFetchedTables);
95
        foreach ($allFetchedTables as $table) {
96
            $tableGroups[$table] = $tableGroupName;
97
        }
98
99
        $columnsList = [];
100
        $columnDescList = [];
101
        $sortColumn = 0;
102
        $reconstructedOrderBy = null;
103
104
        if (is_string($orderBy)) {
105
            $orderBy = trim($orderBy);
106
            if ($orderBy === '') {
107
                $orderBy = null;
108
            }
109
        }
110
111
        // Now, let's deal with "order by columns"
112
        if ($orderBy !== null) {
113
            $securedOrderBy = true;
114
            $reconstructedOrderBys = [];
115
            if ($orderBy instanceof UncheckedOrderBy) {
116
                $securedOrderBy = false;
117
                $orderBy = $orderBy->getOrderBy();
118
                $reconstructedOrderBy = $orderBy;
119
            }
120
            $orderByColumns = $this->orderByAnalyzer->analyzeOrderBy($orderBy);
121
122
            // If we sort by a column, there is a high chance we will fetch the bean containing this column.
123
            // Hence, we should add the table to the $additionalTablesFetch
124
            foreach ($orderByColumns as $orderByColumn) {
125
                if ($orderByColumn['type'] === 'colref') {
126
                    if ($orderByColumn['table'] !== null) {
127
                        if ($canAddAdditionalTablesFetch) {
128
                            $additionalTablesFetch[] = $orderByColumn['table'];
129
                        } else {
130
                            $sortColumnName = 'sort_column_'.$sortColumn;
131
                            $mysqlPlatform = new MySqlPlatform();
132
                            $columnsList[] = $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.'.$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' as '.$sortColumnName;
133
                            $columnDescList[$sortColumnName] = [
134
                                'tableGroup' => null,
135
                            ];
136
                            ++$sortColumn;
137
                        }
138
                    }
139
                    if ($securedOrderBy) {
140
                        // Let's protect via MySQL since we go through MagicJoin
141
                        $mysqlPlatform = new MySqlPlatform();
142
                        $reconstructedOrderBys[] = ($orderByColumn['table'] !== null ? $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.' : '').$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' '.$orderByColumn['direction'];
143
                    }
144
                } elseif ($orderByColumn['type'] === 'expr') {
145
                    $sortColumnName = 'sort_column_'.$sortColumn;
146
                    $columnsList[] = $orderByColumn['expr'].' as '.$connection->quoteIdentifier($sortColumnName);
147
                    $columnDescList[$sortColumnName] = [
148
                        'tableGroup' => null,
149
                    ];
150
                    ++$sortColumn;
151
152
                    if ($securedOrderBy) {
153
                        throw new TDBMInvalidArgumentException('Invalid ORDER BY column: "'.$orderByColumn['expr'].'". If you want to use expression in your ORDER BY clause, you must wrap them in a UncheckedOrderBy object. For instance: new UncheckedOrderBy("col1 + col2 DESC")');
154
                    }
155
                }
156
            }
157
158
            if ($reconstructedOrderBy === null) {
159
                $reconstructedOrderBy = implode(', ', $reconstructedOrderBys);
160
            }
161
        }
162
163
        foreach ($additionalTablesFetch as $additionalTable) {
164
            if (in_array($additionalTable, $allFetchedTables, true)) {
165
                continue;
166
            }
167
168
            $relatedTables = $this->tdbmService->_getRelatedTablesByInheritance($additionalTable);
169
            $tableGroupName = $this->getTableGroupName($relatedTables);
170
            foreach ($relatedTables as $table) {
171
                $tableGroups[$table] = $tableGroupName;
172
            }
173
            $allFetchedTables = array_merge($allFetchedTables, $relatedTables);
174
        }
175
176
        // Let's remove any duplicate
177
        $allFetchedTables = array_unique($allFetchedTables);
178
179
        // We quote in MySQL because MagicJoin requires MySQL style quotes
180
        $mysqlPlatform = new MySqlPlatform();
181
182
        // Now, let's build the column list
183
        foreach ($allFetchedTables as $table) {
184
            foreach ($this->schema->getTable($table)->getColumns() as $column) {
185
                $columnName = $column->getName();
186
                $alias = self::getColumnAlias($table, $columnName);
187
                $columnDescList[$alias] = [
188
                    'as' => $alias,
189
                    'table' => $table,
190
                    'column' => $columnName,
191
                    'type' => $column->getType(),
192
                    'tableGroup' => $tableGroups[$table],
193
                ];
194
                $columnsList[] = sprintf(
195
                    '%s.%s as %s',
196
                    $mysqlPlatform->quoteIdentifier($table),
197
                    $mysqlPlatform->quoteIdentifier($columnName),
198
                    $connection->quoteIdentifier($alias)
199
                );
200
            }
201
        }
202
203
        return [$columnDescList, $columnsList, $reconstructedOrderBy];
204
    }
205
206
    public static function getColumnAlias(string $tableName, string $columnName): string
207
    {
208
        $alias = $tableName.'____'.$columnName;
209
        if (strlen($alias) <= 30) { // Older oracle version had a limit of 30 characters for identifiers
210
            return $alias;
211
        }
212
        return substr($columnName, 0, 20) . crc32($tableName.'____'.$columnName);
213
    }
214
215
    abstract protected function compute(): void;
216
217
    /**
218
     * Returns an identifier for the group of tables passed in parameter.
219
     *
220
     * @param string[] $relatedTables
221
     *
222
     * @return string
223
     */
224
    protected function getTableGroupName(array $relatedTables): string
225
    {
226
        sort($relatedTables);
227
228
        return implode('_``_', $relatedTables);
229
    }
230
231
    public function getMagicSql(): string
232
    {
233
        if ($this->magicSql === null) {
234
            $this->compute();
235
        }
236
237
        return $this->magicSql;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->magicSql could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
238
    }
239
240
    public function getMagicSqlCount(): string
241
    {
242
        if ($this->magicSqlCount === null) {
243
            $this->compute();
244
        }
245
246
        return $this->magicSqlCount;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->magicSqlCount could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
247
    }
248
249
    public function getMagicSqlSubQuery(): string
250
    {
251
        if ($this->magicSqlSubQuery === null) {
252
            $this->compute();
253
        }
254
255
        return $this->magicSqlSubQuery;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->magicSqlSubQuery could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
256
    }
257
258
    public function getColumnDescriptors(): array
259
    {
260
        if ($this->columnDescList === null) {
261
            $this->compute();
262
        }
263
264
        return $this->columnDescList;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->columnDescList could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
265
    }
266
267
    /**
268
     * @return array<int, array{table: string, column: string}> An array of column descriptors.
269
     */
270
    public function getSubQueryColumnDescriptors(): array
271
    {
272
        if ($this->subQueryColumnDescList === null) {
273
            $columns = $this->tdbmService->getPrimaryKeyColumns($this->mainTable);
274
            $descriptors = [];
275
            foreach ($columns as $column) {
276
                $descriptors[] = [
277
                    'table' => $this->mainTable,
278
                    'column' => $column
279
                ];
280
            }
281
            $this->subQueryColumnDescList = $descriptors;
282
        }
283
284
        return $this->subQueryColumnDescList;
285
    }
286
287
288
    /**
289
     * Sets the ORDER BY directive executed in SQL.
290
     *
291
     * For instance:
292
     *
293
     *  $queryFactory->sort('label ASC, status DESC');
294
     *
295
     * **Important:** TDBM does its best to protect you from SQL injection. In particular, it will only allow column names in the "ORDER BY" clause. This means you are safe to pass input from the user directly in the ORDER BY parameter.
296
     * If you want to pass an expression to the ORDER BY clause, you will need to tell TDBM to stop checking for SQL injections. You do this by passing a `UncheckedOrderBy` object as a parameter:
297
     *
298
     *  $queryFactory->sort(new UncheckedOrderBy('RAND()'))
299
     *
300
     * @param string|UncheckedOrderBy|null $orderBy
301
     */
302
    public function sort($orderBy): void
303
    {
304
        $this->orderBy = $orderBy;
305
        $this->magicSql = null;
306
        $this->magicSqlCount = null;
307
        $this->columnDescList = null;
308
    }
309
}
310