1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Mouf\Database\TDBM\QueryFactory; |
4
|
|
|
|
5
|
|
|
use Doctrine\DBAL\Schema\Schema; |
6
|
|
|
use Mouf\Database\TDBM\OrderByAnalyzer; |
7
|
|
|
use Mouf\Database\TDBM\TDBMInvalidArgumentException; |
8
|
|
|
use Mouf\Database\TDBM\TDBMService; |
9
|
|
|
use Mouf\Database\TDBM\UncheckedOrderBy; |
10
|
|
|
|
11
|
|
|
abstract class AbstractQueryFactory implements QueryFactory |
12
|
|
|
{ |
13
|
|
|
/** |
14
|
|
|
* @var TDBMService |
15
|
|
|
*/ |
16
|
|
|
protected $tdbmService; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* @var Schema |
20
|
|
|
*/ |
21
|
|
|
protected $schema; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var OrderByAnalyzer |
25
|
|
|
*/ |
26
|
|
|
protected $orderByAnalyzer; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var string|UncheckedOrderBy|null |
30
|
|
|
*/ |
31
|
|
|
protected $orderBy; |
32
|
|
|
|
33
|
|
|
protected $magicSql; |
34
|
|
|
protected $magicSqlCount; |
35
|
|
|
protected $columnDescList; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @param TDBMService $tdbmService |
39
|
|
|
*/ |
40
|
|
|
public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, $orderBy) |
41
|
|
|
{ |
42
|
|
|
$this->tdbmService = $tdbmService; |
43
|
|
|
$this->schema = $schema; |
44
|
|
|
$this->orderByAnalyzer = $orderByAnalyzer; |
45
|
|
|
$this->orderBy = $orderBy; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Returns the column list that must be fetched for the SQL request. |
50
|
|
|
* |
51
|
|
|
* Note: MySQL dictates that ORDER BYed columns should appear in the SELECT clause. |
52
|
|
|
* |
53
|
|
|
* @param string $mainTable |
54
|
|
|
* @param array $additionalTablesFetch |
55
|
|
|
* @param string|UncheckedOrderBy|null $orderBy |
56
|
|
|
* |
57
|
|
|
* @return array |
58
|
|
|
* |
59
|
|
|
* @throws \Doctrine\DBAL\Schema\SchemaException |
60
|
|
|
*/ |
61
|
|
|
protected function getColumnsList(string $mainTable, array $additionalTablesFetch = array(), $orderBy = null) |
62
|
|
|
{ |
63
|
|
|
// From the table name and the additional tables we want to fetch, let's build a list of all tables |
64
|
|
|
// that must be part of the select columns. |
65
|
|
|
|
66
|
|
|
$connection = $this->tdbmService->getConnection(); |
67
|
|
|
|
68
|
|
|
$tableGroups = []; |
69
|
|
|
$allFetchedTables = $this->tdbmService->_getRelatedTablesByInheritance($mainTable); |
70
|
|
|
$tableGroupName = $this->getTableGroupName($allFetchedTables); |
71
|
|
|
foreach ($allFetchedTables as $table) { |
72
|
|
|
$tableGroups[$table] = $tableGroupName; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
$columnsList = []; |
76
|
|
|
$columnDescList = []; |
77
|
|
|
$sortColumn = 0; |
78
|
|
|
$reconstructedOrderBy = null; |
79
|
|
|
|
80
|
|
|
if (is_string($orderBy)) { |
81
|
|
|
$orderBy = trim($orderBy); |
82
|
|
|
if ($orderBy === '') { |
83
|
|
|
$orderBy = null; |
84
|
|
|
} |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
// Now, let's deal with "order by columns" |
88
|
|
|
if ($orderBy !== null) { |
89
|
|
|
if ($orderBy instanceof UncheckedOrderBy) { |
90
|
|
|
$securedOrderBy = false; |
91
|
|
|
$orderBy = $orderBy->getOrderBy(); |
92
|
|
|
$reconstructedOrderBy = $orderBy; |
93
|
|
|
} else { |
94
|
|
|
$securedOrderBy = true; |
95
|
|
|
$reconstructedOrderBys = []; |
96
|
|
|
} |
97
|
|
|
$orderByColumns = $this->orderByAnalyzer->analyzeOrderBy($orderBy); |
98
|
|
|
|
99
|
|
|
// If we sort by a column, there is a high chance we will fetch the bean containing this column. |
100
|
|
|
// Hence, we should add the table to the $additionalTablesFetch |
101
|
|
|
foreach ($orderByColumns as $orderByColumn) { |
102
|
|
|
if ($orderByColumn['type'] === 'colref') { |
103
|
|
|
if ($orderByColumn['table'] !== null) { |
104
|
|
|
$additionalTablesFetch[] = $orderByColumn['table']; |
105
|
|
|
} |
106
|
|
|
if ($securedOrderBy) { |
107
|
|
|
$reconstructedOrderBys[] = ($orderByColumn['table'] !== null ? $connection->quoteIdentifier($orderByColumn['table']).'.' : '').$connection->quoteIdentifier($orderByColumn['column']).' '.$orderByColumn['direction']; |
|
|
|
|
108
|
|
|
} |
109
|
|
|
} elseif ($orderByColumn['type'] === 'expr') { |
110
|
|
|
$sortColumnName = 'sort_column_'.$sortColumn; |
111
|
|
|
$columnsList[] = $orderByColumn['expr'].' as '.$sortColumnName; |
112
|
|
|
$columnDescList[] = [ |
113
|
|
|
'tableGroup' => null, |
114
|
|
|
]; |
115
|
|
|
++$sortColumn; |
116
|
|
|
|
117
|
|
|
if ($securedOrderBy) { |
118
|
|
|
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")'); |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
if ($reconstructedOrderBy === null) { |
124
|
|
|
$reconstructedOrderBy = implode(', ', $reconstructedOrderBys); |
125
|
|
|
} |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
foreach ($additionalTablesFetch as $additionalTable) { |
129
|
|
|
$relatedTables = $this->tdbmService->_getRelatedTablesByInheritance($additionalTable); |
130
|
|
|
$tableGroupName = $this->getTableGroupName($relatedTables); |
131
|
|
|
foreach ($relatedTables as $table) { |
132
|
|
|
$tableGroups[$table] = $tableGroupName; |
133
|
|
|
} |
134
|
|
|
$allFetchedTables = array_merge($allFetchedTables, $relatedTables); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
// Let's remove any duplicate |
138
|
|
|
$allFetchedTables = array_flip(array_flip($allFetchedTables)); |
139
|
|
|
|
140
|
|
|
// Now, let's build the column list |
141
|
|
|
foreach ($allFetchedTables as $table) { |
142
|
|
|
foreach ($this->schema->getTable($table)->getColumns() as $column) { |
143
|
|
|
$columnName = $column->getName(); |
144
|
|
|
$columnDescList[] = [ |
145
|
|
|
'as' => $table.'____'.$columnName, |
146
|
|
|
'table' => $table, |
147
|
|
|
'column' => $columnName, |
148
|
|
|
'type' => $column->getType(), |
149
|
|
|
'tableGroup' => $tableGroups[$table], |
150
|
|
|
]; |
151
|
|
|
$columnsList[] = $connection->quoteIdentifier($table).'.'.$connection->quoteIdentifier($columnName).' as '. |
152
|
|
|
$connection->quoteIdentifier($table.'____'.$columnName); |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
return [$columnDescList, $columnsList, $reconstructedOrderBy]; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
abstract protected function compute(); |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Returns an identifier for the group of tables passed in parameter. |
163
|
|
|
* |
164
|
|
|
* @param string[] $relatedTables |
165
|
|
|
* |
166
|
|
|
* @return string |
167
|
|
|
*/ |
168
|
|
|
protected function getTableGroupName(array $relatedTables) |
169
|
|
|
{ |
170
|
|
|
sort($relatedTables); |
171
|
|
|
|
172
|
|
|
return implode('_``_', $relatedTables); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
public function getMagicSql() : string |
176
|
|
|
{ |
177
|
|
|
if ($this->magicSql === null) { |
178
|
|
|
$this->compute(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
return $this->magicSql; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
public function getMagicSqlCount() : string |
185
|
|
|
{ |
186
|
|
|
if ($this->magicSqlCount === null) { |
187
|
|
|
$this->compute(); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
return $this->magicSqlCount; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
public function getColumnDescriptors() : array |
194
|
|
|
{ |
195
|
|
|
if ($this->columnDescList === null) { |
196
|
|
|
$this->compute(); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
return $this->columnDescList; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Sets the ORDER BY directive executed in SQL. |
204
|
|
|
* |
205
|
|
|
* For instance: |
206
|
|
|
* |
207
|
|
|
* $queryFactory->sort('label ASC, status DESC'); |
208
|
|
|
* |
209
|
|
|
* **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. |
210
|
|
|
* 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: |
211
|
|
|
* |
212
|
|
|
* $queryFactory->sort(new UncheckedOrderBy('RAND()')) |
213
|
|
|
* |
214
|
|
|
* @param string|UncheckedOrderBy|null $orderBy |
215
|
|
|
*/ |
216
|
|
|
public function sort($orderBy) |
217
|
|
|
{ |
218
|
|
|
$this->orderBy = $orderBy; |
219
|
|
|
$this->magicSql = null; |
220
|
|
|
$this->magicSqlCount = null; |
221
|
|
|
$this->columnDescList = null; |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
If you define a variable conditionally, it can happen that it is not defined for all execution paths.
Let’s take a look at an example:
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.
Available Fixes
Check for existence of the variable explicitly:
Define a default value for the variable:
Add a value for the missing path: