Passed
Push — master ( 6df40c...6c827b )
by Thomas
13:46
created

ProxyDBExtension::addCustomQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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