單例模式 是創建型設計模式之一,確保某個類別在全域僅有一個實例,並提供全域存取點。在 PHP 中常用於資料庫連線、配置管理、日誌記錄器等「重資源、共享狀態」的場景。
new ClassName() 直接實例化static private $instance)getInstance())補充:實作 __clone() 並拋出錯誤,防止 clone 破壞單一性;PHP 8.1+ 可加上 readonly 屬性強化不可變性。
<?php
declare(strict_types=1);
namespace App\Patterns;
class DatabaseConnection
{
// 1. 靜態私有屬性:儲存唯一實例
private static ?self $instance = null;
// 連線資源(示意)
private \PDO $pdo;
// 2. 私有建構子:禁止外部實例化
private function __construct()
{
$dsn = 'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4';
$this->pdo = new \PDO($dsn, 'user', 'pass', [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
]);
echo "[單例] 資料庫連線已建立\n";
}
// 3. 禁止克隆
public function __clone(): void
{
throw new \LogicException('單例模式禁止克隆');
}
// 4. 禁止反序列化(可選)
public function __wakeup(): void
{
throw new \LogicException('單例模式禁止反序列化');
}
// 5. 公開靜態存取方法
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// 業務方法
public function query(string $sql, array $params = []): array
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function getPdo(): \PDO
{
return $this->pdo;
}
}
<?php
require 'vendor/autoload.php';
use App\Patterns\DatabaseConnection;
// 正確用法:透過 getInstance() 取得實例
$db1 = DatabaseConnection::getInstance();
$users = $db1->query('SELECT * FROM users LIMIT 3');
print_r($users);
// 再次呼叫:回傳同一實例
$db2 = DatabaseConnection::getInstance();
// 驗證是否同一物件
var_dump($db1 === $db2); // true
// 錯誤示範(會拋出例外):
// $db3 = new DatabaseConnection(); // Error: 建構子為 private
// $db4 = clone $db1; // LogicException: 禁止克隆
// $db5 = unserialize(serialize($db1)); // LogicException: 禁止反序列化
依參數建立不同單例(如多資料庫連線):
class MultiDatabase
{
/** @var self[] */
private static array $instances = [];
private \PDO $pdo;
private function __construct(string $dsn, string $user, string $pass)
{
$this->pdo = new \PDO($dsn, $user, $pass);
}
public static function getInstance(string $name = 'default'): self
{
if (!isset(self::$instances[$name])) {
// 這裡可從配置檔讀取對應參數
$config = [
'default' => ['mysql:host=127.0.0.1;dbname=app', 'user', 'pass'],
'logs' => ['mysql:host=127.0.0.1;dbname=logs', 'user', 'pass'],
];
[$dsn, $u, $p] = $config[$name] ?? $config['default'];
self::$instances[$name] = new self($dsn, $u, $p);
}
return self::$instances[$name];
}
// ... 禁止 clone/wakeup、業務方法同前
}
| 場景 | 理由 |
|---|---|
| 資料庫連線池 / PDO 連線 | 避免重複建立 TCP 連線、認證握手,減少資源消耗 |
| 配置管理 | 全域唯一配置來源,運行期熱更新友善 |
| 日誌記錄器 | 統一格式、輸出目標,避免多實例競爭檔案鎖 |
| 快取客戶端 | Redis/Memcached 連線複用,支援連線池 |
| API 客戶端 | HTTP 客戶端連線複用、Token 快取 |
Singleton::getInstance(),違反依賴倒置原則與其硬編碼單例,不如交由 DI 容器(如 PHP-DI、Symfony DI、Laravel Service Container)管理:
// 容器設定範例 (PHP-DI / PSR-11)
return [
\App\Patterns\DatabaseConnection::class => \DI\object()
->constructor() // 單例由容器保證
->scope(\DI\Scope::SINGLETON), // 或 SingletonScope
];
類別改為接收介面注入:
class UserService
{
public function __construct(
private \App\Contracts\DatabaseInterface $db // 依賴抽象
) {}
public function getUsers(): array
{
return $this->db->query('SELECT * FROM users');
}
}
單例模式在 PHP 中確有其價值(資源重、共享狀態),但濫用會導致強耦合、難測試。現代 PHP 開發建議:
::getInstance()這樣既保留單例的資源複用優勢,又維持架構的彈性與可測試性。