Passed
Push — main ( a71465...da05ca )
by Thierry
03:24 queued 59s
created

SelectFacade::query()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Lagdo\DbAdmin\Db\Driver\Facades;
4
5
use Lagdo\DbAdmin\Db\Page\Dql\SelectEntity;
6
use Lagdo\DbAdmin\Db\Page\Dql\SelectQuery;
7
use Lagdo\DbAdmin\Db\Service\TimerService;
8
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
9
use Exception;
10
11
use function array_map;
12
use function compact;
13
use function count;
14
use function current;
15
use function is_string;
16
use function key;
17
use function md5;
18
use function next;
19
use function preg_match;
20
use function strlen;
21
use function strpos;
22
use function trim;
23
24
/**
25
 * Facade to table select functions
26
 */
27
class SelectFacade extends AbstractFacade
28
{
29
    /**
30
     * @var SelectQuery|null
31
     */
32
    private SelectQuery|null $selectQuery = null;
33
34
    /**
35
     * @var SelectEntity|null
36
     */
37
    private SelectEntity|null $selectEntity = null;
38
39
    /**
40
     * @param AbstractFacade $dbFacade
41
     * @param TimerService $timer
42
     */
43
    public function __construct(AbstractFacade $dbFacade, protected TimerService $timer)
44
    {
45
        parent::__construct($dbFacade);
46
    }
47
48
    /**
49
     * @return SelectQuery
50
     */
51
    private function query(): SelectQuery
52
    {
53
        return $this->selectQuery ??= new SelectQuery($this->page, $this->driver, $this->utils);
0 ignored issues
show
Bug introduced by
It seems like $this->driver can also be of type null; however, parameter $driver of Lagdo\DbAdmin\Db\Page\Dq...ectQuery::__construct() does only seem to accept Lagdo\DbAdmin\Driver\DriverInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

53
        return $this->selectQuery ??= new SelectQuery($this->page, /** @scrutinizer ignore-type */ $this->driver, $this->utils);
Loading history...
54
    }
55
56
    /**
57
     * @param string $table The table name
58
     * @param array $queryOptions The query options
59
     *
60
     * @return void
61
     * @throws Exception
62
     */
63
    private function setSelectEntity(string $table, array $queryOptions = []): void
64
    {
65
        $tableStatus = $this->driver->tableStatusOrName($table);
0 ignored issues
show
Bug introduced by
The method tableStatusOrName() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

65
        /** @scrutinizer ignore-call */ 
66
        $tableStatus = $this->driver->tableStatusOrName($table);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
66
        $tableName = $this->page->tableName($tableStatus);
67
        $this->selectEntity = new SelectEntity($table,
68
            $tableName, $tableStatus, $queryOptions);
69
        $this->query()->prepareSelect($this->selectEntity);
70
    }
71
72
    /**
73
     * Get required data for create/update on tables
74
     *
75
     * @param string $table The table name
76
     * @param array $queryOptions The query options
77
     *
78
     * @return SelectEntity
79
     * @throws Exception
80
     */
81
    public function getSelectData(string $table, array $queryOptions = []): SelectEntity
82
    {
83
        $this->setSelectEntity($table, $queryOptions);
84
        return $this->selectEntity;
85
    }
86
87
    /**
88
     * @return void
89
     */
90
    private function executeSelect(): void
91
    {
92
        // From driver.inc.php
93
        $this->timer->start();
94
        $statement = $this->driver->execute($this->selectEntity->query);
95
        $this->selectEntity->duration = $this->timer->duration();
96
        $this->selectEntity->rows = [];
97
98
        // From adminer.inc.php
99
        if (!$statement) {
100
            $this->selectEntity->error = $this->driver->error();
101
            return;
102
        }
103
104
        // From select.inc.php
105
        $this->selectEntity->rows = [];
106
        while (($row = $statement->fetchAssoc())) {
107
            if ($this->selectEntity->page && $this->driver->jush() === "oracle") {
108
                unset($row["RNUM"]);
109
            }
110
            $this->selectEntity->rows[] = $row;
111
        }
112
    }
113
114
    /**
115
     * @param string $key
116
     * @param int $rank
117
     *
118
     * @return array
119
     */
120
    private function getResultHeaderItem(string $key, int $rank): array
121
    {
122
        $valueKey = key($this->selectEntity->select);
123
        $value = $this->selectEntity->queryOptions["columns"][$valueKey] ?? [];
124
125
        $fun = $value["fun"] ?? '';
126
        $fieldKey = !$this->selectEntity->select ? $key :
127
            ($value["col"] ?? current($this->selectEntity->select));
128
        $field = $this->selectEntity->fields[$fieldKey];
129
        $name = !$field ? ($fun ? "*" : $key) :
130
            $this->page->fieldName($field, $rank);
131
132
        return [$fun, $name, $field];
133
    }
134
135
    /**
136
     * @param string $key
137
     * @param mixed $value
138
     * @param int $rank
139
     *
140
     * @return array
141
     */
142
    private function getResultHeader(string $key, $value, int $rank): array
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

142
    private function getResultHeader(string $key, /** @scrutinizer ignore-unused */ $value, int $rank): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
143
    {
144
        if (isset($this->selectEntity->unselected[$key])) {
145
            return [];
146
        }
147
148
        [$fun, $name, $field] = $this->getResultHeaderItem($key, $rank);
149
        $header = compact('field', 'name');
150
        if ($name != "") {
151
            $this->selectEntity->names[$key] = $name;
152
            // $href = remove_from_uri('(order|desc)[^=]*|page') . '&order%5B0%5D=' . urlencode($key);
153
            // $desc = "&desc%5B0%5D=1";
154
            $header['column'] = $this->driver->escapeId($key);
155
            // $header['key'] = $this->utils->str
156
            //     ->html($this->driver->bracketEscape($key));
157
            //! columns looking like functions
158
            $header['title'] = $this->query()->applySqlFunction($fun, $name);
159
        }
160
        // $functions[$key] = $fun;
161
        next($this->selectEntity->select);
162
        return $header;
163
    }
164
165
    /**
166
     * Get the result headers from the first result row
167
     * @return void
168
     */
169
    private function getResultHeaders(): void
170
    {
171
        // Results headers
172
        $this->selectEntity->headers = [
173
            '', // !$group && $select ? '' : lang('Modify');
174
        ];
175
        $this->selectEntity->names = [];
176
        // $this->selectEntity->functions = [];
177
        reset($this->selectEntity->select);
178
179
        $rank = 1;
180
        $firstResultRow = $this->selectEntity->rows[0];
181
        foreach ($firstResultRow as $key => $value) {
182
            $header = $this->getResultHeader($key, $value, $rank);
183
            if ($header['name'] ?? '' !== '') {
184
                $rank++;
185
            }
186
            $this->selectEntity->headers[] = $header;
187
        }
188
    }
189
190
    /**
191
     * @param array $rows
192
     * @param array $queryOptions
193
     *
194
     * @return array
195
     */
196
    /*private function getValuesLengths(array $rows, array $queryOptions): array
197
    {
198
        $lengths = [];
199
        if($queryOptions["modify"])
200
        {
201
            foreach($rows as $row)
202
            {
203
                foreach($row as $key => $value)
204
                {
205
                    $lengths[$key] = \max($lengths[$key], \min(40, strlen(\utf8_decode($value))));
206
                }
207
            }
208
        }
209
        return $lengths;
210
    }*/
211
212
    /**
213
     * @param array $row
214
     *
215
     * @return array
216
     */
217
    private function getUniqueIds(array $row): array
218
    {
219
        $uniqueIds = $this->utils->uniqueIds($row, $this->selectEntity->indexes);
220
        if (empty($uniqueIds)) {
221
            $pattern = '~^(COUNT\((\*|(DISTINCT )?`(?:[^`]|``)+`)\)' .
222
                '|(AVG|GROUP_CONCAT|MAX|MIN|SUM)\(`(?:[^`]|``)+`\))$~';
223
            foreach ($row as $key => $value) {
224
                if (!preg_match($pattern, $key)) {
225
                    //! columns looking like functions
226
                    $uniqueIds[$key] = $value;
227
                }
228
            }
229
        }
230
        return $uniqueIds;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $uniqueIds could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
231
    }
232
233
    /**
234
     * @param string $type
235
     * @param mixed $value
236
     *
237
     * @return bool
238
     */
239
    private function shouldEncodeRowId(string $type, $value): bool
240
    {
241
        $jush = $this->driver->jush();
242
        return ($jush === "sql" || $jush === "pgsql") &&
243
            is_string($value) && strlen($value) > 64 &&
244
            preg_match('~char|text|enum|set~', $type);
245
    }
246
247
    /**
248
     * @param string $key
249
     * @param string $collation
250
     *
251
     * @return string
252
     */
253
    private function getRowIdMd5Key(string $key, string $collation): string
254
    {
255
        return $this->driver->jush() != 'sql' ||
256
            preg_match("~^utf8~", $collation) ? $key :
257
                "CONVERT($key USING " . $this->driver->charset() . ")";
258
    }
259
260
    /**
261
     * @param string $key
262
     * @param mixed $value
263
     *
264
     * @return array
265
     */
266
    private function getRowIdValue(string $key, $value): array
267
    {
268
        $key = trim($key);
269
        $type = '';
270
        $collation = '';
271
        if (isset($this->selectEntity->fields[$key])) {
272
            $type = $this->selectEntity->fields[$key]->type;
273
            $collation = $this->selectEntity->fields[$key]->collation;
274
        }
275
        if ($this->shouldEncodeRowId($type, $value)) {
276
            if (!strpos($key, '(')) {
277
                //! columns looking like functions
278
                $key = $this->driver->escapeId($key);
279
            }
280
            $key = "MD5(" . $this->getRowIdMd5Key($key, $collation) . ")";
281
            $value = md5($value);
282
        }
283
        return [$key, $value];
284
    }
285
286
    /**
287
     * @param array $row
288
     *
289
     * @return array
290
     */
291
    private function getRowIds(array $row): array
292
    {
293
        $uniqueIds = $this->getUniqueIds($row);
294
        // Unique identifier to edit returned data.
295
        // $unique_idf = "";
296
        $rowIds = ['where' => [], 'null' => []];
297
        foreach ($uniqueIds as $key => $value) {
298
            [$key, $value] = $this->getRowIdValue($key, $value);
299
            // $unique_idf .= "&" . ($value !== null ? \urlencode("where[" .
300
            // $this->driver->bracketEscape($key) . "]") . "=" .
301
            // \urlencode($value) : \urlencode("null[]") . "=" . \urlencode($key));
302
            if ($value === null) {
303
                $rowIds['null'][] = $this->driver->bracketEscape($key);
304
                continue;
305
            }
306
            $rowIds['where'][$this->driver->bracketEscape($key)] = $value;
307
        }
308
        return $rowIds;
309
    }
310
311
    /**
312
     * @param string $key
313
     * @param mixed $value
314
     *
315
     * @return array
316
     */
317
    private function getColumnValue(string $key, $value): array
318
    {
319
        $field = $this->selectEntity->fields[$key] ?? new TableFieldEntity();
320
        $textLength = $this->selectEntity->textLength;
321
        $value = $this->driver->value($value, $field);
322
        return $this->page->getFieldValue($field, $textLength, $value);
323
    }
324
325
    /**
326
     * @param array $row
327
     *
328
     * @return array
329
     */
330
    private function getRowValues(array $row): array
331
    {
332
        $cols = [];
333
        foreach ($row as $key => $value) {
334
            if (isset($this->selectEntity->names[$key])) {
335
                $cols[] = $this->getColumnValue($key, $value);
336
            }
337
        }
338
        return $cols;
339
    }
340
341
    /**
342
     * @return bool
343
     */
344
    private function hasGroupsInFields(): bool
345
    {
346
        return count($this->selectEntity->group) <
347
            count($this->selectEntity->select);
348
    }
349
350
    /**
351
     * Get required data for create/update on tables
352
     *
353
     * @param string $table The table name
354
     * @param array $queryOptions The query options
355
     *
356
     * @return int
357
     */
358
    public function countSelect(string $table, array $queryOptions): int
359
    {
360
        $this->setSelectEntity($table, $queryOptions);
361
362
        try {
363
            $query = $this->driver->getRowCountQuery($table,
364
                $this->selectEntity->where, $this->hasGroupsInFields(),
365
                $this->selectEntity->group);
366
            return (int)$this->driver->result($query);
367
        } catch(Exception) {
368
            return -1;
369
        }
370
    }
371
372
    /**
373
     * Get required data for create/update on tables
374
     *
375
     * @param string $table The table name
376
     * @param array $queryOptions The query options
377
     *
378
     * @return array
379
     * @throws Exception
380
     */
381
    public function execSelect(string $table, array $queryOptions): array
382
    {
383
        $this->setSelectEntity($table, $queryOptions);
384
385
        $this->executeSelect();
386
        if ($this->selectEntity->error !== null) {
387
            return ['message' => $this->selectEntity->error];
388
        }
389
        if (!$this->selectEntity->rows) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->selectEntity->rows of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
390
            return ['message' => $this->utils->trans->lang('No rows.')];
391
        }
392
        // $backward_keys = $this->driver->backwardKeys($table, $tableName);
393
        // lengths = $this->getValuesLengths($rows, $this->selectEntity->queryOptions);
394
395
        $this->getResultHeaders();
396
397
        return [
398
            'headers' => $this->selectEntity->headers,
399
            'query' => $this->selectEntity->query,
400
            'limit' => $this->selectEntity->limit,
401
            'duration' => $this->selectEntity->duration,
402
            'message' => null,
403
            'rows' => array_map(fn($row) => [
404
                // Unique identifier to edit returned data.
405
                'ids' => $this->getRowIds($row),
406
                'cols' => $this->getRowValues($row),
407
            ], $this->selectEntity->rows),
408
        ];
409
    }
410
}
411