Skip to content

Instantly share code, notes, and snippets.

@kmuenkel
Last active September 22, 2021 04:14
Show Gist options
  • Save kmuenkel/a6b4e34e5bf6ca44e3c85bf753111850 to your computer and use it in GitHub Desktop.
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)
<?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;
}
}
<?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();
//
}
}
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