new 建立實例clone 複製實例unserialize() 重建實例 (PHP 7.4+)getInstance() 提供全域存取點<?php
abstract class Singleton
{
private static ?self $instance = null;
final private function __construct() {}
final private function __clone() {}
final public function __wakeup() {
throw new \Exception("不能反序列化單例");
}
final public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new static();
}
return self::$instance;
}
}
// 使用:繼承即可
class Database extends Singleton {
private PDO $pdo;
protected function __construct() {
$this->pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function getConnection(): PDO {
return $this->pdo;
}
}
// 取得實例
$db = Database::getInstance()->getConnection();
?>
<?php
class Db
{
private static ?self $instance = null;
private PDO $pdo;
private function __construct(array $config)
{
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
$this->pdo = new PDO($dsn, $config['user'], $config['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES {$config['charset']}",
]);
}
public static function getInstance(array $config = []): self
{
if (self::$instance === null) {
if (empty($config)) {
throw new \LogicException('首次呼叫需提供配置');
}
self::$instance = new self($config);
}
return self::$instance;
}
// 禁止克隆與反序列化
private function __clone() {}
public function __wakeup() {
throw new \Exception("不支援反序列化");
}
// 代理 PDO 方法
public function __call(string $method, array $args)
{
return $this->pdo->$method(...$args);
}
public function getPdo(): PDO
{
return $this->pdo;
}
}
// 使用
// 啟動時初始化 (如 bootstrap.php)
Db::getInstance([
'host' => 'localhost',
'dbname' => 'myapp',
'user' => 'dbuser',
'pass' => 'secret',
'charset' => 'utf8mb4',
]);
// 業務代碼任意處取得
$stmt = Db::getInstance()->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([1]);
$user = $stmt->fetch();
?>
<?php
class Config
{
private static ?self $instance = null;
private array $data = [];
private function __construct(string $envFile = '.env')
{
$this->loadEnv($envFile);
// 可再合併 config/*.php 陣列檔
}
public static function getInstance(string $envFile = '.env'): self
{
if (self::$instance === null) {
self::$instance = new self($envFile);
}
return self::$instance;
}
private function loadEnv(string $file): void
{
if (!is_readable($file)) return;
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) continue;
if (strpos($line, '=') === false) continue;
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value, " \t\n\r\0\x0B'\"");
// 支援 ${VAR} 參考
$value = preg_replace_callback('/\$\{([^}]+)\}/', function($m) {
return $_ENV[$m[1]] ?? $_SERVER[$m[1]] ?? getenv($m[1]) ?? '';
}, $value);
$_ENV[$key] = $_SERVER[$key] = $value;
$this->data[$key] = $value;
}
}
public function get(string $key, $default = null)
{
return $this->data[$key] ?? $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key) ?? $default;
}
public function set(string $key, $value): void
{
$this->data[$key] = $value;
}
public function all(): array
{
return $this->data;
}
private function __clone() {}
public function __wakeup() { throw new \Exception("不支援"); }
}
// 使用
$appEnv = Config::getInstance()->get('APP_ENV', 'production');
$dbHost = Config::getInstance()->get('DB_HOST');
?>
<?php
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
class Logger implements LoggerInterface
{
private static ?self $instance = null;
private string $logDir;
private int $maxFiles = 30;
private function __construct(string $logDir = 'logs')
{
$this->logDir = $logDir;
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
}
public static function getInstance(string $logDir = 'logs'): self
{
if (self::$instance === null) {
self::$instance = new self($logDir);
}
return self::$instance;
}
public function log($level, $message, array $context = []): void
{
$level = is_string($level) ? strtoupper($level) : $level;
$timestamp = date('Y-m-d H:i:s.u');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[$timestamp] [$level] $message$contextStr\n";
$file = $this->logDir . '/' . date('Y-m-d') . '.log';
file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
// 簡易輪轉:刪除 30 天前日誌
$this->rotate();
}
private function rotate(): void
{
$files = glob($this->logDir . '/*.log');
$cutoff = time() - ($this->maxFiles * 86400);
foreach ($files as $file) {
if (filemtime($file) < $cutoff) @unlink($file);
}
}
// PSR-3 便利方法
public function emergency($message, array $context = []) { $this->log(LogLevel::EMERGENCY, $message, $context); }
public function alert($message, array $context = []) { $this->log(LogLevel::ALERT, $message, $context); }
public function critical($message, array $context = []) { $this->log(LogLevel::CRITICAL, $message, $context); }
public function error($message, array $context = []) { $this->log(LogLevel::ERROR, $message, $context); }
public function warning($message, array $context = []) { $this->log(LogLevel::WARNING, $message, $context); }
public function notice($message, array $context = []) { $this->log(LogLevel::NOTICE, $message, $context); }
public function info($message, array $context = []) { $this->log(LogLevel::INFO, $message, $context); }
public function debug($message, array $context = []) { $this->log(LogLevel::DEBUG, $message, $context); }
private function __clone() {}
public function __wakeup() { throw new \Exception("不支援"); }
}
// 使用
Logger::getInstance()->info('用戶登入', ['user_id' => 123, 'ip' => '1.2.3.4']);
Logger::getInstance()->error('資料庫連線失敗', ['error' => $e->getMessage()]);
?>
現代框架 (Laravel、Symfony、Hyperf) 使用 **DI 容器管理單例生命週期**,而非手寫單例類:
<?php
// Laravel 服務提供者註冊
$this->app->singleton(Db::class, function ($app) {
return new Db(config('database'));
});
// 控制器自動注入
class UserController {
public function __construct(private Db $db) {}
public function show(int $id) {
return $this->db->prepare('SELECT * FROM users WHERE id=?')->execute([$id])->fetch();
}
}
?>
| 爭議點 | 說明 | 替代 |
|---|---|---|
| 全域狀態 | 難以測試、隱性耦合、併發風險 | DI 容器、工廠模式、服務定位器 |
| 單一職責違反 | 類別同時負責業務邏輯與實例控制 | 分離 InstanceManager |
| 繼承困難 | final 方法阻止繼承覆寫 | 組合優於繼承 |
| 記憶體洩漏 | 長驅動程式中單例累積資源 | 實作 reset()/shutdown() 釋放資源 |
resetInstance() (測試用) 與 shutdown() (資源釋放) 方法。