Passed
Push — master ( 2c7b3b...fa7655 )
by Thomas
02:43
created

DatabaseCollector::collect()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 7
eloc 21
c 3
b 0
f 0
nc 9
nop 0
dl 0
loc 34
rs 8.6506
1
<?php
2
3
namespace LeKoala\DebugBar\Collector;
4
5
use LeKoala\DebugBar\DebugBar;
6
use SilverStripe\Control\Director;
7
use DebugBar\DataCollector\Renderable;
8
use DebugBar\DataCollector\AssetProvider;
9
use DebugBar\DataCollector\DataCollector;
10
use LeKoala\DebugBar\Extension\ProxyDBExtension;
11
12
/**
13
 * Collects data about SQL statements executed through the proxied behaviour
14
 */
15
class DatabaseCollector extends DataCollector implements Renderable, AssetProvider
16
{
17
    /**
18
     * @var TimeDataCollector
19
     */
20
    protected $timeCollector;
21
22
    /**
23
     * @return array
24
     */
25
    public function collect()
26
    {
27
        $this->timeCollector = DebugBar::getDebugBar()->getCollector('time');
28
29
        $data = $this->collectData($this->timeCollector);
30
31
        // Check for excessive number of queries
32
        $dbQueryWarningLevel = DebugBar::config()->get('warn_query_limit');
33
        if ($dbQueryWarningLevel && $data['nb_statements'] > $dbQueryWarningLevel) {
34
            $helpLink = DebugBar::config()->get('performance_guide_link');
35
            $messages = DebugBar::getMessageCollector();
36
            if ($messages) {
37
                $messages->addMessage(
38
                    "This page ran more than $dbQueryWarningLevel database queries." .
39
                        "\nYou could reduce this by implementing caching." .
40
                        "\nYou can find more info here: $helpLink",
41
                    'warning',
42
                    true
43
                );
44
            }
45
        }
46
47
        if (isset($data['has_unclosed_transaction']) && $data['has_unclosed_transaction']) {
48
            $messages = DebugBar::getMessageCollector();
49
            if ($messages) {
50
                $messages->addMessage(
51
                    "There is an unclosed transaction on your page and some statement may not be persisted to the database",
52
                    'warning',
53
                    true
54
                );
55
            }
56
        }
57
58
        return $data;
59
    }
60
61
    /**
62
     * Explode comma separated elements not within parenthesis or quotes
63
     *
64
     * @param string $str
65
     * @return array
66
     */
67
    protected static function explodeFields($str)
68
    {
69
        return preg_split("/(?![^(]*\)),/", $str);
70
    }
71
72
    /**
73
     * Collects data
74
     *
75
     * @param TimeDataCollector $timeCollector
76
     * @return array
77
     */
78
    protected function collectData(TimeDataCollector $timeCollector = null)
79
    {
80
        $stmts = array();
81
82
        $total_duration = 0;
83
        $total_mem = 0;
84
85
        $failed = 0;
86
87
        $i = 0;
88
89
        // Get queries gathered by proxy
90
        $queries = ProxyDBExtension::getQueries();
91
92
        $limit = DebugBar::config()->get('query_limit');
93
        $warnDurationThreshold = DebugBar::config()->get('warn_dbqueries_threshold_seconds');
94
95
        // only show db if there is more than one database in use
96
        $showDb = count(array_filter(array_unique(array_map(function ($stmt) {
0 ignored issues
show
Unused Code introduced by
The assignment to $showDb is dead and can be removed.
Loading history...
97
            return $stmt['database'];
98
        }, $queries)))) > 1;
99
100
        $showDb = false;
101
        $hasUnclosedTransaction = false;
102
        foreach ($queries as $stmt) {
103
            $i++;
104
105
            $total_duration += $stmt['duration'];
106
            $total_mem += $stmt['memory'];
107
108
            if (!$stmt['success']) {
109
                $failed++;
110
            }
111
112
            if (str_starts_with($stmt['short_query'], 'SHOW DATABASES LIKE')) {
113
                $showDb = true;
114
            }
115
116
            if (str_contains($stmt['short_query'], 'START TRANSACTION')) {
117
                $hasUnclosedTransaction = true;
118
            }
119
            if (str_contains($stmt['short_query'], 'END TRANSACTION')) {
120
                $hasUnclosedTransaction = false;
121
            }
122
123
            if ($limit && $i > $limit) {
124
                $stmts[] = array(
125
                    'sql' => "Only the first $limit queries are shown"
126
                );
127
                break;
128
            }
129
130
            $stmts[] = array(
131
                'sql' => $stmt['short_query'],
132
                'row_count' => $stmt['rows'],
133
                'params' => $stmt['select'] ? $stmt['select'] : null,
134
                'duration' => $stmt['duration'],
135
                'duration_str' => $this->getDataFormatter()->formatDuration($stmt['duration']),
136
                'memory' => $stmt['memory'],
137
                'memory_str' => $this->getDataFormatter()->formatBytes($stmt['memory']),
138
                'is_success' => $stmt['success'],
139
                'database' => $showDb ? $stmt['database'] : null,
140
                'source' => $stmt['source'],
141
                'warn' => $stmt['duration'] > $warnDurationThreshold
142
            );
143
144
            if ($timeCollector !== null) {
145
                $timeCollector->addMeasure(
146
                    $stmt['short_query'],
147
                    $stmt['start_time'],
148
                    $stmt['end_time']
149
                );
150
            }
151
        }
152
153
        // Save as CSV
154
        $db_save_csv = DebugBar::config()->get('db_save_csv');
155
        if ($db_save_csv && !empty($queries)) {
156
            $filename = date('Ymd_His') . '_' . count($queries) . '_' . uniqid() . '.csv';
157
            $isOutput = false;
158
            if (isset($_REQUEST['downloadqueries']) && Director::isDev()) {
159
                $isOutput = true;
160
                if (headers_sent()) {
161
                    die('Cannot download queries, headers are already sent');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
162
                }
163
                header('Content-Type: text/csv');
164
                header('Content-Disposition: attachment;filename=' . $filename);
165
                $fp = fopen('php://output', 'w');
166
            } else {
167
                $tempFolder = TEMP_FOLDER . '/debugbar/db';
168
                if (!is_dir($tempFolder)) {
169
                    mkdir($tempFolder, 0755, true);
170
                }
171
                $fp = fopen($tempFolder . '/' . $filename, 'w');
172
            }
173
            $headers = array_keys($queries[0]);
174
            fputcsv($fp, $headers);
175
            foreach ($queries as $query) {
176
                fputcsv($fp, $query);
177
            }
178
            fclose($fp);
179
180
            if ($isOutput) {
181
                die();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
182
            }
183
        }
184
185
        return array(
186
            'nb_statements' => count($queries),
187
            'nb_failed_statements' => $failed,
188
            'show_db' => $showDb,
189
            'has_unclosed_transaction' => $hasUnclosedTransaction,
190
            'statements' => $stmts,
191
            'accumulated_duration' => $total_duration,
192
            'accumulated_duration_str' => $this->getDataFormatter()->formatDuration($total_duration),
193
            'memory_usage' => $total_mem,
194
            'memory_usage_str' => $this->getDataFormatter()->formatBytes($total_mem),
195
        );
196
    }
197
198
    /**
199
     * @return string
200
     */
201
    public function getName()
202
    {
203
        return 'db';
204
    }
205
206
    /**
207
     * @return array
208
     */
209
    public function getWidgets()
210
    {
211
        return array(
212
            "database" => array(
213
                "icon" => "database",
214
                "widget" => "PhpDebugBar.Widgets.SQLQueriesWidget",
215
                "map" => "db",
216
                "default" => "[]"
217
            ),
218
            "database:badge" => array(
219
                "map" => "db.nb_statements",
220
                "default" => 0
221
            )
222
        );
223
    }
224
225
    /**
226
     * @return array
227
     */
228
    public function getAssets()
229
    {
230
        return array(
231
            'base_path' => '/' . DebugBar::moduleResource('javascript')->getRelativePath(),
232
            'base_url' => Director::makeRelative(DebugBar::moduleResource('javascript')->getURL()),
233
            'css' => 'sqlqueries/widget.css',
234
            'js' => 'sqlqueries/widget.js'
235
        );
236
    }
237
}
238