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; |
|
|
|
|
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
public function getMagicSqlCount(): string |
241
|
|
|
{ |
242
|
|
|
if ($this->magicSqlCount === null) { |
243
|
|
|
$this->compute(); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
return $this->magicSqlCount; |
|
|
|
|
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
public function getMagicSqlSubQuery(): string |
250
|
|
|
{ |
251
|
|
|
if ($this->magicSqlSubQuery === null) { |
252
|
|
|
$this->compute(); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
return $this->magicSqlSubQuery; |
|
|
|
|
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
public function getColumnDescriptors(): array |
259
|
|
|
{ |
260
|
|
|
if ($this->columnDescList === null) { |
261
|
|
|
$this->compute(); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
return $this->columnDescList; |
|
|
|
|
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
|
|
|
|