Issues (52)

src/Db/Facades/CommandFacade.php (3 issues)

Labels
Severity
1
<?php
2
3
namespace Lagdo\DbAdmin\Db\Facades;
4
5
use Lagdo\DbAdmin\Command\LoggingService;
6
use Lagdo\DbAdmin\Command\TimerService;
7
use Lagdo\DbAdmin\Driver\Db\ConnectionInterface;
8
use Lagdo\DbAdmin\Driver\Entity\QueryEntity;
9
10
use function compact;
11
use function count;
12
use function function_exists;
13
use function ini_set;
14
use function max;
15
use function memory_get_usage;
16
use function strlen;
17
18
/**
19
 * Facade to command functions
20
 */
21
class CommandFacade extends AbstractFacade
22
{
23
    /**
24
     * Connection for exploring indexes and EXPLAIN (to not replace FOUND_ROWS())
25
     * //! PDO - silent error
26
     *
27
     * @var ConnectionInterface
28
     */
29
    protected $connection = null;
30
31
    /**
32
     * @var array
33
     */
34
    protected $results;
35
36
    /**
37
     * @var float
38
     */
39
    protected $duration;
40
41
    /**
42
     * Initialize the facade
43
     *
44
     * @param AbstractFacade $dbFacade
45
     * @param TimerService $timer
46
     * @param LoggingService|null $logging
47
     */
48
    public function __construct(AbstractFacade $dbFacade,
49
        protected TimerService $timer, protected LoggingService|null $logging)
50
    {
51
        parent::__construct($dbFacade);
52
    }
53
54
    /**
55
     * Open a second connection to the server
56
     *
57
     * @return void
58
     */
59
    private function createConnection()
60
    {
61
        // Connection for exploring indexes and EXPLAIN (to not replace FOUND_ROWS())
62
        //! PDO - silent error
63
        if ($this->connection === null && $this->driver->database() !== '') {
0 ignored issues
show
The method database() 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

63
        if ($this->connection === null && $this->driver->/** @scrutinizer ignore-call */ database() !== '') {

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...
64
            $this->connection = $this->driver ->newConnection(
65
                $this->driver->database(), $this->driver->schema());
66
        }
67
    }
68
69
    /**
70
     * @param array $row
71
     * @param array $blobs
72
     * @param array $types
73
     *
74
     * @return array
75
    */
76
    protected function values(array $row, array $blobs, array $types): array
77
    {
78
        $values = [];
79
        foreach ($row as $key => $value) {
80
            // $link = $this->editLink($val);
81
            if ($value === null) {
82
                $value = '<i>NULL</i>';
83
            } elseif (isset($blobs[$key]) && $blobs[$key] && !$this->utils->str->isUtf8($value)) {
84
                //! link to download
85
                $value = '<i>' . $this->utils->trans->lang('%d byte(s)', strlen($value)) . '</i>';
86
            } else {
87
                $value = $this->utils->str->html($value);
88
                if (isset($types[$key]) && $types[$key] == 254) { // 254 - char
89
                    $value = "<code>$value</code>";
90
                }
91
            }
92
            $values[$key] = $value;
93
        }
94
        return $values;
95
    }
96
97
    /**
98
     * @param mixed $statement
99
     * @param int $limit
100
     *
101
     * @return string
102
    */
103
    private function message($statement, int $limit): string
104
    {
105
        $numRows = $statement->rowCount();
106
        $message = '';
107
        if ($numRows > 0) {
108
            if ($limit > 0 && $numRows > $limit) {
109
                $message = $this->utils->trans->lang('%d / ', $limit);
110
            }
111
            $message .= $this->utils->trans->lang('%d row(s)', $numRows);
112
        }
113
        return $message;
114
    }
115
116
    /**
117
     * Print select result
118
     * From editing.inc.php
119
     *
120
     * @param mixed $statement
121
     * @param int $limit
122
     *
123
     * @return array
124
    */
125
    protected function select($statement, int $limit = 0): array
126
    {
127
        // No resultset
128
        if ($statement === true) {
129
            $affected = $this->driver->affectedRows();
130
            $message = $this->utils->trans
131
                ->lang('Query executed OK, %d row(s) affected.', $affected); //  . "$time";
132
            return [null, [$message]];
133
        }
134
        // Fetch the first row.
135
        if (!($row = $statement->fetchRow())) {
136
            // Empty resultset.
137
            $message = $this->utils->trans->lang('No rows.');
138
            return [null, [$message]];
139
        }
140
141
        $blobs = []; // colno => bool - display bytes for blobs
142
        $types = []; // colno => type - display char in <code>
143
        $tables = []; // table => orgtable - mapping to use in EXPLAIN
144
        $headers = [];
145
        $details = [];
146
        // Table headers.
147
        $colCount = count($row);
148
        for ($j = 0; $j < $colCount; $j++) {
149
            $field = $statement->fetchField();
150
            // PostgreSQL fix: the table field can be missing.
151
            $tables[$field->tableName()] = $field->orgTable();
152
            // $this->indexes($field);
153
            if ($field->isBinary()) {
154
                $blobs[$j] = true;
155
            }
156
            $types[$j] = $field->type(); // Some drivers don't set the type field.
157
            $headers[] = $this->utils->str->html($field->name());
158
        }
159
160
        // Table rows (the first was already fetched).
161
        $rowCount = 0;
162
        do {
163
            $rowCount++;
164
            $details[] = $this->values($row, $blobs, $types);
165
        } while (($limit === 0 || $rowCount < $limit) && ($row = $statement->fetchRow()));
166
167
        $message = $this->message($statement, $limit);
168
        return [compact('tables', 'headers', 'details'), [$message]];
169
    }
170
171
    /**
172
     * @param QueryEntity $queryEntity
173
     *
174
     * @return bool
175
     */
176
    private function executeCommand(QueryEntity $queryEntity): bool
177
    {
178
        if ($this->logging !== null) {
179
            $this->logging->setCategoryToHistory();
180
        }
181
        $this->timer->start();
182
        //! Don't allow changing of character_set_results, convert encoding of displayed query
183
        if ($this->driver->multiQuery($queryEntity->query)) {
184
            $this->driver->execUseQuery($queryEntity->query);
185
        }
186
        $this->duration += $this->timer->duration();
187
188
        do {
189
            $select = null;
190
            $errors = [];
191
            $messages = [];
192
            $statement = $this->driver->storedResult();
193
194
            if ($this->connection->hasError()) {
0 ignored issues
show
The method hasError() does not exist on Lagdo\DbAdmin\Driver\Db\ConnectionInterface. It seems like you code against a sub-type of Lagdo\DbAdmin\Driver\Db\ConnectionInterface such as Lagdo\DbAdmin\Driver\Db\Connection. ( Ignorable by Annotation )

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

194
            if ($this->connection->/** @scrutinizer ignore-call */ hasError()) {
Loading history...
195
                $errors[] = $this->connection->errorMessage();
0 ignored issues
show
The method errorMessage() does not exist on Lagdo\DbAdmin\Driver\Db\ConnectionInterface. It seems like you code against a sub-type of Lagdo\DbAdmin\Driver\Db\ConnectionInterface such as Lagdo\DbAdmin\Driver\Db\Connection. ( Ignorable by Annotation )

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

195
                /** @scrutinizer ignore-call */ 
196
                $errors[] = $this->connection->errorMessage();
Loading history...
196
            } elseif (!$queryEntity->onlyErrors) {
197
                [$select, $messages] = $this->select($statement, $queryEntity->limit);
198
            }
199
200
            $result = compact('errors', 'messages', 'select');
201
            $result['query'] = $queryEntity->query;
202
            $this->results[] = $result;
203
            if ($this->connection->hasError() && $queryEntity->errorStops) {
204
                return false;
205
            }
206
        } while ($this->driver->nextResult());
207
208
        return true;
209
    }
210
211
    /**
212
     * Execute a set of queries
213
     *
214
     * @param string $queries       The queries to execute
215
     * @param int    $limit         The max number of rows to return
216
     * @param bool   $errorStops    Stop executing the requests in case of error
217
     * @param bool   $onlyErrors    Return only errors
218
     *
219
     * @return array
220
     */
221
    public function executeCommands(string $queries, int $limit, bool $errorStops, bool $onlyErrors): array
222
    {
223
        if (function_exists('memory_get_usage')) {
224
            // @ - may be disabled, 2 - substr and trim, 8e6 - other variables
225
            try {
226
                ini_set('memory_limit', max($this->admin->iniBytes('memory_limit'),
227
                    2 * strlen($queries) + memory_get_usage() + 8e6));
228
            }
229
            catch(\Exception $e) {
230
                // Do nothing if the option is not modified.
231
            }
232
        }
233
234
        // The second connection must be created before executing the queries.
235
        $this->createConnection();
236
237
        $this->results = [];
238
        $this->duration = 0;
239
        $commands = 0;
240
        $errors = 0;
241
        $queryEntity = new QueryEntity($queries, $limit, $errorStops, $onlyErrors);
242
        while ($this->driver->parseQueries($queryEntity)) {
243
            $commands++;
244
            if (!$this->executeCommand($queryEntity)) {
245
                $errors++;
246
                if ($errorStops) {
247
                    break;
248
                }
249
            }
250
        }
251
252
        $messages = [];
253
        if ($commands === 0) {
254
            $messages[] = $this->utils->trans->lang('No commands to execute.');
255
        } elseif ($onlyErrors) {
256
            $messages[] =  $this->utils->trans->lang('%d query(s) executed OK.', $commands - $errors);
257
        }
258
        return [
259
            'results' => $this->results,
260
            'messages' => $messages,
261
            'duration' => $this->duration,
262
        ];
263
    }
264
}
265