Passed
Pull Request — 4 (#10364)
by Guy
06:37
created

Backtrace   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 1
Metric Value
eloc 91
c 5
b 0
f 1
dl 0
loc 202
rs 8.8798
wmc 44

6 Methods

Rating   Name   Duplication   Size   Complexity  
B get_rendered_backtrace() 0 30 11
A matchesFilterableClass() 0 3 3
A backtrace() 0 9 4
A filtered_backtrace() 0 3 1
B full_func_name() 0 28 11
C filter_backtrace() 0 60 14

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Core\Config\Configurable;
7
8
/**
9
 * Backtrace helper
10
 */
11
class Backtrace
12
{
13
    use Configurable;
14
15
    /**
16
     * Replaces all arguments with a '<filtered>' string,
17
     * mostly for security reasons. Use string values for global functions,
18
     * and array notation for class methods.
19
     * PHP's debug_backtrace() doesn't allow to inspect the argument names,
20
     * so all arguments of the provided functions will be filtered out.
21
     * @var array
22
     */
23
    private static $ignore_function_args = [];
0 ignored issues
show
introduced by
The private property $ignore_function_args is not used, and could be removed.
Loading history...
24
25
    /**
26
     * Return debug_backtrace() results with functions filtered
27
     * specific to the debugging system, and not the trace.
28
     *
29
     * @param null|array $ignoredFunctions If an array, filter these functions out of the trace
30
     * @return array
31
     */
32
    public static function filtered_backtrace($ignoredFunctions = null)
33
    {
34
        return self::filter_backtrace(debug_backtrace(), $ignoredFunctions);
35
    }
36
37
    /**
38
     * Filter a backtrace so that it doesn't show the calls to the
39
     * debugging system, which is useless information.
40
     *
41
     * @param array $bt Backtrace to filter
42
     * @param null|array $ignoredFunctions List of extra functions to filter out
43
     * @return array
44
     */
45
    public static function filter_backtrace($bt, $ignoredFunctions = null)
46
    {
47
        $defaultIgnoredFunctions = [
48
            'SilverStripe\\Logging\\Log::log',
49
            'SilverStripe\\Dev\\Backtrace::backtrace',
50
            'SilverStripe\\Dev\\Backtrace::filtered_backtrace',
51
            'Zend_Log_Writer_Abstract->write',
52
            'Zend_Log->log',
53
            'Zend_Log->__call',
54
            'Zend_Log->err',
55
            'SilverStripe\\Dev\\DebugView->writeTrace',
56
            'SilverStripe\\Dev\\CliDebugView->writeTrace',
57
            'SilverStripe\\Dev\\Debug::emailError',
58
            'SilverStripe\\Dev\\Debug::warningHandler',
59
            'SilverStripe\\Dev\\Debug::noticeHandler',
60
            'SilverStripe\\Dev\\Debug::fatalHandler',
61
            'errorHandler',
62
            'SilverStripe\\Dev\\Debug::showError',
63
            'SilverStripe\\Dev\\Debug::backtrace',
64
            'exceptionHandler'
65
        ];
66
67
        if ($ignoredFunctions) {
68
            foreach ($ignoredFunctions as $ignoredFunction) {
69
                $defaultIgnoredFunctions[] = $ignoredFunction;
70
            }
71
        }
72
73
        while ($bt && in_array(self::full_func_name($bt[0]), $defaultIgnoredFunctions ?? [])) {
74
            array_shift($bt);
75
        }
76
77
        $ignoredArgs = static::config()->get('ignore_function_args');
78
79
        // Filter out arguments
80
        foreach ($bt as $i => $frame) {
81
            $match = false;
82
            if (!empty($frame['class'])) {
83
                foreach ($ignoredArgs as $fnSpec) {
84
                    if (is_array($fnSpec)
85
                        && self::matchesFilterableClass($frame['class'], $fnSpec[0])
86
                        && $frame['function'] == $fnSpec[1]
87
                    ) {
88
                        $match = true;
89
                        break;
90
                    }
91
                }
92
            } else {
93
                if (in_array($frame['function'], $ignoredArgs ?? [])) {
94
                    $match = true;
95
                }
96
            }
97
            if ($match) {
98
                foreach ($frame['args'] as $j => $arg) {
99
                    $bt[$i]['args'][$j] = '<filtered>';
100
                }
101
            }
102
        }
103
104
        return $bt;
105
    }
106
107
    /**
108
     * Render or return a backtrace from the given scope.
109
     *
110
     * @param mixed $returnVal
111
     * @param bool $ignoreAjax
112
     * @param array $ignoredFunctions
113
     * @return mixed
114
     */
115
    public static function backtrace($returnVal = false, $ignoreAjax = false, $ignoredFunctions = null)
116
    {
117
        $plainText = Director::is_cli() || (Director::is_ajax() && !$ignoreAjax);
118
        $result = self::get_rendered_backtrace(debug_backtrace(), $plainText, $ignoredFunctions);
119
        if ($returnVal) {
120
            return $result;
121
        } else {
122
            echo $result;
123
            return null;
124
        }
125
    }
126
127
    /**
128
     * Return the full function name.  If showArgs is set to true, a string representation of the arguments will be
129
     * shown
130
     *
131
     * @param Object $item
132
     * @param bool $showArgs
133
     * @param int $argCharLimit
134
     * @return string
135
     */
136
    public static function full_func_name($item, $showArgs = false, $argCharLimit = 10000)
137
    {
138
        $funcName = '';
139
        if (isset($item['class'])) {
140
            $funcName .= $item['class'];
141
        }
142
        if (isset($item['type'])) {
143
            $funcName .= $item['type'];
144
        }
145
        if (isset($item['function'])) {
146
            $funcName .= $item['function'];
147
        }
148
149
        if ($showArgs && isset($item['args'])) {
150
            $args = [];
151
            foreach ($item['args'] as $arg) {
152
                if (!is_object($arg) || method_exists($arg, '__toString')) {
153
                    $sarg = is_array($arg) ? 'Array' : strval($arg);
154
                    $args[] = (strlen($sarg ?? '') > $argCharLimit) ? substr($sarg, 0, $argCharLimit) . '...' : $sarg;
155
                } else {
156
                    $args[] = get_class($arg);
157
                }
158
            }
159
160
            $funcName .= "(" . implode(", ", $args) . ")";
161
        }
162
163
        return $funcName;
164
    }
165
166
    /**
167
     * Render a backtrace array into an appropriate plain-text or HTML string.
168
     *
169
     * @param array $bt The trace array, as returned by debug_backtrace() or Exception::getTrace()
170
     * @param boolean $plainText Set to false for HTML output, or true for plain-text output
171
     * @param array $ignoredFunctions List of functions that should be ignored. If not set, a default is provided
172
     * @return string The rendered backtrace
173
     */
174
    public static function get_rendered_backtrace($bt, $plainText = false, $ignoredFunctions = null)
175
    {
176
        if (empty($bt)) {
177
            return '';
178
        }
179
        $bt = self::filter_backtrace($bt, $ignoredFunctions);
180
        $result = ($plainText) ? '' : '<ul>';
181
        foreach ($bt as $item) {
182
            if ($plainText) {
183
                $result .= self::full_func_name($item, true) . "\n";
184
                if (isset($item['line']) && isset($item['file'])) {
185
                    $result .= basename($item['file'] ?? '') . ":$item[line]\n";
186
                }
187
                $result .= "\n";
188
            } else {
189
                if ($item['function'] == 'user_error') {
190
                    $name = $item['args'][0];
191
                } else {
192
                    $name = self::full_func_name($item, true);
193
                }
194
                $result .= "<li><b>" . htmlentities($name ?? '', ENT_COMPAT, 'UTF-8') . "</b>\n<br />\n";
195
                $result .=  isset($item['file']) ? htmlentities(basename($item['file']), ENT_COMPAT, 'UTF-8') : '';
196
                $result .= isset($item['line']) ? ":$item[line]" : '';
197
                $result .= "</li>\n";
198
            }
199
        }
200
        if (!$plainText) {
201
            $result .= '</ul>';
202
        }
203
        return $result;
204
    }
205
206
    /**
207
     * Checks if the filterable class is wildcard, of if the class name is the filterable class, or a subclass of it,
208
     * or implements it.
209
     */
210
    private static function matchesFilterableClass(string $className, string $filterableClass): bool
211
    {
212
        return $filterableClass === '*' || $className === $filterableClass || is_subclass_of($className, $filterableClass);
213
    }
214
}
215