首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

Smarty 自定義資源插件開發:從數據庫、Redis、API 動態加載模板

     編輯:PHP 相關知識

Smarty 自定義資源插件開發:從數據庫、Redis、API 動態加載模板

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');`

二、 回調函數簽名規範 (Smarty 3/4 兼容)

回調參數返回值說明
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...)

三、 實戰一:MySQL 數據庫資源 (`db:`)

<?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;

四、 實戰二:Redis 高速緩存資源 (`redis:`)

<?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();
    }
}

五、 實戰三:遠程 API 動態下發 (`api:`)

適用於邊緣計算、微前端模板下發、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;
    }
}

六、 實戰四:對象存儲 (S3/OSS/COS) 資源 (`oss:`)

<?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');

八、 性能優化與最佳實踐

  1. 編譯緩存分離:自定義資源的編譯緩存默認存文件系統 (`compile_dir`),高併發建議掛載 tmpfs 或配置 Redis 緩存 (`$smarty->caching = true; $smarty->cache_resource = 'redis';`)。
  2. 時間戳精度:數據庫 `updated_at` 精度秒級可能導致緩存不失效,建議用 `MICROSECOND` 或維護 `version` 字段 (整數遞增)。
  3. 預編譯存儲:極致性能可在 DB 存 `compiled` 字段,`populate()` 直接返回,跳過詞法分析/編譯 (需同步機制)。
  4. 安全邊界:`secure()` 必須嚴格校驗 `$name`,防止路徑遍歷 (`../../etc/passwd`) 或任意文件讀取。
  5. 監控指標:資源命中率、回調耗時、失敗率、降級觸發次數,接入 Prometheus/Grafana。

九、 Smarty 4 新特性適配

  • TypeScript 風格類型提示:回調簽名建議添加 `Smarty_Template_Source` 類型。
  • PSR-16 緩存整合:`$smarty->setCaching(Smarty::CACHING_LIFETIME_CURRENT, new Symfony\Component\Cache\Adapter\RedisAdapter($client));`
  • 編譯器插件:自定義資源配合 `{function}`、`{block}` 等新標簽,構建領域模板 DSL。

Smarty 自定義資源插件打破了「模板即文件」的固有認知,將模板納入基礎設施統一管理體系:版本控制、灰度發布、多租戶隔離、邊緣分發。掌握 `source`/`timestamp`/`secure`/`trusted`/`populate` 五大回調,即可構建企業級模板交付管線。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved