5.4. 插件例子

这里我们通过一个简单的插件例子来展示如何开发一个插件,例子代码可以在这里 下载

[警告]

在你进入插件开发之前,请你把系统设置为调试模式(第 4.1 节 “开启调试模式”),这样你才能看到一些错误的输出,否则你就只能是个瞎子摸黑了。

5.4.1. 安装插件

我们假设你已经 "开启了调试模式" ,并且已经下载了我们的例子代码。现在,把代码解压缩,放到 protected/Plugin 目录下,最终目录结构如下:

图 5.2. 插件Example1目录结构

插件Example1目录结构

现在登录到你的系统 manage 后台,查看 插件主题 → 插件管理 你应该能看到 Example1 插件了,如下图所示:

图 5.3. 插件Example1安装

插件Example1安装

在这里点击 "安装" 插件就完成了安装,但是注意,现在插件还不可用,你需要再点击 "启用" 然后插件才可以真的使用。

[注意]

插件 "安装" 和 "启用" 的区别:安装是允许插件创建自己的数据,比如创建自己的数据表之类。启用是让 bzfshop 加载这个插件,这样插件才能真正工作。也就是说,你可以安装了插件,但是不启用,这样插件不会起作用,对你的系统没有任何影响。具体可以查看 第 5.1 节 “插件生命周期”

插件启用之后访问 http://192.168.2.100/PHPStorm/bzfshop-opensource/src/Example1/Test/Index?goods_id=42107 你就能看到插件的输出结果了。我们这个 Example1 插件程序是完全使用了 第 4.5 节 “使用Smarty模板” 中的代码,无非是把这些代码放到了插件中而已。可以看到我们其实并不需要去修改 bzfshop 本身的代码,只需要使用插件就可以为 bzfshop 扩展功能了。

接下来我们将详细的分解这个插件,让你明白一个插件是如何编写的。

5.4.2. 插件配置

在 Example1 插件的代码中,我们看到一个 config.php ,这是插件的配置文件里面配置了插件的基本信息(这些信息在插件安装的时候会被 bzfshop 写入到数据库中,具体请见 bzfshop的设计文档 ),这个文件的内容如下:

config.php. 

/**
 * @author QiangYu
 *
 * 这里我们返回插件缺省配置
 *
 */
return array(

    // 插件基本信息
    'version'      => '1.0.0',
    'display_name' => 'Example1',
    'description'  => <<<DESC
<p>开发商:棒主妇开源</p>
<p>主&nbsp;&nbsp;页:<a href="http://www.bzfshop.net" target="_blank">www.bzfshop.net</a></p>
<p>适用系统:shop</p>
<p>这是一个插件例子,我们扩充了一个访问地址:</p>
<ul>
    <li>测试地址: /Example1/Test/Index</li>
</ul>
DESC

);

这里面有几个项目是 必须要的 ,包括 version, display_name, description

version
插件的版本号,bzfshop 插件升级的时候就是比较版本号的大小来决定是否需要升级的
display_name
查询显示的名字叫什么
description
插件的描述,和下面的图对比你就知道这里写的内容是什么意思了
插件Example1安装

这里的配置信息用于 bzfshop 识别插件,并且决定插件是否需要升级,如果你有其它的配置属性也可以在这里自行添加,当然这些属性就需要你的程序自己去解释和使用了。

5.4.3. 插件实例

bzfshop 加载插件的时候需要你初始化一个插件的实例(简单的说就是 new 一个对象出来)。我们规定插件加载的文件名为 plugin_load.php ,在这个文件里面你需要给 bzfshop 返回一个插件实例,bzfshop 会用这个插件实例来调用插件的接口,完成插件的初始化工作。下面是具体的代码:

plugin_load.php. 

<?php

/**
 * @author QiangYu
 *
 * 插件加载文件,用于初始化插件同时返回一个插件的 instance 对象
 *
 * */

// 定义自己的 namespace ,防止和别的插件冲突
namespace Plugin\Example\Example1 {

    use Core\Plugin\AbstractBasePlugin;
    use Core\Plugin\PluginHelper;
    use Core\Plugin\SystemHelper;

    class Example1Plugin extends AbstractBasePlugin
    {
        // 使用 UUID 作为插件的唯一 ID
        protected static $pluginUniqueId = '868514AB-D33F-45AB-A413-11674E1D3E3F';

        // 允许哪些系统加载本插件
        protected static $systemAllowLoad = array(
            PluginHelper::SYSTEM_MANAGE,
            PluginHelper::SYSTEM_SHOP,
        );

        public function pluginActivate($system)
        {
            return true;
        }

        public function pluginDeactivate($system)
        {
            return true;
        }

        public function pluginGetConfigureUrl($system)
        {
            // 插件不需要配置
            return null;
        }

        public function pluginAction($system)
        {
            // 为 manage 系统加载运行环境
            if (PluginHelper::SYSTEM_MANAGE === $system) {
                return true;
            }

            // 为 shop 系统加载运行环境
            if (PluginHelper::SYSTEM_SHOP === $system) {
                return $this->doShopAction();
            }

            return false;
        }


        /**
         * 为系统设置运行环境
         *
         * @return bool
         */
        private function doShopAction()
        {
            // 获取当前插件的根地址
            $currentPluginBasePath = dirname(__FILE__);

            // code 目录加入到 auto load 的路径中,这样系统就能自动做 class 加载
            SystemHelper::addAutoloadPath($currentPluginBasePath . '/shop/Code');

            // 增加 smarty 的搜索路径,这样才能找到我们的模板
            global $smarty;
            $smarty->addTemplateDir($currentPluginBasePath . '/shop/Tpl/');

            // 设置路由,这样用户就能访问到我们的程序了
            SystemHelper::addRouteMap(
                '/Example1/Test/Index',
                'Controller\Test\Index'
            );

            return true;
        }

    }

}

// 全局命名空间代码,我们在这里生成一个插件的实例返回给加载程序
namespace {
    // 返回 plugin instance
    return Plugin\Example\Example1\Example1Plugin::instance();
}

首先,插件从 AbstractBasePlugin 派生,我们在这个基类中已经实现了 插件接口 的大部分功能,从这里派生你就不用去实现各种通用功能了(当然,你也可以不从基类派生,完全自己实现一个插件接口,高手可以考虑这么做)。我们需要定义插件的 $pluginUniqueId ,bzfshop 系统使用这个值来唯一区别插件(bzfshop 怎么知道两个名字不一样的东西其实是同一个插件呢?用的就是这个值)。我们建议你使用 UUID 值作为插件的 id,这样可以完全避免冲突。

接下来插件需要设置 $systemAllowLoad 表明这个插件允许哪些系统加载它(比如这里是允许 manage、shop 加载,其它的 mobile, supplier 就不允许加载了,因为这个插件不在 mobile, supplier 下工作)。bzfshop 会根据你这里的设置来为各个系统做插件加载,比如 shop 启动的时候这里允许加载,我们就会加载这个插件。

[警告]

注意:所有插件必须允许 PluginHelper::SYSTEM_MANAGE 也就是 mange 系统加载,否则 manage 系统就没法管理这个插件了(在 插件主题 → 插件管理 里面也就无法操作这个插件)

public function pluginAction($system) 方法的实现是重头戏。bzfshop 加载插件之后,会根据当前系统来调用 pluginAction($system) 方法,比如 mange 系统加载这个插件,$system 就会是 PluginHelper::SYSTEM_MANAGE。我们需要根据当前是哪个系统加载插件 来决定做不同的初始化工作。比如 PluginHelper::SYSTEM_SHOP === $system 意味着是商城系统加载了这个插件,于是我们就需要做商城系统的环境设置工作,也就是下面的 doShopAction() 所做的工作。

        /**
         * 为系统设置运行环境
         *
         * @return bool
         */
        private function doShopAction()
        {
            // 获取当前插件的根地址
            $currentPluginBasePath = dirname(__FILE__);

            // code 目录加入到 auto load 的路径中,这样系统就能自动做 class 加载
            SystemHelper::addAutoloadPath($currentPluginBasePath . '/shop/Code');

            // 增加 smarty 的搜索路径,这样才能找到我们的模板
            global $smarty;
            $smarty->addTemplateDir($currentPluginBasePath . '/shop/Tpl/');

            // 设置路由,这样用户就能访问到我们的程序了
            SystemHelper::addRouteMap(
                '/Example1/Test/Index',
                'Controller\Test\Index'
            );

            return true;
        }

doShopAction() 我们单独拿出来说明,这里做了插件最重要的初始化工作:

  1. 把 code 目录加入到 auto load 路径中,这样 bzfshop 就能对 Plugin/Example1/shop/Code 下的代码做自动加载了(简单的说就是能找到这里的代码了)
  2. 由于我们的代码用了自己的 模板,所以需要设置 $smarty 让它知道去哪里找我们的模板文件(注意,你自己的模板文件不要和系统已有的文件名相同,否则无法加载你的模板文件)
  3. 设置路由,我们让 /Example1/Test/Index 这个 URL 映射到 Controller\Test\Index 这个 PHP 文件里面,这样用户访问 http://…/Example1/Test/Index bzfshop 就知道应该调用哪个PHP文件
  4. return true; 表明初始化成功,这样你的插件就可以等待别调用了(如果你 return false; 的话 bzfshop 会认为初始化失败,就会卸载你的插件,你的插件也就不会起作用)

插件的初始化其实是一个很简单的过程,就是设置好插件运行所需要的各种代码路径、环境设置之类,一个简单的总结,插件初始化需要做的工作包括:

  • 设置代码路径,让 bzfshop 能找到你的代码
  • 设置 smarty 路径,让 smarty 能找到你的模板文件
  • 设置 URL 路由映射,这样用户就能通过 URL 访问你的插件功能

上面都是插件程序的定义,我们最终需要返回一个插件实例给 bzfshop。bzfshop 会根据这个实例来调用插件的所有功能。

// 全局命名空间代码,我们在这里生成一个插件的实例返回给加载程序
namespace {
    // 返回 plugin instance
    return Plugin\Example\Example1\Example1Plugin::instance();
}

这里的代码很简单,我们只是返回了插件的实例。注意:AbstractBasePlugin 实现了 Singleton 设计模式,所以我们调用 instance() 方法就会自动生成一个实例了。(如果不知道什么是 Singleton 设计模式的,请自己 Google)

Exampe1 插件代码总共没有多少行,很容易理解。下面是插件运行时候的描述:

  • 用户在浏览器里面输入 http://…/Example1/Test/Index
  • bzfshop 根据路由映射,发现这个 URL 应该对应代码 Controller\Test\Index.php
  • bzfshop 在 autoload 的路径中查找这个 PHP 文件,因为我们把插件的 Code 目录加入了 autoload 路径,所以 bzfshop 会找到这个 PHP 文件
  • 由于这是一个 HTTP GET 请求,所以 bzfshop 会调用 public function get($f3) 方法,在这个方法里面我们用 smarty 输出了页面显示
  • 由于插件配置了 smarty 的路径,所以 smarty 能找到我们的模板文件,并且成功解析模板生成最终显示页面
[注意]

插件加载最重要的就是 插件初始化自身的运行环境,确保 bzfshop 能够找到你的代码。环境设置好之后,剩下的事情和 bzfshop 自身代码的执行就完全没有区别了。