Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php namespace Clockwork\DataSource;use Clockwork\Helpers\Serializer;use Clockwork\Helpers\StackTrace;use Clockwork\Request\Request;use Clockwork\Support\Laravel\Eloquent\ResolveModelLegacyScope;use Clockwork\Support\Laravel\Eloquent\ResolveModelScope;use Illuminate\Database\ConnectionResolverInterface;use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;// Data source for Eloquent (Laravel ORM), provides database queries, stats, model actions and countsclass EloquentDataSource extends DataSource{use Concerns\EloquentDetectDuplicateQueries;// Database manager instanceprotected $databaseManager;// Event dispatcher instanceprotected $eventDispatcher;// Array of collected queriesprotected $queries = [];// Query counts by typeprotected $count = ['total' => 0, 'slow' => 0, 'select' => 0, 'insert' => 0, 'update' => 0, 'delete' => 0, 'other' => 0];// Collected models actionsprotected $modelsActions = [];// Model action counts by model, eg. [ 'retrieved' => [ User::class => 1 ] ]protected $modelsCount = ['retrieved' => [], 'created' => [], 'updated' => [], 'deleted' => []];// Whether we are collecting database queries or stats onlyprotected $collectQueries = true;// Whether we are collecting models actions or stats onlyprotected $collectModelsActions = true;// Whether we are collecting retrieved models as well when collecting models actionsprotected $collectModelsRetrieved = false;// Query execution time threshold in ms after which the query is marked as slowprotected $slowThreshold;// Enable duplicate queries detectionprotected $detectDuplicateQueries = false;// Model name to associate with the next executed query, used to map queries to modelspublic $nextQueryModel;// Create a new data source instance, takes a database manager, an event dispatcher as arguments and additional// options as argumentspublic function __construct(ConnectionResolverInterface $databaseManager, EventDispatcher $eventDispatcher, $collectQueries = true, $slowThreshold = null, $slowOnly = false, $detectDuplicateQueries = false, $collectModelsActions = true, $collectModelsRetrieved = false){$this->databaseManager = $databaseManager;$this->eventDispatcher = $eventDispatcher;$this->collectQueries = $collectQueries;$this->slowThreshold = $slowThreshold;$this->detectDuplicateQueries = $detectDuplicateQueries;$this->collectModelsActions = $collectModelsActions;$this->collectModelsRetrieved = $collectModelsRetrieved;if ($slowOnly) $this->addFilter(function ($query) { return $query['duration'] > $this->slowThreshold; });}// Adds ran database queries, query counts, models actions and models counts to the requestpublic function resolve(Request $request){$request->databaseQueries = array_merge($request->databaseQueries, $this->queries);$request->databaseQueriesCount += $this->count['total'];$request->databaseSlowQueries += $this->count['slow'];$request->databaseSelects += $this->count['select'];$request->databaseInserts += $this->count['insert'];$request->databaseUpdates += $this->count['update'];$request->databaseDeletes += $this->count['delete'];$request->databaseOthers += $this->count['other'];$request->modelsActions = array_merge($request->modelsActions, $this->modelsActions);$request->modelsRetrieved = $this->modelsCount['retrieved'];$request->modelsCreated = $this->modelsCount['created'];$request->modelsUpdated = $this->modelsCount['updated'];$request->modelsDeleted = $this->modelsCount['deleted'];$this->appendDuplicateQueriesWarnings($request);return $request;}// Reset the data source to an empty state, clearing any collected datapublic function reset(){$this->queries = [];$this->count = ['total' => 0, 'slow' => 0, 'select' => 0, 'insert' => 0, 'update' => 0, 'delete' => 0, 'other' => 0];$this->modelsActions = [];$this->modelsCount = ['retrieved' => [], 'created' => [], 'updated' => [], 'deleted' => []];$this->nextQueryModel = null;}// Start listening to Eloquent eventspublic function listenToEvents(){if ($scope = $this->getModelResolvingScope()) {$this->eventDispatcher->listen('eloquent.booted: *', function ($model, $data = null) use ($scope) {if (is_string($model) && is_array($data)) { // Laravel 5.4 wildcard event$model = reset($data);}$model->addGlobalScope($scope);});}if (class_exists(\Illuminate\Database\Events\QueryExecuted::class)) {// Laravel 5.2 and up$this->eventDispatcher->listen(\Illuminate\Database\Events\QueryExecuted::class, function ($event) {$this->registerQuery($event);});} else {// Laravel 5.0 to 5.1$this->eventDispatcher->listen('illuminate.query', function ($event) {$this->registerLegacyQuery($event);});}// register all event listeners individually so we don't have to regex the event type and support Laravel <5.4$this->listenToModelEvent('retrieved');$this->listenToModelEvent('created');$this->listenToModelEvent('updated');$this->listenToModelEvent('deleted');}// Register a listener collecting model events of specified typeprotected function listenToModelEvent($event){$this->eventDispatcher->listen("eloquent.{$event}: *", function ($model, $data = null) use ($event) {if (is_string($model) && is_array($data)) { // Laravel 5.4 wildcard event$model = reset($data);}$this->collectModelEvent($event, $model);});}// Collect an executed database queryprotected function registerQuery($event){$trace = StackTrace::get([ 'arguments' => $this->detectDuplicateQueries ])->resolveViewName();if ($this->detectDuplicateQueries) $this->detectDuplicateQuery($trace);$query = ['query' => $this->createRunnableQuery($event->sql, $event->bindings, $event->connectionName),'duration' => $event->time,'connection' => $event->connectionName,'time' => microtime(true) - $event->time / 1000,'trace' => (new Serializer)->trace($trace),'model' => $this->nextQueryModel,'tags' => $this->slowThreshold !== null && $event->time > $this->slowThreshold ? [ 'slow' ] : []];$this->nextQueryModel = null;if (! $this->passesFilters([ $query, $trace ], 'early')) return;$this->incrementQueryCount($query);if (! $this->collectQueries || ! $this->passesFilters([ $query, $trace ])) return;$this->queries[] = $query;}// Collect an executed database query (pre Laravel 5.2)protected function registerLegacyQuery($sql, $bindings, $time, $connection){return $this->registerQuery((object) ['sql' => $sql,'bindings' => $bindings,'time' => $time,'connectionName' => $connection]);}// Collect a model event and update statsprotected function collectModelEvent($event, $model){$lastQuery = ($queryCount = count($this->queries)) ? $this->queries[$queryCount - 1] : null;$action = ['model' => $modelClass = get_class($model),'key' => $this->getModelKey($model),'action' => $event,'attributes' => $this->collectModelsRetrieved && $event == 'retrieved' ? $model->getOriginal() : [],'changes' => $this->collectModelsActions ? $model->getChanges() : [],'time' => microtime(true) / 1000,'query' => $lastQuery ? $lastQuery['query'] : null,'duration' => $lastQuery ? $lastQuery['duration'] : null,'connection' => $lastQuery ? $lastQuery['connection'] : null,'trace' => null,'tags' => []];if ($lastQuery) $this->queries[$queryCount - 1]['model'] = $modelClass;if (! $this->passesFilters([ $action ], 'models-early')) return;$this->incrementModelsCount($action['action'], $action['model']);if (! $this->collectModelsActions) return;if (! $this->collectModelsRetrieved && $event == 'retrieved') return;if (! $this->passesFilters([ $action ], 'models')) return;$action['trace'] = (new Serializer)->trace(StackTrace::get()->resolveViewName());$this->modelsActions[] = $action;}// Takes a query, an array of bindings and the connection as arguments, returns runnable query with upper-cased keywordsprotected function createRunnableQuery($query, $bindings, $connection){// add bindings to query$bindings = $this->databaseManager->connection($connection)->prepareBindings($bindings);$index = 0;$query = preg_replace_callback('/\?/', function ($matches) use ($bindings, $connection, &$index) {$binding = $this->quoteBinding($bindings[$index++], $connection);// convert binary bindings to hexadecimal representationif (! preg_match('//u', (string) $binding)) $binding = '0x' . bin2hex($binding);// escape backslashes in the binding (preg_replace requires to do so)return (string) $binding;}, $query, count($bindings));// highlight keywords$keywords = ['select', 'insert', 'update', 'delete', 'into', 'values', 'set', 'where', 'from', 'limit', 'is', 'null','having', 'group by', 'order by', 'asc', 'desc'];$regexp = '/\b' . implode('\b|\b', $keywords) . '\b/i';return preg_replace_callback($regexp, function ($match) { return strtoupper($match[0]); }, $query);}// Takes a query binding and a connection name, returns a quoted binding valueprotected function quoteBinding($binding, $connection){$connection = $this->databaseManager->connection($connection);if (! method_exists($connection, 'getPdo')) return;$pdo = $connection->getPdo();if ($pdo === null) return;if ($pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'odbc') {// PDO_ODBC driver doesn't support the quote method, apply simple MSSQL style quoting insteadreturn "'" . str_replace("'", "''", $binding) . "'";}return is_string($binding) ? $pdo->quote($binding) : $binding;}// Increment query counts for collected queryprotected function incrementQueryCount($query){$sql = ltrim($query['query']);$this->count['total']++;if (preg_match('/^select\b/i', $sql)) {$this->count['select']++;} elseif (preg_match('/^insert\b/i', $sql)) {$this->count['insert']++;} elseif (preg_match('/^update\b/i', $sql)) {$this->count['update']++;} elseif (preg_match('/^delete\b/i', $sql)) {$this->count['delete']++;} else {$this->count['other']++;}if (in_array('slow', $query['tags'])) {$this->count['slow']++;}}// Increment model counts for collected model actionprotected function incrementModelsCount($action, $model){if (! isset($this->modelsCount[$action][$model])) {$this->modelsCount[$action][$model] = 0;}$this->modelsCount[$action][$model]++;}// Returns model resolving scope for the installed Laravel versionprotected function getModelResolvingScope(){if (interface_exists(\Illuminate\Database\Eloquent\ScopeInterface::class)) {// Laravel 5.0 to 5.1return new ResolveModelLegacyScope($this);}return new ResolveModelScope($this);}// Returns model key without crashing when using Eloquent strict mode and it's not loadedprotected function getModelKey($model){try {return $model->getKey();} catch (\Illuminate\Database\Eloquent\MissingAttributeException $e) {}}}