Completed
Push — master ( 78f962...a745ae )
by Anton
21s
created

SenseServiceProvider::explainQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 17
rs 9.9332
1
<?php
2
3
/*
4
 * This file is part of Laravel Sense.
5
 *
6
 * (c) Anton Komarev <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cog\Laravel\Sense\Providers;
15
16
use Cog\Laravel\Sense\Db\Query\Models\Query;
17
use Cog\Laravel\Sense\Request\Id;
18
use Cog\Laravel\Sense\Request\Models\Request;
19
use Cog\Laravel\Sense\RequestSummary\Models\RequestSummary;
20
use Cog\Laravel\Sense\Statement\Models\Statement;
21
use Cog\Laravel\Sense\StatementSummary\Models\StatementSummary;
22
use Cog\Laravel\Sense\Url\Models\Url;
23
use Illuminate\Database\Events\QueryExecuted;
24
use Illuminate\Support\Facades\DB;
25
use Illuminate\Support\Facades\Route;
26
use Illuminate\Support\ServiceProvider;
27
use PDO;
28
29
class SenseServiceProvider extends ServiceProvider
30
{
31
    public function boot(): void
32
    {
33
        $this->registerDbConnection();
34
        $this->registerRoutes();
35
        $this->registerResources();
36
        $this->registerSensors();
37
    }
38
39
    public function register(): void
40
    {
41
        $this->configure();
42
        $this->offerPublishing();
43
    }
44
45
    // TODO: Remove this hacky solution
46
    protected function registerDbConnection(): void
47
    {
48
        if (is_null($config = config('database.connections.sense'))) {
49
            $defaultConnection = config('database.default');
50
            if (is_null($fallbackConfig = config("database.connections.{$defaultConnection}"))) {
51
                throw new \Exception('Database connection [sense] has not been configured.');
52
            }
53
            config(['database.connections.sense' => $fallbackConfig]);
54
        }
55
    }
56
57
    /**
58
     * Register the Sense routes.
59
     *
60
     * @return void
61
     */
62
    protected function registerRoutes(): void
63
    {
64
        Route::group([
65
            'prefix' => config('sense.uri', 'sense'),
66
            'namespace' => 'Cog\Laravel\Sense\Http\Controllers',
67
            'middleware' => config('sense.middleware', 'web'),
68
        ], function () {
69
            $this->loadRoutesFrom(__DIR__ . '/../../routes/web.php');
70
        });
71
    }
72
73
    /**
74
     * Register the Sense resources.
75
     *
76
     * @return void
77
     */
78
    protected function registerResources(): void
79
    {
80
        $this->loadViewsFrom(__DIR__ . '/../../resources/views', 'sense');
81
    }
82
83
    /**
84
     * Setup the configuration for Sense.
85
     *
86
     * @return void
87
     */
88
    protected function configure(): void
89
    {
90
        $this->mergeConfigFrom(
91
            __DIR__ . '/../../config/sense.php', 'sense'
92
        );
93
    }
94
95
    /**
96
     * Setup the resource publishing groups for Sense.
97
     *
98
     * @return void
99
     */
100
    protected function offerPublishing(): void
101
    {
102
        if ($this->app->runningInConsole()) {
103
            $migrationsPath = __DIR__ . '/../../database/migrations';
104
105
            $this->publishes([
106
                $migrationsPath => database_path('migrations'),
107
            ], 'sense-migrations');
108
109
            $this->publishes([
110
                __DIR__ . '/../../config/sense.php' => config_path('sense.php'),
111
            ], 'sense-config');
112
113
            $this->loadMigrationsFrom($migrationsPath);
114
        }
115
    }
116
117
    private function registerSensors(): void
118
    {
119
        if ($this->isSenseDisabled()) {
120
            return;
121
        }
122
123
        /** @var \Cog\Laravel\Sense\Url\Models\Url $url */
124
        $url = Url::query()->firstOrCreate([
125
            'address' => request()->fullUrl(),
126
        ]);
127
128
        /** @var \Cog\Laravel\Sense\Request\Models\Request $request */
129
        $request = $url->requests()->create([
130
            'correlation_id' => Id::make(),
131
            'method' => request()->method(),
132
            // TODO: Add headers
133
            // TODO: Add body
134
        ]);
135
        $request->summary()->create();
136
137
        DB::listen(function (QueryExecuted $query) use ($request) {
138
            if ($this->isQueryShouldBeStored($query)) {
139
                $this->storeStatement($request, $query);
140
                $this->updateRequestSummary($request->getAttribute('summary'), $query);
141
            }
142
        });
143
    }
144
145
    private function storeStatement(Request $request, QueryExecuted $query): void
146
    {
147
        /** @var \Cog\Laravel\Sense\Statement\Models\Statement $statement */
148
        $statement = Statement::query()->firstOrCreate([
149
            'value' => $query->sql,
150
        ]);
151
152
        /** @var \Cog\Laravel\Sense\Db\Query\Models\Query $queryModel */
153
        $queryModel = $request->dbQueries()->create([
154
            'statement_id' => $statement->getKey(),
155
            'connection' => $query->connectionName,
156
            'sql' => $this->buildSqlString($query),
157
            'bindings' => $query->bindings,
158
            'time' => $query->time,
159
        ]);
160
161
        $this->explainQuery($queryModel, $query);
162
163
        /** @var \Cog\Laravel\Sense\StatementSummary\Models\StatementSummary $summary */
164
        $summary = $statement->summaries()
165
            ->where([
166
                'request_id' => $request->getKey(),
167
                'connection' => $query->connectionName,
168
            ])->first();
169
        if (!$summary) {
0 ignored issues
show
introduced by
$summary is of type Cog\Laravel\Sense\Statem...Models\StatementSummary, thus it always evaluated to true. If $summary can have other possible types, add them to src/Providers/SenseServiceProvider.php:163
Loading history...
170
            $summary = $statement->summaries()->create([
171
                'request_id' => $request->getKey(),
172
                'connection' => $query->connectionName,
173
                'time_min' => 0.0,
174
                'time_max' => 0.0,
175
                'time_total' => 0.0,
176
            ]);
177
        }
178
        $this->updateStatementSummary($summary, $query);
179
    }
180
181
    private function updateStatementSummary(StatementSummary $summary, QueryExecuted $query): void
182
    {
183
        if ($summary->getAttribute('time_min') === 0.0 || $summary->getAttribute('time_min') > $query->time) {
184
            $summary->setAttribute('time_min', $query->time);
185
        }
186
        if ($summary->getAttribute('time_max') < $query->time) {
187
            $summary->setAttribute('time_max', $query->time);
188
        }
189
        $summary->setAttribute('time_total', $summary->getAttribute('time_total') + $query->time);
190
        $summary->setAttribute('queries_count', $summary->getAttribute('queries_count') + 1);
191
        $summary->save();
192
    }
193
194
    private function updateRequestSummary(RequestSummary $requestSummary, QueryExecuted $query): void
195
    {
196
        $requestSummary->setAttribute('time_total', $requestSummary->getAttribute('time_total') + $query->time);
197
        $requestSummary->setAttribute('queries_count', $requestSummary->getAttribute('queries_count') + 1);
198
        $requestSummary->save();
199
    }
200
201
    private function buildSqlString(QueryExecuted $query): string
202
    {
203
        $pdo = $query->connection->getPdo();
204
        $sql = $query->sql;
205
206
        foreach ($query->bindings as $key => $binding) {
207
            // This regex matches placeholders only, not the question marks,
208
            // nested in quotes, while we iterate through the bindings
209
            // and substitute placeholders by suitable values.
210
            $regex = is_numeric($key)
211
                ? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
212
                : "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
213
            $sql = preg_replace($regex, $pdo->quote((string) $binding), $sql, 1);
214
        }
215
216
        return $sql;
217
    }
218
219
    private function explainQuery(Query $queryModel, QueryExecuted $query): void
220
    {
221
        $explainTypes = [
222
            'select',
223
//            'insert',
224
//            'update',
225
//            'delete',
226
        ];
227
        if (starts_with(strtolower($query->sql), $explainTypes)) {
228
            $pdo = $query->connection->getPdo();
229
            $bindings = $query->connection->prepareBindings($query->bindings);
230
            $statement = $pdo->prepare('EXPLAIN ' . $query->sql);
231
            $statement->execute($bindings);
232
            $explanation = $statement->fetchAll(PDO::FETCH_CLASS);
233
234
            $queryModel->explanation()->create([
235
                'result' => $explanation,
236
            ]);
237
        }
238
    }
239
240
    private function isQueryShouldBeStored(QueryExecuted $query): bool
241
    {
242
        return $query->connectionName !== 'sense';
243
    }
244
245
    private function isSenseDisabled(): bool
246
    {
247
        $uri = config('sense.uri', 'sense');
248
249
        return $this->app->environment() !== 'local'
250
            || $this->app->runningInConsole()
251
            || request()->is($uri, "{$uri}/*")
0 ignored issues
show
Bug introduced by
$uri.'/*' of type string is incompatible with the type Illuminate\Http\dynamic expected by parameter $patterns of Illuminate\Http\Request::is(). ( Ignorable by Annotation )

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

251
            || request()->is($uri, /** @scrutinizer ignore-type */ "{$uri}/*")
Loading history...
252
            || request()->method() === 'OPTIONS';
253
    }
254
}
255