概览

Biny是一款高性能的轻量级PHP框架

遵循 MVC 模式,用于快速开发现代 Web 应用程序

Biny代码简洁优雅,对应用层,数据层,模板渲染层的封装简单易懂,能够快速上手使用

高性能,框架响应时间在1ms以内,单机qps轻松上3000

介绍

支持跨库连表,条件复合筛选,查询PK缓存等

同步异步请求分离,类的自动化加载管理

支持Form表单验证,支持事件触发机制

支持浏览器端调试,快速定位程序问题和性能瓶颈

具有sql防注入,html自动防xss等特性

框架 Wiki:http://www.billge.cc

GitHub 地址:https://github.com/Tencent/Biny

目录结构

/app/ 总工作目录

/app/config/ 业务配置层

/app/controller/ 路由入口Action层

/app/dao/ 数据库表实例层

/app/event/ 事件触发及定义层

/app/form/ 表单定义及验证层

/app/model/ 自定义模型层

/app/service/ 业务逻辑层

/app/template/ 页面渲染层

/config/ 框架配置层

/lib/ 系统Lib层

/extends/ 自定义Lib层(替代原vendor目录,该目录下内容用户都可以根据需要自行替换删除)

/logs/ 工作日志目录

/web/ 总执行入口

/web/static/ 静态资源文件

/web/index.php 总执行文件

/shell.php shell模式执行入口

调用关系

Action为总路由入口,Action可调用私有对象Service业务层 和 DAO数据库层

Service业务层 可调用私有对象DAO数据库层

程序全局可调用lib库下系统方法,例如:Logger(调试组件)

App::$base为全局单例类,可全局调用

App::$base->request 为当前请求,可获取当前地址,客户端ip等

App::$base->session 为系统session,可直接获取和复制,设置过期时间

App::$base->memcache 为系统memcache,可直接获取和复制,设置过期时间

App::$base->redis 为系统redis,可直接获取和复制,设置过期时间

用户可以在/app/model/下自定义model数据类,通过App::$model获取,例如:

App::$model->person 为当前用户,可在/app/model/person.php中定义

简单示例

namespace app\controller;
use App;
/**
* 主页Action
* @property \app\service\projectService $projectService
* @property \app\dao\projectDAO $projectDAO
*/  
class testAction extends baseAction
{
    // init方法会在action执行前被执行
    public function init()
    {
        // 未登录时调整登录页面
        if(!App::$model->person->exist()){
            return App::$base->request->redirect('/auth/login/');
        }
    }

    //默认路由index
    public function action_index()
    {
        // 获取当前用户
        $person = App::$model->person;
        $members = App::$base->memcache->get('cache_'.$person->project_id);
        if (!$members){
            // 获取用户所在项目成员
            $project = $this->projectDAO->find(array('id'=>$person->project_id));
            $members = $this->projectService->getMembers($project['id']);
            App::$base->memcache->set('cache_'.$person->project_id, $members);
        }
        //返回 project/members.tpl.php
        return $this->display('project/members', array('members'=>$members));
    }
}

P.S: 示例中的用法会在下面具体展开介绍

环境配置

PHP版本必须在5.5以上,包含5.5

如果需要用到数据库,则需要安装并启用mysqli扩展

php.ini配置中则需要把short_open_tag打开

/config/autoload.php 为自动加载配置类,必须具有写权限

/logs/ 目录为日志记录文件夹,也必须具有写权限

本例子中主要介绍linux下nginx的配置

nginx根目录需要指向/web/目录下,示例如下

location / {
    root   /data/billge/biny/web/; // 这里为框架/web目录的绝对路径
    index  index.php index.html index.htm;
    try_files $uri $uri/ /index.php?$args;
}

Apache 配置如下:

# 设置文档根目录为框架/web目录
DocumentRoot "/data/billge/biny/web/"

<Directory "/data/billge/biny/web/">
    RewriteEngine on
    # 如果请求的是真实存在的文件或目录,直接访问
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # 如果请求的不是真实文件或目录,分发请求至 index.php
    RewriteRule . index.php

    # 以下三行apache默认会有,如无法正常使用请自行添加
    # Options +Indexes +Includes +FollowSymLinks +MultiViews
    # AllowOverride All
    # Require local

    # ...other settings...  
</Directory> 

/web/index.php是程序的主入口,其中有几个关键配置

//默认时区配置
date_default_timezone_set('Asia/Shanghai');
// 开启debug调试模式(会输出异常)
defined('SYS_DEBUG') or define('SYS_DEBUG', true);
// 开启Logger页面调试
defined('SYS_CONSOLE') or define('SYS_CONSOLE', true);
// dev pre pub 当前环境
defined('SYS_ENV') or define('SYS_ENV', 'dev');
// 系统维护中。。。
defined('isMaintenance') or define('isMaintenance', false);

其中SYS_ENV的环境值也有bool型,方便判断使用

// 在\lib\App.php 中配置
// 测试环境
defined('ENV_DEV') or define('ENV_DEV', SYS_ENV === 'dev');
// 预发布环境
defined('ENV_PRE') or define('ENV_PRE', SYS_ENV === 'pre');
// 线上正式环境
defined('ENV_PUB') or define('ENV_PUB', SYS_ENV === 'pub');

路由

基本MVC架构路由模式,第一层对应action,第二层对应method(默认index

默认路由

/app/controller目录下,文件可以放在任意子目录或孙目录中。但必须确保文件名与类名一致,且不重复

示例:/app/controller/Main/testAction.php

// http://www.billge.cc/test/
class testAction extends baseAction
{
    //默认路由index
    public function action_index()
    {
        //返回 test/test.tpl.php
        return $this->display('test/test');
    }
}

同时也能在同一文件内配置多个子路由

//子路由查找action_{$router}
// http://www.billge.cc/test/demo1
public function action_demo1()
{
    //返回 test/demo1.tpl.php
    return $this->display('test/demo1');
}

// http://www.billge.cc/test/demo2
public function action_demo2()
{
    //返回 test/demo2.tpl.php
    return $this->display('test/demo2');
}

自定义路由

除了上述默认路由方式外还可以自定义路由规则,可在/config/config.php中配置

自定义路由规则会先被执行,匹配失败后走默认规则,参数冒号后面的字符串会自动转化为正则匹配符

/config/config.php
'routeRule' => array(
    // test/(\d+).html 的路由会自动转发到testAction中的 action_view方法
    'test/<id:\d+>.html' => 'test/view',
    // 匹配的参数可在转发路由中动态使用
    'test/<method:[\w_]+>/<id:\d+>.html' => 'test/<method>',
),

/app/controller/testAction.php
// test/272.html 正则匹配的内容会传入方法
public function action_view($id)
{
    echo $id; // 272
}

// test/my_router/123.html
public function action_my_router($id)
{
    echo $id; // 123
}

异步请求

异步请求包含POST,ajax等多种请求方式,系统会自动进行异步验证(csrf)及处理

程序中响应方法和同步请求保持一致,返回$this->error()会自动和同步请求作区分,返回json数据

// http://www.billge.cc/test/demo3
public function action_demo3()
{
    $ret = array('result'=>1);
    //返回 json {"flag": true, "ret": {"result": 1}}
    return $this->correct($ret);

    //返回 json {"flag": false, "error": {"result": 1}}
    return $this->error($ret);
}

框架提供了一整套csrf验证机制,默认开启,可通过在Action中将$csrfValidate = false关闭。

// http://www.billge.cc/test/
class testAction extends baseAction
{
    //关闭csrf验证
    protected $csrfValidate = false;

    //默认路由index
    public function action_index()
    {
        //返回 test/test.tpl.php
        return $this->correct();
    }
}

当csrf验证开启时,前端ajax请求需要预先加载引用/static/js/main.js文件,ajax提交时,系统会自动加上验证字段。

POST请求同样也会触发csrf验证,需要在form中添加如下数据字段:

// 加在form中提交
<input type="text" name="_csrf" hidden value="<?=$this->getCsrfToken()?>"/>

同样也可以在js中获取(前提是引用/static/js/main.jsJS文件),加在POST参数中即可。

var _csrf = getCookie('csrf-token');

Restful

Biny也同时支持restful协议的请求,可以在Action类中将$restApi置为true,则该Action会以restful的协议来解析路由

namespace app\controller;
/**
 * restful演示
 * @property \app\dao\userDAO $userDAO
 */
class restAction extends baseAction
{
    // 该action以restful协议解析路由
    protected $restApi = true;

    // [GET] http://www.billge.cc/rest/?id=xxx
    public function GET_index($id)
    {
        $user = $this->userDAO->filter(['id'=>$id])->find();
        return $user ? $this->correct($user) : $this->error('user not found');
    }

    // [POST] http://www.billge.cc/rest/test
    public function POST_test()
    {
        $user = $this->param('user');
        $user_id = $this->userDAO->add($user);
        return $user_id ? $this->correct($user) : $this->error('data error');
    }

    // [PUT] http://www.billge.cc/rest/?id=xxx
    public function PUT_index($id)
    {
        $user = $this->param('user');
        $ret = $this->userDAO->filter(['id'=>$id])->update($user);
        return $ret ? $this->correct() : $this->error('data error');
    }

    // [PATCH] http://www.billge.cc/rest/test?id=xxx
    public function PATCH_test($id)
    {
        $sets = $this->param('sets');
        $ret = $this->userDAO->filter(['id'=>$id])->update($sets);
        return $ret ? $this->correct() : $this->error('data error');
    }

    // [DELETE] http://www.billge.cc/rest/test?id=xxx
    public function DELETE_test($id)
    {
        $ret = $this->userDAO->filter(['id'=>$id])->delete();
        return $ret ? $this->correct() : $this->error('data error');
    }
}

同样,restful协议也可以通过自定义路由的模式来配置,例如

/config/config.php
'routeRule' => array(
    // rest/(\d+) 的restful路由会自动转发到restAction中的 {method}_test方法
    'rest/<id:\d+>' => 'rest/test',
    // 匹配的参数可在转发路由中动态使用
    'v<version:\d+>/rest/<id:\d+>/<method:[\w_]+>' => 'rest/<method>',
),

/app/controller/restAction.php
// [DELETE] http://www.billge.cc/v2/rest/123/person
public function DELETE_person($version, $id)
{
    echo $version; // 2
    echo $id; // 123
}
// [PUT] http://www.billge.cc/rest/272 正则匹配的内容会传入方法
public function PUT_test($id)
{
    echo $id; // 272
}

参数传递

方法可以直接接收 GET 参数,并可以赋默认值,空则返回null

// http://www.billge.cc/test/demo4/?id=33
public function action_demo4($id=10, $type, $name='biny')
{
    // 33
    echo($id);
    // NULL
    echo($type);
    // 'biny'
    echo($name);
}

同时也可以调用paramgetpost 方法获取参数。

param($key, $default) 获取GET/POST/JSON参数{$key}, 默认值为{$default}

get($key, $default) 获取GET参数{$key}, 默认值为{$default}

post($key, $default) 获取POST参数{$key}, 默认值为{$default}

getJson($key, $default) 如果传递过来的参数为完整json流可使用该方法获取

// http://www.billge.cc/test/demo5/?id=33
public function action_demo5()
{
    // NULL
    echo($this->param('name'));
    // 'install'
    echo($this->post('type', 'install'));
    // 33
    echo($this->get('id', 1));
}

注意:旧版本的getParam/getPost/getGet效果与上面的一致,但已不建议使用

权限验证

框架中提供了一套完整的权限验证逻辑,可对路由下所有method进行权限验证

用户需要在action中添加privilege方法,具体返回字段如下

class testAction extends baseAction
{
    private $key = 'test';

    protected function privilege()
    {
        return array(
            // 登录验证(在privilegeService中定义)
            'login_required' => array(
                'actions' => '*', // 绑定action,*为所有method
                'params' => [],   // 传参(能获取到$this,不用另外传)可不传
                'callBack' => [], // 验证失败回调函数, 可不传
            ),
            'my_required' => array(
                'actions' => ['index'], // 对action_index进行验证
                'params' => [$this->key],   // 传参
                'callBack' => [$this, 'test'], // 验证失败后调用$this->test()
            ),
        );
    }
    // 根据逻辑被调用前会分别进行login_required和my_required验证,都成功后进入该方法
    public function action_index()
    {
        // do something
    }
    // my_required验证失败后调用, $action为验证失败的action(这里是$this)
    public function test($action, $error)
    {
        // do something
    }
}

然后在privilegeService中定义验证方法

第一个参数$action为testAction,$key为params传入参数
public function my_required($action, $key=NULL)
{
    if($key){
        // 通过校验
        return $this->correct(); // 等同于 return true;
    } else {
        // 校验失败,错误信息可通过$this->privilegeService->getError()获取
        return $this->error('key not exist');
    }
}

callBack参数为校验失败时调用的方法,默认不填会抛出错误异常,程序不会再继续执行。

如果需要不同路由都使用同一个验证方法,而分别传入不同参数验证,可以使用requires参数,用法参考下例:

class testAction extends baseAction
{
    protected function privilege()
    {
        return array(
            'privilege_required' => array(
                // 根据不同路由传入相应操作权限
                'requires' => [
                    ['actions'=>['index', 'view'], 'params'=>[Privilege::user]],
                    ['actions'=>['edit', 'delete'], 'params'=>[Privilege::admin]],
                ],
                'callBack' => [$this, 'test'], // 验证失败后调用$this->test()
            ),
        );
    }

// privilegeService
public function privilege_required($action, $privilege)
{
    if(App::$model->person->hasPrivilege($privilege)){
        // 该用户有相应权限
        return $this->correct(); // 等同于 return true;
    } else {
        // 校验失败,错误信息可通过$this->privilegeService->getError()获取
        return $this->error('forbidden');
    }
}

注意:使用requires参数时,actionsparams参数将被覆盖

配置

程序配置分两块,一块是系统配置,一块是程序配置

/config/ 系统配置路径

/app/config/ 程序逻辑配置路径

系统配置

/config/config.php 系统基本配置(包括默认路由,自定义路由配置等)

return array(
    //路由配置
    'router' => array(
        'base_action' => 'demo', //默认路由入口
        'base_shell' => 'index', //默认shell入口

        //静态化配置
        'routeRule' => array(
            // test/123 => test/view
            'test/<id:[\w_]+>' => 'test/view',
            // abc/test/123 => test/abc
            '<method:\w+>/test/<id:\d+>.html' => 'test/<method>',
        ),
    ),

    //自动加载配置
    'autoload' => array(
        'autoPath' => 'config/autoload.php',
        //重新构建间隔时间s
        'autoSkipLoad' => 5,
        'autoThrow' => true, //使用外部autoload机制(如composer) 需设置为false
    ),

    //请求配置
    'request' => array(
        'trueToken' => 'biny-csrf',
        'csrfToken' => 'csrf-token',
        'csrfPost' => '_csrf',
        'csrfHeader' => 'X-CSRF-TOKEN',

        // 约定userIP字段 X_REAL_IP
        'userIP' => '',
        // 强制返回页面协议
        'showTpl' => 'X_SHOW_TEMPLATE',
        //csrf白名单
        'csrfWhiteIps' => array(
            '127.0.0.1/24'
        ),
        //多语言cookie字段
        'languageCookie' => 'biny_language'
    ),

    //响应配置
    'response' => array(
        'jsonContentType' => 'application/json',
        //兼容老版本 新版本都用one就可以了
        'paramsType' => 'one',  // one or keys
        // 以下配置在paramsType == one 时有效
        'paramsKey' => 'PRM',
        'objectEncode' => true, //object对象是否转义
    ),

    //日志相关配置
    'logger' => array(
        // 是否记录日志文件
        'files' => true,
        // 自定义日志记录方法
//        'sendLog' => array('Common', 'sendLog'),
        // 自定义日志错误方法
//        'sendError' => array('Common', 'sendError'),
        // 错误级别 NOTICE以上都会记录
        'errorLevel' => NOTICE,
        // 慢查询阀值(ms)
        'slowQuery' => 1000,
    ),

    // 数据库相关配置
    'database' => array(
        'returnIntOrFloat' => true, // 是否返回int或者float类型
        'returnAffectedRows' => false, // 是否返回受影响行数,false下返回成功true/失败false, true情况下-1为失败
    ),

    //缓存相关配置
    'cache' => array(
        'pkCache' => 'tb:%s',
        'session' => array(
            'save_handler'=>'files',  //redis memcache
            'maxlifetime' => 86400    //过期时间s
        ),
        // 开启redis自动序列化存储
        'serialize' => true,
    ),

    //异常配置
    'exception' => array(
        //返回页面
        'exceptionTpl' => 'error/exception',
        'errorTpl' => 'error/msg',

        'messages' => array(
            500 => '网站有一个异常,请稍候再试',
            404 => '您访问的页面不存在',
            403 => '权限不足,无法访问'
        )
    ),



)

/config/autoload.php 系统自动加载类的配置,会根据用户代码自动生成,无需配置,但必须具有写权限

/config/exception.php 系统异常配置类

/config/http.php HTTP请求基本错误码

/config/database.php DAO映射配置

用户可通过App::$base->config->get方法获取

简单例子:

/config/config.php
return array(
    'session_name' => 'biny_sessionid'
}

// 程序中获取方式 第二个参数为文件名(默认为config可不传)第三个参数为是否使用别名(默认为true)
App::$base->config->get('session_name', 'config', true);

程序配置

程序配置目录在/app/config/

默认有dns.php(连接配置) 和 config.php(默认配置路径)

使用方式也与系统配置基本一致

/app/config/dns.php
return array(
    'memcache' => array(
        'host' => '10.1.163.35',
        'port' => 12121
    )
}

// 程序中获取方式 第二个参数为文件名(默认为config可不传)第三个参数为是否使用别名(默认为true)
App::$base->app_config->get('memcache', 'dns');

环境配置

系统对不同环境的配置是可以做区分的

系统配置在/web/index.php

// dev pre pub 当前环境
defined('SYS_ENV') or define('SYS_ENV', 'dev');

当程序调用App::$base->config->get时,系统会自动查找对应的配置文件

// 当前环境dev 会自动查找 /config/config_dev.php文件
App::$base->config->get('test', 'config');

// 当前环境pub 会自动查找 /config/dns_pub.php文件
App::$base->config->get('test2', 'dns');

公用配置文件可以放在不添加环境名的文件中,如/config/config.php

在系统中同时存在config.phpconfig_dev.php时,带有环境配置的文件内容会覆盖通用配置

/app/config/dns.php
return array(
    'test' => 'dns',
    'demo' => 'dns',
}

/app/config/dns_dev.php
return array(
    'test' => 'dns_dev
}

// 返回 'dns_dev' 
App::$base->app_config->get('test', 'dns');

// 返回 'dns' 
App::$base->app_config->get('demo', 'dns');

系统配置和程序配置中的使用方法相同

别名使用

配置中是支持别名的使用的,在别名两边加上@即可

系统默认有个别名 web会替换当前路径

/config/config.php
return array(
    'path' => '@web@/my-path/'
}

// 返回 '/biny/my-path/' 
App::$base->config->get('path');

用户也可以自定义别名,例如

// config->get 之前执行
App::$base->config->setAlias('time', time());

// config.php
return array(
    'path' => '@web@/my-path/?time=@time@'
}

// 返回 '/biny/my-path/?time=1461141347'
App::$base->config->get('path');

// 返回 '@web@/my-path/?time=@time@'
App::$base->config->get('path', 'config', false);

当然如果需要避免别名转义,也可以在App::$base->config->get第三个参数传false,就不会执行别名转义了。

数据库使用

框架要求每个数据库表都需要建一个单独的类,放在/dao目录下。跟其他目录一样,支持多层文件结构,写在子目录或孙目录中,但类名必须唯一

所有传入DAO 方法的参数都会自动进行转义,可以完全避免SQL注入的风险

例如:

// testDAO.php 与类名保持一致
class testDAO extends baseDAO
{
    // 链接库 数组表示主库从库分离:['database', 'slaveDb'] 对应dns里配置 默认为'database'
    protected $dbConfig = 'database';
    // 表名
    protected $table = 'Biny_Test';
    // 键值 多键值用数组表示:['id', 'type']
    protected $_pk = 'id';
    // 是否使用数据库键值缓存,默认false
    protected $_pkCache = true;

    // 分表逻辑,默认为表名直接加上分表id
    public function choose($id)
    {
        $sub = $id % 100;
        $this->setDbTable(sprintf('%s_%02d', $this->table, $sub));
        return $this;
    }
}

连接配置

数据库库信息都配置在/app/config/dns.php中,也可根据环境配置在dns_dev.php/dns_pre.php/dns_pub.php里面

基本参数如下:

/app/config/dns_dev.php
return array(
    'database' => array(
        // 库ip
        'host' => '127.0.0.1',
        // 库名
        'database' => 'Biny',
        // 用户名
        'user' => 'root',
        // 密码
        'password' => 'pwd',
        // 编码格式
        'encode' => 'utf8',
        // 端口号
        'port' => 3306,
        // 是否长链接(默认关闭)
        'keep-alive' => true,
    )
)

这里同时也可以配置多个,只需要在DAO类中指定该表所选的库即可(默认为'database'

DAO映射

上诉DAO都需要写PHP文件,框架这边也提供了一个简易版的映射方式

用户可在/config/database.php中配置,示例如下

// database.php
return array(
    'dbConfig' => array(
        // 相当于创建了一个testDAO.php
        'test' => 'Biny_Test'
    )
);

然后就可以在Action、Service、Model各层中使用testDAO

// testAction.php
namespace app\controller;
/**
* DAO 或者 Service 会自动映射 生成对应类的单例
* @property \biny\lib\SingleDAO $testDAO
*/
class testAction extends baseAction
{
    public function action_index()
    {
        // 此处的testDAO为映射生成的,没有baseDAO中对于缓存的操作
            [['id'=>1, 'name'=>'xx', 'type'=>2], ['id'=>2, 'name'=>'yy', 'type'=>3]]
        $data = $this->testDAO->query();
    }
}

需要注意的是,映射的DAO不具备设置数据库功能(主从库都是默认的database配置)

也不具备缓存操作(getByPK、updateByPK、deleteByPK等)的功能

如果需要使用上述功能,还是需要在dao目录下创建php文件自定义相关参数

基础查询

DAO提供了queryfind等基本查询方式,使用也相当简单

// testAction.php
namespace app\controller;
/**
 * DAO 或者 Service 会自动映射 生成对应类的单例
 * @property \app\dao\testDAO $testDAO
 */
class testAction extends baseAction
{
    public function action_index()
    {
        // 返回 testDAO所对应表的全部内容 格式为二维数组
            [['id'=>1, 'name'=>'xx', 'type'=>2], ['id'=>2, 'name'=>'yy', 'type'=>3]]
        $data = $this->testDAO->query();
        // 第一个参数为返回的字段 [['id'=>1, 'name'=>'xx'], ['id'=>2, 'name'=>'yy']]
        $data = $this->testDAO->query(array('id', 'name'));
        // 第二个参数返回键值,会自动去重 [1 => ['id'=>1, 'name'=>'xx'], 2 => ['id'=>2, 'name'=>'yy']]
        $data = $this->testDAO->query(array('id', 'name'), 'id');

        // 返回 表第一条数据 格式为一维 ['id'=>1, 'name'=>'xx', 'type'=>2]
        $data = $this->testDAO->find();
        // 参数为返回的字段名 可以为字符串或者数组 ['name'=>'xx']
        $data = $this->testDAO->find('name');
    }
}

同时还支持countmaxsumminavg等基本运算,count带参数即为参数去重后数量

// count(*) 返回数量
$count = $this->testDAO->count();
// count(distinct `name`) 返回去重后数量
$count = $this->testDAO->count('name');
// max(`id`)
$max = $this->testDAO->max('id');
// min(`id`)
$min = $this->testDAO->min('id');
// avg(`id`)
$avg = $this->testDAO->avg('id');
// sum(`id`)
$sum = $this->testDAO->sum('id');

这里运算都为简单运算,需要用到复合运算或者多表运算时,建议使用addition方法

==============v2.8更新分割线=============

Biny2.8.1之后添加了pluck(快速拉取列表)具体用法如下:

// ['test1', 'test2', 'test3']
$list = $this->testDAO->filter(array('type'=>5))->pluck('name');
// 同样也可以运用到多联表中,
$filter = $this->testDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(
        array('type'=>5),
    ));
// 如果所使用字段在多表中重复会报错
$list = $filter->pluck('name');
// 如果所使用字段在多表中重复出现需要指明所属的表
$list = $filter->pluck(array('project'=>'name'));

Biny2.8.1之后还添加了paginate(自动分页)方法,具体用法如下:

// 返回一个以10条数据为一组的二维数组
$results = $this->testDAO->filter(array('type'=>5))->paginate(10);
// 同样也可以运用到多联表中,
$filter = $this->testDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(
        array('type'=>5),
    ));
// 第二个参数默认为null,非null返回第n+1页(计数从0开始)的内容
// 第三个参数等同于fields的用法,为筛选的字段集合
$results = $filter->paginate(10, 3, array(array('project'=>'id', 'name'));

Biny2.9.0之后还添加了tables检索所有表方法 和 columns获取所有列信息方法:

// 获取DB实例中所有表名,参数true则返回表详细数据
$tables = $this->userDAO->tables();
$tableDetail = $this->userDAO->tables(true);
// 获取user表列名 / 列详情
$columns = $this->userDAO->columns();
$columnDetail = $this->userDAO->columns(true);

删改数据

在单表操作中可以用到删改数据方法,包括update(多联表也可),deleteadd

update方法为更新数据,返回成功(true)或者失败(false),条件内容参考后面选择器的使用

// update `DATABASE`.`TABLE` set `name`='xxx', `type`=5
$result = $this->testDAO->update(array('name'=>'xxx', 'type'=>5));

delete方法返回成功(true)或者失败(false),条件内容参考后面选择器的使用

// delete from `DATABASE`.`TABLE`
$result = $this->testDAO->delete();

add方法 insert成功时默认返回数据库新插入自增ID,第二个参数为false时 返回成功(true)或者失败(false

// insert into `DATABASE`.`TABLE` (`name`,`type`) values('test', 1)
$sets = array('name'=>'test', 'type'=>1);
// false 时返回true/false
$id = $this->testDAO->add($sets, false);

框架同时也提供了受影响行数的返回,可以在/config/config.php中,将字段returnAffectedRows置为true即可

addCount方法返回成功(true)或者失败(false),相当于update set count = count+n

// update `DATABASE`.`TABLE` set `type`=`type`+5
$result = $this->testDAO->addCount(array('type'=>5);

注意:新版本addCount方法可以被update方法替代,目前暂时还保留,但已不建议使用。使用方法如下:

// update `DATABASE`.`TABLE` set `type`=`type`+5
$result = $this->testDAO->update(['type'=>['+'=>5]]);
// update `DATABASE`.`TABLE` set `type`=`count`-`num`-4
$result = $this->testDAO->update(['type'=>['-'=>['count', 'num', 4]]]);
        

createOrUpdate方法 为添加数据,但当有重复键值时会自动update数据

// 第一个参数为insert数组,第二个参数为失败时update参数,不传即为第一个参数
$sets = array('name'=>'test', 'type'=>1);
$result = $this->testDAO->createOrUpdate($sets);

addList(或insertList)方法为批量添加数据,第二个参数为批量执行的个数,默认一次执行100行
返回成功(true)或者失败(false

// 参数为批量数据值(二维数组),键值必须统一
$sets = array(
    array('name'=>'test1', 'type'=>1),
    array('name'=>'test2', 'type'=>2),
);
$result = $this->testDAO->addList($sets);

Biny 2.9.0之后,支持insert方法,作用等同于add

addList/insertList支持replace into / insert ignore逻辑, 默认null

第三个参数为true时,会以replace into 逻辑执行,false时,会以insert ignore into 逻辑执行

// REPLACE INTO TABLE ...
$sets = array(
    array('name'=>'test1', 'type'=>1),
    array('name'=>'test2', 'type'=>2),
);
$result = $this->testDAO->addList($sets, 100, true);

多联表

框架支持多连表模型,DAO类都有join(全联接),leftJoin(左联接),rightJoin(右联接)方法

参数为联接关系

// on `user`.`projectId` = `project`.`id` and `user`.`type` = `project`.`type`
$DAO = $this->userDAO->join($this->projectDAO, array('projectId'=>'id', 'type'=>'type'));

$DAO可以继续联接,联接第三个表时,联接关系为二维数组,第一个数组对应第一张表与新表关系,第二个数组对应第二张表与新表关系

// on `user`.`testId` = `test`.`id` and `project`.`type` = `test`.`status`
$DAO = $DAO->leftJoin($this->testDAO, array(
    array('testId'=>'id'),
    array('type'=>'status')
));

可以继续联接,联接关系同样为二维数组,三个对象分别对应原表与新表关系,无关联则为空,最后的空数组可以省略

// on `project`.`message` = `message`.`name`
$DAO = $DAO->rightJoin($this->messageDAO, array(
    array(),
    array('message'=>'name'),
//  array()
));

以此类推,理论上可以建立任意数量的关联表

参数有两种写法,上面那种是位置对应表,另外可以根据别名做对应,别名即DAO之前的字符串

// on `project`.`message` = `message`.`name` and `user`.`mId` = `message`.`id`
$DAO = $DAO->rightJoin($this->messageDAO, array(
    'project' => array('message'=>'name'),
    'user' => array('mId'=>'id'),
));

多联表同样可以使用queryfindcount等查询语句。参数则改为二维数组

和联表参数一样,参数有两种写法,一种是位置对应表,另一种即别名对应表,同样也可以混合使用。

// SELECT `user`.`id` AS 'uId', `user`.`cash`, `project`.`createTime` FROM ...
$this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->query(array(
      array('id'=>'uId', 'cash'),
      'project' => array('createTime'),
    ));

联表条件中有时需要用到等于固定值的情况,可以通过on方法添加

// ... on `user`.`projectId` = `project`.`id` and `user`.`type` = 10 and `project`.`cash` > 100
$this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->on(array(
        array('type'=>10),
        array('cash'=>array('>', 100)),
    ))->query();

多联表的查询和修改(update),和单表操作基本一致,需要注意的是单表参数为一维数组,多表则为二维数组,写错会导致执行失败。

注意:多联表中的选择器应该使用二维数组,例如:

// ... where `user`.`type` = 10 and `project`.`cash` = 100
$this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(
        array('type'=>10),
        array('cash'=>100),
    ))->query();

具体选择器使用请参考选择器文档内容。

Biny 2.8.6之后join/leftJoin/rightJoin可以在第一张表添加选择器后再使用,使用方法如下:

// ... where `user`.`type` = 10
$this->userDAO->filter(array('type'=>10))
    ->join($this->projectDAO, array('projectId'=>'id'))
    ->query();
// 等同于下方原来的写法,这样在第一张表中参数会自动带入到联表参数中
$this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(
        array('type'=>10),
    ))->query();

选择器

DAO类都可以调用filter(与选择器),merge(或选择器),效果相当于筛选表内数据

同样选择器支持单表和多表操作,参数中单表为一维数组,多表则为二维数组

// ... WHERE `user`.`id` = 1 AND `user`.`type` = 'admin'
$filter = $this->userDAO->filter(array('id'=>1, 'type'=>'admin'));

而用merge或选择器筛选,条件则用or相连接

// ... WHERE `user`.`id` = 1 OR `user`.`type` = 'admin'
$merge = $this->userDAO->merge(array('id'=>1, 'type'=>'admin'));

同样多表参数也可用别名对应表,用法跟上面一致,这里就不展开了

// ... WHERE `user`.`id` = 1 AND `project`.`type` = 'outer'
$filter = $this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(
        array('id'=>1),
        array('type'=>'outer'),
    ));

$filter条件可以继续调用filter/merge方法,条件会在原来的基础上继续筛选

// ... WHERE (...) OR (`user`.`name` = 'test')
$filter = $filter->merge(array('name'=>'test');

$filter条件也可以作为参数传入filter/merge方法。效果为条件的叠加。

// ... WHERE (`user`.`id` = 1 AND `user`.`type` = 'admin') OR (`user`.`id` = 2 AND `user`.`type` = 'user')
$filter1 = $this->userDAO->filter(array('id'=>1, 'type'=>'admin');
$filter2 = $this->userDAO->filter(array('id'=>2, 'type'=>'user'));
$merge = $filter1->merge($filter2);

无论是与选择器还是或选择器,条件本身作为参数时,条件自身的DAO必须和被选择对象的DAO保持一致,否者会抛出异常

值得注意的是filtermerge的先后顺序对条件筛选是有影响的 (Biny v2.9.5之后支持自定义连接符)

可以参考下面这个例子

// WHERE (`user`.`id`=1 AND `user`.`type`='admin') OR `user`.`id`=2
$this->userDAO->filter(array('id'=>1, 'type'=>'admin'))->merge(array('id'=>2));

// WHERE `user`.`id`=2 AND (`user`.`id`=1 AND `user`.`type`='admin')
$this->userDAO->merge(array('id'=>2))->filter(array('id'=>1, 'type'=>'admin'));

// Biny v2.9.5之后支持自定义连接符
// WHERE `user`.`id`=2 AND (`user`.`id`=1 OR `user`.`type`='admin')
$this->userDAO->filter(array('id'=>2))->filter(array('id'=>1, 'type'=>'admin'), 'or');

// WHERE `user`.`id`=2 OR (`user`.`id`=1 AND `user`.`type`='admin')
$this->userDAO->filter(array('id'=>2))->merge(array('id'=>1, 'type'=>'admin'), 'and');

由上述例子可知,添加之间关联符是跟后面的选择器表达式保持一致

选择器获取数据跟DAO方法一致,单表的选择器具有单表的所有查询,删改方法,而多表的选择器具有多表的所有查询,修改方法

// UPDATE `DATABASE`.`TABLE` AS `user` SET `user`.`name` = 'test' WHERE `user`.`id` = 1
$result = $this->userDAO->filter(array('id'=>1)->update(array('name'=>'test'));

// SELECT * FROM ... WHERE `project`.`type` = 'admin'
$result = $this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(array(),array('type'=>'admin')))
    ->query();

另外,如果想实现where start=end或者where start=end+86400这类的条件也是支持的,方法如下:

// ... WHERE `user`.`lastLoginTime` = `user`.`registerTime` and `user`.`lastLoginTime` <= refreshTime+86400
$filter = $this->userDAO->filter(array(
    'lastLoginTime'=>Database::field('`user`.`registerTime`'),
    '<='=>array('lastLoginTime'=>Database::field('refreshTime+86400')),
));

无论是filter还是merge,在执行SQL语句前都不会被执行,不会增加sql负担,可以放心使用。

复杂选择

除了正常的匹配选择以外,filtermerge里还提供了其他复杂选择器。

如果数组中值为数组的话,会自动变为in条件语句

// WHERE `user`.`type` IN (1,2,3,'test')
$this->userDAO->filter(array('id'=>array(1,2,3,'test')));

其他还包括 ><>=<=!=<>isis not ,同样,多表的情况下需要用二维数组去封装

// WHERE `user`.`id` >= 10 AND `user`.`time` >= 1461584562 AND `user`.`type` is not null
$filter = $this->userDAO->filter(array(
    '>='=>array('id'=>10, 'time'=>1461584562),
    'is not'=>array('type'=>NULL),
));

// WHERE `user`.`id` != 3 AND `user`.`id` != 4 AND `user`.`id` != 5
$filter = $this->userDAO->filter(array(
    '!='=>array('id'=>array(3, 4, 5))
));

另外,like语句也是支持的,可匹配正则符的开始结尾符,具体写法如下:

// WHERE `user`.`name` LIKE '%test%' OR `user`.`type` LIKE 'admin%' OR `user`.`type` LIKE '%admin'
$filter = $this->userDAO->merge(array(
    '__like__'=>array('name'=>'test', 'type'=>'^admin', 'type'=>'admin$'),
));

// WHERE `user`.`name` LIKE '%test%' OR `user`.`name` LIKE 'admin%' OR `user`.`name` LIKE '%demo'
$filter = $this->userDAO->merge(array(
    '__like__'=>array(
        'name'=>array('test', '^admin', 'demo$'),
    )
));

not in语法暂时并未支持,可以暂时使用多个!=或者<>替代

同时filter/merge也可以被迭代调用,以应对不确定筛选条件的复杂查询

// 某一个返回筛选数据的Action
$DAO = $this->userDAO;
if ($status=$this->param('status')){
    $DAO = $DAO->filter(array('status'=>$status));
}
if ($startTime=$this->param('start', 0)){
    $DAO = $DAO->filter(array('>='=>array('start'=>$startTime)));
}
if ($endTime=$this->param('end', time())){
    $DAO = $DAO->filter(array('<'=>array('end'=>$endTime)));
}
// 获取复合条件数量
$count = $DAO->count();
// 获取复合条件前10条数据
$data = $DAO->limit(10)->query();

其他条件

DAO或者选择器里都可以调用条件方法,方法可传递式调用,相同方法内的条件会自动合并

其中包括groupadditionorderlimithaving

// SELECT `user`.`id`, avg(`user`.`cash`) AS 'a_c' FROM `TABLE` `user` WHERE ...
        GROUP BY `user`.`id`,`user`.`type` HAVING `a_c` >= 1000 ORDER BY `a_c` DESC, `id` ASC LIMIT 20,10;
$this->userDAO //->filter(...)
    ->addition(array('avg'=>array('cash'=>'a_c'))
    ->group(array('id', 'type'))
    ->having(array('>='=>array('a_c'=> 1000)))
    ->order(array('a_c'=>'DESC', 'id'=>'ASC'))
    // limit 第一个参数为取的条数,第二个参数为起始位置(默认为0)
    ->limit(10, 20)
    ->query(array('id'));

Biny v2.9.2之后支持自定义排序(order by field),随机排序(order by rand)逻辑,同时也支持了order方法多次调用

// ... ORDER BY `type` asc,FIELD(`status`,'1','9','3','6'),RAND()
$this->userDAO->order(['type'=>'asc', 'status'=>[1,9,3,6]])
    ->order('rand')->query();

addition是对数据做计算处理的方法,提供了maxcountsumminavg等计算方法

多联表时同样需要用到二维数组

// SELECT avg(`user`.`cash`) AS 'a_c', avg(`user`.`time`) AS 'time',
        sum(`user`.`cash`) AS 'total', min(`test`.`testid`) AS 'testid'
        FROM `TABLE1` `user` join `TABLE2` `test` ON `user`.`id` = `test`.`user_id` WHERE ...
        GROUP BY `user`.`id`,`user`.`type` HAVING `a_c` >= 1000 ORDER BY `a_c` DESC, `id` ASC LIMIT 0,10;
$DAO = $this->userDAO->join($this->testDAO, array('id'=>'user_id'))
$DAO //->filter(...)
    ->addition(array(
        array(
            'avg'=>array('cash'=>'a_c', 'time'),
            'sum'=>array('cash'=>'total'),
        ),
        array(
            'min'=>array('testid'),
        ),
    )->query();

每次添加条件后都是独立的,不会影响原DAO 或者 选择器,可以放心的使用

// 这个对象不会因添加条件而变化
$filter = $this->userDAO->filter(array('id'=>array(1,2,3,'test')));
// 2
$count = $filter->limit(2)->count()
// 4
$count = $filter->count()
// 100 (user表总行数)
$count = $this->userDAO->count()

Biny同时也可以使用Database::field()来支持复杂的Group By语句,例如:

// SELECT FROM_UNIXTIME(time,'%Y-%m-%d') AS time, count(*) AS 'count'
                FROM `user` Group By FROM_UNIXTIME(time,'%Y-%m-%d')
$result = $this->userDAO->group(Database::field("FROM_UNIXTIME(time,'%Y-%m-%d')"))
    ->addition(array('count'=>'*'))
    ->query("FROM_UNIXTIME(time,'%Y-%m-%d') AS time");

SQL模版

框架中提供了上述选择器条件语句联表等,基本覆盖了所有sql语法,但可能还有部分生僻的用法无法被实现, 于是这里提供了一种SQL模版的使用方式,支持用户自定义SQL语句,但并不推荐用户使用,如果一定要使用的话,请务必自己做好防SQL注入

这里提供了两种方式,select(查询,返回数据),以及command(执行,返回bool)

方法会自动替换:where,:table,:order,:group,:addition字段

// select * from `DATABASE`.`TABLE` WHERE ...
$result = $this->userDAO->select('select * from :table WHERE ...;');

// update `DATABASE`.`TABLE` `user` set name = 'test' WHERE `user`.`id` = 10 AND type = 2
$result = $this->userDAO->filter(array('id'=>10))
    ->command("update :table set name = 'test' WHERE :where AND type = 2;");

// select id,sum(`cash`) as 'cash' from `DATABASE`.`TABLE` WHERE `id`>10
    GROUP BY `type` HAVING `cash`>=100 ORDER BY `id` desc;
$result = $this->userDAO->filter(array('>'=>array('id'=>10)))
    ->group(array('type'))->having(array('>='=>array('cash'=>100)))->order(array('id'=>'desc'))
    ->addition(array('sum'=>array('cash'=>'cash')))
    ->select('select id,:addition from :table WHERE :where :group :order;');

另外还可以添加一些自定义变量,这些变量会自动进行sql转义,防止sql注入

其中键值的替换符为;,例如;key,值的替换符为:,例如:value

// select `name` from `DATABASE`.`TABLE` WHERE `name`=2
$result = $this->userDAO->select('select ;key from :table WHERE ;key=:value;', array('key'=>'name', 'value'=>2));

同时替换内容也可以是数组,系统会自动替换为以,连接的字符串

// select `id`,`name` from `DATABASE`.`TABLE` WHERE `name` in (1,2,3,'test')
$result = $this->userDAO->select('select ;fields from :table WHERE ;key in (:value);',
    array('key'=>'name', 'value'=>array(1,2,3,'test'), 'fields'=>array('id', 'name')));

以上替换方式都会进行SQL转义,建议用户使用模版替换,而不要自己将变量放入SQL语句中,防止SQL注入

游标数据

如果DB中取出的数据非常大,而PHP中却无法承受这么大量的内存可以用来处理,这时候就需要用到cursor游标了

游标可以将复合条件的数据逐一取出,在程序中进行分批处理,从而降低大数据所带来的内存瓶颈

// 选择器,条件类模式完全一样,在获取数据时使用cursor方法
$rs = $this->testDAO->filter(array('type'=>1))->cursor(array('id', 'name'));
// 通过 Database::step 逐个取出data数据,e.g: ['id'=>2, 'name'=>'test']
while ($data=Database::step($rs)){
    do something...
}

如果在游标数据中需要再使用其他sql语句,则需要在cursor方法中传第二个参数false,否则在cursor未执行完之前其他语句无法执行

// 选择器,条件类模式完全一样,在获取数据时使用cursor方法
$rs = $this->testDAO->filter(array('type'=>1))->cursor(array('id', 'name'), false);
// 通过 Database::step 逐个取出data数据,e.g: ['id'=>2, 'name'=>'test']
while ($data=Database::step($rs)){
    // other sql...
    $count = $this->testDAO->count();
}

如果使用SQL模版的话,也可以通过传递第三个参数Database::FETCH_TYPE_CURSOR来实现游标的使用

// 使用方法跟上诉方式一样
$rs = $this->testDAO->filter(array('type'=>1))
  ->select('SELECT * FROM :table WHERE :where AND status=:status', array('status'=>2), Database::FETCH_TYPE_CURSOR);
// 通过 Database::step 逐个取出data数据,e.g: ['id'=>2, 'name'=>'test', 'type'=>1, 'status'=>2]
while ($data=Database::step($rs)){
    do something...
}

Biny 2.8.2之后cursor第二个参数可传匿名函数function作为数据回调使用,使用方法如下:

$result = array();
// $data为迭代的数据,$index为索引
$this->testDAO->filter(array('type'=>1))
  ->cursor('*', function($data, $index) use(&$result){
    do something...
});

事务处理

框架为DAO提供了一套简单的事务处理机制,默认是关闭的,可以通过Datebase::start()方法开启

注意:请确保连接的数据表是innodb的存储引擎,否者事务并不会生效。

Datebase::start()之后可以通过Datebase::commit()来进行完整事务的提交保存,但并不会影响start之前的操作

同理,可以通过Datebase::rollback()进行整个事务的回滚,回滚所有当前未提交的事务

当程序调用Datebase::end()方法后事务会全部终止,未提交的事务也会自动回滚,另外,程序析构时,也会自动回滚未提交的事务

// 在事务开始前的操作都会默认提交,num:0
$this->testDAO->filter(['id'=>1])->update(['num'=>0]);
// 开始事务
Database::start();
// set num = num+2
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
// 回滚事务
Database::rollback();
// 当前num还是0
$num = $this->testDAO->filter(['id'=>1])->find()['num'];
// set num = num+2
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
// 提交事务
Database::commit();
// num = 2
$num = $this->testDAO->filter(['id'=>1])->find()['num'];
// 关闭事务
Database::end();

另外,事务的开启并不会影响select操作,只对增加,删除,修改操作有影响

数据缓存

框架这边针对pk键值索引数据可以通过继承baseDAO进行缓存操作,默认为关闭,可在DAO中定义$_pkCache = true来开启

然后需要在DAO中制定表键值,复合索引需要传数组,例如:['id', 'type']

因为系统缓存默认走redis,所以开启缓存的话,需要在/app/config/dns_xxx.php中配置环境相应的redis配置

// testDAO
namespace app\dao;
class testDAO extends baseDAO
{
    protected $dbConfig = ['database', 'slaveDb'];
    protected $table = 'Biny_Test';
    // 表pk字段 复合pk为数组 ['id', 'type']
    protected $_pk = 'id';
    // 开启pk缓存
    protected $_pkCache = true;
}

baseDAO中提供了getByPkupdateByPkdeleteByPk方法, 当$_pkCache参数为true时,数据会走缓存,加快数据读取速度。

getByPk 读取键值数据,返回一维数组数据

//参数为pk值 返回 ['id'=>10, 'name'=>'test', 'time'=>1461845038]
$data = $this->testDAO->getByPk(10);

//复合pk需要传数组
$data = $this->userDAO->getByPk(array(10, 'test'));

updateByPk 更新单条数据

//参数为pk值,update数组,返回true/false
$result = $this->testDAO->updateByPk(10, array('name'=>'test'));

deleteByPk 删除单条数据

//参数为pk值,返回true/false
$result = $this->testDAO->deleteByPk(10);

注意:开启$_pkCache的DAO不允许再使用updatedelete方法,这样会导致缓存与数据不同步的现象。

如果该表频繁删改数据,建议关闭$_pkCache字段,或者在删改数据后调用clearCache()方法来清除缓存内容,从而与数据库内容保持同步。

语句调试

SQL调试方法已经集成在框架事件中,只需要在需要调试语句的方法前调用Event::on(onSql)就可以在页面控制台中输出sql语句了

// one方法绑定一次事件,输出一次后自动释放
Event::one(onSql);
$data = $this->testDAO->query();

// on方法绑定事件,直到off释放前都会有效
Event::on(onSql);
$data = $this->testDAO->query();
$data = $this->testDAO->query();
$data = $this->testDAO->query();
Event::off(onSql);

该SQL事件功能还可自行绑定方法,具体用法会在后面事件介绍中详细展开

页面渲染

请在php.ini配置中打开short_open_tag,使用简写模版,提高开发效率

页面view层目录在/app/template/下面,可以在Action层中通过$this->display()方法返回

一般Action类都会继承baseAction类,在baseAction中可以将一些页面通用参数一起下发,减少开发,维护成本

渲染参数

display方法有三个参数,第一个为指定template文件,第二个为页面参数数组,第三个为系统类数据(没有可不传)。

// 返回/app/template/main/test.tpl.php 
return $this->display('main/test', array('test'=>1), array('path'=>'/test.png'));

/* /app/template/main/test.tpl.php
返回:
<div class="container">
    <span> 1  </span>
    <img src="/test.png"/>
</div> */
<div class="container">
    <span> <?=$PRM['test']?>  </span>
    <img src="<?=$path?>"/>
</div>

第二个参数的数据都会放到$PRM这个页面对象中。第三个参数则会直接被渲染,适合静态资源地址或者类数据

自定义TKD

页面TKD一般都默认在common.tpl.php定义好,如果页面单独需要修改对应的title,keywords,description的话, 也可以在Response生成后对其赋值

$view = $this->display('main/test', $params);
$view->title = 'Biny';
$view->keywords = 'biny,php,框架';
$view->description = '一款轻量级好用的框架';
return $view;

反XSS注入

使用框架display方法,自动会进行参数html实例化,防止XSS注入。

$PRM获取参数时有两种写法,普通的数组内容获取,会自动进行转义

// 显示 <div> 源码为 &lt;div&gt;
<span> <?=$PRM['test']?>  </span>

另外可以用私用参数的方式获取,则不会被转义,适用于需要显示完整页面结构的需求(普通页面不推荐使用,隐患很大

// 显示 <div> 源码为 <div> 
<span> <?=$PRM->test?>  </span>
// 效果同上
<span> <?=$PRM->get('test')?>  </span>

在多层数据结构中,也一样可以递归使用

// 显示 <div> 源码为 &lt;div&gt;
<span> <?=$PRM['array']['key1']?>  </span>
<span> <?=$PRM['array']->get(0)?>  </span>

而多层结构数组参数会在使用时自动转义,不使用时则不会进行转义,避免资源浪费,影响渲染效率。

注意:第三个参数是否html实例化,可在/config/config.php中对字段objectEncode进行配置。

参数方法

渲染参数除了渲染外,还提供了一些原有array的方法,例如:

in_array 判断字段是否在数组中

// 等同于 in_array('value', $array)
<? if ($PRM['array']->in_array('value') {
    // do something
}?>

array_key_exists 判断key字段是否在数组中

// 等同于 array_key_exists('key1', $array)
<? if ($PRM['array']->array_key_exists('key1') {
    // do something
}?>

其他方法以此类推,使用方式是相同的,其他还有json_encode

// 赋值给js参数 var jsParam = {'test':1, "demo": {"key": "test"}};
var jsParam = <?=$PRM['array']->json_encode()?>;

判断数组参数是否为空,可以直接调用$PRM['array']()或者$PRM('array')方法判断,效果等同!empty()方法

// 等同于 if (!empty($array))
<? if ($PRM'array')) {
    // do something
}?>

其他参数方法可以自行在/lib/data/BinyArray.php中进行定义

比如:定义一个len方法,返回数组长度

/lib/data/BinyArray.php
public function len()
{
    return count($this->storage);
}

然后就可以在tpl中开始使用了

// 赋值给js参数 var jsParam = 2;
var jsParam = <?=$PRM['array']->len()?>;

多语言

Biny中支持多语言的使用,可指定cookie中存储当前语言环境。在/config/config.php中对字段

languageCookie

进行配置。而多语言的渲染分为两种。

1. 不同的tpl渲染文件

Action文件中当调用$this->display('xxx')方法时,默认会查找/app/template/main/xxx.tpl.php模板文件

而多语言环境下只需要在/app/template/main/目录下创建多个语言文件,如xxx.en.tpl.phpxxx.jp.tpl.php

框架会自动根据当前的语言环境,自动匹配对应的xxx.{$lan}.tpl.php文件做模板渲染。

2. 同tpl渲染文件下的文本翻译

Biny中提供了语言翻译配置文件,在根目录/language。可以在该目录下配置多个语言文件,如en.phpjp.php等。

配置文件中,定义字符串的翻译映射

// /language/en.php
return array(
    '概览' => 'overview',
    'Biny是一个轻量级易用性强的web Server框架' => 'Biny is Good'
);

然后在tpl模板文件中就可以调用<?=_L('概览')?>,就会自动根据当前语言环境

显示对应语言文件/language/{$lan}.php中的翻译字符串了

事件

框架中提供了事件机制,可以方便全局调用。其中系统默认已提供的有beforeActionafterActiononExceptiononErroronSql这几个

beforeAction为Action执行前执行的事件(在init()方法之后被触发)

afterAction为Action执行后执行的事件(会在渲染页面之前触发)

onException系统抛出异常时被触发,会传递错误code,在/config/exception.php中定义code

onError程序调用$this->error($data)方法时被触发,传递$data参数

onSql执行语句时被触发,上述例子中的Event::on(onSql)就是使用了该事件

定义事件

系统提供了两种定义事件的方式,一种是定义长期事件$fd = Event::on($event, [$class, $method]),直到被off之前都会生效。

参数分别为事件名方法[类,方法名] 方法可以不传,默认为Logger::event()方法,会在console中打印

$fd返回的是该事件的操作符。在调用off方法时,可以通过传递该操作符解绑该事件。

namespace app\controller;
/**
* 主页Action
* @property \app\service\testService $testService
*/  
class testAction extends baseAction
{
    //构造函数
    public function init()
    {
        // 要触发beforeAction事件,可在init里定义,会在init之后被触发
        Event::on(beforeAction, array($this, 'test_event'));
    }

    //默认路由index
    public function action_index()
    {
        // 绑定testService里的my_event1方法 和 my_event2方法 到 myEvent事件中,两个方法都会被执行,按绑定先后顺序执行
        $fd1 = Event::on('myEvent', array($this->testService, 'my_event1'));
        $fd2 = Event::on('myEvent', array($this->testService, 'my_event2'));

        // do something ..... 

        // 解绑myEvent事件的 my_event1方法
        Event::off('myEvent', $fd1);

        // 解绑myEvent事件,所有绑定在该事件上的方法都不会再被执行
        Event::off('myEvent');

        return $this->error('测试一下');
    }

    // 自定义的事件类
    public function test_event($event)
    {
        // addLog为写日志的方法
        Logger::addLog('触发beforeAction事件');
    }
}

另一种绑定则为一次绑定事件Event::one(),调用参数相同,返回$fd操作符,当该事件被触发一次后会自动解绑

$fd = Event::one('myEvent', array($this, 'my_event'));

当然如果想要绑定多次但非长期绑定时,系统也提供了bind方法,参数用法类似。

// 第一个参数绑定方法,第二个为事件名,第三个为绑定次数,触发次数满后自动释放
$fd = Event::bind(array($this, 'my_event'), 'myEvent', $times);

触发事件

用户可以自定义事件,同时也可以选择性的触发,可以直接使用Event::trigger($event, $params)方法

参数有两个,第一个为触发的事件名,第二个为触发传递的参数,会传递到触发方法中执行

// 触发myEvent事件
Event::trigger('myEvent', array(get_class($this), 'test'))

// 定义事件时绑定的方法
public function my_event($event, $params)
{
    // array('testService', 'test')
    var_dump($params);
}

表单验证

框架提供了一套完整的表单验证解决方案,适用于绝大多数场景。

表单验证支持所有类型的验证以及自定义方法

简单示例:

namespace app\form;
use biny\lib\Form;
/**
 * @property \app\service\testService $testService
 * 自定义一个表单验证类型类 继承Form
 */
class testForm extends Form
{
    // 定义表单参数,类型及默认值(可不写,默认null)
    protected $_rules = [
        // id必须为整型, 默认10
        'id'=>[self::typeInt, 10],
        // name必须非空(包括null, 空字符串)
        'name'=>[self::typeNonEmpty],
        // 自定义验证方法(valid_testCmp)
        'status'=>['testCmp']
    ];

    // 自定义验证方法
    public function valid_testCmp()
    {
        // 和Action一样可以调用Service和DAO作为私有方法
        if ($this->testService->checkStatus($this->status)){
            // 验证通过
            return $this->correct();
        } else {
            // 验证失败,参数可以通过getError方法获取
            return $this->error('非法类型');
        }
    }
}

定义完验证类,然后就可以在Action中使用了,可以通过getForm方法加载表单

// 加载testForm
$form = $this->getForm('test');
// 验证表单字段,true/false
if (!$form->check()){
    // 获取错误信息
    $error = $form->getError();
    return $this->error('参数错误');
}
// 获取对应字段
$status = $form->status;
// 获取全部字段 返回数组类型 ['id'=>1, 'name'=>'billge', 'status'=>2]
$data = $form->values();
        

注意:$_rules中未定义的字段,无法在$form中被获取到,就算不需要验证,也最好定义一下

在很多情况下,表单参数并不是都完全相同的,系统支持Form复用,即可以在通用的Form类中自定义一些内容

比如,还是上述例子的testForm,有个类似的表单,但是多了一个字段type,而且对于status的验证方式也需要变化

可以在testForm中添加一个方法

// 在testForm中添加
public function addType()
{
    // 添加type字段, 默认'default', 规则为非空
    $this->_rules['type'] = [self::typeNonEmpty,'default'];
    // 修改status的判断条件,改为valid_typeCmp()方法验证,记得要写这个方法哦
    $this->_rules['status'][0] = 'typeCmp';
}

然后在Action中加载表单也需要添加'addType'作为参数,其他使用方法一致

$form = $this->getForm('test', 'addType');

一个表单验证类里可以写多个附加方法,相互直接并不会有任何影响

验证类型

系统提供了7种默认验证方式,验证失败时都会记录错误信息,用户可以通过getError方法获取

self::typeInt 数字类型,包括整型浮点型,负数

self::typeBool 判断是否为true/false

self::typeArray 判断是否为数组类型

self::typeObject 判断是否为对象数据

self::typeDate 判断是否为一个合法的日期

self::typeDatetime 判断是否为一个合法的日期时间

self::typeNonEmpty 判断是否非空(包括null, 空字符串)

self::typeRequired 有该参数即可,可以为空字符串

验证类型几乎涵盖了所有情况,如果有不能满足的类型,用户可以自定义验证方法,上述例子中已有,不再过多阐述

调试

框架中有两种调试方式,一种是在页面控制台中输出的调试,方便用户对应网页调试。

另一种则是和其他框架一样,在日志中调试

控制台调试

Biny的一大特色既是这控制台调试方式,用户可以调试自己想要的数据,同时也不会对当前的页面结构产生影响。

调试的开关在/web/index.php

// console调试开关,关闭后控制台不会输出内容
defined('SYS_CONSOLE') or define('SYS_CONSOLE', true);

控制台调试的方式,同步异步都可以调试,但异步的调试是需要引用/static/js/main.js文件,这样异步ajax的请求也会把调试信息输出在控制台里了。

调试方式很简单,全局可以调用Logger::info($message, $key),另外还有warn,error,log等

第一个参数为想要调试的内容,同时也支持数组,Object类的输出。第二个参数为调试key,不传默认为phpLogs

Logger::info()消息 输出

Logger::warn()警告 输出

Logger::error()异常 输出

Logger::log()日志 输出

下面是一个简单例子,和控制台的输出结果。结果会因为浏览器不一样而样式不同,效果上是一样的。

// 以下代码全局都可以使用
Logger::log(array('cc'=>'dd'));
Logger::error('this is a error');
Logger::info(array(1,2,3,4,5));
Logger::warn("ss", "warnKey");

另外Logger调试类中还支持time,memory的输出,可以使用其对代码性能做优化。

// 开始结尾处加上时间 和 memory 就可以获取中间程序消耗的性能了
Logger::time('start-time');
Logger::memory('start-memory');
Logger::log('do something');
Logger::time('end-time');
Logger::memory('end-memory');

日志调试

平台的日志目录在/logs/,请确保该目录有写权限

异常记录会生成在error_{日期}.log文件中,如:error_2016-05-05.log

调试记录会生成在log_{日期}.log文件中,如:log_2016-05-05.log

程序中可以通过调用Logger::addLog($log, INFO)方法添加日志,Logger::addError($log, ERROR)方法添加异常

$log参数支持传数组,会自动排列打印

$LEVEL可使用常量(INFODEBUGNOTICEWARNINGERROR)不填即默认级别

系统程序错误也都会在error日志中显示,如页面出现500时可在错误日志中查看定位

脚本执行

Biny框架除了提供HTTP的请求处理以外,同时还提供了一套完整的脚本执行逻辑

执行入口为根目录下的shell.php文件,用户可以通过命令行执行php shell.php {router} {param}方式调用

其中router为脚本路由,param为执行参数,可缺省或多个参数

// shell.php
//默认时区配置
date_default_timezone_set('Asia/Shanghai');
// 开启脚本执行(shell.php固定为true)
defined('RUN_SHELL') or define('RUN_SHELL', true);
// dev pre pub 当前环境
defined('SYS_ENV') or define('SYS_ENV', 'dev');

脚本路由

路由跟http请求模式基本保持一致,分为{module}/{method}的形式,其中{method}可以缺省,默认为index

例如:index/test就会执行indexShell中的action_test方法,而demo则会执行demoShell中的action_index方法

如果router缺省的话,默认会读取/config/config.php中的router内容作为默认路由

// /config/config.php
return array(
    'router' => array(
        // http 默认路由
        'base_action' => 'demo',
        // shell 默认路由
        'base_shell' => 'index'
    )
)
// /app/shell/indexShell.php
namespace app\shell;
use biny\lib\Shell;
class testShell extends Shell
{
    // 和http一样都会先执行init方法
    public function init()
    {
        //return 0 或者 不return 则程序继续执行。如果返回其他内容则输出内容后程序终止。
        return 0;
    }

    //默认路由index
    public function action_index()
    {
        //返回异常,会记录日志并输出在终端
        return $this->error('执行错误');
    }
}

脚本参数

脚本执行可传复数的参数,同http请求可在方法中直接捕获,顺序跟参数顺序保持一致,可缺省

另外,可以用param方法获取对应位置的参数

例如:终端执行php shell.php test/demo 1 2 aaa,结果如下:

// php shell.php test/demo 1 2 aaa
namespace app\shell;
use biny\lib\Shell;
class testShell extends Shell
{
    test/demo => testShell/action_demo
    public function action_demo($prm1, $prm2, $prm3, $prm4='default')
    {
        //1, 2, aaa, default
        echo "$prm1, $prm2, $prm3, $prm4";
        //1
        echo $this->param(0);
        //2
        echo $this->param(1);
        //aaa
        echo $this->param(2);
        //default
        echo $this->param(3, 'default');
    }
}

同时框架还提供了变量化的参数传递方式,用法与http模式保持一致

例如:终端执行php shell.php test/demo --name="test" --id=23 demo,结果如下:

// php shell.php test/demo --name="test" --id=23 demo
namespace app\shell;
use biny\lib\Shell;
class testShell extends Shell
{
    test/demo => testShell/action_demo
    public function action_demo($id, $name='demo', $prm='default')
    {
        //23, test, default
        echo "$id, $name, $prm";
        //23
        echo $this->param('id');
        //demo
        echo $this->param('name');
        //default
        echo $this->param('prm', 'default');

        // 不带参数话模式的变量 将顺序从第0位开始
        // demo
        echo $this->param(0);
    }
}

注意:使用变量化传递后,方法中默认参数将不会捕获非变量化的参数,如上例的demo需要通过param方法获取

脚本日志

脚本执行不再具有HTTP模式的其他功能,例如表单验证页面渲染浏览器控制台调试。 所以在Logger调试类中,info/error/debug/warning这几个方法将改为在终端输出

同时也可以继续调用Logger::addLogLogger::addError方法来进行写日志的操作

日志目录则保存在/logs/shell/目录下,请确保该目录有写权限。格式与http模式保持一致。

注意:当程序返回$this->error($msg)的时候,系统会默认调用Logger::addError($msg),请勿重复调用。

其他

系统有很多单例都可以直接通过App::$base直接获取

App::$base->request 为当前请求,可获取当前地址,客户端ip等

App::$base->cache 为请求静态缓存,只在当前请求中有效

App::$base->session 为系统session,可直接获取和复制,设置过期时间

App::$base->memcache 为系统memcache,可直接获取和复制,设置过期时间

App::$base->redis 为系统redis,可直接获取和复制,设置过期时间

Request

在进入Controller层后,Request就可以被调用了,以下是几个常用操作

// 以请求 /test/demo/?id=10 为例

// 获取Action名 返回test
App::$base->request->getModule();

// 获取Action对象 返回testAction
App::$base->request->getModule(true);

// 获取Method名 返回action_demo
App::$base->request->getMethod();

// 获取纯Method名 返回demo
App::$base->request->getMethod(true);

// 是否异步请求 返回false
App::$base->request->isAjax();

// 返回当前路径  /test/demo/
App::$base->request->getBaseUrl();

// 返回完整路径  http://www.billge.cc/test/demo/
App::$base->request->getBaseUrl(true);

// 返回带参数URL  /test/demo/?id=10
App::$base->request->getUrl();

// 获取来源网址 (上一个页面地址)
App::$base->request->getReferrer();

// 获取浏览器UA
App::$base->request->getUserAgent();

// 获取用户IP
App::$base->request->getUserIP();

Cache

框架提供了程序运行生命周期内的全局缓存,使用非常简单

// 只需要赋值就可以实现cache的设置了
App::$base->cache->testkey = 'test';
// 获取则是直接取元素,不存在则返回null
$testKey = App::$base->cache->testkey;

同时Cache也支持isset判断及unset操作

// isset 相当于先get 后isset 返回 true/false
$bool = isset(App::$base->cache->testKey);
// 删除缓存
unset(App::$base->cache->testKey);
        

Session

session的设置和获取都比较简单(与cache相同),在未调用session时,对象不会被创建,避免性能损耗。

// 只需要赋值就可以实现session的设置了
App::$base->session->testkey = 'test';
// 获取则是直接取元素,不存在则返回null
$testKey = App::$base->session->testkey;

同时也可以通过方法close()来关闭session,避免session死锁的问题

// close之后再获取数据时会重新开启session
App::$base->session->close();

clear()方法则会清空当前session中的内容

// clear之后再获取则为null
App::$base->session->clear();

同时session也是支持isset判断的

// isset 相当于先get 后isset 返回 true/false
$bool = isset(App::$base->session->testKey);

cookie的获取和设置都是在App::$base->request中完成的,分别提供了getCookiesetCookie方法

getCookie参数为需要的cookie键值,如果不传,则返回全部cookie,以数组结构返回

$param = App::$base->request->getCookie('param');

setCookie参数有4个,分别为键值,值,过期时间(单位秒),cookie所属路径,过期时间不传默认1天,路径默认'/'

App::$base->request->setCookie('param', 'test', 86400, '/');

模型数据

用户可以在/app/model/下自定义model数据类,通过App::$model获取,例如:

App::$model->person 为当前用户,可在/app/model/person.php中定义

除了系统预设的person模型外,用户也可自定义模型,例如我们新建一个team模型

第一步,我们在/app/model/目录或者子目录/孙目录下新建一个文件/app/model/team.php

// team.php
namespace app\model;
use App;
/**
* @property \app\dao\teamDAO $teamDAO
* @property \app\dao\userDAO $userDAO
*/
class team extends baseModel
{
    /**
     * @var array 单例对象
     */
    protected static $_instance = [];

    /**
     * 构造函数
     * @param $id
     */
    protected function __construct($id)
    {
        $this->DAO = $this->teamDAO;
        if ($id !== NULL){
            $this->_data = $this->DAO->getByPk($id);
            $this->_pk = $id;
        }
    }

    /**
     * 自定义方法 返回用户人数
     */
    public function getTotal()
    {
        // 获取team_id标记为当前team的用户数
        return $this->userDAO->filter(['team_id'=>$this->id])->count();
    }
}

然后就可以在代码中调用了,例如一个标记团队vip等级的功能,如下:

// 获取team数据模型
$team = App::$model->team($id)
if ($team->getTotal() > 100) {
    // 修改对应数据库字段并保存,以下方法为baseModel中公共方法,继承baseModel即可使用
    $team->vipLevel = 1;
    $team->save();
}

注意:类名,文件名,model变量名,三者需要保持一致,否者系统会找不到对应的模型。

数据模型也可以定义参数的调用方式,或者多参数模式的函数调用方式,都通过init方法来实现

App::$model->team 相当于调用 \app\model\team::init()

App::$model->team(10, false) 相当于调用 \app\model\team::init(10, false)

所以只需要覆盖掉baseModel中的init方法,即可自定义初始化模型了。

另外,可以在/lib/Model.php中添加 @property@method 使得IDE能够认识变量并具有补全的功能。

/**
 * Class Model
 * @package biny\lib
 * @property \app\model\person $person
 * @method \app\model\person person($id)
 * @method \app\model\team team($id)
 */