Last active
September 22, 2021 04:14
-
-
Save kmuenkel/a6b4e34e5bf6ca44e3c85bf753111850 to your computer and use it in GitHub Desktop.
Laravel Event that parses all queries and their execution stack traces. (Dependent on StackTrace.php)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Helpers; | |
use PDO; | |
use DateTime; | |
use Exception; | |
use Illuminate\Support\Facades\DB; | |
use Illuminate\Database\QueryException; | |
use Illuminate\Support\{Str, Arr, Carbon}; | |
use Illuminate\Database\Events\QueryExecuted; | |
use Illuminate\Contracts\Support\{Jsonable, Arrayable}; | |
use Doctrine\SqlFormatter\{SqlFormatter, NullHighlighter}; | |
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; | |
use Illuminate\Database\Query\{Expression, Builder as QueryBuilder}; | |
/** | |
* Class DbQuery | |
* @package App\Helpers | |
*/ | |
class DbQuery implements Arrayable, Jsonable | |
{ | |
/** | |
* @var bool | |
*/ | |
protected static $recording = false; | |
/** | |
* @var array | |
*/ | |
protected static $queries = []; | |
/** | |
* @var bool | |
*/ | |
protected static $trace = false; | |
/** | |
* @param bool $trace | |
* @return $this | |
*/ | |
public function setTrace($trace = false): self | |
{ | |
self::$trace = $trace; | |
return $this; | |
} | |
/** | |
* @param QueryExecuted $query | |
*/ | |
public function handle(QueryExecuted $query) | |
{ | |
if (self::$recording) { | |
$config = config('database.connections.' . $query->connection->getName()); | |
$config = Arr::only($config, ['driver', 'host', 'port', 'username']); | |
$database = $query->connection->getDatabaseName(); | |
$time = round($query->time / 1000, 2); | |
$sql = $this->compileQuery($query); | |
$highlighter = app(NullHighlighter::class); | |
$formatter = app(SqlFormatter::class, compact('highlighter')); | |
try { | |
$sql = $formatter->format($sql); | |
} catch (Exception $exception) { | |
// | |
} | |
//Not all configs will exist depending on the source selection | |
$driver = isset($config['driver']) ? $config['driver'] . ':' : ''; | |
$username = isset($config['username']) ? $config['username'] . '@' : ''; | |
$host = $config['host'] ?? ''; | |
$port = isset($config['port']) ? ':' . $config['port'] : ''; | |
$source = $driver.$username.$host.$port; | |
$args = compact('source', 'database', 'sql', 'time'); | |
if (self::$trace) { | |
$args = app(DebugTrace::class)->generate($args)->truncate(); | |
} | |
self::$queries[] = $args; | |
} | |
} | |
/** | |
* @return array | |
*/ | |
public function dump(): array | |
{ | |
$queries = self::$queries; | |
self::$queries = []; | |
return $queries; | |
} | |
/** | |
* @return $this | |
*/ | |
public function record(): self | |
{ | |
self::$recording = true; | |
self::$queries = []; | |
return $this; | |
} | |
/** | |
* @return array | |
*/ | |
public function toArray(): array | |
{ | |
return self::$queries; | |
} | |
/** | |
* @param int $options | |
* @return false|string | |
*/ | |
public function toJson($options = 0) | |
{ | |
$traces = $this->toArray(); | |
$options = $options ?: JSON_PRETTY_PRINT; | |
return json_encode($traces, $options); | |
} | |
/** | |
* @return false|string | |
*/ | |
public function __toString() | |
{ | |
return print_r($this->toArray(), true); | |
} | |
/** | |
* @param string|EloquentBuilder|Expression $sql | |
* @param array $values | |
* @param PDO|string|null $pdo | |
* @return string | |
*/ | |
public function compileQuery($sql, array $values = [], $pdo = null): string | |
{ | |
if (is_object($sql)) { | |
if ($sql instanceof EloquentBuilder || $sql instanceof QueryBuilder) { | |
$values = $sql->getBindings(); | |
$sql = $sql->toSql(); | |
} elseif ($sql instanceof QueryExecuted) { | |
$pdo = $sql->connection->getPdo(); | |
$values = $sql->bindings; | |
$sql = $sql->sql; | |
} elseif ($sql instanceof QueryException) { | |
$values = $sql->getBindings(); | |
$sql = $sql->getSql(); | |
} | |
} | |
$enclosures = [ | |
'back_tick' => '`', | |
'apostrophe' => "'" | |
]; | |
$matches = []; | |
foreach ($enclosures as $name => $enclosure) { | |
$matches[$name] = []; | |
preg_match_all("/$enclosure.*?$enclosure/", $sql, $matches[$name]); | |
$matches[$name] = Arr::last($matches[$name]); | |
$sql = preg_replace("/$enclosure.*?$enclosure/", "$enclosure?$enclosure", $sql); | |
} | |
$sql = strpos($sql, $enclosures['back_tick']) ? strtoupper($sql) : $sql; | |
foreach ($enclosures as $name => $enclosure) { | |
$sql = Str::replaceArray("$enclosure?$enclosure", $matches[$name], $sql); | |
} | |
$values = array_map(function ($value) use ($pdo) { | |
if (!is_numeric($value) && !is_null($value)) { | |
/** @var PDO|Carbon|DateTime $value */ | |
$value = $value instanceof Carbon ? $value->toDateTimeString() : $value; | |
$value = $value instanceof DateTime ? $value->format('Y-m-d H:i:s') : $value; | |
$value = ($pdo instanceof PDO ? $pdo : DB::connection($pdo)->getPdo())->quote($value); | |
} | |
return $value; | |
}, $values); | |
$sql = str_replace(' AND 1', '', $sql); | |
$sql = str_replace('WHERE 1 AND ', 'WHERE ', $sql); | |
$sql = Str::replaceArray('?', $values, $sql); | |
$sql = preg_replace('/\s+/', ' ', $sql); | |
// $sql = preg_replace('/(` AS `)(.+?)(`)/', "$1'$2'", $sql); | |
// $sql = str_replace('`', '', $sql); | |
// $sql = preg_replace("/( AS )(')(.+?)(')/", "$1`$3`", $sql); | |
$sql = rtrim($sql, ';').';'; | |
#$sql .= '# '.$query['time'].'ms'; | |
return $sql; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Providers; | |
use Closure; | |
use App\Listeners; | |
use App\Helpers\DbQuery; | |
use Illuminate\Support\Facades\Event; | |
use Illuminate\Database\Events\QueryExecuted; | |
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; | |
/** | |
* Class EventServiceProvider | |
* @package App\Providers | |
*/ | |
class EventServiceProvider extends ServiceProvider | |
{ | |
/** | |
* The event listener mappings for the application. | |
* | |
* @var array | |
*/ | |
protected $listen = [ | |
QueryExecuted::class => [ | |
DbQuery::class, | |
] | |
]; | |
/** | |
* Register any events for your application. | |
* | |
* @return void | |
*/ | |
public function boot() | |
{ | |
parent::boot(); | |
// | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if (!function_exists('query_log')) { | |
/** | |
* (De)activate monitoring of SQL query executions and return all that were run while active | |
* | |
* @param bool $trace Indicate whether you want a stack-trace pointing to where the query came from | |
* @return Helpers\DbQuery | |
*/ | |
function query_log(bool $trace = false): Helpers\DbQuery | |
{ | |
static $ready = false; | |
$ready = !$ready; | |
//Closure may not be needed | |
Helpers::setLog($ready ? function ($sql, $start) { | |
$dummyConnection = Model::resolveConnection(config('database.default')); | |
$time = round((microtime(true) - $start) * 1000, 2); | |
$dummyEvent = app(QueryExecuted::class, [ | |
'sql' => $sql, | |
'bindings' => [], | |
'time' => $time, | |
'connection' => $dummyConnection | |
]); | |
event($dummyEvent); | |
} : null); | |
$queryLog = app(Helpers\DbQuery::class)->setTrace($trace); | |
// $ready && Facades\Event::listen(QueryExecuted::class, Helpers\DbQuery::class); | |
return $ready ? $queryLog->record() : $queryLog->setTrace(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment