Passed
Pull Request — master (#161)
by David
04:19
created

AbstractQueryFactory   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 31
eloc 99
c 2
b 0
f 0
dl 0
loc 250
rs 9.92

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getMagicSql() 0 7 2
A __construct() 0 6 1
A getColumnDescriptors() 0 7 2
A getTableGroupName() 0 5 1
A sort() 0 6 1
A getMagicSqlCount() 0 7 2
F getColumnsList() 0 114 20
A getMagicSqlSubQuery() 0 7 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
51
    /**
52
     * @param TDBMService $tdbmService
53
     * @param Schema $schema
54
     * @param OrderByAnalyzer $orderByAnalyzer
55
     * @param string|UncheckedOrderBy|null $orderBy
56
     */
57
    public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, $orderBy)
58
    {
59
        $this->tdbmService = $tdbmService;
60
        $this->schema = $schema;
61
        $this->orderByAnalyzer = $orderByAnalyzer;
62
        $this->orderBy = $orderBy;
63
    }
64
65
    /**
66
     * Returns the column list that must be fetched for the SQL request.
67
     *
68
     * Note: MySQL dictates that ORDER BYed columns should appear in the SELECT clause.
69
     *
70
     * @param string $mainTable
71
     * @param string[] $additionalTablesFetch
72
     * @param string|UncheckedOrderBy|null $orderBy
73
     *
74
     * @param bool $canAddAdditionalTablesFetch Set to true if the function can add additional tables to fetch (so if the factory generates its own FROM clause)
75
     * @return mixed[] A 3 elements array: [$columnDescList, $columnsList, $reconstructedOrderBy]
76
     */
77
    protected function getColumnsList(string $mainTable, array $additionalTablesFetch = array(), $orderBy = null, bool $canAddAdditionalTablesFetch = false): array
78
    {
79
        // From the table name and the additional tables we want to fetch, let's build a list of all tables
80
        // that must be part of the select columns.
81
82
        $connection = $this->tdbmService->getConnection();
83
84
        $tableGroups = [];
85
        $allFetchedTables = $this->tdbmService->_getRelatedTablesByInheritance($mainTable);
86
        $tableGroupName = $this->getTableGroupName($allFetchedTables);
87
        foreach ($allFetchedTables as $table) {
88
            $tableGroups[$table] = $tableGroupName;
89
        }
90
91
        $columnsList = [];
92
        $columnDescList = [];
93
        $sortColumn = 0;
94
        $reconstructedOrderBy = null;
95
96
        if (is_string($orderBy)) {
97
            $orderBy = trim($orderBy);
98
            if ($orderBy === '') {
99
                $orderBy = null;
100
            }
101
        }
102
103
        // Now, let's deal with "order by columns"
104
        if ($orderBy !== null) {
105
            $securedOrderBy = true;
106
            $reconstructedOrderBys = [];
107
            if ($orderBy instanceof UncheckedOrderBy) {
108
                $securedOrderBy = false;
109
                $orderBy = $orderBy->getOrderBy();
110
                $reconstructedOrderBy = $orderBy;
111
            }
112
            $orderByColumns = $this->orderByAnalyzer->analyzeOrderBy($orderBy);
113
114
            // If we sort by a column, there is a high chance we will fetch the bean containing this column.
115
            // Hence, we should add the table to the $additionalTablesFetch
116
            foreach ($orderByColumns as $orderByColumn) {
117
                if ($orderByColumn['type'] === 'colref') {
118
                    if ($orderByColumn['table'] !== null) {
119
                        if ($canAddAdditionalTablesFetch) {
120
                            $additionalTablesFetch[] = $orderByColumn['table'];
121
                        } else {
122
                            $sortColumnName = 'sort_column_'.$sortColumn;
123
                            $mysqlPlatform = new MySqlPlatform();
124
                            $columnsList[] = $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.'.$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' as '.$sortColumnName;
125
                            $columnDescList[$sortColumnName] = [
126
                                'tableGroup' => null,
127
                            ];
128
                            ++$sortColumn;
129
                        }
130
                    }
131
                    if ($securedOrderBy) {
132
                        // Let's protect via MySQL since we go through MagicJoin
133
                        $mysqlPlatform = new MySqlPlatform();
134
                        $reconstructedOrderBys[] = ($orderByColumn['table'] !== null ? $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.' : '').$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' '.$orderByColumn['direction'];
135
                    }
136
                } elseif ($orderByColumn['type'] === 'expr') {
137
                    $sortColumnName = 'sort_column_'.$sortColumn;
138
                    $columnsList[] = $orderByColumn['expr'].' as '.$sortColumnName;
139
                    $columnDescList[$sortColumnName] = [
140
                        'tableGroup' => null,
141
                    ];
142
                    ++$sortColumn;
143
144
                    if ($securedOrderBy) {
145
                        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")');
146
                    }
147
                }
148
            }
149
150
            if ($reconstructedOrderBy === null) {
151
                $reconstructedOrderBy = implode(', ', $reconstructedOrderBys);
152
            }
153
        }
154
155
        foreach ($additionalTablesFetch as $additionalTable) {
156
            if (in_array($additionalTable, $allFetchedTables, true)) {
157
                continue;
158
            }
159
160
            $relatedTables = $this->tdbmService->_getRelatedTablesByInheritance($additionalTable);
161
            $tableGroupName = $this->getTableGroupName($relatedTables);
162
            foreach ($relatedTables as $table) {
163
                $tableGroups[$table] = $tableGroupName;
164
            }
165
            $allFetchedTables = array_merge($allFetchedTables, $relatedTables);
166
        }
167
168
        // Let's remove any duplicate
169
        $allFetchedTables = array_unique($allFetchedTables);
170
        
171
        // We quote in MySQL because MagicJoin requires MySQL style quotes
172
        $mysqlPlatform = new MySqlPlatform();
173
174
        // Now, let's build the column list
175
        foreach ($allFetchedTables as $table) {
176
            foreach ($this->schema->getTable($table)->getColumns() as $column) {
177
                $columnName = $column->getName();
178
                $columnDescList[$table.'____'.$columnName] = [
179
                    'as' => $table.'____'.$columnName,
180
                    'table' => $table,
181
                    'column' => $columnName,
182
                    'type' => $column->getType(),
183
                    'tableGroup' => $tableGroups[$table],
184
                ];
185
                $columnsList[] = $mysqlPlatform->quoteIdentifier($table).'.'.$mysqlPlatform->quoteIdentifier($columnName).' as '.
186
                    $connection->quoteIdentifier($table.'____'.$columnName);
187
            }
188
        }
189
190
        return [$columnDescList, $columnsList, $reconstructedOrderBy];
191
    }
192
193
    abstract protected function compute(): void;
194
195
    /**
196
     * Returns an identifier for the group of tables passed in parameter.
197
     *
198
     * @param string[] $relatedTables
199
     *
200
     * @return string
201
     */
202
    protected function getTableGroupName(array $relatedTables): string
203
    {
204
        sort($relatedTables);
205
206
        return implode('_``_', $relatedTables);
207
    }
208
209
    public function getMagicSql() : string
210
    {
211
        if ($this->magicSql === null) {
212
            $this->compute();
213
        }
214
215
        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...
216
    }
217
218
    public function getMagicSqlCount() : string
219
    {
220
        if ($this->magicSqlCount === null) {
221
            $this->compute();
222
        }
223
224
        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...
225
    }
226
227
    public function getMagicSqlSubQuery() : string
228
    {
229
        if ($this->magicSqlSubQuery === null) {
230
            $this->compute();
231
        }
232
233
        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...
234
    }
235
236
    public function getColumnDescriptors() : array
237
    {
238
        if ($this->columnDescList === null) {
239
            $this->compute();
240
        }
241
242
        return $this->columnDescList;
243
    }
244
245
    /**
246
     * Sets the ORDER BY directive executed in SQL.
247
     *
248
     * For instance:
249
     *
250
     *  $queryFactory->sort('label ASC, status DESC');
251
     *
252
     * **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.
253
     * 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:
254
     *
255
     *  $queryFactory->sort(new UncheckedOrderBy('RAND()'))
256
     *
257
     * @param string|UncheckedOrderBy|null $orderBy
258
     */
259
    public function sort($orderBy): void
260
    {
261
        $this->orderBy = $orderBy;
262
        $this->magicSql = null;
263
        $this->magicSqlCount = null;
264
        $this->columnDescList = null;
265
    }
266
}
267