Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

在文章Laravel 將「點閱記錄」改成非同步 queue 寫入資料庫中,有時會發生Redis服務沒啟動,導致系統發生錯誤,針對這個問題,可以建立一個 Laravel Trait,用來檢查資料庫與 Redis 是否有正常連線,並可封裝為共用的「健康檢查套件」。這在系統監控、API 心跳檢查(health check)、DevOps 整合中很實用。
讓它能檢查以下項目:
| 項目 | 驗證內容 |
|---|---|
| Database | 資料庫是否可連線 |
| Redis | Redis 是否能 set/get 成功 |
| Queue | Laravel queue driver 是否正常 |
| Cache | Laravel cache 是否能寫入與讀取 |
<?php
namespace App\Traits;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Cache;
use Illuminate\Queue\RedisQueue;
use App\Jobs\TestQueueJob;
use Throwable;
trait SystemHealthCheck
{
/**
* 檢查資料庫連線
* @return bool
*/
public function checkDatabaseConnection(): bool
{
try {
DB::connection()->getPdo();
return true;
} catch (Throwable $e) {
report($e);
return false;
}
}
/**
* 檢查 Redis 連線
* @return bool
*/
public function checkRedisConnection(): bool
{
try {
Redis::set('health_check', 'ok');
return Redis::get('health_check') === 'ok';
} catch (Throwable $e) {
report($e);
return false;
}
}
/**
* 檢查 Queue 是否可用
* @return bool
*/
public function checkQueue(): bool
{
try {
$connection = Queue::connection();
// 若為 Redis queue 才呼叫 ping()
if ($connection instanceof RedisQueue) {
$ping = $connection->getRedis()->ping();
return $ping === '+PONG';
}
// 其他 driver fallback:派送同步 job 測試
TestQueueJob::dispatchSync();
return true;
} catch (\Throwable $e) {
report($e);
return false;
}
}
/**
* 檢查暫存 Cache 是否可用
* @return bool
*/
public function checkCache(): bool
{
try {
Cache::put('health_check_cache', 'ok', now()->addSeconds(5));
return Cache::get('health_check_cache') === 'ok';
} catch (Throwable $e) {
report($e);
return false;
}
}
/**
* 回報系統狀態
* @return array{cache: string, database: string, queue: string, redis: string}
*/
public function systemStatus(): array
{
return [
'database' => $this->checkDatabaseConnection() ? 'ok' : 'fail',
'redis' => $this->checkRedisConnection() ? 'ok' : 'fail',
'queue' => $this->checkQueue() ? 'ok' : 'fail',
'cache' => $this->checkCache() ? 'ok' : 'fail',
];
}
}
建立 Job: TestQueueJob
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Foundation\Queue\Queueable;
class TestQueueJob implements ShouldQueue
{
use Queueable, Dispatchable;
/**
* Execute the job.
*/
public function handle()
{
return true;
}
}// app/Http/Controllers/SystemCheckController.php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use App\Traits\SystemHealthCheck;
class SystemCheckController extends Controller
{
use SystemHealthCheck;
public function status(): JsonResponse
{
$status = $this->systemStatus();
$isHealthy = collect($status)->every(fn ($v) => $v === 'ok');
return response()->json([
'status' => $isHealthy ? 'ok' : 'fail',
'services' => $status
], $isHealthy ? 200 : 500);
}
}// routes/web.php 或 routes/api.php
use App\Http\Controllers\SystemCheckController;
Route::get('/health-check', [SystemCheckController::class, 'status']);<?php
namespace App\Http\Middleware;
use App\Jobs\LogBlogPostViewJob;
use App\Traits\SystemHealthCheck;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Models\Blog\BlogPost;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Queue;
class LogBlogPostView
{
use SystemHealthCheck;
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$postId = $request->route('id') ?? $request->route('post'); // 支援不同路由命名
$user = Auth::guard('sanctum')->user();
$user_id = $user?->id;
if ($postId) {
$blogPost = BlogPost::findOrFail($postId);
if ($blogPost) {
$ip = $request->ip();
$host = gethostbyaddr($ip) ?? 'unknown';
$userAgent = $request->header('User-Agent') ?? 'unknown';
$today = Carbon::now()->toDateString();
$cacheKey = "blog_viewed:{$postId}:{$ip}:{$today}:{$user_id}";
// 檢查是否使用 Redis 及可用性
if (env('QUEUE_CONNECTION') == 'redis' && !$this->checkRedisConnection()) {
throw new \Exception("Redis Service is not available", 404);
}
// 先檢查 Cache 若有值,改為非同步任務派送
if (!Cache::has($cacheKey)) {
// 放入table:cache裡
Cache::put($cacheKey, true, now()->endOfDay());
// 放入table:jobs裡, 下面兩行指令是相同, 擇一即可
LogBlogPostViewJob::dispatch($postId, $ip, $host, $userAgent, $user_id);
//Queue::push(new LogBlogPostViewJob($postId, $ip, $host, $userAgent, $user_id));
// 更新文章總點閱數
$blogPost->increment('view_count');
}
}
}
return $next($request);
}
}
Call to undefined method Illuminate\Queue\DatabaseQueue::getConnection()表示這行出錯:
$connection = Queue::connection();
$connection->getRedis()->ping(); // ❌ DatabaseQueue 沒這個方法getRedis() 只在 RedisQueue 類別中存在,不適用於 database、sync、sqs 等其他 driver。
我們應該偵測目前的 queue driver,只有在使用 redis 時才呼叫 ping(),否則用 fallback 方法(例如 dispatch 測試 job)。
✔ 修正後 checkQueue() 實作:
use Illuminate\Support\Facades\Queue;
use Illuminate\Queue\RedisQueue;
use App\Jobs\TestQueueJob;
public function checkQueue(): bool
{
try {
$connection = Queue::connection();
// 若為 Redis queue 才呼叫 ping()
if ($connection instanceof RedisQueue) {
$ping = $connection->getRedis()->ping();
return $ping === '+PONG';
}
// 其他 driver fallback:派送同步 job 測試
TestQueueJob::dispatchSync();
return true;
} catch (\Throwable $e) {
report($e);
return false;
}
}