BetterDebugView::debugVariable()   A
last analyzed

Complexity

Conditions 6
Paths 9

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 17
c 1
b 0
f 0
nc 9
nop 4
dl 0
loc 29
rs 9.0777
1
<?php
2
3
namespace LeKoala\DevToolkit;
4
5
use Exception;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Dev\Backtrace;
9
use SilverStripe\Dev\DebugView;
10
use SilverStripe\Core\ClassInfo;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Core\Environment;
13
use SilverStripe\ORM\Connect\DatabaseException;
14
use LeKoala\Base\Helpers\DatabaseHelper;
0 ignored issues
show
Bug introduced by
The type LeKoala\Base\Helpers\DatabaseHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
16
class BetterDebugView extends DebugView
17
{
18
    /**
19
     * @config
20
     * @var string
21
     */
22
    private static $ide_placeholder = 'vscode://file/{file}:{line}:0';
23
24
    /**
25
     * @param string $file
26
     * @param string|int $line
27
     * @return string
28
     */
29
    public static function makeIdeLink($file, $line)
30
    {
31
        $shortname = basename($file);
32
33
        // does not make sense in testing or live
34
        if (!Director::isDev()) {
35
            return "$shortname:$line";
36
        }
37
38
        $placeholder = self::config()->ide_placeholder;
39
40
        // each dev can define their own settings
41
        $envPlaceholder = Environment::getEnv('IDE_PLACEHOLDER');
42
        if ($envPlaceholder) {
43
            $placeholder = $envPlaceholder;
44
        }
45
46
        $ide_link = str_replace(['{file}', '{line}'], [$file, $line], $placeholder);
47
        $link = "<a href=\"$ide_link\">$shortname:$line</a>";
48
        return $link;
49
    }
50
51
    /**
52
     * Similar to renderVariable() but respects debug() method on object if available
53
     *
54
     * @param mixed $val
55
     * @param array<string,mixed> $caller
56
     * @param bool $showHeader
57
     * @param int|null $argumentIndex
58
     * @return string
59
     */
60
    public function debugVariable($val, $caller, $showHeader = true, $argumentIndex = 0)
61
    {
62
        // Get arguments name
63
        $args = $this->extractArgumentsName($caller['file'], $caller['line']);
64
65
        if ($showHeader) {
66
            $callerFormatted = $this->formatCaller($caller);
67
            $defaultArgumentName = is_int($argumentIndex) ? 'Debug' : $argumentIndex;
68
            $argumentName = $args[$argumentIndex] ?? $defaultArgumentName;
69
70
            // Sql trick
71
            if (strpos(strtolower($argumentName), 'sql') !== false && is_string($val)) {
0 ignored issues
show
Bug introduced by
It seems like $argumentName can also be of type null; however, parameter $string of strtolower() does only seem to accept string, 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

71
            if (strpos(strtolower(/** @scrutinizer ignore-type */ $argumentName), 'sql') !== false && is_string($val)) {
Loading history...
72
                $text = DatabaseHelper::formatSQL($val);
73
            } else {
74
                $text = $this->debugVariableText($val);
75
            }
76
77
            $html = "<div style=\"background-color: white; text-align: left;\">\n<hr>\n"
78
                . "<h3>$argumentName <span style=\"font-size: 65%\">($callerFormatted)</span>\n</h3>\n"
79
                . $text
80
                . "</div>";
81
82
            if (Director::is_ajax()) {
83
                $html = strip_tags($html);
84
            }
85
86
            return $html;
87
        }
88
        return $this->debugVariableText($val);
89
    }
90
91
    /**
92
     * @param string $file
93
     * @param int $line
94
     * @return array<mixed>
95
     */
96
    protected function extractArgumentsName($file, $line)
97
    {
98
        // Arguments passed to the function are stored in matches
99
        $src = file($file);
100
        $src_line = $src[$line - 1];
101
        preg_match("/d\((.+)\)/", $src_line, $matches);
102
        // Find all arguments, ignore variables within parenthesis
103
        $arguments = array();
104
        if (!empty($matches[1])) {
105
            $arguments = array_map('trim', preg_split("/(?![^(]*\)),/", $matches[1]));
106
        }
107
        return $arguments;
108
    }
109
110
    /**
111
     * Get debug text for this object
112
     *
113
     * Use symfony dumper if it exists
114
     *
115
     * @param mixed $val
116
     * @return string
117
     */
118
    public function debugVariableText($val)
119
    {
120
        // Empty stuff is tricky
121
        if (empty($val)) {
122
            $valtype = gettype($val);
123
            return "<em>(empty $valtype)</em>";
124
        }
125
126
        if (Director::is_ajax()) {
127
            // In ajax context, we can still use debug info
128
            if (is_object($val)) {
129
                if (ClassInfo::hasMethod($val, 'debug')) {
130
                    return $val->debug();
131
                }
132
                return print_r($val, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return print_r($val, true) also could return the type true which is incompatible with the documented return type string.
Loading history...
133
            }
134
        } else {
135
            // Otherwise, we'd rater a full and usable object dump
136
            if (function_exists('dump') && (is_object($val) || is_array($val))) {
137
                ob_start();
138
                dump($val);
139
                return ob_get_clean();
140
            }
141
        }
142
143
        // Check debug
144
        if (is_object($val) && ClassInfo::hasMethod($val, 'debug')) {
145
            return $val->debug();
146
        }
147
148
        // Format as array
149
        if (is_array($val)) {
150
            return print_r($val, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return print_r($val, true) also could return the type true which is incompatible with the documented return type string.
Loading history...
151
        }
152
153
        // Format object
154
        if (is_object($val)) {
155
            return var_export($val, true);
156
        }
157
158
        // Format bool
159
        if (is_bool($val)) {
160
            return '(bool) ' . ($val ? 'true' : 'false');
161
        }
162
163
        // Format text
164
        $html = Convert::raw2xml($val);
165
        return "<pre style=\"font-family: Courier new, serif\">{$html}</pre>\n";
166
    }
167
168
    /**
169
     * Formats the caller of a method
170
     *
171
     * Improve method by creating the ide link
172
     *
173
     * @param  array $caller
174
     * @return string
175
     */
176
    protected function formatCaller($caller)
177
    {
178
        $return = self::makeIdeLink($caller['file'], $caller['line']);
179
        if (!empty($caller['class']) && !empty($caller['function'])) {
180
            $return .= " - {$caller['class']}::{$caller['function']}()";
181
        }
182
        return $return;
183
    }
184
185
    /**
186
     * Render an error.
187
     *
188
     * @param string $httpRequest the kind of request
189
     * @param int $errno Codenumber of the error
190
     * @param string $errstr The error message
191
     * @param string $errfile The name of the soruce code file where the error occurred
192
     * @param int $errline The line number on which the error occured
193
     * @return string
194
     */
195
    public function renderError($httpRequest, $errno, $errstr, $errfile, $errline)
196
    {
197
        $errorType = isset(self::$error_types[$errno]) ? self::$error_types[$errno] : self::$unknown_error;
198
        $httpRequestEnt = htmlentities($httpRequest, ENT_COMPAT, 'UTF-8');
199
        if (ini_get('html_errors')) {
200
            $errstr = strip_tags($errstr);
201
        } else {
202
            $errstr = Convert::raw2xml($errstr);
203
        }
204
205
        $infos = self::makeIdeLink($errfile, $errline);
206
207
        $output = '<div class="header info ' . $errorType['class'] . '">';
208
        $output .= "<h1>[" . $errorType['title'] . '] ' . $errstr . "</h1>";
209
        $output .= "<h3>$httpRequestEnt</h3>";
210
        $output .= "<p>$infos</p>";
211
        $output .= '</div>';
212
213
        return $output;
214
    }
215
216
    /**
217
     * @param Exception $exception
218
     * @return void
219
     */
220
    public function writeException(Exception $exception)
221
    {
222
        $infos = self::makeIdeLink($exception->getFile(), $exception->getLine());
223
224
        $output = '<div class="build error">';
225
        $output .= "<p><strong>" . get_class($exception) . "</strong> in $infos</p>";
226
        $message = $exception->getMessage();
227
        if ($exception instanceof DatabaseException) {
228
            $sql = $exception->getSQL();
229
            // Some database errors don't have sql
230
            if ($sql) {
231
                $parameters = $exception->getParameters();
232
                $sql = DB::inline_parameters($sql, $parameters);
233
                $formattedSQL = DatabaseHelper::formatSQL($sql);
234
                $message .= "<br/><br/>Couldn't run query:<br/>" . $formattedSQL;
235
            }
236
        }
237
        $output .= "<p>" . $message . "</p>";
238
        $output .= '</div>';
239
240
        $output .= $this->renderTrace($exception->getTrace());
241
242
        echo $output;
243
    }
244
245
    /**
246
     * Render a call track
247
     *
248
     * @param  array<mixed> $trace The debug_backtrace() array
249
     * @return string
250
     */
251
    public function renderTrace($trace)
252
    {
253
        $output = '<div class="info">';
254
        $output .= '<h3>Trace</h3>';
255
        $output .= self::get_rendered_backtrace($trace);
256
        $output .= '</div>';
257
258
        return $output;
259
    }
260
261
    /**
262
     * Render a backtrace array into an appropriate plain-text or HTML string.
263
     *
264
     * @param array<string,mixed> $bt The trace array, as returned by debug_backtrace() or Exception::getTrace()
265
     * @return string The rendered backtrace
266
     */
267
    public static function get_rendered_backtrace($bt)
268
    {
269
        if (empty($bt)) {
270
            return '';
271
        }
272
        $result = '<ul>';
273
        foreach ($bt as $item) {
274
            if ($item['function'] == 'user_error') {
275
                $name = $item['args'][0];
276
            } else {
277
                $name = Backtrace::full_func_name($item, true);
278
            }
279
            $result .= "<li><b>" . htmlentities($name, ENT_COMPAT, 'UTF-8') . "</b>\n<br />\n";
280
            if (!isset($item['file']) || !isset($item['line'])) {
281
                $result .= isset($item['file']) ? htmlentities(basename($item['file']), ENT_COMPAT, 'UTF-8') : '';
282
                $result .= isset($item['line']) ? ":$item[line]" : '';
283
            } else {
284
                $result .= self::makeIdeLink($item['file'], $item['line']);
285
            }
286
            $result .= "</li>\n";
287
        }
288
        $result .= '</ul>';
289
        return $result;
290
    }
291
}
292