Typecho 的插件系统是其扩展能力的核心。随着 1.3 版本全面引入 PHP 命名空间,插件开发的代码组织和类引用方式发生了显著变化。本文将手把手带你编写一个兼容 Typecho 1.3 的完整插件,涵盖从目录结构到钩子注册的全部流程。
一、插件目录结构
一个标准的 Typecho 1.3 风格插件应遵循以下结构:
usr/plugins/MyPlugin/
├── Plugin.php # 插件主文件(入口)
├── Action.php # 独立页面控制器(可选)
├── lib/
│ ├── Parser.php # 业务逻辑类
│ └── Helper.php # 工具函数
├── static/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── main.js
└── README.md二、Plugin.php 核心文件
<?php
/**
* MyPlugin - 示例插件
*
* @package MyPlugin
* @author YourName
* @version 1.0.0
* @link https://your-site.com
*/
namespace TypechoPlugin\MyPlugin;
use Typecho\Common;
use Typecho\Db;
use Typecho\Exception;
use Typecho\Plugin\Interface as PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Text;
use Typecho\Widget\Helper\Form\Element\Textarea;
use Typecho\Widget\Helper\Form\Element\Radio;
use Widget\Options;
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
class Plugin implements PluginInterface
{
/**
* 激活插件
*/
public static function activate()
{
// 注册内容处理钩子(双钩子机制)
\Typecho\Plugin::factory('Widget\Abstract\Contents')->excerpt = [__CLASS__, 'parseExcerpt'];
\Typecho\Plugin::factory('Widget\Abstract\Contents')->content = [__CLASS__, 'parseContent'];
// 注册页脚输出钩子
\Typecho\Plugin::factory('Widget\Archive')->footer = [__CLASS__, 'injectFooter'];
// 创建数据库表(如果需要)
self::createTable();
return _t('MyPlugin 已激活,请在设置中配置参数');
}
/**
* 禁用插件
*/
public static function deactivate()
{
return _t('MyPlugin 已禁用');
}
/**
* 插件配置面板
*/
public static function config(Form $form)
{
$apiKey = new Text('apiKey', null, null,
_t('API Key'),
_t('请输入你的服务 API Key'));
$form->addInput($apiKey->addRule('required', _t('API Key 不能为空')));
$maxItems = new Text('maxItems', null, '10',
_t('最大条目数'),
_t('每页显示的最大条目数量'));
$form->addInput($maxItems->addRule('isInteger', _t('必须为整数')));
$enableCache = new Radio('enableCache',
['0' => _t('关闭'), '1' => _t('开启')],
'1',
_t('启用缓存'));
$form->addInput($enableCache);
$customCss = new Textarea('customCss', null, null,
_t('自定义 CSS'),
_t('此处填写的 CSS 将被注入到页面头部'));
$form->addInput($customCss);
}
/**
* 个人配置面板(多用户模式)
*/
public static function personalConfig(Form $form) {}
/**
* 摘要处理
*/
public static function parseExcerpt($text, $widget)
{
$options = Options::widget();
$pluginOptions = $options->plugin('MyPlugin');
// 简单的字数截断逻辑
$maxLen = intval($pluginOptions->maxItems ?? 10) * 20;
if (mb_strlen(strip_tags($text), 'UTF-8') > $maxLen) {
$text = mb_substr(strip_tags($text), 0, $maxLen, 'UTF-8') . '...';
}
return $text;
}
/**
* 正文内容处理
*/
public static function parseContent($text, $widget)
{
$options = Options::widget();
$pluginOptions = $options->plugin('MyPlugin');
// 为所有外链添加 target="_blank"
$text = preg_replace(
'/<a(.*?)href="(https?:\/\/[^"]+)"(.*?)>/i',
'<a$1href="$2"$3 target="_blank" rel="noopener noreferrer">',
$text
);
return $text;
}
/**
* 页脚注入
*/
public static function injectFooter()
{
$options = Options::widget();
$pluginOptions = $options->plugin('MyPlugin');
if (!empty($pluginOptions->customCss)) {
echo '<style>' . htmlspecialchars($pluginOptions->customCss) . '</style>' . "\n";
}
}
/**
* 创建数据表
*/
private static function createTable()
{
$db = Db::get();
$adapter = $db->getAdapterName();
if ('Pdo_Mysql' === $adapter || 'Mysqli' === $adapter) {
$db->query("CREATE TABLE IF NOT EXISTS {$db->getPrefix()}myplugin_logs (
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
action VARCHAR(50) NOT NULL,
content TEXT,
created INT(10) UNSIGNED DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}
}
}三、双内容钩子机制详解
Typecho 1.3 引入了双内容钩子,允许插件在 excerpt 和 content 两个不同阶段介入:
// 注册到摘要钩子(列表页调用)
\Typecho\Plugin::factory('Widget\Abstract\Contents')->excerpt = [__CLASS__, 'excerptHook'];
// 注册到正文钩子(详情页调用)
\Typecho\Plugin::factory('Widget\Abstract\Contents')->content = [__CLASS__, 'contentHook'];两个钩子接收相同的参数签名:
public static function excerptHook($text, $widget)
{
// $text: 当前内容文本(Markdown 或 HTML)
// $widget: 当前 Widget 实例,可通过它获取文章元数据
$cid = $widget->cid; // 文章 ID
$title = $widget->title; // 文章标题
return $text; // 必须返回处理后的文本
}四、数据库操作示例
use Typecho\Db;
class Plugin implements PluginInterface
{
public static function queryData()
{
$db = Db::get();
$prefix = $db->getPrefix();
// 查询最近发布的 5 篇文章
$result = $db->fetchAll(
$db->select()->from('table.contents')
->where('type = ?', 'post')
->where('status = ?', 'publish')
->order('created', Db::SORT_DESC)
->limit(5)
);
return $result;
}
public static function insertLog($action, $content = '')
{
$db = Db::get();
$prefix = $db->getPrefix();
$db->query(
$db->insert('table.myplugin_logs')->rows([
'action' => $action,
'content' => $content,
'created' => time()
])
);
}
}五、独立页面控制器(Action.php)
如果你的插件需要提供独立的前端页面(如一个自定义 API 接口),可以创建 Action.php:
<?php
namespace TypechoPlugin\MyPlugin;
use Typecho\Widget;
use Widget\Options;
class Action extends Widget\Base\Contents
{
public function action()
{
// 简单的 JSON API 示例
$this->response->setHeader('Content-Type', 'application/json');
$data = [
'status' => 'ok',
'time' => date('c'),
'posts' => $this->getRecentPosts()
];
echo json_encode($data);
}
private function getRecentPosts()
{
$db = \Typecho\Db::get();
return $db->fetchAll(
$db->select('cid', 'title', 'slug', 'created')
->from('table.contents')
->where('type = ?', 'post')
->where('status = ?', 'publish')
->order('created', \Typecho\Db::SORT_DESC)
->limit(5)
);
}
}访问地址:https://your-blog.com/action/myplugin
六、配置持久化
Typecho 使用 options 表存储插件配置:
// 读取配置
$options = Options::widget();
$myConfig = $options->plugin('MyPlugin');
$value = $myConfig->apiKey;
// 配置值会自动序列化存储,支持数组和对象七、调试技巧
// 1. 查看所有可用钩子
$hooks = \Typecho\Plugin::export();
var_dump($hooks);
// 2. 在插件中记录日志
error_log('MyPlugin: ' . print_r($data, true), 3, __DIR__ . '/debug.log');
// 3. 开启 Typecho 调试模式
// 在 config.inc.php 中定义:
// define('__TYPECHO_DEBUG__', true);八、总结
Typecho 1.3 的命名空间化让插件开发更加规范化、现代化。虽然旧插件在兼容层保护下仍能运行,但新插件应当全面采用命名空间风格,以获得更好的可维护性和扩展性。
延伸阅读:Typecho 官方插件模板
暂无评论