// +---------------------------------------------------------------------- namespace ywxapp; use think\facade\Config; use think\View; use ywxapp\service\AddonService; /** * 插件基类 * Class addon * @author Ywxapp * @package think\addon */ abstract class AddonBase { protected $result; // 视图实例对象 protected $view = null; // 当前错误信息 protected $error; // 插件目录 public $addon_path = ''; public $addonPath = ''; // 插件标识 protected $addonName = ''; // 插件配置作用域 protected $configRange = 'addonconfig'; // 插件信息作用域 protected $infoRange = 'addoninfo'; /** * 插件菜单 */ protected $AddonMenu = [ 'frontend' => [[ 'name' => 'mydemo', //权限规则标识,首个菜单标识必须和插件标识相同 'title' => 'Mydemo管理', //菜单标题 'icon' => 'fa fa-map-marker', //菜单按钮,可使用Font-Awesome的图标 'ismenu' => 1, //是否为菜单 'weigh' => 1, //权重,值越大越靠前 'remark' => 'Demo管理描述内容', //菜单描述内容,在列表处显示 'sublist' => [ //子菜单配置,子菜单name必须以 插件标识/ 开始,这里如 mydemo/ ["name" => "mydemo/signin/index", "title" => "查看"], ["name" => "mydemo/signin/add", "title" => "添加"], ["name" => "mydemo/signin/edit", "title" => "编辑"], ["name" => "mydemo/signin/del", "title" => "删除"], ["name" => "mydemo/signin/multi", "title" => "批量更新"], ] ]], 'member' => [], 'backend' => [], ]; /** * 架构函数 * @access public */ public function __construct($name = null) { $this->result = app()->result; $name = is_null($name) ? $this->getName() : $name; $this->addonName = $name; $this->addonPath = ADDON_PATH . $name . DIRECTORY_SEPARATOR; $this->addon_path = $this->addonPath; $config = ['view_path' => $this->addonPath]; $config = array_merge(Config::get('template'), $config); // 必须创建独立的 View 实例,绝不能复用全局单例 app()->view: // 否则会把全局视图的 view_path 改成 addon//,污染后续所有页面的模板解析。 $this->view = new View(app()); $this->view->config($config); if (method_exists($this, 'initialize')) { $this->initialize(); } } /** * 读取基础配置信息 * @param string $name * @return array */ final public function getInfo($name = '', $force = false) { if (empty($name)) { $name = $this->getName(); } if (! $force) { $info = Config::get($name, $this->infoRange); if ($info) { return $info; } } $info = []; $infoFile = $this->addonPath . 'info.php'; if (is_file($infoFile)) { $info = (array) include $infoFile; $info['url'] = addon_url($name); } Config::set($info, $name, $this->infoRange); return $info ? $info : []; } /** * 获取插件的配置数组 * @param string $name 可选模块名 * @return array */ /** * 读取插件合并配置(静态、无实例)。 * 供运行时 getConfig 与 AppInit 预热共用,避免在预热阶段为每个启用插件实例化 Addon * (其构造函数会 new View)带来的每请求开销。 * 合并顺序与历史 getConfig 完全一致:静态 config/.php → 表单 config.php → 后台保存值。 * @param string $name * @return array */ public static function readConfig(string $name): array { $addonPath = (defined('ADDON_PATH') ? ADDON_PATH : app()->getRootPath() . 'addon' . DIRECTORY_SEPARATOR) . $name . DIRECTORY_SEPARATOR; $config = []; // ① 静态代码级配置:addon//config/.php(随插件分发、支持 env() 覆盖)。 // 与表单 config.php 分工:此处放「运行期/部署相关、需在代码中用 env() 取默认」的项 // (如令牌、远程开关、SSL 校验);表单 config.php 放「后台可编辑」的项。 $staticFile = $addonPath . 'config' . DIRECTORY_SEPARATOR . $name . '.php'; if (is_file($staticFile)) { $staticArr = include $staticFile; if (is_array($staticArr)) { $config = array_merge($config, $staticArr); } } // ② 表单配置:addon//config.php(后台可编辑字段,静态 value 为默认值) $configFile = $addonPath . 'config.php'; if (is_file($configFile)) { $configArr = include $configFile; if (is_array($configArr)) { foreach ($configArr as $key => $value) { // 兼容两种 config.php 格式: // ① 标准表单格式 [['name'=>'x','value'=>'y'], ...] // ② 关联数组格式(代码级配置)['key'=>'val', ...] if (is_array($value) && array_key_exists('name', $value) && array_key_exists('value', $value)) { $config[$value['name']] = $value['value']; } else { $config[$key] = $value; } } } } // 统一配置源:用后台保存的值覆盖上面的默认值, // 避免插件运行期只能读到默认值、忽略后台配置保存(修复配置"双入口"不一致) $saved = AddonService::config($name); if (!empty($saved) && is_array($saved)) { $config = array_merge($config, $saved); } return $config; } final public function getConfig($name = '', $force = false) { if (empty($name)) { $name = $this->getName(); } if (! $force) { $config = Config::get($name); if ($config) { return $config; } } $config = self::readConfig($name); Config::set($config, $name); return $config; } /** * 设置配置数据 * @param $name * @param array $value * @return array */ final public function setConfig($name = '', $value = []) { if (empty($name)) { $name = $this->getName(); } $config = $this->getConfig($name); $config = array_merge($config, $value); Config::set($config, $name); return $config; } /** * 设置插件信息数据 * @param $name * @param array $value * @return array */ final public function setInfo($name = '', $value = []) { if (empty($name)) { $name = $this->getName(); } $info = $this->getInfo($name); $info = array_merge($info, $value); Config::set($info, $name, $this->infoRange); return $info; } /** * 获取完整配置列表 * @param string $name * @return array */ final public function getFullConfig($name = '') { $fullConfigArr = []; if (empty($name)) { $name = $this->getName(); } $configFile = $this->addonPath . 'info.php'; if (is_file($configFile)) { $fullConfigArr = include $configFile; } return $fullConfigArr; } /** * 获取当前模块名 * @return string */ final public function getName() { if ($this->addonName) { return $this->addonName; } // 插件引导类固定命名为 addon\<标识>\Addon(如 addon\appmall\Addon)。 // 取「类名末段(Addon)之前」的命名空间末段作为插件标识, // 不要直接取类名末段(会得到 'addon' 导致 addonPath/view_path 全部错乱)。 $data = explode('\\', get_class($this)); array_pop($data); return strtolower(array_pop($data)); } /** * 设置插件标识 * @param $name */ final public function setName($name) { $this->addonName = $name; } /** * 检查基础配置信息是否完整 * @return bool */ final public function checkInfo() { $info = $this->getInfo(); $info_check_keys = ['name', 'title', 'intro', 'author', 'version', 'state']; foreach ($info_check_keys as $value) { if (! array_key_exists($value, $info)) { return false; } } return true; } /** * 加载模板和页面输出 可以返回输出内容 * @access public * @param string $template 模板文件名或者内容 * @param array $vars 模板输出变量 * @param array $replace 替换内容 * @param array $config 模板参数 * @return mixed * @throws \Exception */ public function fetch($template = '', $vars = [], $replace = [], $config = []) { if (! is_file($template)) { $template = '/' . $template; } // 关闭模板布局 $this->view->engine->layout(false); echo $this->view->fetch($template, $vars, $replace, $config); } /** * 渲染内容输出 * @access public * @param string $content 内容 * @param array $vars 模板输出变量 * @param array $replace 替换内容 * @param array $config 模板参数 * @return mixed */ public function display($content, $vars = [], $replace = [], $config = []) { // 关闭模板布局 $this->view->engine->layout(false); echo $this->view->display($content, $vars, $replace, $config); } /** * 渲染内容输出 * @access public * @param string $content 内容 * @param array $vars 模板输出变量 * @return mixed */ public function show($content, $vars = []) { // 关闭模板布局 $this->view->engine->layout(false); echo $this->view->fetch($content, $vars, [], [], true); } /** * 模板变量赋值 * @access protected * @param mixed $name 要显示的模板变量 * @param mixed $value 变量的值 * @return void */ public function assign($name, $value = '') { $this->view->assign($name, $value); } /** * 获取当前错误信息 * @return mixed */ public function getError() { return $this->result->error; } /** * 插件安装方法 */ abstract public function install(); /** * 插件卸载方法 */ abstract public function uninstall(); /** * 启用插件时回调(可重写,返回 false 表示阻断启用) * 由 AddonService::enable() 通过 callAddonHook 调用 */ public function enable() { return true; } /** * 禁用插件时回调(可重写,返回 false 表示阻断禁用) * 由 AddonService::disable() 通过 callAddonHook 调用 */ public function disable() { return true; } /** * 升级插件时回调(可重写) * 由 AddonService::upgrade() 通过 callAddonHook 调用 * @param string $currentVersion 已安装版本号 */ public function upgrade($currentVersion = '') { return true; } /** * 注册钩子(在 loadAddonRelevant 时可被调用,默认空实现) * @param mixed $hookService * @return $this */ public function registerHooks($hookService = null) { return $this; } /** * 注册路由(默认空实现) */ public function registerRoutes() { } /** * 注册服务(默认空实现) */ public function registerServices() { } /** * 声明本插件依赖的其它插件标识(用于安装前置校验) * @return array */ public function getDependencies(): array { return []; } /** * 声明本插件依赖的 PHP 扩展(用于安装前置校验) * @return array */ public function getExtensions(): array { return []; } /** * 要求的最低 ThinkPHP 版本 * @return string */ public function getThinkVersion(): string { return '>=8.0'; } }