Smarty 默認從文件系統讀取模板 (`file:` 資源),但 CMS 後台可視化編輯、多租戶隔離、模板版本控制、邊緣計算下發等場景需要從數據庫、Redis、遠程 API、對象存儲等異構數據源加載模板。本文詳細講解 Smarty 3/4 资源插件機制,提供四種實戰資源類型的完整實現代碼。
Smarty 通過 `registerResource(type, callbacks)` 註冊自定義協議:
$smarty->registerResource('db', [
'source' => 'db_get_source', // 必須:返回模板源碼
'timestamp' => 'db_get_timestamp', // 必須:返回最後修改 Unix 時間戳
'secure' => 'db_is_secure', // 可選:是否允許訪問 (安全檢查)
'trusted' => 'db_is_trusted', // 可選:是否受信 (緩存鍵生成用)
'populate' => 'db_populate', // Smarty 4+ 新增:填充模板屬性對象
]);
使用時在 `display/fetch` 中指定協議前綴:`$smarty->display('db:home_index');`
| 回調 | 參數 | 返回值 | 說明 |
|---|---|---|---|
| source | `$name, &$source, $smarty` | `bool` (成功/失敗) | 將模板內容賦值給 `$source` 引用 |
| timestamp | `$name, &$timestamp, $smarty` | `bool` | 賦值 Unix 時間戳給 `$timestamp`,用於編譯緩存失效判斷 |
| secure | `$name, $smarty` | `bool` | 返回 `false` 將拋出 `Security` 異常,阻止訪問 |
| trusted | `$name, $smarty` | `bool` | 影響緩存鍵生成,`true` 表示可信任,緩存鍵不包含路徑哈希 |
| populate | `$name, $template, $smarty` | `bool` | Smarty 4: 填充 `Smarty_Template_Source` 對象屬性 (uid, filepath, timestamp, content...) |
<?php
// 資源回調類 (靜態方法,便於註冊)
class DbTemplateResource {
private static PDO $pdo;
private static string $table = 'cms_templates';
public static function init(PDO $pdo, string $table = 'cms_templates'): void {
self::$pdo = $pdo;
self::$table = $table;
}
// 獲取模板源碼
public static function source(string $name, &$source, Smarty $smarty): bool {
$stmt = self::$pdo->prepare(
"SELECT `content` FROM `".self::$table."` WHERE `name` = ? AND `status` = 1 LIMIT 1"
);
$stmt->execute([$name]);
if ($row = $stmt->fetch(PDO::FETCH_COLUMN)) {
$source = $row;
return true;
}
return false;
}
// 獲取修改時間 (用於編譯緩存自動失效)
public static function timestamp(string $name, &$timestamp, Smarty $smarty): bool {
$stmt = self::$pdo->prepare(
"SELECT UNIX_TIMESTAMP(`updated_at`) FROM `".self::$table."` WHERE `name` = ? LIMIT 1"
);
$stmt->execute([$name]);
if ($ts = $stmt->fetch(PDO::FETCH_COLUMN)) {
$timestamp = (int)$ts;
return true;
}
return false;
}
// 安全檢查:僅允許啟用狀態、合法名稱
public static function secure(string $name, Smarty $smarty): bool {
return (bool)preg_match('/^[a-z0-9_\/.-]+$/i', $name);
}
// 受信任:數據庫模板視為可信,緩存鍵簡化
public static function trusted(string $name, Smarty $smarty): bool {
return true;
}
// Smarty 4 populate (可選,增強元數據)
public static function populate(string $name, Smarty_Template_Source $source, Smarty $smarty): bool {
$stmt = self::$pdo->prepare("SELECT `id`,`name`,`content`,`updated_at`,`compiled` FROM `".self::$table."` WHERE `name` = ? LIMIT 1");
$stmt->execute([$name]);
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$source->uid = 'db_'.$row['id'];
$source->name = $name;
$source->filepath = 'db://'.$name;
$source->timestamp = strtotime($row['updated_at']);
$source->content = $row['content'];
$source->exists = true;
// 可選:直接設置編譯後內容,跳過編譯步驟 (需自行維護)
// $source->compiled = $row['compiled'];
return true;
}
$source->exists = false;
return false;
}
}
// 註冊使用
$smarty->registerResource('db', [
'source' => [DbTemplateResource::class, 'source'],
'timestamp' => [DbTemplateResource::class, 'timestamp'],
'secure' => [DbTemplateResource::class, 'secure'],
'trusted' => [DbTemplateResource::class, 'trusted'],
'populate' => [DbTemplateResource::class, 'populate'], // Smarty 4
]);
// 控制器中
$smarty->display('db:home_index'); // 讀取 name='home_index' 的記錄
$smarty->display('db:user/profile'); // 支持層級名稱
CREATE TABLE `cms_templates` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(128) NOT NULL UNIQUE COMMENT '模板標識,如 home_index, user/profile',
`content` MEDIUMTEXT NOT NULL COMMENT 'Smarty 模板源碼',
`compiled` MEDIUMTEXT NULL COMMENT '可選:預編譯 PHP 代碼,極致性能',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1=啟用 0=禁用',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
class RedisTemplateResource {
private static Redis $redis;
private static string $prefix = 'tpl:';
private static int $ttl = 86400; // 24h 本地緩存 TTL
public static function init(Redis $redis, string $prefix = 'tpl:', int $ttl = 86400): void {
self::$redis = $redis;
self::$prefix = $prefix;
self::$ttl = $ttl;
}
public static function source(string $name, &$source, Smarty $smarty): bool {
$key = self::$prefix.$name;
// 1. 嘗試從 Redis 讀取 (熱數據)
if (($source = self::$redis->get($key)) !== false) {
return true;
}
// 2. 回源數據庫 (這裡簡化,實際應調用 DbTemplateResource::source)
// 假設有 fallback 服務
if ($source = self::fetchFromFallback($name)) {
self::$redis->setex($key, self::$ttl, $source); // 回填緩存
return true;
}
return false;
}
public static function timestamp(string $name, &$timestamp, Smarty $smarty): bool {
// Redis 存儲結構: Hash {content, timestamp, version}
$key = self::$prefix.$name.':meta';
$meta = self::$redis->hgetall($key);
if ($meta && isset($meta['timestamp'])) {
$timestamp = (int)$meta['timestamp'];
return true;
}
return false; // 兜底文件系統 timestamp
}
public static function secure(string $name, Smarty $smarty): bool {
return true; // 內部緩存視為安全
}
public static function trusted(string $name, Smarty $smarty): bool {
return true;
}
private static function fetchFromFallback(string $name): string|false {
// 實際對接 DB/API,這裡略
return false;
}
// 管理接口:模板更新後調用
public static function invalidate(string $name): void {
self::$redis->del(self::$prefix.$name);
self::$redis->del(self::$prefix.$name.':meta');
}
public static function update(string $name, string $content, int $timestamp = 0): void {
$key = self::$prefix.$name;
$metaKey = $key.':meta';
$timestamp = $timestamp ?: time();
self::$redis->multi()
->set($key, $content)
->hset($metaKey, ['timestamp'=>$timestamp, 'version'=>$timestamp])
->expire($key, self::$ttl)
->expire($metaKey, self::$ttl)
->exec();
}
}
適用於邊緣計算、微前端模板下發、SaaS 多租戶模板中心:
<?php
class ApiTemplateResource {
private static string $baseUrl;
private static string $token;
private static int $timeout = 3;
private static array $localCache = []; // 進程級緩存
public static function init(string $baseUrl, string $token, int $timeout = 3): void {
self::$baseUrl = rtrim($baseUrl, '/');
self::$token = $token;
self::$timeout = $timeout;
}
public static function source(string $name, &$source, Smarty $smarty): bool {
// 進程級緩存 (PHP-FPM worker 生命周期內)
if (isset(self::$localCache[$name])) {
$source = self::$localCache[$name]['content'];
return true;
}
$url = self::$baseUrl.'/templates/'.rawurlencode($name);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::$timeout,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.self::$token,
'Accept: application/json',
'X-Tenant-ID: '.(defined('TENANT_ID') ? TENANT_ID : 'default'),
],
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
if ($data && isset($data['content'])) {
$source = $data['content'];
self::$localCache[$name] = ['content'=>$source, 'time'=>time()];
return true;
}
}
// 降級:嘗試讀取本地文件備份
$localFile = __DIR__.'/../templates_fallback/'.$name.'.tpl';
if (is_file($localFile)) {
$source = file_get_contents($localFile);
return true;
}
return false;
}
public static function timestamp(string $name, &$timestamp, Smarty $smarty): bool {
if (isset(self::$localCache[$name])) {
$timestamp = self::$localCache[$name]['time'];
return true;
}
return false; // 無法獲取遠程時間戳,依賴編譯緩存機制
}
public static function secure(string $name, Smarty $smarty): bool {
return true; // 受信任 API
}
public static function trusted(string $name, Smarty $smarty): bool {
return true;
}
}
<?php
class OssTemplateResource {
private static $client; // Aws\S3\S3Client / OSS\OssClient / Qcloud\Cos\Client
private static string $bucket;
private static string $prefix = 'templates/';
public static function init($client, string $bucket, string $prefix = 'templates/'): void {
self::$client = $client;
self::$bucket = $bucket;
self::$prefix = $prefix;
}
public static function source(string $name, &$source, Smarty $smarty): bool {
$key = self::$prefix.$name.'.tpl';
try {
$result = self::$client->getObject([
'Bucket' => self::$bucket,
'Key' => $key,
]);
$source = (string)$result['Body'];
return true;
} catch (Exception $e) {
error_log("OSS Template fetch failed [$key]: ".$e->getMessage());
return false;
}
}
public static function timestamp(string $name, &$timestamp, Smarty $smarty): bool {
$key = self::$prefix.$name.'.tpl';
try {
$meta = self::$client->headObject(['Bucket'=>self::$bucket, 'Key'=>$key]);
$timestamp = strtotime($meta['LastModified']);
return true;
} catch (Exception $e) {
return false;
}
}
public static function secure(string $name, Smarty $smarty): bool { return true; }
public static function trusted(string $name, Smarty $smarty): bool { return true; }
}
實現「優先 Redis → 其次 DB → 最後文件/默認」的回退鏈:
<?php
class ChainTemplateResource {
private static array $resources = []; // ['redis', 'db', 'file']
public static function register(array $resourceTypes): void {
self::$resources = $resourceTypes;
}
public static function source(string $name, &$source, Smarty $smarty): bool {
foreach (self::$resources as $type) {
$callbacks = $smarty->getRegisteredResource($type);
if ($callbacks && isset($callbacks['source'])) {
if (call_user_func($callbacks['source'], $name, $source, $smarty)) {
return true;
}
}
}
return false;
}
public static function timestamp(string $name, &$timestamp, Smarty $smarty): bool {
foreach (self::$resources as $type) {
$callbacks = $smarty->getRegisteredResource($type);
if ($callbacks && isset($callbacks['timestamp'])) {
if (call_user_func($callbacks['timestamp'], $name, $timestamp, $smarty)) {
return true;
}
}
}
return false;
}
public static function secure(string $name, Smarty $smarty): bool { return true; }
public static function trusted(string $name, Smarty $smarty): bool { return true; }
}
// 註冊鏈資源
$smarty->registerResource('chain', [
'source' => [ChainTemplateResource::class, 'source'],
'timestamp' => [ChainTemplateResource::class, 'timestamp'],
'secure' => [ChainTemplateResource::class, 'secure'],
'trusted' => [ChainTemplateResource::class, 'trusted'],
]);
ChainTemplateResource::register(['redis', 'db', 'file']);
// 使用
$smarty->display('chain:home_index');
Smarty 自定義資源插件打破了「模板即文件」的固有認知,將模板納入基礎設施統一管理體系:版本控制、灰度發布、多租戶隔離、邊緣分發。掌握 `source`/`timestamp`/`secure`/`trusted`/`populate` 五大回調,即可構建企業級模板交付管線。