Lumen 實現 SQL 監聽

首發於:我的博客

之前 Lumen 框架從 5.6 升級到 5.7。發現 laravel-sql-logger 包不能正常紀錄日誌了。進行排查,發現是 Lumen 框架沒有對 DB 類型注入 event 對象,導致不能正常對其進行SQL監聽。

那麼解決方案也非常簡單。

// file: bootstrap/app.php
$app["db"]->connection()->setEventDispatcher($app["events"]); // 在下面的註冊前加入這一行即可
$app->register(Mnabialek\LaravelSqlLogger\Providers\ServiceProvider::class);

但是這也讓我對如何實現SQL紀錄產生了興趣。接下來,我們就具體瞭解一下如何實現SQL監聽。

我們知道在Larvel上非常簡單。只需要如下方法即可對其進行SQL監聽:

namespace App\Providers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 啓動應用服務
     *
     * @return void
     */
    public function boot()
    {
        DB::listen(function ($query) {
            // $query->sql
            // $query->bindings
            // $query->time
        });
    }
    
    //...
}

但是在 Lumen 上這種辦法是沒有辦法使用的。Lumen有一些自己的調試SQL的方法,但是這些並不是我們想要的。所以我們只能自己寫監聽事件。

具體的解決方案是,我們首先創建一個Listener文件。

// file: app\Listeners\QueryListener.php

namespace App\Listeners;

use Illuminate\Database\Events\QueryExecuted;

class QueryListener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  ExampleEvent  $event
     * @return void
     */
    public function handle(QueryExecuted $event)
    {
        dd($event); // 此處直接對其進行打印
    }
}

接下來我們直接對其進行註冊

// file: app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Listeners\QueryListener;
use Illuminate\Database\Events\QueryExecuted;
use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        QueryExecuted::class => [  // 這行
            QueryListener::class, // 還有這行
        ],
    ];
}

別忘了註冊 Service Providers

// file: bootstrap/app.php'

/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/

// $app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
$app->register(App\Providers\EventServiceProvider::class); // 取消對這一行的註釋

接下來就是實操了。首先我們創建一個控制器

//file: app/Http/Controllers/UserController.php
namespace App\Http\Controllers;

use App\User;

class UserController extends Controller
{
    public function one() {
        return User::where("id", 1)->first(); // 在控制器執行查詢方法
    }
}

註冊路由

// file: routes/web.php
$router->get('/one', "UserController@one"); // 定義訪問路由

最後別忘了開啓DB功能以及填寫數據庫配置(這一段大家肯定都會,我就不貼代碼了)。

那麼我們來嘗試運行一下:

QueryExecuted {#65 ▼
  +sql: "select * from `users` where `id` = ? limit 1"
  +bindings: array:1 [▶]
  +time: 2.06
  +connection: MySqlConnection {#66 ▶}
  +connectionName: "mysql"
}

發現已經成功了。那麼就可以執行日誌記錄了。

我們修改一下代碼:

// file: app/Listeners/QueryListener.php
namespace App\Listeners;

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Log;

class QueryListener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  ExampleEvent  $event
     * @return void
     */
    public function handle(QueryExecuted $event)
    {
        $query = $event->sql; // 獲取SQL語句
        foreach ($event->bindings as $bind) {
            $query = preg_replace('/\?/', (is_numeric($bind) ? $bind : '\'' . $bind . '\''), $query, 1); // 將SQL中的?替換爲實際的值
        }
        Log::info("query: {$query} time: {$event->time}ms"); // 將SQL和執行時間打印到日誌
    }
}

雖然已經實現了SQL紀錄,但是這並不是我們想要的,因爲將SQL和錯誤日誌放在一起。閱讀起來非常難受。那麼我就需要放到一個單獨的文件裏面去就好了。

// file: app/Listeners/QueryListener.php
namespace App\Listeners;

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Http\Request;

class QueryListener
{
    /**
     * 寫入的文件路徑
     * @var string
     */
    protected $writeFile;

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        $this->writeFile = storage_path() . DIRECTORY_SEPARATOR . "logs" . DIRECTORY_SEPARATOR . "sql-" . date("Ymd") . ".log"; // 定義輸出的日誌路徑ß
    }

    /**
     * Handle the event.
     *
     * @param  QueryExecuted  $event
     * @param Request $request
     * @return void
     */
    public function handle(QueryExecuted $event)
    {
        $query = $event->sql;
        foreach ($event->bindings as $bind) {
            $query = preg_replace('/\?/', (is_numeric($bind) ? $bind : '\'' . $bind . '\''), $query, 1);
        }

        file_put_contents($this->writeFile, "query: {$query} time: {$event->time}ms", FILE_APPEND); // 直接使用 file_put_contents 對內容進行輸入。並且注意 FILE_APPEND 如果不加就變成覆蓋了,這個常量的作用是對文件進行追加寫入。
    }
}

接下來繼續改進,就是說我們目前是在所有的環境中進行打印的,但是我們僅僅需要在開發環境進行調試。所以我們可以進行如下修改:

// file: app/Providers/EventServiceProvider.php
namespace App\Providers;

use App\Listeners\QueryListener;
use Illuminate\Database\Events\QueryExecuted;
use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [];

    public function register()
    {
        if (env("APP_ENV") == "local") { // 判斷環境
            $events = app('events');

            $events->listen(QueryExecuted::class, QueryListener::class); // 手動註冊監聽器
        }
    }
}

那麼SQL監聽功能就實現了。

其實 laravel-sql-logger還有一些高級的顯示功能。比如說打印日誌的時候會順帶着打印請求URL。打印請求時間等。這些我就不這裏具體完善了。如果大家有興趣,可以自己想辦法實現。很簡單的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章