• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

dg / dibi / 22284472534

22 Feb 2026 08:11PM UTC coverage: 76.31% (-0.2%) from 76.552%
22284472534

push

github

dg
phpstan

56 of 106 new or added lines in 23 files covered. (52.83%)

82 existing lines in 8 files now uncovered.

1762 of 2309 relevant lines covered (76.31%)

0.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.17
/src/Dibi/Helpers.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Dibi, smart database abstraction layer (https://dibi.nette.org)
5
 * Copyright (c) 2005 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Dibi;
9

10
use function array_map, array_unique, explode, fclose, fgets, fopen, fstat, getenv, htmlspecialchars, is_float, is_int, is_string, levenshtein, max, mb_strlen, ob_end_flush, ob_get_clean, ob_start, preg_match, preg_replace, preg_replace_callback, rtrim, set_time_limit, str_ends_with, str_repeat, str_starts_with, strlen, strtoupper, strval, substr, trim, wordwrap;
11
use const PHP_SAPI;
12

13

14
class Helpers
15
{
16
        private static HashMap $types;
17

18

19
        /**
20
         * Prints out a syntax highlighted version of the SQL command or Result.
21
         */
22
        public static function dump(string|Result|null $sql = null, bool $return = false): ?string
23
        {
24
                ob_start();
×
25
                if ($sql instanceof Result && PHP_SAPI === 'cli') {
×
26
                        $hasColors = (str_starts_with((string) getenv('TERM'), 'xterm'));
×
27
                        $maxLen = 0;
×
28
                        foreach ($sql as $i => $row) {
×
29
                                if ($i === 0) {
×
30
                                        foreach ($row as $col => $foo) {
×
NEW
31
                                                $len = mb_strlen((string) $col);
×
32
                                                $maxLen = max($len, $maxLen);
×
33
                                        }
34
                                }
35

36
                                echo $hasColors ? "\033[1;37m#row: $i\033[0m\n" : "#row: $i\n";
×
37
                                foreach ($row as $col => $val) {
×
NEW
38
                                        $spaces = $maxLen - mb_strlen((string) $col) + 2;
×
39
                                        echo "$col" . str_repeat(' ', $spaces) . "$val\n";
×
40
                                }
41

42
                                echo "\n";
×
43
                        }
44

45
                        echo empty($row) ? "empty result set\n\n" : "\n";
×
46

47
                } elseif ($sql instanceof Result) {
×
48
                        foreach ($sql as $i => $row) {
×
49
                                if ($i === 0) {
×
50
                                        echo "\n<table class=\"dump\">\n<thead>\n\t<tr>\n\t\t<th>#row</th>\n";
×
51
                                        foreach ($row as $col => $foo) {
×
52
                                                echo "\t\t<th>" . htmlspecialchars((string) $col) . "</th>\n";
×
53
                                        }
54

55
                                        echo "\t</tr>\n</thead>\n<tbody>\n";
×
56
                                }
57

58
                                echo "\t<tr>\n\t\t<th>", $i, "</th>\n";
×
59
                                foreach ($row as $col) {
×
60
                                        echo "\t\t<td>", htmlspecialchars((string) $col), "</td>\n";
×
61
                                }
62

63
                                echo "\t</tr>\n";
×
64
                        }
65

66
                        echo empty($row)
×
67
                                ? '<p><em>empty result set</em></p>'
×
68
                                : "</tbody>\n</table>\n";
×
69

70
                } else {
71
                        if ($sql === null) {
×
72
                                $sql = \dibi::$sql;
×
73
                        }
74

75
                        $keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|FETCH\s+NEXT|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE|START\s+TRANSACTION|BEGIN|COMMIT|ROLLBACK(?:\s+TO\s+SAVEPOINT)?|(?:RELEASE\s+)?SAVEPOINT';
×
76
                        $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|LIKE|RLIKE|REGEXP|TRUE|FALSE';
×
77

78
                        // insert new lines
79
                        $sql = " $sql ";
×
80
                        $sql = preg_replace("#(?<=[\\s,(])($keywords1)(?=[\\s,)])#i", "\n\$1", $sql);
×
81

82
                        // reduce spaces
83
                        $sql = preg_replace('#[ \t]{2,}#', ' ', $sql);
×
84

85
                        $sql = wordwrap($sql, 100);
×
86
                        $sql = preg_replace("#([ \t]*\r?\n){2,}#", "\n", $sql);
×
87

88
                        // syntax highlight
89
                        $highlighter = "#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is";
×
90
                        if (PHP_SAPI === 'cli') {
×
91
                                if (str_starts_with((string) getenv('TERM'), 'xterm')) {
×
92
                                        $sql = preg_replace_callback($highlighter, function (array $m) {
×
93
                                                if (!empty($m[1])) { // comment
94
                                                        return "\033[1;30m" . $m[1] . "\033[0m";
95

96
                                                } elseif (!empty($m[2])) { // error
97
                                                        return "\033[1;31m" . $m[2] . "\033[0m";
98

99
                                                } elseif (!empty($m[3])) { // most important keywords
100
                                                        return "\033[1;34m" . $m[3] . "\033[0m";
101

102
                                                } elseif ($m[4] !== '') { // other keywords
103
                                                        return "\033[1;32m" . $m[4] . "\033[0m";
104
                                                }
105
                                        }, $sql);
×
106
                                }
107

108
                                echo trim($sql) . "\n\n";
×
109

110
                        } else {
111
                                $sql = htmlspecialchars($sql);
×
112
                                $sql = preg_replace_callback($highlighter, function (array $m) {
×
113
                                        if (!empty($m[1])) { // comment
114
                                                return '<em style="color:gray">' . $m[1] . '</em>';
115

116
                                        } elseif (!empty($m[2])) { // error
117
                                                return '<strong style="color:red">' . $m[2] . '</strong>';
118

119
                                        } elseif (!empty($m[3])) { // most important keywords
120
                                                return '<strong style="color:blue">' . $m[3] . '</strong>';
121

122
                                        } elseif ($m[4] !== '') { // other keywords
123
                                                return '<strong style="color:green">' . $m[4] . '</strong>';
124
                                        }
125
                                }, $sql);
×
126
                                echo '<pre class="dump">', trim($sql), "</pre>\n\n";
×
127
                        }
128
                }
129

130
                if ($return) {
×
131
                        return ob_get_clean();
×
132
                } else {
133
                        ob_end_flush();
×
134
                        return null;
×
135
                }
136
        }
137

138

139
        /**
140
         * Finds the best suggestion.
141
         * @param  string[]  $items
142
         * @internal
143
         */
144
        public static function getSuggestion(array $items, string $value): ?string
1✔
145
        {
146
                $best = null;
1✔
147
                $min = (strlen($value) / 4 + 1) * 10 + .1;
1✔
148
                $items = array_map(strval(...), $items);
1✔
149
                foreach (array_unique($items) as $item) {
1✔
150
                        if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) {
1✔
151
                                $min = $len;
1✔
152
                                $best = $item;
1✔
153
                        }
154
                }
155

156
                return $best;
1✔
157
        }
158

159

160
        /** @internal */
161
        public static function escape(Driver $driver, mixed $value, string $type): string
162
        {
163
                $types = [
×
164
                        Type::Text => 'text',
×
165
                        Type::Binary => 'binary',
×
166
                        Type::Bool => 'bool',
×
167
                        Type::Date => 'date',
×
168
                        Type::DateTime => 'datetime',
×
169
                        Fluent::Identifier => 'identifier',
×
170
                ];
171
                if (isset($types[$type])) {
×
172
                        return $driver->{'escape' . $types[$type]}($value);
×
173
                } else {
174
                        throw new \InvalidArgumentException('Unsupported type.');
×
175
                }
176
        }
177

178

179
        /**
180
         * Heuristic type detection.
181
         * @internal
182
         */
183
        public static function detectType(string $type): ?string
1✔
184
        {
185
                $patterns = [
1✔
186
                        '^_' => Type::Text, // PostgreSQL arrays
1✔
187
                        'RANGE$' => Type::Text, // PostgreSQL range types
1✔
188
                        'BYTEA|BLOB|BIN' => Type::Binary,
1✔
189
                        'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::Text,
1✔
190
                        'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::Integer,
1✔
191
                        'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::Float,
1✔
192
                        '^TIME$' => Type::Time,
1✔
193
                        'TIME' => Type::DateTime, // DATETIME, TIMESTAMP
1✔
194
                        'DATE' => Type::Date,
1✔
195
                        'BOOL' => Type::Bool,
1✔
196
                        'JSON' => Type::JSON,
1✔
197
                ];
198

199
                foreach ($patterns as $s => $val) {
1✔
200
                        if (preg_match("#$s#i", $type)) {
1✔
201
                                return $val;
1✔
202
                        }
203
                }
204

205
                return null;
1✔
206
        }
207

208

209
        /** @internal */
210
        public static function getTypeCache(): HashMap
211
        {
212
                if (!isset(self::$types)) {
1✔
213
                        self::$types = new HashMap(self::detectType(...));
1✔
214
                }
215

216
                return self::$types;
1✔
217
        }
218

219

220
        /**
221
         * Apply configuration alias or default values.
222
         * @param  array<string, mixed>  $config
223
         */
224
        public static function alias(array &$config, string $key, string $alias): void
1✔
225
        {
226
                $foo = &$config;
1✔
227
                foreach (explode('|', $key) as $key) {
1✔
228
                        $foo = &$foo[$key];
1✔
229
                }
230

231
                if (!isset($foo) && isset($config[$alias])) {
1✔
232
                        $foo = $config[$alias];
1✔
233
                        unset($config[$alias]);
1✔
234
                }
235
        }
1✔
236

237

238
        /**
239
         * Import SQL dump from file.
240
         * Returns count of sql commands
241
         * @param  (callable(int, float|null): void)|null  $onProgress
242
         */
243
        public static function loadFromFile(Connection $connection, string $file, ?callable $onProgress = null): int
1✔
244
        {
245
                @set_time_limit(0); // intentionally @
1✔
246

247
                $handle = @fopen($file, 'r'); // intentionally @
1✔
248
                if (!$handle) {
1✔
249
                        throw new \RuntimeException("Cannot open file '$file'.");
×
250
                }
251

252
                $stat = fstat($handle);
1✔
253
                $count = $size = 0;
1✔
254
                $delimiter = ';';
1✔
255
                $sql = '';
1✔
256
                $driver = $connection->getDriver();
1✔
257
                while (($s = fgets($handle)) !== false) {
1✔
258
                        $size += strlen($s);
1✔
259
                        if (strtoupper(substr($s, 0, 10)) === 'DELIMITER ') {
1✔
260
                                $delimiter = trim(substr($s, 10));
×
261

262
                        } elseif (str_ends_with($ts = rtrim($s), $delimiter)) {
1✔
263
                                $sql .= substr($ts, 0, -strlen($delimiter));
1✔
264
                                $driver->query($sql);
1✔
265
                                $sql = '';
1✔
266
                                $count++;
1✔
267
                                if ($onProgress) {
1✔
NEW
268
                                        $onProgress($count, $stat ? $size * 100 / $stat['size'] : null);
×
269
                                }
270
                        } else {
271
                                $sql .= $s;
1✔
272
                        }
273
                }
274

275
                if (rtrim($sql) !== '') {
1✔
276
                        $driver->query($sql);
1✔
277
                        $count++;
1✔
278
                        if ($onProgress) {
1✔
NEW
279
                                $onProgress($count, $stat ? 100 : null);
×
280
                        }
281
                }
282

283
                fclose($handle);
1✔
284
                return $count;
1✔
285
        }
286

287

288
        /** @internal */
289
        public static function false2Null(mixed $val): mixed
1✔
290
        {
291
                return $val === false ? null : $val;
1✔
292
        }
293

294

295
        /** @internal */
296
        public static function intVal(mixed $value): int
1✔
297
        {
298
                if (is_int($value)) {
1✔
299
                        return $value;
1✔
300
                } elseif (is_string($value) && preg_match('#-?\d++\z#A', $value)) {
1✔
301
                        if (is_float($value * 1)) {
1✔
302
                                throw new Exception("Number $value is greater than integer.");
1✔
303
                        }
304

305
                        return (int) $value;
1✔
306
                } else {
307
                        throw new Exception("Expected number, '$value' given.");
1✔
308
                }
309
        }
310
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc