Passed
Pull Request — master (#208)
by David
08:37
created

AbstractQueryFactory::getColumnAlias()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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