Passed
Pull Request — master (#181)
by
unknown
05:45
created

AbstractQueryFactory::setResultIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\QueryFactory;
5
6
use PHPSQLParser\utils\ExpressionType;
7
use TheCodingMachine\TDBM\ResultIterator;
8
use function array_unique;
9
use Doctrine\DBAL\Platforms\MySqlPlatform;
10
use Doctrine\DBAL\Schema\Schema;
11
use function in_array;
12
use TheCodingMachine\TDBM\OrderByAnalyzer;
13
use TheCodingMachine\TDBM\TDBMInvalidArgumentException;
14
use TheCodingMachine\TDBM\TDBMService;
15
use TheCodingMachine\TDBM\UncheckedOrderBy;
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
    protected $columnDescList;
52
    protected $subQueryColumnDescList;
53
    /**
54
     * @var string
55
     */
56
    protected $mainTable;
57
    /** @var null|ResultIterator */
58
    protected $resultIterator;
59
60
    /**
61
     * @param TDBMService $tdbmService
62
     * @param Schema $schema
63
     * @param OrderByAnalyzer $orderByAnalyzer
64
     * @param string|UncheckedOrderBy|null $orderBy
65
     */
66
    public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, string $mainTable, $orderBy)
67
    {
68
        $this->tdbmService = $tdbmService;
69
        $this->schema = $schema;
70
        $this->orderByAnalyzer = $orderByAnalyzer;
71
        $this->orderBy = $orderBy;
72
        $this->mainTable = $mainTable;
73
    }
74
75
    public function setResultIterator(ResultIterator $resultIterator): void
76
    {
77
        $this->resultIterator = $resultIterator;
78
    }
79
80
    /**
81
     * Returns the column list that must be fetched for the SQL request.
82
     *
83
     * Note: MySQL dictates that ORDER BYed columns should appear in the SELECT clause.
84
     *
85
     * @param string $mainTable
86
     * @param string[] $additionalTablesFetch
87
     * @param string|UncheckedOrderBy|null $orderBy
88
     *
89
     * @param bool $canAddAdditionalTablesFetch Set to true if the function can add additional tables to fetch (so if the factory generates its own FROM clause)
90
     * @return mixed[] A 3 elements array: [$columnDescList, $columnsList, $reconstructedOrderBy]
91
     */
92
    protected function getColumnsList(string $mainTable, array $additionalTablesFetch = array(), $orderBy = null, bool $canAddAdditionalTablesFetch = false): array
93
    {
94
        // From the table name and the additional tables we want to fetch, let's build a list of all tables
95
        // that must be part of the select columns.
96
97
        $connection = $this->tdbmService->getConnection();
98
99
        $tableGroups = [];
100
        $allFetchedTables = $this->tdbmService->_getRelatedTablesByInheritance($mainTable);
101
        $tableGroupName = $this->getTableGroupName($allFetchedTables);
102
        foreach ($allFetchedTables as $table) {
103
            $tableGroups[$table] = $tableGroupName;
104
        }
105
106
        $columnsList = [];
107
        $columnDescList = [];
108
        $sortColumn = 0;
109
        $reconstructedOrderBy = null;
110
111
        if (is_string($orderBy)) {
112
            $orderBy = trim($orderBy);
113
            if ($orderBy === '') {
114
                $orderBy = null;
115
            }
116
        }
117
118
        // Now, let's deal with "order by columns"
119
        if ($orderBy !== null) {
120
            $securedOrderBy = true;
121
            $reconstructedOrderBys = [];
122
            if ($orderBy instanceof UncheckedOrderBy) {
123
                $securedOrderBy = false;
124
                $orderBy = $orderBy->getOrderBy();
125
                $reconstructedOrderBy = $orderBy;
126
            }
127
            $orderByColumns = $this->orderByAnalyzer->analyzeOrderBy($orderBy);
128
129
            // If we sort by a column, there is a high chance we will fetch the bean containing this column.
130
            // Hence, we should add the table to the $additionalTablesFetch
131
            foreach ($orderByColumns as $orderByColumn) {
132
                if ($orderByColumn['type'] === ExpressionType::COLREF) {
133
                    if ($orderByColumn['table'] !== null) {
134
                        if ($canAddAdditionalTablesFetch) {
135
                            $additionalTablesFetch[] = $orderByColumn['table'];
136
                        } else {
137
                            $sortColumnName = 'sort_column_'.$sortColumn;
138
                            $mysqlPlatform = new MySqlPlatform();
139
                            $columnsList[] = $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.'.$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' as '.$sortColumnName;
140
                            $columnDescList[$sortColumnName] = [
141
                                'tableGroup' => null,
142
                            ];
143
                            ++$sortColumn;
144
                        }
145
                    }
146
                    if ($securedOrderBy) {
147
                        // Let's protect via MySQL since we go through MagicJoin
148
                        $mysqlPlatform = new MySqlPlatform();
149
                        $reconstructedOrderBys[] = ($orderByColumn['table'] !== null ? $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.' : '').$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' '.$orderByColumn['direction'];
150
                    }
151
                } elseif ($orderByColumn['type'] === 'expr') {
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
                    $sortColumnName = 'sort_column_'.$sortColumn;
157
                    $columnsList[] = $orderByColumn['expr'].' as '.$sortColumnName;
158
                    $columnDescList[$sortColumnName] = [
159
                        'tableGroup' => null,
160
                    ];
161
                    ++$sortColumn;
162
                }
163
            }
164
165
            if ($reconstructedOrderBy === null) {
166
                $reconstructedOrderBy = implode(', ', $reconstructedOrderBys);
167
            }
168
        }
169
170
        foreach ($additionalTablesFetch as $additionalTable) {
171
            if (in_array($additionalTable, $allFetchedTables, true)) {
172
                continue;
173
            }
174
175
            $relatedTables = $this->tdbmService->_getRelatedTablesByInheritance($additionalTable);
176
            $tableGroupName = $this->getTableGroupName($relatedTables);
177
            foreach ($relatedTables as $table) {
178
                $tableGroups[$table] = $tableGroupName;
179
            }
180
            $allFetchedTables = array_merge($allFetchedTables, $relatedTables);
181
        }
182
183
        // Let's remove any duplicate
184
        $allFetchedTables = array_unique($allFetchedTables);
185
        
186
        // We quote in MySQL because MagicJoin requires MySQL style quotes
187
        $mysqlPlatform = new MySqlPlatform();
188
189
        // Now, let's build the column list
190
        foreach ($allFetchedTables as $table) {
191
            foreach ($this->schema->getTable($table)->getColumns() as $column) {
192
                $columnName = $column->getName();
193
                if ($this->resultIterator === null // @TODO (gua) don't take care of whitelist in case of LIMIT below 2
194
                    || $table !== $mainTable
195
                    || $this->resultIterator->isInWhitelist($columnName, $table)
196
                ) {
197
                    $columnDescList[$table . '____' . $columnName] = [
198
                        'as' => $table . '____' . $columnName,
199
                        'table' => $table,
200
                        'column' => $columnName,
201
                        'type' => $column->getType(),
202
                        'tableGroup' => $tableGroups[$table],
203
                    ];
204
                    $columnsList[] = $mysqlPlatform->quoteIdentifier($table) . '.' . $mysqlPlatform->quoteIdentifier($columnName) . ' as ' .
205
                        $connection->quoteIdentifier($table . '____' . $columnName);
206
                }
207
            }
208
        }
209
210
        return [$columnDescList, $columnsList, $reconstructedOrderBy];
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