Passed
Push — master ( 1605ad...3053a1 )
by Thomas
03:07
created

ProxyDBExtension   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Importance

Changes 9
Bugs 2 Features 1
Metric Value
eloc 157
c 9
b 2
f 1
dl 0
loc 299
rs 8.8798
wmc 44

5 Methods

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

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