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
introduced
by
![]() |
|||
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 |