Completed
Push — master ( 1fb91c...78f962 )
by Anton
03:04 queued 56s
created

SenseServiceProvider::buildSqlString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

225
            || request()->is($uri, /** @scrutinizer ignore-type */ "{$uri}/*")
Loading history...
226
            || request()->method() === 'OPTIONS';
227
    }
228
}
229