OneThink1.0开发手册

OneThink系列开发手册在线版

插件开发指南

完整的插件运行流程

插件安装流程

首先 ,我们打开Editor插件的定义类

<?php
// +----------------------------------------------------------------------
// | OneThink [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2013 http://www.onethink.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: yangweijie <yangweijiester@gmail.com> <code-tech.diandian.com>
// +----------------------------------------------------------------------

namespace Addons\Editor;
use Common\Controller\Addon;

/**
 * 编辑器插件
 * @author yangweijie <yangweijiester@gmail.com>
 */

class EditorAddon extends Addon{

    public $info = array(
            'name'=>'Editor',
            'title'=>'前台编辑器',
            'description'=>'用于增强整站长文本的输入和显示',
            'status'=>1,
            'author'=>'thinkphp',
            'version'=>'0.1'
        );

    public function install(){
        return true;
    }

    public function uninstall(){
        return true;
    }

    /**
     * 编辑器挂载的文章内容钩子
     * @param array('name'=>'表单name','value'=>'表单对应的值')
     */
    public function documentEditFormContent($data){
        $this->assign('addons_data', $data);
        $this->assign('addons_config', $this->getConfig());
        $this->display('content');
    }

    /**
     * 讨论提交的钩子使用编辑器插件扩展
     * @param array('name'=>'表单name','value'=>'表单对应的值')
     */
    public function topicComment ($data){
        $this->assign('addons_data', $data);
        $this->assign('addons_config', $this->getConfig());
        $this->display('content');
    }

}

整个插件就是一个特殊的继承了 Addon抽象类的子类。必须实现 install和uninstall方法。 然后必须有一个自己的info属性,作为插件自己的信息。name、titile、description、status、author、version这6个是必须的。到时候后台列表里在未安装时会读取插件信息,显示出来。status为1或者0,表示安装插件后是否立即启用。 效果见下图: image install和uninstall方法用于后台插件安装和卸载时候调用。返回true或者false用于告诉后台我安装卸载的准备工作是否做好了。比如我安装时候创建了某些表,创建成功可以安装,不成功提示错误。卸载前应该将安装时做的操作恢复到安装前状态。 其次,插件被安装后才能配置插件,卸载后会同时去除钩子处挂载的插件名,安装会添加钩子对应的插件名。

后台AddonsController.class.php

/**
 * 安装插件
 */
public function install(){
    $addon_name     =   trim(I('addon_name'));
    $class          =   get_addon_class($addon_name);
    if(!class_exists($class))
        $this->error('插件不存在');
    $addons  =   new $class;
    $info = $addons->info;
    if(!$info || !$addons->checkInfo())//检测信息的正确性
        $this->error('插件信息缺失');
    session('addons_install_error',null);
    $install_flag   =   $addons->install();
    if(!$install_flag){
        $this->error('执行插件预安装操作失败'.session('addons_install_error'));
    }
    $addonsModel    =   D('Addons');
    $data           =   $addonsModel->create($info);
    if(is_array($addons->admin_list) && $addons->admin_list !== array()){
        $data['has_adminlist'] = 1;
    }else{
        $data['has_adminlist'] = 0;
    }
    if(!$data)
        $this->error($addonsModel->getError());
    if($addonsModel->add($data)){
        $config         =   array('config'=>json_encode($addons->getConfig()));
        $addonsModel->where("name='{$addon_name}'")->save($config);
        $hooks_update   =   D('Hooks')->updateHooks($addon_name);
        if($hooks_update){
            S('hooks', null);
            $this->success('安装成功');
        }else{
            $addonsModel->where("name='{$addon_name}'")->delete();
            $this->error('更新钩子处插件失败,请卸载后尝试重新安装');
        }

    }else{
        $this->error('写入插件数据失败');
    }
}

实例化插件类->插件info是否正确->执行install方法,预安装操作->添加插件数据到数据库Addons表和Hooks表

每个步骤出错都会提示,install 方法中错误用 session('addons_install_error', 'error')传递。

插件卸载流程

卸载流程和安装相反

后台AddonsController.class.php

/**
 * 卸载插件
 */
public function uninstall(){
    $addonsModel   =   M('Addons');
    $id            =   trim(I('id'));
    $db_addons     =   $addonsModel->find($id);
    $class        =   get_addon_class($db_addons['name']);
    $this->assign('jumpUrl',U('index'));
    if(!$db_addons || !class_exists($class))
        $this->error('插件不存在');
    session('addons_uninstall_error',null);
    $addons =   new $class;
    $uninstall_flag = $addons->uninstall();
    if(!$uninstall_flag)
        $this->error('执行插件预卸载操作失败'.session('addons_uninstall_error'));
    $hooks_update = D('Hooks')->removeHooks($db_addons['name']);
    if($hooks_update === false){
        $this->error('卸载插件所挂载的钩子数据失败');
    }
    S('hooks', null);
    $delete = $addonsModel->where("name='{$db_addons['name']}'")->delete();
    if($delete === false){
        $this->error('卸载插件失败');
    }else{
        $this->success('卸载成功');
    }
}

实例化插件类->执行预卸载->卸载插件所挂载过的钩子处信息->删除插件文件。 每个步骤出错都会提示,install 方法中错误用 session('addons_uninstall_error', 'error')传递。

插件运行流程

hook函数调用Hook类的listen静态方法触发钩子->获取钩子挂载的开启的插件->执行对应插件实现的钩子同名方法(这个在init_hooks函数会初始化)

代码上就是如下的过程:

hook('documentEditFormContent');

然后hook函数遍历 Hook类里的$tag属性,知道有哪些插件可被调用,接下来去读取配置和状态,启用就去执行钩子

/**
 * 处理插件钩子
 * @param string $hook   钩子名称
 * @param mixed $params 传入参数
 * @return void
 */
function hook($hook,$params=array()){
    \Think\Hook::listen($hook,$params);
}

而Hook::listen方法里面

    /**
 * 监听标签的插件
 * @param string $tag 标签名称
 * @param mixed $params 传入参数
 * @return void
 */
static public function listen($tag, &$params=NULL) {
    if(isset(self::$tags[$tag])) {
        if(APP_DEBUG) {
            G($tag.'Start');
            trace('[ '.$tag.' ] --START--','','INFO');
        }
        foreach (self::$tags[$tag] as $name) {
            APP_DEBUG && G($name.'_start');
            $result =   self::exec($name, $tag,$params);
            if(APP_DEBUG){
                G($name.'_end');
                trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
            }
            if(false === $result) {
                // 如果返回false 则中断插件执行
                return ;
            }
        }
        if(APP_DEBUG) { // 记录行为的执行日志
            trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
        }
    }
    return;
}

/**
 * 执行某个插件
 * @param string $name 插件名称
 * @param Mixed $params 传入的参数
 * @return void
 */
static public function exec($name, $tag,&$params=NULL) {
    if(false === strpos($name,'\\')) {
        // 插件(多个入口)
        $class   =  "Addons\\{$name}\\{$name}Addon";
    }else{
        // 行为扩展(只有一个run入口方法)
        $class   =  $name.'Behavior';
        $tag    =   'run';
    }
    $addon   = new $class();
    return $addon->$tag($params);
}

#实例化一个插件类,我们有get_addon_classs()函数,传入插件名即可或得插件类名,然后直接new 就行了,区分大小写 
$addons_class = new get_addon_classs($addon_name);

$addon->$tag() 这个方法就是去执行钩子处挂载的和钩子同名的插件方法。

这个方法里可以display渲染模板,默认插件的模板就在插件目录下,比如Editor插件类里用的$this->display('content');

如果你想有目录层可以传入目录/模板这样的参数,这样是不支持主题的,如果要支持主题,也可以传入具体的模板路径,使用T函数如T('Addons://Attachment@Article/edit')这样,到时候想切换主题了,T函数定位模板之前C('DEFAULT_THEME','default')这样就行了。

当然这个方法支持传参,只允许一个,为了能实现引用。所以多个参数,请封装成数组,传入hook函数的第二个参数。

执行完毕,这个钩子的某个插件前台功能方法就运行完了。

插件被禁用,钩子处的插件不会被执行该同名方法,并且插件后台列表里不会出现该插件的列表。

插件不光前台能用 ,后台有钩子,并且插件里实现了该钩子,也可以用。为了前后台编辑器插件可以配置不同的编辑器,我们复制了一份Editor插件,改名为EditroForAdmin了。

还有后台首页,其实是用AdminIndex 钩子挂载了几个插件。

后台中,插件默认会在 插件列表里出现。默认没有安装过的会显示在前面。基本的数据字段都是读取的插件类里的info属性数组。插件未安装的时候是不可以设置的。

假如插件需要url访问,就必须插件里有Controller目录和tp结构一样。 只不过生成这个方法的用addons_url('插件名://控制器名/操作方法'),生成访问url。并且访问权限由插件去做。具体写法,参照附件Attachment插件 里的控制器写法。和模板里url调用。