Issues (8)

src/Providers/SenseServiceProvider.php (1 issue)

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