CommandFacade   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 105
c 4
b 0
f 0
dl 0
loc 245
rs 9.0399
wmc 42

7 Methods

Rating   Name   Duplication   Size   Complexity  
A openSecondConnection() 0 8 3
B values() 0 19 8
A message() 0 11 4
A __construct() 0 4 1
B select() 0 44 8
B executeCommand() 0 36 10
B executeCommands() 0 41 8

How to fix   Complexity   

Complex Class

Complex classes like CommandFacade often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CommandFacade, and based on these observations, apply Extract Interface, too.

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

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

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