ProxyDBExtension   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Importance

Changes 9
Bugs 2 Features 1
Metric Value
eloc 155
c 9
b 2
f 1
dl 0
loc 295
rs 8.96
wmc 43

5 Methods

Rating   Name   Duplication   Size   Complexity  
A addCustomQuery() 0 14 1
A resetQueries() 0 3 1
C updateProxy() 0 110 15
F findSource() 0 117 25
A getQueries() 0 3 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace LeKoala\DebugBar\Extension;
4
5
use SilverStripe\ORM\DB;
6
use LeKoala\DebugBar\DebugBar;
7
use SilverStripe\Core\Extension;
8
use SilverStripe\Control\Controller;
9
use TractorCow\ClassProxy\Generators\ProxyGenerator;
10
use LeKoala\DebugBar\DebugBarUtils;
11
use SilverStripe\Dev\Debug;
12
13
class ProxyDBExtension extends Extension
14
{
15
    const MAX_FIND_SOURCE_LEVEL = 3;
16
17
    /**
18
     * Store queries
19
     *
20
     * @var array<array<mixed>>
21
     */
22
    protected static $queries = [];
23
24
    /**
25
     * Find source toggle (set by config find_source)
26
     *
27
     * @var ?bool
28
     */
29
    protected static $findSource = null;
30
31
    /**
32
     * @param ProxyGenerator $proxy
33
     * @return void
34
     */
35
    public function updateProxy(ProxyGenerator &$proxy)
36
    {
37
        if (self::$findSource === null) {
38
            self::$findSource = DebugBar::config()->get('find_source');
39
        }
40
41
        // In the closure, $this is the proxied database
42
        $callback = function ($args, $next) {
43
            // The first argument is always the sql query
44
            $sql = $args[0];
45
            $parameters = isset($args[2]) ? $args[2] : [];
46
47
            // Sql can be an array
48
            if (is_array($sql)) {
49
                $parameters = $sql[1];
50
                $sql = $sql[0];
51
            }
52
53
            // Inline sql
54
            $sql = DB::inline_parameters($sql, $parameters);
55
56
            // Get time and memory for the request
57
            $startTime = microtime(true);
58
            $startMemory = memory_get_usage(true);
59
60
            // Execute all middleware
61
            $handle = $next(...$args);
62
63
            // Get time and memory after the request
64
            $endTime = microtime(true);
65
            $endMemory = memory_get_usage(true);
66
67
            // Show query on screen
68
            if (DebugBar::getShowQueries()) {
69
                $formattedSql = DebugBarUtils::formatSql($sql);
70
                $rows = $handle->numRecords();
71
72
                echo '<pre>The following query took <b>' . round($endTime - $startTime, 4) . '</b>s an returned <b>' . $rows . "</b> row(s) \n";
73
                echo 'Triggered by: <i>' . self::findSource() . '</i></pre>';
74
                echo $formattedSql;
75
76
                // Preview results
77
                $results = iterator_to_array($handle);
78
                if ($rows > 0) {
79
                    if ($rows == 1) {
80
                        dump($results[0]);
81
                    } else {
82
                        $linearValues = count($results[0]);
83
                        if ($linearValues) {
84
                            dump(implode(
85
                                ',',
86
                                array_map(
87
                                    function ($item) {
88
                                        return $item[key($item)];
89
                                    },
90
                                    $results
91
                                )
92
                            ));
93
                        } else {
94
                            if ($rows < 20) {
95
                                dump($results);
96
                            } else {
97
                                dump("Too many results to display");
98
                            }
99
                        }
100
                    }
101
                }
102
                echo '<hr/>';
103
104
                $handle->rewind(); // Rewind the results
105
            }
106
107
            // Sometimes, ugly spaces are there
108
            $sql = preg_replace('/[[:blank:]]+/', ' ', trim($sql));
109
110
            // Sometimes, the select statement can be very long and unreadable
111
            $shortsql = $sql;
112
            $matches = null;
113
            preg_match_all('/SELECT(.+?) FROM/is', $sql, $matches);
114
            $select = empty($matches[1]) ? null : trim($matches[1][0]);
115
            if ($select !== null) {
116
                if (strlen($select) > 100) {
117
                    $shortsql = str_replace($select, '"ClickToShowFields"', $sql);
118
                } else {
119
                    $select = null;
120
                }
121
            }
122
123
            // null on the first query, since it's the select statement itself
124
            $db = DB::get_conn()->getSelectedDatabase();
125
126
            self::$queries[] = [
127
                'short_query' => $shortsql,
128
                'select' => $select,
129
                'query' => $sql,
130
                'start_time' => $startTime,
131
                'end_time' => $endTime,
132
                'duration' => $endTime - $startTime,
133
                'memory' => $endMemory - $startMemory,
134
                'rows' => $handle ? $handle->numRecords() : null,
135
                'success' => $handle ? true : false,
136
                'database' => $db,
137
                'source' => self::$findSource ? self::findSource() : null
138
            ];
139
140
            return $handle;
141
        };
142
143
        // Attach to benchmarkQuery to fire on both query and preparedQuery
144
        $proxy = $proxy->addMethod('benchmarkQuery', $callback);
145
    }
146
147
    /**
148
     * Reset queries array
149
     *
150
     * Helpful for long running process and avoid accumulating queries
151
     *
152
     * @return void
153
     */
154
    public static function resetQueries()
155
    {
156
        self::$queries = [];
157
    }
158
159
    /**
160
     * @return array<array<mixed>>
161
     */
162
    public static function getQueries()
163
    {
164
        return self::$queries;
165
    }
166
167
    /**
168
     * @param string $str
169
     * @return void
170
     */
171
    public static function addCustomQuery(string $str)
172
    {
173
        self::$queries[] = [
174
            'short_query' => $str,
175
            'select' => null,
176
            'query' => $str,
177
            'start_time' => 0,
178
            'end_time' => 0,
179
            'duration' => 0,
180
            'memory' => 0,
181
            'rows' => null,
182
            'success' => true,
183
            'database' => null,
184
            'source' => null
185
        ];
186
    }
187
188
    /**
189
     * @return string
190
     */
191
    protected static function findSource()
192
    {
193
        $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
194
195
        // Not relevant to determine source
196
        $internalClasses = [
197
            '',
198
            get_called_class(),
199
            // DebugBar
200
            DebugBar::class,
201
            \LeKoala\DebugBar\Middleware\DebugBarMiddleware::class,
202
            // Proxy
203
            ProxyDBExtension::class,
204
            \TractorCow\ClassProxy\Proxied\ProxiedBehaviour::class,
205
            // Orm
206
            \SilverStripe\ORM\Connect\Database::class,
207
            \SilverStripe\ORM\Connect\DBSchemaManager::class,
208
            \SilverStripe\ORM\Connect\MySQLDatabase::class,
209
            \SilverStripe\ORM\Connect\MySQLSchemaManager::class,
210
            \SilverStripe\ORM\DataObjectSchema::class,
211
            \SilverStripe\ORM\DB::class,
212
            \SilverStripe\ORM\Queries\SQLExpression::class,
213
            \SilverStripe\ORM\DataList::class,
214
            \SilverStripe\ORM\DataObject::class,
215
            \SilverStripe\ORM\DataQuery::class,
216
            \SilverStripe\ORM\Queries\SQLSelect::class,
217
            \SilverStripe\ORM\Map::class,
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\Map 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...
218
            \SilverStripe\ORM\ListDecorator::class,
0 ignored issues
show
Bug introduced by
The type SilverStripe\ORM\ListDecorator 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...
219
            // Core
220
            \SilverStripe\Control\Director::class,
221
        ];
222
223
        $viewerClasses = [
224
            \SilverStripe\View\SSViewer_DataPresenter::class,
0 ignored issues
show
Bug introduced by
The type SilverStripe\View\SSViewer_DataPresenter 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...
225
            \SilverStripe\View\SSViewer_Scope::class,
0 ignored issues
show
Bug introduced by
The type SilverStripe\View\SSViewer_Scope 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...
226
            \SilverStripe\View\SSViewer::class,
227
            \LeKoala\DebugBar\Proxy\SSViewerProxy::class,
228
            \SilverStripe\View\ViewableData::class
0 ignored issues
show
Bug introduced by
The type SilverStripe\View\ViewableData 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...
229
        ];
230
231
        $sources = [];
232
        foreach ($traces as $i => $trace) {
233
            // We need to be able to look ahead one item in the trace, because the class/function values
234
            // are talking about what is being *called* on this line, not the function this line lives in.
235
            if (!isset($traces[$i + 1])) {
236
                break;
237
            }
238
239
            $file = isset($trace['file']) ? pathinfo($trace['file'], PATHINFO_FILENAME) : null;
240
            $class = isset($traces[$i + 1]['class']) ? $traces[$i + 1]['class'] : null;
241
            $line = isset($trace['line']) ? $trace['line'] : null;
242
            $function = isset($traces[$i + 1]['function']) ? $traces[$i + 1]['function'] : null;
243
            $type = isset($traces[$i + 1]['type']) ? $traces[$i + 1]['type'] : '::';
244
245
            /* @var $object SSViewer */
246
            $object = isset($traces[$i + 1]['object']) ? $traces[$i + 1]['object'] : null;
247
248
            if (in_array($class, $internalClasses)) {
249
                continue;
250
            }
251
252
            // Viewer classes need special handling
253
            if (in_array($class, $viewerClasses)) {
254
                if ($function == 'includeGeneratedTemplate') {
255
                    $templates = $object->templates();
256
257
                    $template = null;
258
                    if (isset($templates['main'])) {
259
                        $template = basename($templates['main']);
260
                    } else {
261
                        $keys = array_keys($templates);
262
                        $key = reset($keys);
263
                        if (isset($templates[$key])) {
264
                            $template = $key . ':' . basename($templates[$key]);
265
                        }
266
                    }
267
                    if (!empty($template)) {
268
                        $sources[] = $template;
269
                    }
270
                }
271
                continue;
272
            }
273
274
            $name = $class;
275
            if ($class && !DebugBar::config()->get('show_namespaces')) {
276
                $nameArray = explode("\\", $class);
277
                $name = array_pop($nameArray);
278
279
                // Maybe we are inside a trait?
280
                if ($file && $file != $name) {
281
                    $name .= '(' . $file . ')';
0 ignored issues
show
Bug introduced by
Are you sure $file of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

281
                    $name .= '(' . /** @scrutinizer ignore-type */ $file . ')';
Loading history...
282
                }
283
            }
284
            if ($function) {
285
                $name .= $type . $function;
286
            }
287
            if ($line) {
288
                // Line number could apply to a trait
289
                $name .= ':' . $line;
290
            }
291
292
            $sources[] = $name;
293
294
            if (count($sources) > self::MAX_FIND_SOURCE_LEVEL) {
295
                break;
296
            }
297
298
            // We reached a Controller, exit loop
299
            if ($object && $object instanceof Controller) {
300
                break;
301
            }
302
        }
303
304
        if (empty($sources)) {
305
            return 'Undefined source';
306
        }
307
        return implode(' > ', $sources);
308
    }
309
}
310