Passed
Push — master ( d5179c...19f8e5 )
by Thomas
10:23
created

BetterDebugView   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 113
c 3
b 1
f 0
dl 0
loc 270
rs 8.96
wmc 43

9 Methods

Rating   Name   Duplication   Size   Complexity  
A debugVariable() 0 29 6
A makeIdeLink() 0 20 3
A extractArgumentsName() 0 12 2
A renderError() 0 19 3
A writeException() 0 23 3
B get_rendered_backtrace() 0 23 8
C debugVariableText() 0 48 14
A formatCaller() 0 7 3
A renderTrace() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like BetterDebugView 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 BetterDebugView, and based on these observations, apply Extract Interface, too.

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\Core\Injector\Injector;
14
use SilverStripe\ORM\Connect\DatabaseException;
15
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...
16
17
class BetterDebugView extends DebugView
18
{
19
    /**
20
     * @config
21
     * @var string
22
     */
23
    private static $ide_placeholder = 'vscode://file/{file}:{line}:0';
24
25
    /**
26
     * @param string $file
27
     * @param string $line
28
     * @return string
29
     */
30
    public static function makeIdeLink($file, $line)
31
    {
32
        $shortname = basename($file);
33
34
        // does not make sense in testing or live
35
        if (!Director::isDev()) {
36
            return "$shortname:$line";
37
        }
38
39
        $placeholder = self::config()->ide_placeholder;
40
41
        // each dev can define their own settings
42
        $envPlaceholder = Environment::getEnv('IDE_PLACEHOLDER');
43
        if ($envPlaceholder) {
44
            $placeholder = $envPlaceholder;
45
        }
46
47
        $ide_link = str_replace(['{file}', '{line}'], [$file, $line], $placeholder);
48
        $link = "<a href=\"$ide_link\">$shortname:$line</a>";
49
        return $link;
50
    }
51
52
    /**
53
     * Similar to renderVariable() but respects debug() method on object if available
54
     *
55
     * @param mixed $val
56
     * @param array $caller
57
     * @param bool $showHeader
58
     * @param int $argumentIndex
59
     * @return string
60
     */
61
    public function debugVariable($val, $caller, $showHeader = true, $argumentIndex = 0)
62
    {
63
        // Get arguments name
64
        $args = $this->extractArgumentsName($caller['file'], $caller['line']);
65
66
        if ($showHeader) {
67
            $callerFormatted = $this->formatCaller($caller);
68
            $defaultArgumentName = is_int($argumentIndex) ? 'Debug' : $argumentIndex;
0 ignored issues
show
introduced by
The condition is_int($argumentIndex) is always true.
Loading history...
69
            $argumentName = $args[$argumentIndex] ?? $defaultArgumentName;
70
71
            // Sql trick
72
            if (strpos(strtolower($argumentName), 'sql') !== false && is_string($val)) {
73
                $text = DatabaseHelper::formatSQL($val);
74
            } else {
75
                $text = $this->debugVariableText($val);
76
            }
77
78
            $html = "<div style=\"background-color: white; text-align: left;\">\n<hr>\n"
79
                . "<h3>$argumentName <span style=\"font-size: 65%\">($callerFormatted)</span>\n</h3>\n"
80
                . $text
81
                . "</div>";
82
83
            if (Director::is_ajax()) {
84
                $html = strip_tags($html);
85
            }
86
87
            return $html;
88
        }
89
        return $this->debugVariableText($val);
90
    }
91
92
    /**
93
     * @param string $file
94
     * @param int $line
95
     * @return array
96
     */
97
    protected function extractArgumentsName($file, $line)
98
    {
99
        // Arguments passed to the function are stored in matches
100
        $src = file($file);
101
        $src_line = $src[$line - 1];
102
        preg_match("/d\((.+)\)/", $src_line, $matches);
103
        // Find all arguments, ignore variables within parenthesis
104
        $arguments = array();
105
        if (!empty($matches[1])) {
106
            $arguments = array_map('trim', preg_split("/(?![^(]*\)),/", $matches[1]));
107
        }
108
        return $arguments;
109
    }
110
111
    /**
112
     * Get debug text for this object
113
     *
114
     * Use symfony dumper if it exists
115
     *
116
     * @param mixed $val
117
     * @return string
118
     */
119
    public function debugVariableText($val)
120
    {
121
        // Empty stuff is tricky
122
        if (empty($val)) {
123
            $valtype = gettype($val);
124
            return "<em>(empty $valtype)</em>";
125
        }
126
127
        if (Director::is_ajax()) {
128
            // In ajax context, we can still use debug info
129
            if (is_object($val)) {
130
                if (ClassInfo::hasMethod($val, 'debug')) {
131
                    return $val->debug();
132
                }
133
                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...
134
            }
135
        } else {
136
            // Otherwise, we'd rater a full and usable object dump
137
            if (function_exists('dump') && (is_object($val) || is_array($val))) {
138
                ob_start();
139
                dump($val);
140
                return ob_get_clean();
141
            }
142
        }
143
144
        // Check debug
145
        if (is_object($val) && ClassInfo::hasMethod($val, 'debug')) {
146
            return $val->debug();
147
        }
148
149
        // Format as array
150
        if (is_array($val)) {
151
            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...
152
        }
153
154
        // Format object
155
        if (is_object($val)) {
156
            return var_export($val, true);
157
        }
158
159
        // Format bool
160
        if (is_bool($val)) {
161
            return '(bool) ' . ($val ? 'true' : 'false');
162
        }
163
164
        // Format text
165
        $html = Convert::raw2xml($val);
166
        return "<pre style=\"font-family: Courier new, serif\">{$html}</pre>\n";
167
    }
168
169
    /**
170
     * Formats the caller of a method
171
     *
172
     * Improve method by creating the ide link
173
     *
174
     * @param  array $caller
175
     * @return string
176
     */
177
    protected function formatCaller($caller)
178
    {
179
        $return = self::makeIdeLink($caller['file'], $caller['line']);
180
        if (!empty($caller['class']) && !empty($caller['function'])) {
181
            $return .= " - {$caller['class']}::{$caller['function']}()";
182
        }
183
        return $return;
184
    }
185
186
    /**
187
     * Render an error.
188
     *
189
     * @param string $httpRequest the kind of request
190
     * @param int $errno Codenumber of the error
191
     * @param string $errstr The error message
192
     * @param string $errfile The name of the soruce code file where the error occurred
193
     * @param int $errline The line number on which the error occured
194
     * @return string
195
     */
196
    public function renderError($httpRequest, $errno, $errstr, $errfile, $errline)
197
    {
198
        $errorType = isset(self::$error_types[$errno]) ? self::$error_types[$errno] : self::$unknown_error;
199
        $httpRequestEnt = htmlentities($httpRequest, ENT_COMPAT, 'UTF-8');
200
        if (ini_get('html_errors')) {
201
            $errstr = strip_tags($errstr);
202
        } else {
203
            $errstr = Convert::raw2xml($errstr);
204
        }
205
206
        $infos = self::makeIdeLink($errfile, $errline);
207
208
        $output = '<div class="header info ' . $errorType['class'] . '">';
209
        $output .= "<h1>[" . $errorType['title'] . '] ' . $errstr . "</h1>";
210
        $output .= "<h3>$httpRequestEnt</h3>";
211
        $output .= "<p>$infos</p>";
212
        $output .= '</div>';
213
214
        return $output;
215
    }
216
217
    public function writeException(Exception $exception)
218
    {
219
        $infos = self::makeIdeLink($exception->getFile(), $exception->getLine());
220
221
        $output = '<div class="build error">';
222
        $output .= "<p><strong>" . get_class($exception) . "</strong> in $infos</p>";
223
        $message = $exception->getMessage();
224
        if ($exception instanceof DatabaseException) {
225
            $sql = $exception->getSQL();
226
            // Some database errors don't have sql
227
            if ($sql) {
228
                $parameters = $exception->getParameters();
229
                $sql = DB::inline_parameters($sql, $parameters);
230
                $formattedSQL = DatabaseHelper::formatSQL($sql);
231
                $message .= "<br/><br/>Couldn't run query:<br/>" . $formattedSQL;
232
            }
233
        }
234
        $output .= "<p>" . $message . "</p>";
235
        $output .= '</div>';
236
237
        $output .= $this->renderTrace($exception->getTrace());
238
239
        echo $output;
240
    }
241
242
    /**
243
     * Render a call track
244
     *
245
     * @param  array $trace The debug_backtrace() array
246
     * @return string
247
     */
248
    public function renderTrace($trace)
249
    {
250
        $output = '<div class="info">';
251
        $output .= '<h3>Trace</h3>';
252
        $output .= self::get_rendered_backtrace($trace);
253
        $output .= '</div>';
254
255
        return $output;
256
    }
257
258
    /**
259
     * Render a backtrace array into an appropriate plain-text or HTML string.
260
     *
261
     * @param array $bt The trace array, as returned by debug_backtrace() or Exception::getTrace()
262
     * @return string The rendered backtrace
263
     */
264
    public static function get_rendered_backtrace($bt)
265
    {
266
        if (empty($bt)) {
267
            return '';
268
        }
269
        $result = '<ul>';
270
        foreach ($bt as $item) {
271
            if ($item['function'] == 'user_error') {
272
                $name = $item['args'][0];
273
            } else {
274
                $name = Backtrace::full_func_name($item, true);
275
            }
276
            $result .= "<li><b>" . htmlentities($name, ENT_COMPAT, 'UTF-8') . "</b>\n<br />\n";
277
            if (!isset($item['file']) || !isset($item['line'])) {
278
                $result .= isset($item['file']) ? htmlentities(basename($item['file']), ENT_COMPAT, 'UTF-8') : '';
279
                $result .= isset($item['line']) ? ":$item[line]" : '';
280
            } else {
281
                $result .= self::makeIdeLink($item['file'], $item['line']);
282
            }
283
            $result .= "</li>\n";
284
        }
285
        $result .= '</ul>';
286
        return $result;
287
    }
288
}
289