Welcome to Diabolo’s documentation!

简介

介绍

Diabolo 是一个微核心的PHP框架,框架本身仅包含基础的资源管理功能,其他的功能由外部的服务或模块实现。

核心

  • 事件管理
  • 服务管理
  • 模块管理

服务

  • 数据库 Mysql/Sqlite/ActiveRecord
  • 缓存 Redis/数据库
  • 会话管理 自定义存储/自动启动
  • 云存储管理 七牛/阿里云/又拍云/S3
  • 动作管理 Web/命令行 动作处理/视图输出
  • OAuth2.0 接口动作/参数验证/文档生成
  • 错误处理 邮件通知/自定义错误界面
  • 国际化
  • 日志 自定义格式/存储方式
  • 资源管理 发布

模块

  • 代码生成 动作/表单/数据库模型
  • 接口测试/文档生成

其他

  • 数据验证
  • 表单模型

开始使用

准备

  • 进入到项目根目录

  • 创建Demo模块:

    $ mkdir Module       # 创建用于存放模块的目录
    $ mkdir Module/Demo  # 创建Demo模块目录
    $ vi Module/Demo/Module.php # 编辑模块类
    

    文件内容:

    <?php
    namespace X\Module\Demo; # 模块命名空间
    use X\Core\Module\XModule; # 引入模块基础类
    class Module extends XModule {
        # 当该模块运行的时候,将会调用该方法
        public function run($parameters = array()) {
            echo "This is a demo module.";
        }
    }
    
  • 创建启动文件,例如index.php, 然后在该文件中初始化并运行:

    <?php
    define("DIABOLO_PATH", __DIR__);    # 定义框架路径
    require DIABOLO_PATH.'/Core/X.php'; # 导入框架
    $config = array(                    # 配置运行信息
        'document_root' => __DIR__,     # 项目根路径
        'module_path' => array(),       # 模块搜索路径
        'service_path' => array(),      # 服务搜索路径
        'library_path' => array(),      # 公共库搜索路径
        'modules' => array(             # 引用模块列表
            'Demo' => array(            # 定义模块
                'enable' => true,       # 启用模块
                'default' => true,      # 设置为默认模块
                'params' => array(),    # 模块配置参数
            ),
        ),
        'services' => array(),          # 引用服务列表
        'params' => array(),            # 其他自定义参数
    );
    X\Core\X::start($config)->run();    # 启动并运行
    
  • 最终的目录结构:

    project
    | -- Core               # Diabolo存放目录
    ` -- Module             # 模块存放目录
         ` - Demo           # Demo 模块目录
             ` - Module.php # 模块主文件
    

启动

使用浏览器访问项目地址 http://127.0.0.1/ ,输出:

This is a demo module.

全局配置

范例

基础配置

array(                              # 配置运行信息
    'document_root' => __DIR__,     # 项目根路径
    'module_path' => array(),       # 模块搜索路径
    'service_path' => array(),      # 服务搜索路径
    'library_path' => array(),      # 公共库搜索路径
    'modules' => array(),           # 引用模块列表
    'services' => array(),          # 引用服务列表
    'params' => array(),            # 其他自定义参数
);

项目说明

  • document_root

    项目根目录路径, 当使用 X::system()->getPath() 的时候会依据该目录计算出绝对路径。 例如,设置 document_root/myproject/demo/

    echo X\Core\X::system()->getPath("View/Index.php");
    

    将会输出

    /myproject/demo/View/Index.php
    
  • module_path

    设置模块搜索路径,当加载模块的时候将在这些目录中寻找模块。 列表中的路径地址必须是绝对路径, 并且可以不在当前项目中。 例如:

    'module_path' => array(
        '/myproject/demo/Module',
        '/usr/local/diabolo/Module',
    )
    

    这样, 框架将会按照上面的顺序寻找模块,找到后将停止寻找。

  • service_path

    设置服务搜索路径,当加载服务的时候将在这些目录中寻找服务。 列表中的路径地址必须是绝对路径, 并且可以不在当前项目中。 例如:

    'service_path' => array(
        '/myproject/demo/Service',
        '/usr/local/diabolo/Service',
    )
    

    这样, 框架将会按照上面的顺序寻找服务,找到后将停止寻找。

  • library_path

    设置第三方库的搜索路径,当加载lib的时候将在这些目录中寻找。 列表中的路径地址必须是绝对路径, 并且可以不在当前项目中。 例如:

    'library_path' => array(
        '/myproject/demo/Library',
        '/usr/local/diabolo/Library',
    )
    

    这样, 框架将会按照上面的顺序寻找库,找到后将停止寻找。

  • modules

    配置项目运行所需的模块列表。 例如:

    'modules' => array(
        'Demo' => array(
            'enable' => true,
            'default' => true,
            'params' => array(
                'param001' => 'value of param001',
            ),
        ),
    )
    

    模块数组的键名是模块名

    • enable : true|false 配置是否启用该模块
    • default : true|false 配置是否为默认模块,当参数没有指定运行参数的时候,默认的模块将会被运行
    • params : 模块的配置项目, 在模块的代码中可以使用 \X\Module\Demo::getModule()->getConfiguration()->get("param001") 获取值 “value of param001”
  • services

    配置项目运行所需的服务列表 例如:

    'services' => array(
        'Demo' => array(
            'class' => '\\X\\Service\\Demo\\Service',
            'enable' => true,
            'delay' => true,
            'params' => array(
                'param001' => 'value of param001',
            ),
        ),
    )
    

    服务数组的键名是服务名

    • class 服务类名称
    • enable true|false 配置是否启用该服务
    • delay true|false 是否延迟加载, 延迟加载将在仅仅服务调用的时候加载服务,如果没有调用,则服务不会被启动。如果为false,则当项目启动的时候自动加载该服务。
    • params : 服务的配置项目, 在服务的代码中可以使用 \X\Service\Demo::getService()->getConfiguration()->get("param001") 获取值 “value of param001”
  • params

    全局配置, 在代码中可以使用 \X\Core\X::system()->getConfiguration()->get("params")->get("param001") 获取值 “value of param001” 例如:

    'params' => array(
        'param001' => 'value of param001',
    ),
    

模块

创建模块

  • 模块目录结构

    project
    `- Module
        |- ModuleName
        |   `- Module.php
        `- Example
            `- Module.php
    
  • 创建存放模块的目录,并将该目录添加到配置文件的 module_path 中。

  • 创建模块文件夹,文件夹名称与模块名相同。

  • 创建模块类文件,文件名固定为 Module.php , 其内容如下:

    <?php
    namespace X\Module\Example;
    use X\Core\Module\XModule;
    
    class Module extends XModule {
        /**
         * {@inheritDoc}
         * @see \X\Core\Module\XModule::run()
         */
        public function run($parameters = array()) {
            echo "This is an example module.";
    
        }
    }
    

    模块的实现类必须要定义 run() 方法,该方法是模块运行时执行实际的请求处理。 比如根据参数调用操作处理方法,然后渲染视图输出等。

  • 注册模块,将模块信息写入 modules

    'modules' => array(
        'Example' => array(
            'enable' => true,
            'default' => true,
            'params' => array(
                'pretty_name' => '测试模块',
            ),
        ),
    ),
    

    params 可以为空,这里是方便演示所以加了一个参数。

运行模块

框架通过获取 module 参数的值来加载指定的模块并运行, 例如

web 方式 : http://example.com/index.php?module=example
cli 方式 : php index.php --module=example

上面两种方式最终都会输出 This is an example module. .

获取模块信息

  • 获取模块下文件路径

    # 在 模块类 中
    echo $this->getPath("Resource/Template.txt");
    
    # 在其他文件中
    \X\Module\Example\Module::getModule()->getPath("Resource/Template.txt");
    

    假如项目地址为 /home/project/example , 那么输出的路径为

    /home/project/example/Module/Resource/Template.txt
    
  • 获取模块的配置信息

    # 在 模块类 中
    echo $this->getConfiguration()->get("pretty_name");
    
    # 在其他文件中
    \X\Module\Example\Module::getModule()->getConfiguration()->get("pretty_name");
    

    将输出 测试模块

服务

服务创建

  • 服务目录结构

    project
    `- Service
        |- ServiceName
        |   `- Service.php
        `- Example
            `- Service.php
    
  • 创建存放服务的目录,并将该目录添加到配置文件的 service_path 中。

  • 创建模块文件夹,文件夹名称与模块名称相同。

  • 创建模块类文件,文件名固定为 Service.php ,其内容如下

    <?php
    namespace X\Service\Example;
    
    class Service extends \X\Core\Service\XService {
        /** 定义服务名称 */
        protected static $serviceName = 'Example';
    
        /** 定义服务接口 */
        public function doSomething() {
            return "something";
        }
    }
    
  • 注册服务,将服务信息写入 services

    'services' => array(
        'Example' => array(
            'class' => '\\X\\Service\\Example\\Service',
            'enable' => true,
            'delay' => true,
            'params' => array(
                'param001' => 'value of param001',
            ),
        ),
    )
    

    params 可以为空,这里是方便演示所以加了一个参数。

调用服务

在项目运行时,服务只会被实例化一次,可以通过 getService() 方法获取服务实例。

echo X\Service\Example\Service::getService()->doSomething();

将输出 something

start()stop() 方法

如果希望在服务启动/停止的时候执行一些操作, 则需要重写 start() 或者 stop() 方法。 例如, 如果我们想知道请求大概运行了多久。

class Service extends \X\Core\Service\XService {
    /** 记录开始时间 */
    private $startTime = null;

    /** 服务启动时记录开始时间 */
    public function start() {
        parent::start();

        $this->startTime = time();
    }

    /** 服务结束的时候输出花费的时间 */
    public function stop() {
        echo time() - $this->startTime();

        parent::stop();
    }
}

当然, 这需要在注册服务的时候,将 delay 设置为 false 来禁用掉延迟加载。

事件

事件注册

目前事件注册只能通过代码手动注册,不支持配置的方式注册, 例如为事件 event001 注册处理器

$eventManager = \X\Core\X::system()->getEventManager();

# 注册事件处理器
$eventManager->registerHandler('event001', function( $param1 ) {
    return "THIS IS CALLBACK RESPONSE";
});

# 注册事件结果
$eventManager->registerHandler('event001', "THIS IS A TEST RESULT");

当事件触发的时候,如果处理器是一个可调用的处理器,那么该处理器将会被执行,其返回结果将会返回给事件触发者,如果处理器不可以执行,那么,将直接返回该数据给触发者。

事件触发

事件需要手动触发, 例如触发上面的 event001

$eventManager = \X\Core\X::system()->getEventManager();
$result = $eventManager->trigger('event001', 'par001');

$result

array(
    "THIS IS CALLBACK RESPONSE",
    "THIS IS A TEST RESULT",
);

事件触发时能够传递任意多个参数,参数将会传递给事件处理器,以上面这个例子来说, 事件触发的时候传递一个参数值为 "par001", 那么, 第一个事件处理器中的 $param1 的值就是 "par001"

事件响应

事件触发完成后,返回的数据是包含每个处理器的返回结果数组。如果处理器没有返回,则对应未知的返回值为 null

动作服务

动作服务用于从请求参数中获取动作名称,然后从一组动作中实例化动作并执行的服务。

注册服务

配置内容:

'services' => array(
    'XAction' => array(
        'class' => '\\X\\Service\\XAction\\Service',
        'enable' => true,
        'delay' => true,
        'params' => array(
            'ActionParamName' => 'action',
            'Error404Handler' => array(Error404::class, 'handle404Error'),
            'Error500Handler' => array(Error500::class, 'handle500Error'),
            'CommonViewPath' => 'View',
        ),
    ),
),
  • ActionParamName 动作参数名,用来告诉服务从哪里获取动作名称, 例如这里为 action , 那么,当web请求的参数中包含 action 的时候, 服务将从取该参数的值作为动作名称。 当请求来自命令行时, 参数来自 --action 。 例如

    GET http://example.com/index.php?module=example&action=show/hello
    

    或者

    php index.php --module=example --action=show/hello
    

    都将调用 show/hello 动作。

  • Error404Handler 404错误处理器, 取值可以为普通字符串或者回调函数, 如果配置为字符串,那么当发生404错误的时候,则显示该字符串; 如果该值能够被调用, 则404错误发生的时候执行该回调。 404能够在任何地方触发,例如

    \X\Service\XAction\\Service::getService()->triggerErrorHandler('404');
    

    不过,推荐仅仅在动作中触发, 例如

    $this->throw404IfTrue(true);
    
  • Error500Handler 500错误处理器, 取值可以为普通字符串或者回调函数, 如果需要禁用该功能,则赋值为 null 。 当该值不为 null 时,服务将注册一个错误处理器,并在发生错误的时候调用该值。

  • CommonViewPath 视图放置路径,该路径是相对于模块或者整个项目的。 这里配置为 View , 则项目目录如下

    project
    |- Module
    |   `- Example
    |       `- View
    |           |- Particle
    |           |   `- Index.php
    |           `- Layout
    |
    `- View
       | - Particle
       |    `- Header.php
       ` - Layout
           `- Default.php
    

初始化服务

动作服务的初始化主要是向服务中添加分组,一般该操作在模块类中进行, 例如

<?php
namespace X\Module\Example;
use X\Core\Module\XModule;
use X\Service\XAction\Service as XActionService;

/**
 * 模块基类, 当前项目的所有模块都应当继承该类。
 * <li>动作类都放到 Action 文件夹</li>
 * <li>模块私有视图文件都放到 View 文件夹, 布局存放在 View/Layout, 片段存放在 View/Particle 下面</li>
 */
class Module extends XModule {
    /**
     * 使用 XAction 服务来分发动作。
     */
    public function run($parameters = array()) {
         $actionService = XActionService::getService();

         # 组名必须唯一
         $group = 'example';
         # 注册分组
         $actionService->addGroup($group, '\\X\\Module\\Example\\Action');
         # 设置该分组的默认动作
         $actionService->setGroupOption($group, 'defaultAction', 'index');
         # 设置分组的视图路径
         $actionService->setGroupOption($group, 'viewPath', $this->getPath('View/'));
         # 设置运行参数
         $actionService->getParameterManager()->merge($parameters);
         # 运行分组
         return $actionService->runGroup($group);
   }
}
  • 组名 组名在动作分组中需要唯一
  • 注册分组时的命名空间名称是动作的基础命名空间名称, 例如动作为 user/login ,则最终的动作类为 \X\Module\Example\Action\User\Login
  • 分组的默认动作是当参数中没有指定动作名称的时候执行的动作。
  • 视图路径是指该分组的视图路径。
  • 当运行分组时,服务将在指定的分组中寻找动作并执行。

实现动作

动作类是实际用来处理请求的操作类。 例如

<?php
namespace X\Module\Example\Action\User;
use X\Service\XAction\Handler\WebPageAction;

class Index extends WebPageAction {
    /** 布局 */
    protected $layout = '/SingleColumn';
    /** 标题 */
    protected $title = '用户登录';

    # 该方法用于接收参数并执行动作处理
    protected function runAction( $userName ) {
        $this->addParticle('/header');

        $this->addParticle('login', array(
            'default' => $userName,  # 视图数据
        ));

        $this->addParticle('/footer');
        $this->display();
    }
}
  • 该动作继承 WebPageAction 用于处理一个网页请求并渲染界面。 目前支持 WebPageAction , CommandActionAjaxAction 这三类动作。

  • 对于视图的名称, 例如 /SingleColumn 或者 /header, login 之类, 如果以 / 开头,则将会使用服务中配置的 CommonViewPath 来获取, 否则 使用该动作所在分组的视图路径来获取。

  • runAction() 方法用于最终的动作处理, 该方法的参数值将在初始化动作服务时获取。 以之前的初始化方式为例, 如果要获取 $userName 的值, 则请求应该如下

    GET http://example.com/index.php?module=example&action=user/login&userName=example
    

    当方法的参数没有默认值的时候,表示该参数必传否则将会出错。 有默认值的时候,如果 请求中不带该参数,则取默认值。

Web视图

web视图为php文件,视图分为布局和片段,布局用来展示网页的大体概貌, 片段是整个视图的一小部分, 并且由布局视图来管理在哪里显示。 web视图最终输出的内容是 html 中的 body 部分, head 部分由视图类来管理。

在布局文件中, $thisX\Service\XAction\Component\WebView\Html 的实例。 在片段文件中, $thisX\Service\XAction\Component\WebView\ParticleView 的实例。

普通视图文件

<div>
    当前时间为:<?php echo date('Y-m-d H:i:s'); ?>
</div>

在布局文件中,通过以下方式增加资源

<?php
/* @var $this \X\Service\XAction\Component\WebView\Html */

/* @var $link \X\Service\XAction\Component\WebView\LinkManager */
$link = $this->getLinkManager();

# 注册CSS文件
$link->addCSS('bootstrap', '/lib/bootstrap/dist/css/bootstrap.css');

/* @var $script \X\Service\XAction\Component\WebView\ScriptManager */
$script = $this->getScriptManager();
# 注册 Js 文件
$script->add('jquery', '/lib/jquery/dist/jquery.min.js');

在片段文件中,通过以下方式增加资源

<?php
/* @var $this \X\Service\XAction\Component\WebView\ParticleView */
$html = $this->getManager()->getParent();

/* @var $link \X\Service\XAction\Component\WebView\LinkManager */
$link = $html->getLinkManager();

# 注册CSS文件
$link->addCSS('bootstrap', '/lib/bootstrap/dist/css/bootstrap.css');

/* @var $script \X\Service\XAction\Component\WebView\ScriptManager */
$script = $html->getScriptManager();
# 注册 Js 文件
$script->add('jquery', '/lib/jquery/dist/jquery.min.js');

数据库服务

注册服务

配置内容

'services' => array(
    'XDatabase' => array(
        'class' => '\\X\\Service\\XDatabase\\Service',
        'enable' => true,
        'delay' => true,
        'params' => array(
            'migration_table_name' => 'xdatabase_dbmigration_histories',
            'databases' => array (
                'example' => array (
                    'dsn' => 'mysql:host=localhost;dbname=suanhetao-service-view',
                    'username' => 'username',
                    'password' => 'password',
                    'charset' => 'UTF8',
                ),
            ),
        ),
    ),
),

初始化服务

数据库服务不用初始化,当配置成功后即可直接使用。

SQL 创建器

服务允许直接以字符串的形式执行SQL语句, 例如

$sql = "DROP TABLE example";
$isSuccess = X\Service\XDatabase\Service::getService()->get()->exec($sql);

在不考虑更换数据库类型的情况下,这种方式是最好的。 但是如果后期数据库从 mysql 迁移到 sqlite 后,就会存在sql语句不兼用的问题,这个时候就推荐使用 SQLBuilder 来创建SQL语句。 虽然 SQLBuilder 不能完全解决SQL不兼容的问题,但是能够减轻 一部分的迁移工作, 尤其是字段,表名的转义等。 例如

Mysql : SELECT `from` FROM `table`

SQLITE : SELECT [from] FROM [table]

SQLBuilder 的目的不仅仅是解决上面的问题, 对于部分内置函数, SQLBuilder提供了统一 的调用方式,不用再关心切换数据库类型后的迁移方式。

使用SQLBuilder创建查询语句时,和普通的字符串方式不相同的是可变部分作为参数来实现, 例如

$dbname = 'example';
$sql = \X\Service\XDatabase\Core\SQL\Builder::build($dbname)->select()
      ->expression('name', 'nickname')
      ->expression('age', 'realage')
      ->expression('sex')
      ->from('example_students')
      ->where(array(
          'name' => 'michael',
          'sex' => 'm',
      ))
      ->offset(10)
      ->limit(10)
      ->orderBy('age', 'DESC')
      ->toString();

最终输出的SQL语句为

SELECT
  `name` AS `nickname`,
  `age` AS `realage`,
  `sex`
FROM `example_students`
WHERE
  `name`='michael'
  AND `sex` = 'm'
ORDER BY `age` DESC
OFFSET 10
LIMIT 10

SQLBuilder 在生成SQL语句时传入的数据库名 $dbname 是用来告诉builder使用哪个 数据来生成, 因为不同的数据库引擎最终生成的SQL语句是不同的,默认情况下SQLBuilder 使用当前的数据库来生成。

Active Record

AR 是将数据表中的数据映射成一个对象,对该对象修改后将会同步到数据库中。 例如

use X\Module\Example\Model\User;

# 通过主键查询
$user = User::model()->findByPrimaryKey(1);

# 通过属性查询
$user = User::model()->find(array('name'=>'michael'));

# 通过属性查询多条
$users = User::model()->findAll(array('sex'=>'m'));

# 修改
$user->name = 'new-name';
$user->save();

# 创建
$user = new User();
$user->name = 'another michael';
$user->save();

# 删除
$user = User::model()->findByPrimaryKey(1);
$user->delete();

AR类的定义

<?php
namespace X\Module\Example\Model;
use X\Service\XDatabase\Core\ActiveRecord\XActiveRecord;

/**
 * @property string $name
 * @property string sex
 */
class User extends XActiveRecord {
   /**
    * 设置数据库配置名称,表明该AR类操作时使用的数据库链接
    */
    public function getDatabaseName() {
        return 'example';
    }

   /**
    * 配置属性描述信息
    */
   protected function describe() {
       return array(
           'name' => 'VARCHAR (256)',
           'sex' => 'VARCHAR (1)',
       );
   }

   /**
    * 配置数据表名称
    */
   protected function getTableName() {
       return 'users';
   }
}

表管理

Table Manager 是进行表管理的快捷方式, 可以在不用写SQL语句的情况下对表数据或者结构进行操作。 例如

use X\Service\XDatabase\Core\Table\Manager;

$tableName = 'example_table';

# 删除表
Manager::open($tableName)->drop();

# 更新表数据
Manager::open($tableName)->truncate();

# 更新表数据
Manager::open($tableName)->update('new-value', array('sex'=>'m'), 10, 5);

# 解锁表
Manager::open($tableName)->unlock();

# 重命名数据表
Manager::open($tableName)->rename('new_table_name');

# 锁表
Manager::open($tableName)->lock('READ');

# 插入一条数据
Manager::open($tableName)->insert(array('id'=>1,'name'=>'michael'));

# 获取表信息
$tableInfo = Manager::open($tableName)->getInformation();

# 创建表
Manager::create(
    'another_table',                   # 表名
    array('id'=>'int', 'age'=>'int'),  # 表列定义
    'id'                               # 主键
);

在操作表的时候, 如果表不存在, 则只能调用 create() 方法创建表之后继续操作, 如果表存在, 则需要调用 open() 方法打开表后再操作。

邮件服务

注册服务

配置内容

'services' => array(
    'XMail'=>array (
        'enable' => true,
        'class' => 'X\\Service\\XMail\\Service',
        'delay' => true,
        'params' => array(
            # 配置邮件服务器列表
            'handlers' => array(
                'default' => array(
                    'handler' => 'smtp',
                    'host' => 'smtp.163.com',
                    'port' => '25',
                    'from' => 'user@example.com',
                    'from_name' => 'Example',
                    'auth_required' => true,
                    'username' => 'user@example.com',
                    'password' => 'password',
                ),
                'another' => array(
                    'handler' => 'smtp',
                    'host' => 'smtp.163.com',
                    'port' => '25',
                    'from' => 'another@example.com',
                    'from_name' => 'Example',
                    'auth_required' => true,
                    'username' => 'another@example.com',
                    'password' => 'password',
                ),
            ),
        ),
    ),
),
  • 邮件服务支持配置多个邮件服务器。
  • 当发送邮件时,如果没有指定服务器名称, 则会使用名称为 default 的邮件服务器。

发送邮件

配置完成后,服务不再需要初始化, 可以直接使用, 例如

use X\Service\XMail\Service;

$mailService = Service::getService();

# 创建一个 Mail 实例
$mail = $mailService->create('Demo Mail');
# 设置邮件发件人, ‘res-name’ 是收件人的名称, 可以不填。
$mail->addAddress('res@example.com', 'res-name');
# 设置邮件内容
$mail->setContent("This is a demo email");
# 发送邮件,成功将返回true.
if ( $mail->send() ) {
   echo "成功";
} else {
   echo $mail->ErrorInfo;
}


$mail = $mailService->create('Demo Mail');
# 使用名称为 another 的邮件服务器发送邮件。
$mail->setHandler('another');
$mail->addAddress('568109749@qq.com', 'GGGG');
$mail->setContent("This is a demo email");
$mail->send();

OAuth 2.0 服务

注册服务

配置内容

'services' => array(
    'OAuth2' => array(
        'class' => '\\X\\Service\\OAuth2\\Service',
        'enable' => true,
        'delay' => true,
        'params' => array(
            # 存储方式
            'storage_handler' => 'Pdo',
            # 存储配置
            'storage_params'=>array(
                'dsn'=>'mysql:dbname=demo_db_name;host=db_host',
                'username'=>'db_user',
                'password'=>'db_password'
            ),
            # 授权方式
            'grant_types' => array(
                'AuthorizationCode',
                'ClientCredentials',
                'RefreshToken',
                'UserCredentials'
            ),
            # oauth 2.0 配置
            'option' => array(
                'use_jwt_access_tokens'        => false,
                'store_encrypted_token_string' => true,
                'use_openid_connect'       => false,
                'id_lifetime'              => 3600,
                'access_lifetime'          => 3600,
                'www_realm'                => 'Service',
                'token_param_name'         => 'access_token',
                'token_bearer_header_name' => 'Bearer',
                'enforce_state'            => true,
                'require_exact_redirect_uri' => true,
                'allow_implicit'           => false,
                'allow_credentials_in_request_body' => true,
                'allow_public_clients'     => true,
                'always_issue_new_refresh_token' => false,
                'unset_refresh_token_after_use' => true,
            ),
        ),
    ),
),

初始化

  • 初始化数据库,SQL

    CREATE TABLE oauth_clients (
      client_id             VARCHAR(80)   NOT NULL,
      client_secret         VARCHAR(80),
      redirect_uri          VARCHAR(2000),
      grant_types           VARCHAR(80),
      scope                 VARCHAR(4000),
      user_id               VARCHAR(80),
      PRIMARY KEY (client_id)
    );
    
    CREATE TABLE oauth_access_tokens (
      access_token         VARCHAR(40)    NOT NULL,
      client_id            VARCHAR(80)    NOT NULL,
      user_id              VARCHAR(80),
      expires              TIMESTAMP      NOT NULL,
      scope                VARCHAR(4000),
      PRIMARY KEY (access_token)
    );
    
    CREATE TABLE oauth_authorization_codes (
      authorization_code  VARCHAR(40)     NOT NULL,
      client_id           VARCHAR(80)     NOT NULL,
      user_id             VARCHAR(80),
      redirect_uri        VARCHAR(2000),
      expires             TIMESTAMP       NOT NULL,
      scope               VARCHAR(4000),
      id_token            VARCHAR(1000),
      PRIMARY KEY (authorization_code)
    );
    
    CREATE TABLE oauth_refresh_tokens (
      refresh_token       VARCHAR(40)     NOT NULL,
      client_id           VARCHAR(80)     NOT NULL,
      user_id             VARCHAR(80),
      expires             TIMESTAMP       NOT NULL,
      scope               VARCHAR(4000),
      PRIMARY KEY (refresh_token)
    );
    
    CREATE TABLE oauth_users (
      username            VARCHAR(80),
      password            VARCHAR(80),
      first_name          VARCHAR(80),
      last_name           VARCHAR(80),
      email               VARCHAR(80),
      email_verified      BOOLEAN,
      scope               VARCHAR(4000),
      PRIMARY KEY (username)
    );
    
    CREATE TABLE oauth_scopes (
      scope               VARCHAR(80)     NOT NULL,
      is_default          BOOLEAN,
      PRIMARY KEY (scope)
    );
    
    CREATE TABLE oauth_jwt (
      client_id           VARCHAR(80)     NOT NULL,
      subject             VARCHAR(80),
      public_key          VARCHAR(2000)   NOT NULL
    );
    
  • 插入演示数据

    INSERT INTO oauth_clients
      (client_id, client_secret, redirect_uri)
    VALUES
      ("testclient", "testpass", "http://fake/");
    

获取 Access Token

在请求资源之前,需要获取一个 access token, 然后才能够调用资源接口

$service = \X\Service\OAuth2\Service::getService();
$service->generateAccessToken()->send();

假设调用该接口的url为 http://example.com/module=oauth2&action=token, 则结果将会输出

{
    "access_token":"03807cb390319329bdf6c777d4dfae9c0d3b3c35",
    "expires_in":3600,
    "token_type":"bearer",
    "scope":null
}

处理资源请求

在请求资源接口时, 需要将上一步请求获取的access token放入请求的参数里面, 并且在处理请求的时候需要判断请求是否已经有效

$service = \X\Service\OAuth2\Service::getService();
if ( !$service->verifyResourceRequest() ) {
    echo json_encode(array(
        'success' => false,
        'message' => 'authoriation required',
    ));
}

echo json_encode(array(
    'success' => true,
    'message' => 'You accessed my APIs!',
    'data'=>array('ver'=>'1.0.0')
));

假设调用该接口的URL为 http://example.com/module=api&action=version , 并且将 access_token 作为POST参数传入, 调用成功后输出

{
    "success" : true,
    "message" : "You accessed my APIs!",
    "data"    : {
        "ver" : "1.0.0"
    }
}

错误处理服务

服务配置

配置内容

'services' => arrary(
    'XError'=>array (
        'enable' => true,
        'class' => 'X\\Service\\XError\\Service',
        'delay' => false,
        'params' => array(
            'types' => E_ALL, # 错误类型
            'handlers' => array( # 错误处理器
                array(
                    'handler' => 'Email',           # 发生错误时发送邮件
                    'mail_handler' => 'default',    # 邮件处理名称
                    'subject' => 'Example Subject', # 邮件标题
                    'recipients' => array( # 收件人列表
                        'NameOfRecipient'=>'address@example.com'
                     ),
                    'template' => 'default', # 邮件模板
                    'isHtml' => false, # 是否启用HTML格式发送
                ),
                array(
                    'handler' => 'FunctionCall', # 函数调用
                    'callback' => array(ErrorHandler::class, 'handle'), # 错误回调函数
                ),
                array(
                    'handler' => 'Url', # 发生错误时请求URL
                    'url' => 'http://www.example.com?action=handleError', # URL地址
                    'parameters' => array( # 请求参数
                        'code' => '?',     # 错误代码
                        'message' => '?',  # 错误消息
                        'file' => '?',     # 错误文件
                        'line' => '?',     # 错误行号
                        'user' => 'user',  # 自定义参数
                        'password'=>'password', # 自定义参数
                    ),
                    'gotoUrl' => true, # 是否跳转到该URL
                    'method' => 'post', # 请求方法
                ),
                array(
                    'handler' => 'View', # 发生错误时渲染该视图
                    'path' => 'View/Error.php', # 视图路径
                ),
            ),
        ),
    ),
);
  • types 错误类型,用于配置该服务处理的错误类型,默认为 E_ALL, 取值参考 : http://php.net/manual/zh/errorfunc.constants.php

  • handlers 错误处理器列表, 该服务支持配置多个错误处理器, 当错误发生时, 处理器将会被按照所配置的顺序进行执行。目前支持的处理类型包括 : Email, FunctionCall, Url, View 这四种。

  • Email 邮件处理, 邮件处理需要启用 XMail 服务来进行邮件的发送。

    • mail_handlerXMail 中配置的 handler 名称。
    • template 邮件模板, 默认为 default, 如果不使用默认模板, 则配置的路径需要以项目的根目录来配置视图路径, 例如 :: View\Error.php
  • FunctionCall 回调处理, 回调处理的回调函数接受一个 $error 参数用于获取错误信息, 例如

    public static function handle( $error ) {
        var_dump($error);
    }
    
  • Url URL处理, URL处理是在错误发生后调用该URL进行处理的操作。

    • parameters 请求URL时传递的参数列表, 参数列表中对应的 codemessage , file , line, 将会被赋值为错误信息中的对应值。
    • gotoUrl 是不是要跳转到对应的url, 如果该参数为 true, 则处理器将会输出一个界面并 然后跳转到指定url, 否则将直接请求该url,不做跳转。
    • cli 模式下, 不能够使用 gotoUrl 功能。
  • View 视图处理, 当错误发生后, 将渲染该视图。

错误处理

该服务不需要从外部调用,配置完成后将会自动处理错误。

会话管理服务

注册服务

配置内容

'services' => array(
    'XSession' => array(
        'enable' => true,
        'class' => 'X\\Service\\XSession\\Service',
        'delay' => false,
        'params' => array(
            'autoStart' => true,
            'name' => 'My-Custom-Session-Name',
            'holders' => array('cookie', 'get', 'post', 'request'),
            'cookie' => array(
                'lifetime'=>3600,
                'path'=>'/',
                'domain'=>'',
                'secure'=> false,
                'httponly'=>false
            ),
            'storage' => null,
        ),
    ),
),
  • autoStart 是否自动启动,默认为 true ,自动启动时将在服务启动的时候自动启动会话, 不用手动调用 seesion_start() 或者 $service->startSession() 来启动。

  • name 会话参数名称,说明会话在初始化时从哪个参数获取会话id。如果你需要获取上传文件的进度信息, 你需要将该名称同步到php.ini或者apache或者其他web服务器的配置文件中,否则无法获取到文件上传信息(@see http://php.net/manual/en/session.upload-progress.php#119631)。

  • holders 这个指明会话ID从哪里获取去,如果没有设置将采用PHP.ini中的设置, 在配置中的变量中, 第一个找到的会话ID将会被使用。

  • cookie 配置cookie信息。

  • storage 配置会话存储方式,如果为null, 将使用默认的存储方式。 目前支持以下的存储方式 :

    • mongodb 使用 mongodb 存储, 配置如下

      # mongodb
      'storage' => array(
          'type' => 'mongodb',
          # 数据库链接
          'uri' => 'mongodb://127.0.0.1:27017',
          #'uri' => 'mongodb://username:password@host:port'
          # 数据库名称
          'database' => 'diabolo',
          # collection 名称
          'collection' => 'sessions',
          # 有效时间秒数
          'lifetime' => 3600,
      ),
      
    • radis 使用 redis 存储, 配置如下

      # redis
      'storage' => array(
          'type' => 'redis',
          # 服务器地址
          'host' => '127.0.0.1',
          # 端口号
          'port' => 6379,
          # 数据库索引号
          'database' => 1,
          # 链接密码
          'password' => 'redis-password',
          # 有效期
          'lifetime' => 3600,
          # 键名前缀
          'prefix' => 'SESSION:',
      ),
      
    • memcached 使用memcached 存储, 配置如下

      # memcached
      'storage' => array(
          'type' => 'memcached',
          # memcached地址
          'host' => '127.0.0.1',
          # 端口
          'port' => 11211,
          # 键名前缀
          'prefix' => 'SESSION:',
          # 过期时间秒数
          'lifetime' => 3600,
      ),
      
    • database 使用数据库存储, 配置如下

      'storage' => array(
          'type' => 'database',
          # 数据库链接
          'dsn' => 'mysql:host=database.host;port=3306;dbname=databasename',
          # 会话存储使用的表名
          'table' => 'sessions',
          # 数据库用户名
          'user' => 'username',
          # 数据库密码
          'password' => 'password',
          # 在写入表时,处理额外的存储字段
          'serializeHandler' => array(SessionSerializeHandler::class, 'serialize'),
          # 会话有效期的秒数
          'lifetime' => 3600
      ),
      

      数据库表结构

      CREATE TABLE `sessions` (
          `ID`  varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , -- 必须字段, 用于存储会话id
          `EXPIRED_AT`  datetime NOT NULL , -- 必须字段,用于存储会话过期时间
          `RAW`  text CHARACTER SET utf8 COLLATE utf8_general_ci NULL , -- 必须字段, 用于存储会话内容
          `USER_ID`  int(11) NULL DEFAULT NULL , -- 自定义字段, 由 serializeHandler 处理后的数据信息
          PRIMARY KEY (`ID`)
      )
      ENGINE=InnoDB
      DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
      ROW_FORMAT=DYNAMIC;
      

      serializeHandler 用于在存储会话的时候为额外的字段赋值

      class SessionSerializeHandler {
          public static function serialize( ) {
              return array(
                  'USER_ID' => 100,
              );
          }
      }
      

会话管理

  • startSession() 启动会话
  • close($save=ture) 关闭当前会话,默认保存变更的会话信息
  • destory() 销毁当前会话
  • clean($lifetime=0) 清理过期的会话数据

Flash 使用

flash 在存储方式上和普通会话变量没有区别,但是,在获取指定flash之后, 该flash将会被自动删除。

$service = Service::getService();
$service->flashAdd('demo-flash', "This is a demo flash");
$service->flashHas('demo-flash'); # true
$content = $service->flashGet('demo-flash');
echo $content; # "This is a demo flash"
$service->flashHas('demo-flash'); # false

日志服务

注册服务

配置内容

'services' => array(
    'XLog' => array(
        'enable' => true,
        'class' => XLogService::class,
        'delay' => false,
        'params' => array(
            'level' => 'trace',
            'cache' => true,
            'handler' => 'file',
            /* 其他配置 */
        ),
    ),
),
  • level 日志记录级别,当程序中记录的日志级别低于该配置项目的值时,日志内容将直接丢弃。目前支持以下几种, 以取值从低到高的顺序排列

    • trace 当设置该级别的时候, 所有级别的日志都将记录。
    • debug 调试
    • info 信息
    • warn 警告
    • error 错误
    • fatal 致命错误
  • cache 是否缓存日志,当缓存日志开启的时候,日志仅仅会在请求结束后写入存储设备, 否则将会在调用记录日志时立即写入。

  • handler 日志记录方式,目前支持以下几种 :

    • file 将日志写入文件,扩展参数如下

      'handler' => 'file', # 使用文件记录
      'path' => '/tmp/diabolo.demo.log', # 日志存储地址
      'enableDailyFile' => true, # 是否每天创建一个日志文件
      
    • database 将日志写入数据库, 扩展参数如下

      'handler' => 'database', # 使用数据库记录
      'dsn' => 'mysql:host=localhost;dbname=test', # 数据库链接
      'user' => 'user',  # 数据库用户名,如果为空则为null
      'password' => 'pass', # 数据库密码
      'table' => 'runtimelog', # 日志表名
      'attrs' => array( # 日志表列映射
          'time' => '$time',
          'type' => '$type',
          'content' => '$content',
      ),
      

      日志表列映射时,如果值以 $ 开头, 则表明使用日志环境变量, 否则将会直接赋值给该列。 目前日志环境变量支持以下取值

      $time 日志记录时间,使用"时间戳.毫秒"的格式
      $type 日志记录类型, 例如:info, error 等
      $content 日志记录内容。
      

记录日志

在服务启动后,日志记录可以直接使用 X\Service\XLog\Logger 类来进行操作。 例如

use X\Service\XLog\Logger;

Logger::debug("This is a debug message");
Logger::error("This is an error message");
Logger::fatal("This is a fatal message");
Logger::info("This is an info message");
Logger::trace("This is a trace message");
Logger::warn("This is a warning message");

路由服务

注册服务

配置内容

'XRouter' => array(
    'class' => '\\X\\Service\\XRouter\\Service',
    'enable' => true,
    'delay' => false,
    'params' => array(
        'router' => XActionRouter::class,
        'fakeExt' => 'html',
    ),
),
  • fakeExt 虚拟扩展名,用于在生成URL或者路由的时候添加或者去掉后缀。

  • router 路由处理类名称,目前支持以下几种路由 :

    • X\Service\XRouter\Router\MapUrlRouter URL映射方式, 配置如下

      'router' => X\Service\XRouter\Router\MapUrlRouter::class,
      'regexAlias' => array(
          'module' => '\w*?',
          'action'=>'\w*?',
          'id' => '[a-z0-9]{16}',
          'example' => '.*?',
      ),
      'rules' => array(
          # 请求URL => 路由后的URL
          '/{module}_{id}/{action}$' => 'module={module}&action={action}&{module}={id}',
          '/{module:$example}/' => 'module={module}&action=index',
          '/{module:@\w}/{target:$id}' => 'module={mdoule}&action=detail&{module}={id}',
          '/help'=>'index.php?module=dionysos&action=help',
      ),
      
      • regexAlias 正则表达式匹配列表, 用于在规则中使用

      • rules 匹配规则, 在规则中,使用 {} 包含起来的将会被转换为正则表达式用于匹配url, 格式为: {anytext}, {anytext:$alias}, {anytext:@regex} 这几种。

        在格式 {anytext} 中, 如果 anytextregexAlias 中的键名, 则该格式效果同 {anytext:$alias}

    • X\Service\XRouter\Router\XActionRouter XAction服务方式, 配置如下

      'router' => X\Service\XRouter\Router\XActionRouter::class,
      'fakeExt' => 'html',
      'mainModuleName' => 'mainmodule',
      'hideMainModuleName' => true,
      'defaultAction' => 'index',
      
      • mainModuleName 主模块名称
      • hideMainModuleName 是否隐藏主模块名称,例如 /mainmodule/index 将会变为 /index, 隐藏了主模块的名称。
      • defaultAction 默认的动作名称,当解析不到动作的时候将会被使用。

      XAction路由不强制要求启用XAction服务, 该路由器匹配和生成URL基于以下规则

      • 模块和动作将会组成请求的路径, 例如 : /module/action/path
      • 如果在参数列表中包含与路径相同名称的参数, 则会被链接到路径中, 例如 : index.php?module=module&action=action/path&module=001 => /module-001/action/path
      • 不在请求路径的参数将会原封不动的添加到url中, 例如 : index.php?module=main&action=search&text=123 => /main/search?text=123

生成URL

当路由器使用的是XActionRouter时,该路由支持根据原生url生成格式化后的url。 例如

# fakeExt = html
use X\Service\XRouter\Router\XActionRouter;
XActionRouter::generate("/index.php?module=food&action=user/partner/edit&food=123&partner=426&from=team")
# 输出 : food-123/user/partner-426/edit.html?from=team

XActionRouter::action('food','user/partner/edit', array('food'=>123,'partner'=>4564,'xxx'=>222))
# 输出 : food-123/user/partner-4564/edit.html?xxx=222

Indices and tables