Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
199 lars 1
<?php namespace Clockwork\DataSource;
2
 
3
use Clockwork\Helpers\Serializer;
4
use Clockwork\Helpers\StackTrace;
5
use Clockwork\Request\Request;
6
use Clockwork\Support\Laravel\Eloquent\ResolveModelLegacyScope;
7
use Clockwork\Support\Laravel\Eloquent\ResolveModelScope;
8
 
9
use Illuminate\Database\ConnectionResolverInterface;
10
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
11
 
12
// Data source for Eloquent (Laravel ORM), provides database queries, stats, model actions and counts
13
class EloquentDataSource extends DataSource
14
{
15
	use Concerns\EloquentDetectDuplicateQueries;
16
 
17
	// Database manager instance
18
	protected $databaseManager;
19
 
20
	// Event dispatcher instance
21
	protected $eventDispatcher;
22
 
23
	// Array of collected queries
24
	protected $queries = [];
25
 
26
	// Query counts by type
27
	protected $count = [
28
		'total' => 0, 'slow' => 0, 'select' => 0, 'insert' => 0, 'update' => 0, 'delete' => 0, 'other' => 0
29
	];
30
 
31
	// Collected models actions
32
	protected $modelsActions = [];
33
 
34
	// Model action counts by model, eg. [ 'retrieved' => [ User::class => 1 ] ]
35
	protected $modelsCount = [
36
		'retrieved' => [], 'created' => [], 'updated' => [], 'deleted' => []
37
	];
38
 
39
	// Whether we are collecting database queries or stats only
40
	protected $collectQueries = true;
41
 
42
	// Whether we are collecting models actions or stats only
43
	protected $collectModelsActions = true;
44
 
45
	// Whether we are collecting retrieved models as well when collecting models actions
46
	protected $collectModelsRetrieved = false;
47
 
48
	// Query execution time threshold in ms after which the query is marked as slow
49
	protected $slowThreshold;
50
 
51
	// Enable duplicate queries detection
52
	protected $detectDuplicateQueries = false;
53
 
54
	// Model name to associate with the next executed query, used to map queries to models
55
	public $nextQueryModel;
56
 
57
	// Create a new data source instance, takes a database manager, an event dispatcher as arguments and additional
58
	// options as arguments
59
	public function __construct(ConnectionResolverInterface $databaseManager, EventDispatcher $eventDispatcher, $collectQueries = true, $slowThreshold = null, $slowOnly = false, $detectDuplicateQueries = false, $collectModelsActions = true, $collectModelsRetrieved = false)
60
	{
61
		$this->databaseManager = $databaseManager;
62
		$this->eventDispatcher = $eventDispatcher;
63
 
64
		$this->collectQueries         = $collectQueries;
65
		$this->slowThreshold          = $slowThreshold;
66
		$this->detectDuplicateQueries = $detectDuplicateQueries;
67
		$this->collectModelsActions   = $collectModelsActions;
68
		$this->collectModelsRetrieved = $collectModelsRetrieved;
69
 
70
		if ($slowOnly) $this->addFilter(function ($query) { return $query['duration'] > $this->slowThreshold; });
71
	}
72
 
73
	// Adds ran database queries, query counts, models actions and models counts to the request
74
	public function resolve(Request $request)
75
	{
76
		$request->databaseQueries = array_merge($request->databaseQueries, $this->queries);
77
 
78
		$request->databaseQueriesCount += $this->count['total'];
79
		$request->databaseSlowQueries  += $this->count['slow'];
80
		$request->databaseSelects      += $this->count['select'];
81
		$request->databaseInserts      += $this->count['insert'];
82
		$request->databaseUpdates      += $this->count['update'];
83
		$request->databaseDeletes      += $this->count['delete'];
84
		$request->databaseOthers       += $this->count['other'];
85
 
86
		$request->modelsActions = array_merge($request->modelsActions, $this->modelsActions);
87
 
88
		$request->modelsRetrieved = $this->modelsCount['retrieved'];
89
		$request->modelsCreated   = $this->modelsCount['created'];
90
		$request->modelsUpdated   = $this->modelsCount['updated'];
91
		$request->modelsDeleted   = $this->modelsCount['deleted'];
92
 
93
		$this->appendDuplicateQueriesWarnings($request);
94
 
95
		return $request;
96
	}
97
 
98
	// Reset the data source to an empty state, clearing any collected data
99
	public function reset()
100
	{
101
		$this->queries = [];
102
		$this->count = [
103
			'total' => 0, 'slow' => 0, 'select' => 0, 'insert' => 0, 'update' => 0, 'delete' => 0, 'other' => 0
104
		];
105
 
106
		$this->modelsActions = [];
107
		$this->modelsCount = [
108
			'retrieved' => [], 'created' => [], 'updated' => [], 'deleted' => []
109
		];
110
 
111
		$this->nextQueryModel = null;
112
	}
113
 
114
	// Start listening to Eloquent events
115
	public function listenToEvents()
116
	{
117
		if ($scope = $this->getModelResolvingScope()) {
118
			$this->eventDispatcher->listen('eloquent.booted: *', function ($model, $data = null) use ($scope) {
119
				if (is_string($model) && is_array($data)) { // Laravel 5.4 wildcard event
120
					$model = reset($data);
121
				}
122
 
123
				$model->addGlobalScope($scope);
124
			});
125
		}
126
 
127
		if (class_exists(\Illuminate\Database\Events\QueryExecuted::class)) {
128
			// Laravel 5.2 and up
129
			$this->eventDispatcher->listen(\Illuminate\Database\Events\QueryExecuted::class, function ($event) {
130
				$this->registerQuery($event);
131
			});
132
		} else {
133
			// Laravel 5.0 to 5.1
134
			$this->eventDispatcher->listen('illuminate.query', function ($event) {
135
				$this->registerLegacyQuery($event);
136
			});
137
		}
138
 
139
		// register all event listeners individually so we don't have to regex the event type and support Laravel <5.4
140
		$this->listenToModelEvent('retrieved');
141
		$this->listenToModelEvent('created');
142
		$this->listenToModelEvent('updated');
143
		$this->listenToModelEvent('deleted');
144
	}
145
 
146
	// Register a listener collecting model events of specified type
147
	protected function listenToModelEvent($event)
148
	{
149
		$this->eventDispatcher->listen("eloquent.{$event}: *", function ($model, $data = null) use ($event) {
150
			if (is_string($model) && is_array($data)) { // Laravel 5.4 wildcard event
151
				$model = reset($data);
152
			}
153
 
154
			$this->collectModelEvent($event, $model);
155
		});
156
	}
157
 
158
	// Collect an executed database query
159
	protected function registerQuery($event)
160
	{
161
		$trace = StackTrace::get([ 'arguments' => $this->detectDuplicateQueries ])->resolveViewName();
162
 
163
		if ($this->detectDuplicateQueries) $this->detectDuplicateQuery($trace);
164
 
165
		$query = [
166
			'query'      => $this->createRunnableQuery($event->sql, $event->bindings, $event->connectionName),
167
			'duration'   => $event->time,
168
			'connection' => $event->connectionName,
169
			'time'       => microtime(true) - $event->time / 1000,
170
			'trace'      => (new Serializer)->trace($trace),
171
			'model'      => $this->nextQueryModel,
172
			'tags'       => $this->slowThreshold !== null && $event->time > $this->slowThreshold ? [ 'slow' ] : []
173
		];
174
 
175
		$this->nextQueryModel = null;
176
 
177
		if (! $this->passesFilters([ $query, $trace ], 'early')) return;
178
 
179
		$this->incrementQueryCount($query);
180
 
181
		if (! $this->collectQueries || ! $this->passesFilters([ $query, $trace ])) return;
182
 
183
		$this->queries[] = $query;
184
	}
185
 
186
	// Collect an executed database query (pre Laravel 5.2)
187
	protected function registerLegacyQuery($sql, $bindings, $time, $connection)
188
	{
189
		return $this->registerQuery((object) [
190
			'sql'            => $sql,
191
			'bindings'       => $bindings,
192
			'time'           => $time,
193
			'connectionName' => $connection
194
		]);
195
	}
196
 
197
	// Collect a model event and update stats
198
	protected function collectModelEvent($event, $model)
199
	{
200
		$lastQuery = ($queryCount = count($this->queries)) ? $this->queries[$queryCount - 1] : null;
201
 
202
		$action = [
203
			'model'      => $modelClass = get_class($model),
204
			'key'        => $this->getModelKey($model),
205
			'action'     => $event,
206
			'attributes' => $this->collectModelsRetrieved && $event == 'retrieved' ? $model->getOriginal() : [],
207
			'changes'    => $this->collectModelsActions ? $model->getChanges() : [],
208
			'time'       => microtime(true) / 1000,
209
			'query'      => $lastQuery ? $lastQuery['query'] : null,
210
			'duration'   => $lastQuery ? $lastQuery['duration'] : null,
211
			'connection' => $lastQuery ? $lastQuery['connection'] : null,
212
			'trace'      => null,
213
			'tags'       => []
214
		];
215
 
216
		if ($lastQuery) $this->queries[$queryCount - 1]['model'] = $modelClass;
217
 
218
		if (! $this->passesFilters([ $action ], 'models-early')) return;
219
 
220
		$this->incrementModelsCount($action['action'], $action['model']);
221
 
222
		if (! $this->collectModelsActions) return;
223
		if (! $this->collectModelsRetrieved && $event == 'retrieved') return;
224
		if (! $this->passesFilters([ $action ], 'models')) return;
225
 
226
		$action['trace'] = (new Serializer)->trace(StackTrace::get()->resolveViewName());
227
 
228
		$this->modelsActions[] = $action;
229
	}
230
 
231
	// Takes a query, an array of bindings and the connection as arguments, returns runnable query with upper-cased keywords
232
	protected function createRunnableQuery($query, $bindings, $connection)
233
	{
234
		// add bindings to query
235
		$bindings = $this->databaseManager->connection($connection)->prepareBindings($bindings);
236
 
237
		$index = 0;
238
		$query = preg_replace_callback('/\?/', function ($matches) use ($bindings, $connection, &$index) {
239
			$binding = $this->quoteBinding($bindings[$index++], $connection);
240
 
241
			// convert binary bindings to hexadecimal representation
242
			if (! preg_match('//u', (string) $binding)) $binding = '0x' . bin2hex($binding);
243
 
244
			// escape backslashes in the binding (preg_replace requires to do so)
245
			return (string) $binding;
246
		}, $query, count($bindings));
247
 
248
		// highlight keywords
249
		$keywords = [
250
			'select', 'insert', 'update', 'delete', 'into', 'values', 'set', 'where', 'from', 'limit', 'is', 'null',
251
			'having', 'group by', 'order by', 'asc', 'desc'
252
		];
253
		$regexp = '/\b' . implode('\b|\b', $keywords) . '\b/i';
254
 
255
		return preg_replace_callback($regexp, function ($match) { return strtoupper($match[0]); }, $query);
256
	}
257
 
258
	// Takes a query binding and a connection name, returns a quoted binding value
259
	protected function quoteBinding($binding, $connection)
260
	{
261
		$connection = $this->databaseManager->connection($connection);
262
 
263
		if (! method_exists($connection, 'getPdo')) return;
264
 
265
		$pdo = $connection->getPdo();
266
 
267
		if ($pdo === null) return;
268
 
269
		if ($pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'odbc') {
270
			// PDO_ODBC driver doesn't support the quote method, apply simple MSSQL style quoting instead
271
			return "'" . str_replace("'", "''", $binding) . "'";
272
		}
273
 
274
		return is_string($binding) ? $pdo->quote($binding) : $binding;
275
	}
276
 
277
	// Increment query counts for collected query
278
	protected function incrementQueryCount($query)
279
	{
280
		$sql = ltrim($query['query']);
281
 
282
		$this->count['total']++;
283
 
284
		if (preg_match('/^select\b/i', $sql)) {
285
			$this->count['select']++;
286
		} elseif (preg_match('/^insert\b/i', $sql)) {
287
			$this->count['insert']++;
288
		} elseif (preg_match('/^update\b/i', $sql)) {
289
			$this->count['update']++;
290
		} elseif (preg_match('/^delete\b/i', $sql)) {
291
			$this->count['delete']++;
292
		} else {
293
			$this->count['other']++;
294
		}
295
 
296
		if (in_array('slow', $query['tags'])) {
297
			$this->count['slow']++;
298
		}
299
	}
300
 
301
	// Increment model counts for collected model action
302
	protected function incrementModelsCount($action, $model)
303
	{
304
		if (! isset($this->modelsCount[$action][$model])) {
305
			$this->modelsCount[$action][$model] = 0;
306
		}
307
 
308
		$this->modelsCount[$action][$model]++;
309
	}
310
 
311
	// Returns model resolving scope for the installed Laravel version
312
	protected function getModelResolvingScope()
313
	{
314
		if (interface_exists(\Illuminate\Database\Eloquent\ScopeInterface::class)) {
315
			// Laravel 5.0 to 5.1
316
			return new ResolveModelLegacyScope($this);
317
		}
318
 
319
		return new ResolveModelScope($this);
320
	}
321
 
322
	// Returns model key without crashing when using Eloquent strict mode and it's not loaded
323
	protected function getModelKey($model)
324
	{
325
		try {
326
			return $model->getKey();
327
		} catch (\Illuminate\Database\Eloquent\MissingAttributeException $e) {}
328
	}
329
}