API参考 - 筋斗云服务端

最后更新:2016-12-01

Modules

app_fw (module)

ext (module)

api_fw (module)

AccessControl (module)


Keywords

$APP (var)

$BASE_DIR (var)

AC0_ (key)

AC1_ (key)

AC2_ (key)

AC_ (key)

AccessControl (module)

AccessControl::$allowedAc (var)

AccessControl::$defaultRes (var)

AccessControl::$defaultSort (var)

AccessControl::$hiddenFields (var)

AccessControl::$id (var)

AccessControl::$maxPageSz (var)

AccessControl::$onAfterActions (var)

AccessControl::$readonlyFields (var)

AccessControl::$readonlyFields2 (var)

AccessControl::$requiredFields (var)

AccessControl::$requiredFields2 (var)

AccessControl::$subobj (var)

AccessControl::$vcolDefs (var)

AccessControl::addCond (fn)

AccessControl::addJoin (fn)

AccessControl::addRes (fn)

AccessControl::addVCol (fn)

AccessControl::getDefaultSort (fn)

AccessControl::getMaxPageSz (fn)

AccessControl::onAfter (fn)

AccessControl::onGenId (fn)

AccessControl::onHandleRow (fn)

AccessControl::onQuery (fn)

AccessControl::onValidate (fn)

AccessControl::onValidateId (fn)

AppBase (class)

CFG_MOCK_MODE (key)

CFG_MOCK_T_MODE (key)

ConfBase (class)

ConfBase::$enableApiLog (var)

ConfBase::onApiInit (fn)

ConfBase::onInitClient (fn)

DBG_LEVEL (var)

DirectReturn (class)

ExtFactory::getObj (fn)

ExtFactory::instance (fn)

JDEvent (class)

JDEvent.on (fn)

JDEvent.trigger (fn)

JDSingleton (class)

MOCK_MODE (var)

MyException (class)

PAGE_SZ_LIMIT (var)

P_DB (key)

P_DBCRED (key)

P_SESSION_DIR (key)

P_URL_PATH (key)

PluginBase (class)

PluginBase.$colMap (var)

PluginBase.mapCol (key)

PluginBase.mapSql (key)

Plugins (class)

Plugins::$map (var)

Plugins::add (fn)

Plugins::exists (fn)

Plugins::getInstance (fn)

Q (fn)

TEST_MODE (var)

addLog (fn)

api_fw (module)

app_fw (module)

callSvc (fn)

dbConfirmFn (key)

dbconn (fn)

errQuit (fn)

execOne (fn)

ext (module)

getAppType (fn)

getBaseUrl (fn)

getCred (fn)

getExt (fn)

hasSignFile (fn)

isCLI (fn)

isCLIServer (fn)

isEqualCollection (fn)

isMockMode (fn)

logext (fn)

logit (fn)

mparam (fn)

myEncrypt (fn)

objarr2table (fn)

param (fn)

param_varr (fn)

plugin.php (key)

plugin/index.php (key)

queryAll (fn)

queryOne (fn)

setParam (fn)

setRet (fn)

setServerRev (fn)

startsWith (fn)

table2objarr (fn)

tableCRUD (fn)

tobool (fn)

varr2objarr (fn)


@fn tobool($s)


@fn startsWith($s, $pat)


@fn isCLI()

command-line interface. e.g. run "php x.php"


@fn isCLIServer()

php built-in web server e.g. run "php -S 0.0.0.0:8080"


@fn isEqualCollection($col1, $col2)


@module app_fw

1 通用函数

2 初始化配置

2.1 数据库配置

3 测试模式与调试等级

4 模拟模式

5 session管理

6 应用框架

筋斗云服务端通用应用框架。

1 通用函数

2 初始化配置

app_fw框架自动包含 $BASE_DIR/conf.php, $BASE_DIR/php/conf.user.php。

前者定义代码中易变的逻辑;后者为项目配置,一般用于定义环境变量、全局变量等。

2.1 数据库配置

@key P_DB 环境变量,指定DB类型与地址。

@key P_DBCRED 环境变量,指定DB登录帐号

P_DB格式为:

P_DB={主机名}/{数据库名}
或
P_DB={主机名}:{端口号}/{数据库名}

例如:

P_DB=localhost/myorder
P_DB=www.myserver.com:3306/myorder

P_DBCRED格式为{用户名}:{密码},或其base64编码后的值,如

P_DBCRED=ganlan:1234
或
P_DBCRED=Z2FubGFuOjEyMzQ=

此外,P_DB还支持SQLite数据库,直接指定以".db"为扩展名的文件即可。例如:

P_DB=../myorder.db

3 测试模式与调试等级

@var TEST_MODE Integer/Boolean. 0-生产模式;1-测试模式;2-自动化回归测试模式(RTEST_MODE)

@var DBG_LEVEL Integer. 调试等级。值范围0-9.

测试模式特点:

用于在测试模式下输出调试信息。

@see addLog

4 模拟模式

@var MOCK_MODE Boolean. 模拟模式. 值:0/1.

@key CFG_MOCK_MODE 符号文件,如文件存在则应用运行于模拟模式。

@key CFG_MOCK_T_MODE 符号文件,如文件存在且在测试模式下,应用运行于模拟模式。

对第三方系统依赖(如微信认证、支付宝支付、发送短信等),可通过设计Mock接口来模拟。

@see ExtMock

5 session管理

@key P_SESSION_DIR ?= $BASE_DIR/session 环境变量,定义session文件存放路径。

@key P_URL_PATH 环境变量。项目的URL路径,如"/jdcloud", 用于定义cookie生效的作用域,也用于拼接相对URL路径。

@see getBaseUrl

6 应用框架

继承AppBase类,可实现提供符合BQP协议接口的模块。api_fw框架就是使用它的一个典型例子。

@see AppBase


@var $BASE_DIR

包含app_fw.php的主文件(如api.php)所在目录。常用于拼接子目录名。
最后不带"/".


@var $APP?=user

客户端应用标识,默认为"user".
根据URL参数"_app"确定值。


@fn param_varr($str, $type, $name)

type的格式如"i:n:b?:dt:tm?".


@fn param($name, $defVal?, $col?=$_REQUEST)

@param $col: key-value collection

获取名为$name的参数。
$name中可以指定类型,返回值根据类型确定。如果该参数未定义或是空串,直接返回缺省值$defVal。

$name中指定类型的方式如下:

示例:

$id = param("id");
$svcId = param("svcId/i", 99);
$wantArray = param("wantArray/b", false);
$startTm = param("startTm/dt", time());

List类型示例。参数"items"类型在文档中定义为list(id/Integer, qty/Double, dscr/String),可用param("items/i:n:s")获取, 值如

items=100:1:洗车,101:1:打蜡

返回

[ [ 100, 1.0, "洗车"], [101, 1.0, "打蜡"] ]

如果某列可缺省,用"?"表示,如param("items/i:n?:s?")可获取值:

items=100:1,101::打蜡

返回

[ [ 100, 1.0, null], [101, null, "打蜡"] ]

TODO: 直接支持 param("items/(id,qty?/n,dscr?)"), 添加param_objarr函数,去掉parseList函数。上例将返回

[
    [ "id"=>100, "qty"=>1.0, dscr=>null],
    [ "id"=>101, "qty"=>null, dscr=>"打蜡"]
]

@fn mparam($name, $col = $_REQUEST)

@brief mandatory param

$name可以是一个数组,表示至少有一个参数有值,这时返回每个参数的值。
参考param函数,查看$name如何支持各种类型。

示例:

$svcId = mparam("svcId");
$svcId = mparam("svcId/i");
$itts = mparam("itts/i+")
list($svcId, $itts) = mparam(["svcId", "itts/i+"]); # require one of the 2 params

@fn setParam($k, $v)

设置参数,其实是模拟客户端传入的参数。以便供tableCRUD等函数使用。

@see tableCRUD


@fn objarr2table ($objarr, $fixedColCnt=null)

将objarr格式转为table格式, 如:

objarr2table(
    [
        ["id"=>100, "name"=>"A"],
        ["id"=>101, "name"=>"B"]
    ]
) -> 
    [
        "h"=>["id", "name"],
        "d"=>[ 
            [100,"A"], 
            [101,"B"]
        ] 
    ]

注意:

@see table2objarr

@see varr2objarr


@fn table2objarr

将table格式转为 objarr, 如:

table2objarr(
    [
        "h"=>["id", "name"],
        "d"=>[ 
            [100,"A"], 
            [101,"B"]
        ] 
    ]
) -> [ ["id"=>100, "name"=>"A"], ["id"=>101, "name"=>"B"] ]

@fn varr2objarr

将类型 varr (仅有值的二维数组, elem=[$col1, $col2] ) 转为 objarr (对象数组, elem={col1=>cell1, col2=>cell2})

例:

varr2objarr(
    [ [100, "A"], [101, "B"] ], 
    ["id", "name"] )
-> [ ["id"=>100, "name"=>"A"], ["id"=>101, "name"=>"B"] ]

@fn getCred($cred) -> [user, pwd]

$cred为"{user}:{pwd}"格式,支持使用base64编码。
示例:

list($user, $pwd) = getCred(getenv("P_ADMIN_CRED"));
if (! isset($user)) {
    // 未设置用户名密码
}

@fn dbconn($fnConfirm=$GLOBALS["dbConfirmFn"])

@param fnConfirm fn(dbConnectionString), 如果返回false, 则程序中止退出。

@key dbConfirmFn 连接数据库前回调。

连接数据库

数据库由全局变量$DB(或环境变量P_DB)指定,格式可以为:

host1/carsvc (无扩展名,表示某主机host1下的mysql数据库名;这时由 全局变量$DBCRED 或环境变量 P_DBCRED 指定用户名密码。

dir1/dir2/carsvc.db (以.db文件扩展名标识的文件路径,表示SQLITE数据库)

环境变量 P_DBCRED 指定用户名密码,格式为 base64(dbuser:dbpwd).


@fn Q($str, $dbh=$DBH)

quote string

一般是把字符串如"abc"转成加单引号的形式"'abc'". 适用于根据用户输入串拼接成SQL语句时,对输入串处理,避免SQL注入。

示例:

$sql = sprintf("SELECT id FROM User WHERE uname=%s AND pwd=%s", Q(param("uname")), Q(param("pwd")));

@fn execOne($sql, $getInsertId?=false)

@param $getInsertId ?=false 取INSERT语句执行后得到的id. 仅用于INSERT语句。

执行SQL语句,如INSERT, UPDATE等。执行SELECT语句请使用queryOne/queryAll.

$token = mparam("token");
execOne("UPDATE cinf SET appleDeviceToken=" . Q($token));

注意:在拼接SQL语句时,对于传入的string类型参数,应使用Q函数进行转义,避免SQL注入攻击。

对于INSERT语句,设置参数$getInsertId=true, 可取新加入数据行的id. 例:

$sql = sprintf("INSERT INTO Hongbao (userId, createTm, src, expireTm, vdays) VALUES ({$uid}, '%s', '{$src}', '%s', {$vdays})", date('c', $createTm), date('c', $expireTm));
$hongbaoId = execOne($sql, true);

@fn queryOne($sql, $fetchMode = PDO::FETCH_NUM)

执行查询语句,只返回一行数据,如果行中只有一列,则直接返回该列数值。
如果执行失败,返回false.

示例:查询用户姓名与电话,默认返回值数组:

$row = queryOne("SELECT name,phone FROM User WHERE id={$id}");
if ($row === false)
    throw new MyException(E_PARAM, "bad user id");
// $row = ["John", "13712345678"]

也可返回关联数组:

$row = queryOne("SELECT name,phone FROM User WHERE id={$id}", PDO::FETCH_ASSOC);
if ($row === false)
    throw new MyException(E_PARAM, "bad user id");
// $row = ["name"=>"John", "phone"=>"13712345678"]

当查询结果只有一列时,直接返回该数值。

$phone = queryOne("SELECT phone FROM User WHERE id={$id}");
if ($phone === false)
    throw new MyException(E_PARAM, "bad user id");
// $phone = "13712345678"

@see queryAll


@fn queryAll($sql, $fetchMode = PDO::FETCH_NUM)

执行查询语句,返回数组。
如果查询失败,返回空数组。

默认返回值数组(varr):

$rows = queryAll("SELECT name, phone FROM User");
if (count($rows) > 0) {
    ...
}
// 值为:
$rows = [
    ["John", "13712345678"],
    ["Lucy", "13712345679"]
    ...
]
// 可转成table格式返回
return ["h"=>["name", "phone"], "d"=>$rows];

也可以返回关联数组(objarr),如:

$rows = queryAll("SELECT name, phone FROM User", PDO::FETCH_ASSOC);
if (count($rows) > 0) {
    ...
}
// 值为:
$rows = [
    ["name"=>"John", "phone"=>"13712345678"],
    ["name"=>"Lucy", "phone"=>"13712345679"]
    ...
]
// 可转成table格式返回
return objarr2table($rows);

@see objarr2table


@fn getBaseUrl($wantHost = true)

返回 $BASE_DIR 对应的网络路径(最后以"/"结尾)。
如果指定了环境变量 P_URL_PATH(可在conf.user.php中设置), 则根据该变量计算;否则自动判断(如果有符号链接可能不准)

例:

P_URL_PATH = "/cheguanjia/" 或 P_URL_PATH = "/cheguanjia"

getBaseUrl() -> "http://host/cheguanjia/"
getBaseUrl(false) -> "/cheguanjia/"

@see $BASE_DIR


@fn logit($s, $addHeader=true, $type="trace")

记录日志。

默认到日志文件 $BASE_DIR/trace.log. 如果指定type=secure, 则写到 $BASE_DIR/secure.log.

可通过在线日志工具 tool/log.php 来查看日志。也可直接打开日志文件查看。


@fn myEncrypt($string,$operation='E',$key='carsvc')

@param operation 'E': encrypt; 'D': decrypt

加密解密字符串

加密:

$cipher = myEncrypt('hello, world!');
or
$cipher = myEncrypt('hello, world!','E','nowamagic');

解密:

$text = myEncrypt($cipher,'D','nowamagic');

参数说明:
$string :需要加密解密的字符串
$operation:判断是加密还是解密:E:加密 D:解密
$key :加密的钥匙(密匙);

http://www.open-open.com/lib/view/open1388916054765.html


@fn errQuit($code, $msg, $msg2 =null)

生成html格式的错误信息并中止执行。
默认地,只显示中文错误,双击可显示详细信息。
例:

errQuit(E_PARAM, "接口错误", "Unknown ac=`$ac`");

@fn addLog($str, $logLevel=1)

输出调试信息到前端。调试信息将出现在最终的JSON返回串中。
如果只想输出调试信息到文件,不想让前端看到,应使用logit.

@see logit


@fn getAppType()

根据应用标识($APP)获取应用类型(AppType)。注意:应用标识一般由前端应用通过URL参数"_app"传递给后端。
不同的应用标识可以对应相同的应用类型,如应用标识"emp", "emp2", "emp-adm" 都表示应用类型"emp",即 应用类型=应用标识自动去除尾部的数字或"-xx"部分。

不同的应用标识会使用不同的cookie名,因而即使用户同时操作多个应用,其session不会相互干扰。
同样的应用类型将以相同的方式登录系统。

@see $APP


@fn hasSignFile($f)

检查应用根目录下($BASE_DIR)下是否存在标志文件。标志文件一般命名为"CFG_XXX", 如"CFG_MOCK_MODE"等。


@class MyException($code, $internalMsg?, $outMsg?)

@param $internalMsg String. 内部错误信息,前端不应处理。

@param $outMsg String. 错误信息。如果为空,则会自动根据$code填上相应的错误信息。

抛出错误,中断执行:

throw new MyException(E_PARAM, "Bad Request - numeric param `$name`=`$ret`.", "需要数值型参数");

@class DirectReturn

抛出该异常,可以中断执行直接返回,不显示任何错误。

例:API返回非BPQ协议标准数据,可以跳出setRet而直接返回:

echo "return data";
throw new DirectReturn();

例:返回指定数据后立即中断处理:

setRet(0, ["id"=>1]);
throw new DirectReturn();

@class AppBase

应用框架,用于提供符合BQP协议的接口。
在onExec中返回协议数据;在onAfter中建议及时关闭DB.


@class JDSingleton (trait)

用于单件类,提供getInstance方法,例:

class PluginCore
{
    use JDSingleton;
}

则可以调用

$pluginCore = PluginCore::getInstance();

@class JDEvent (trait)

提供事件监听(on)与触发(trigger)方法,例:

class PluginCore
{
    use JDEvent;

    // 提供事件"event1", 注释如下:
    /// @event PluginCore.event.event1($arg1, $arg2)
}

则可以调用

$pluginCore->on('event1', 'onEvent1');
$pluginCore->trigger('event1', [$arg1, $arg2]);

function onEvent1($arg1, $arg2)
{
}

@fn JDEvent.on($ev, $fn)


@fn JDEvent.trigger($ev, $args)

返回最后次调用的返回值,false表示中止之后事件调用

如果想在事件处理函数中返回复杂值,可使用$args传递,如下面返回一个数组:

$obj->on('getResult', 'onGetResult');
$out = new stdclass();
$out->result = [];
$obj->trigger('getArray', [$out]);

function onGetResult($out)
{
    $out->result[] = 100;
}

@module ext 集成外部系统

调用外部系统(如短信集成、微信集成等)将引入依赖,给开发和测试带来复杂性。
筋斗云框架通过使用“模拟模式”(MOCK_MODE),模拟这些外部功能,从而简化开发和测试。

对于一个简单的外部依赖,可以用函数isMockMode来分支。例如添加对象存储服务(OSS)支持,接口定义为:

getOssParam() -> {url, expire, dir, param={policy, OSSAccessKeyId, signature} }
模拟模式返回:
getOssParam() -> {url="mock"}

在实现时,先在ext.php中定义外部依赖类型,如Ext_Oss,然后实现函数:

function api_getOssParam()
{
    if (isMockMode(Ext_Oss)) {
        return ["url"=>"mock"];
    }
    // 实际实现代码 ...
}

添加一个复杂的(如支持多个函数调用的)支持模拟的外部依赖,也则可以定义接口,步骤如下,以添加短信支持(SmsSupport)为例:

使用举例:

$sms = getExt(Ext_SmsSupport);
$sms->sendSms(...);

当在运行目录中放置了文件CFG_MOCK_MODE后,则不必依赖外部系统,也可模拟执行这些操作。

@see getExt

@see CFG_MOCK_MODE CFG_MOCK_T_MODE MOCK_MODE


@fn isMockMode($extType)

判断是否模拟某外部扩展模块。如果$extType为null,则只要处于MOCK_MODE就返回true.


@fn ExtFactory::instance()

@see getExt


@fn ExtFactory::getObj($extType, $allowMock?=true)

获取外部依赖对象。一般用getExt替代更简单。

示例:

$sms = ExtFactory::instance()->getObj(Ext_SmsSupport);

@see getExt


@fn getExt($extType, $allowMock = true)

用于取外部接口对象,如:

$sms = getExt(Ext_SmsSupport);

@fn logext($s, $addHeader?=true)

写日志到ext.log中,可在线打开tool/init.php查看。
(logit默认写日志到trace.log中)

@see logit


@module api_fw

1 函数型接口

2 对象型接口

3 接口复用

4 常用操作

5 插件机制

服务接口实现框架。

服务接口包含:

1 函数型接口

假设在文档有定义以下接口

用户修改密码
chpwd(oldpwd, pwd) -> {_token, _expire}

权限:AUTH_USER

则在 api_functions.php 中创建该接口的实现:

function api_chpwd()
{
    checkAuth(AUTH_USER);
    $oldPwd = mparam("oldpwd");
    $pwd = mparam("pwd");
    ...
    $ret = [
        "_token" => $token,
        "_expire" => $expire,
    ];
    return $ret;
}

说明:

@see checkAuth

@see mparam 取必选参数,如果缺少该参数则报错。

@see param 取可选参数,可指定缺省值。

2 对象型接口

@see AccessControl 对象型接口框架。

3 接口复用

api.php可以单独执行,也可直接被调用,如

// set_include_path(get_include_path() . PATH_SEPARATOR . "..");
require_once("api.php");
...
$GLOBALS["errorFn"] = function($code, $msg, $msg2=null) {...}
$ret = callSvc("genVoucher");
// 如果没有异常,返回数据;否则调用指定的errorFn函数(未指定则调用errQuit)

@see callSvc

4 常用操作

错误处理

@see MyException

中断执行,直接返回

@see DirectReturn

调试日志

可使用addLog输出调试信息而不破坏协议输出格式。

@see addLog

@see logit

5 插件机制

@key plugin/index.php 插件配置

plugin/{pluginName}为插件目录。

plugin/index.php是插件配置文件,在后端应用框架函数apiMain中引入,内容示例如下:

<?php

Plugins::add([ "plugin1", "plugin2" ]);

表示当前应用使用两个插件"plugin1"和"plugin2", 分别对应目录 plugin/plugin1和plugin/plugin2.

@see Plugins::add

@key plugin.php 插件定义

插件实现在文件plugin/{pluginName}/plugin.php中,包括交互接口,以及插件API(后端调用接口),以优惠券插件"coupon"为例:

<?php

// 可选:定义插件API, 必须继承 PluginBase类
class PluginCoupon extends PluginBase
{
    // 声明插件API支持的事件, 如init, genCoupons事件:
    // @event PluginCoupon.event.init()
    // @event PluginCoupon.event.genCoupons($src)

    // 插件API函数
    // @fn PluginCoupon.func1($arg1)
    function func1($arg1)
    {
    }
}

// 实现函数型交互接口takeCoupon
function api_takeCoupon() {}

// 实现对象型交互接口 Coupon.query/get/set/del/add
class AC1_Coupon extends AccessControl {}

// 可选:返回插件配置
return [
    "js" => "m2/plugin.js", // 如果前端需要包含文件
    "class" => "PluginCoupon" // 如果提供插件内部接口
];

注意:

在插件内部获取插件实例可以用:

    $plugin = PluginCoupon::getInstance();

如果在插件外部则需要用:

    $pluginCoupon = Plugins::getInstance('coupon');

调用插件API函数:

    $pluginCoupon->func1($arg1);

监听或触发插件事件:

    $pluginCoupon->on('init', 'onInit');
    $pluginCoupon->trigger('genCoupons', [$src]);

事件命名规范与函数名相同,而事件处理函数的命名一般用"on{事件名}".

@see PluginBase

在应用初始化时(apiMain中),会创建所有插件类的实例(如果有的话)。


@fn setRet($code, $data?, $internalMsg?)

@param $code Integer. 返回码, 0表示成功, 否则表示操作失败。

@param $data 返回数据。

@param $internalMsg 当返回错误时,作为额外调试信息返回。

设置返回数据,最终返回JSON格式数据为 [ code, data, internalMsg, debugInfo1, ...]
其中按照BQP协议,前两项为必须,后面的内容一般仅用于调试,前端应用不应处理。

当成功时,返回数据可以是任何类型(根据API设计返回相应数据)。
当失败时,为String类型错误信息。
如果参数$data未指定,则操作成功时值为null(按BQP协议返回null表示客户端应忽略处理,一般无特定返回应指定$data="OK");操作失败时使用默认错误信息。

调用完后,要返回的数据存储在全局数组 $X_RET 中,以JSON字符串形式存储在全局字符串 $X_RET_STR 中。
注意:$X_RET_STR也可以在调用setRet前设置为要返回的字符串,从而避免setRet函数对返回对象进行JSON序列化,如

$GLOBALS["X_RET_STR"] = "{id:100, name:'aaa'}";
setRet(0, "OK");
throw new DirectReturn();
// 最终返回字符串为 "[0, {id:100, name:'aaa'}]"

@see $X_RET

@see $X_RET_STR

@see $errorFn

@see errQuit ()


@fn setServerRev()

根据全局变量"SERVER_REV"或应用根目录下的文件"revision.txt", 来设置HTTP响应头"X-Daca-Server-Rev"表示服务端版本信息(最多6位)。

客户端框架可本地缓存该版本信息,一旦发现不一致,可刷新应用。


@class ConfBase

在conf.php中定义Conf类并继承ConfBase, 实现代码配置:

class Conf extends ConfBase
{
}

@var ConfBase::$enableApiLog?=true

设置为false可关闭ApiLog. 例:

static $enableApiLog = false;

@fn ConfBase::onApiInit()

所有API执行时都会先走这里。

例:对所有API调用检查ios版本:

static function onApiInit()
{
    $iosVer = getIosVersion();
    if ($iosVer !== false && $iosVer<=15) {
        throw new MyException(E_FORBIDDEN, "unsupport ios client version", "您使用的版本太低,请升级后使用!");
    }
}

@fn ConfBase::onInitClient(&$ret)

客户端初始化应用时会调用initClient接口,返回plugins等信息。若要加上其它信息,可在这里扩展。

例:假如定义应用初始化接口为(plugins是框架默认返回的):

initClient(app) -> {plugins, appName}

实现:

static function onInitClient(&$ret)
{
    $app = mparam('app');
    $ret['appName'] = 'my-app';
}

@class PluginBase

插件内部接口应继承该类, 它具有以下方法:

static function getInstance();
funcion on($eventName, $eventHandler);
funcion trigger($eventName, array $args = []);

@see JDSingleton JDEvent

@see plugin/index.php 插件配置

@see plugin.php 插件定义


@var PluginBase.$colMap

%colMap = {tbl => [tblAlias, %cols]}
cols = {col => colAlias}

先在插件接口文档DESIGN.wiki中声明本插件的数据库依赖:

@see @Store: id, name, dscr
@see @Ordr: id

在PluginCore::__construct中实现接口依赖,指定表名或列名对应(如果名称相同不必声明)

function __construct() {
    $plugin1 = Plugins::getInstance('coupon');
    $plugin1->colMap = [
        "Store" => ["MyStore", [
            "dscr" => "description"
        ]],
        "Ordr" => ["MyOrder"]
    ];
}

在plugin实现时,使用mapCol/mapSql来使表名、列名可配置:

$plugin = PluginCoupon::getInstance();
$tbl = $plugin->mapCol("Store"); // $tbl="MyStore"
$tbl = $plugin->mapCol("User"); // $tbl="User" 未定义时,直接取原值
$col = $plugin->mapCol("Store.dscr"); // $col="description"
$col = $plugin->mapCol("Store.name"); // $col="name" 未定义时,直接取原值

$sql = $plugin->mapSql("SELECT s.id, s.{Store.name}, s.{Store.dscr} FROM {Store} s INNER JOIN {Ordr} o ON o.id=s.{Store.storeId}");
// $sql = "SELECT s.id, s.name, s.description FROM MyStore s INNER JOIN MyOrder o ON o.id=s.storeId"

@key PluginBase.mapCol ($tbl, $col=null)

@key PluginBase.mapSql ($sql)


@class Plugins

@see plugin/index.php


@var Plugins::$map


@fn Plugins::add($plugins)

@param $plugins ={ pluginName => {js, php, getInterface} }


@fn Plugins::exists($pluginName)


@fn Plugins::getInstance($pluginName, $allowNull = false)

获取插件类的实例,用于调用插件API。

假设有插件coupon, 一般可定义插件接口类 PluginCoupon如:

class PluginCoupon extends PluginBase
{
}

在插件内部,应直接调用 PluginCoupon::getInstance() 来获得插件实例。
在插件外部才调用本函数。

注意:应通过在plugin.php最后返回的插件配置中指定插件类:

[ 'class' => 'PluginCoupon' ]

主应用作为特殊模块,名称为'core', 对应类为 PluginCore.


@fn tableCRUD($ac, $tbl, $asAdmin?=false)

对象型接口的入口。
也可直接被调用,常与setParam一起使用, 提供一些定制的操作。

@param $asAdmin 默认根据用户身份自动选择"AC_"类; 如果为true, 则以超级管理员身份调用,即使用"AC0_"类。

设置$asAdmin=true好处是对于超级管理员权限来说,即使未定义"AC0_"类,默认也可以访问所有内容。

假如有Rating(订单评价)对象,不想通过对象型接口来查询,而是通过函数型接口来定制输出,接口设计为:

queryRating(storeId, cond?) -> tbl(id, score, dscr, tm, orderDscr)

查询店铺storeId的订单评价。

应用逻辑:
- 按时间tm倒排序

底层利用tableCRUD实现它,这样便于保留分页、参数cond/gres等特性:

function api_queryRating()
{
    $storeId = mparam("storeId");

    // 定死输出内容。
    setParam("res", "id, score, dscr, tm, orderDscr");

    // 相当于AccessControl框架中调用 addCond,用Obj.query接口的内部参数cond2以保证用户还可以使用cond参数。
    setParam("cond2", ["o.storeId=$storeId"]); 

    // 定死排序条件
    setParam("orderby", "tm DESC");

    $ret = tableCRUD("query", "Rating", true);
    return $ret;
}

注意:

@see setParam


@module AccessControl

1 基本权限控制

2 虚拟字段

2.1 关联字段

2.2 关联字段依赖

2.3 计算字段

2.4 子表压缩字段

2.5 自定义字段

3 子表

4 操作完成回调

5 其它

5.1 编号自定义生成

5.2 缺省排序

5.3 缺省输出字段列表

5.4 最大每页数据条数

5.5 虚拟表和视图

对象型接口框架。
AccessControl简写为AC,同时AC也表示自动补全(AutoComplete).

在设计文档中完成数据库设计后,通过添加AccessControl的继承类,可以很方便的提供诸如 {Obj}.query/add/get/set/del 这些对象型接口。

例如,设计文档中已定义订单对象(Ordr)的主表(Ordr)和订单日志子表(OrderLog):

@Ordr: id, userId, status, amount
@OrderLog: id, orderId, tm, dscr

注意:之所以用对象和主表名用Ordr而不是Order词是避免与SQL关键字冲突。

有了表设计,订单的标准接口就已经自动生成好了:

// 查询订单
Ordr.query() -> tbl(id, userId, ...)
// 添加订单
Ordr.add()(userId=1, status='CR', amount=100) -> id
// 查看订单
Ordr.get(id=1)
// 修改订单状态
Ordr.set(id=1)(status='PA')
// 删除订单
Ordr.del(id=1)

但是,只有超级管理员登录后(例如从示例应用中的超级管理端登录后,web/adm.html),才有权限使用这些接口。

如果希望用户登录后,也可以使用这些接口,只要添加一个继承AccessControl的类,且命名为"AC1_Ordr"即可:

class AC1_Ordr extends AccessControl
{
}

有了以上定义,在用户登录系统后,就可以使用上述和超级管理员一样的标准订单接口了。

说明:

类的命名规则为AC前缀加对象名(或主表名,因为对象名与主表名一致)。框架默认提供的前缀如下:

@key AC_ 游客权限(AUTH_GUEST),如未定义则调用时报“无权操作”错误。

@key AC0_ 超级管理员权限(AUTH_ADMIN),如未定义,默认拥有所有权限。

@key AC1_ 用户权限(AUTH_USER),如未定义,则降级使用游客权限接口(AC_)。

@key AC2_ 员工权限(AUTH_EMP/AUTH_MGR), 如未定义,报权限不足错误。

因而上例中命名为 "AC1_Ordr" 就表示用户登录后调用Ordr对象接口,将受该类控制。而这是个空的类,所以拥有一切操作权限。

框架为AUTH_ADMIN权限自动选择AC0_类,其它类可以通过函数 onCreateAC 进行自定义,仍未定义的框架使用AC_类。

@see onCreateAC

1 基本权限控制

@var AccessControl::$allowedAc ?=["add", "get", "set", "del", "query"] 设定允许的操作,如不指定,则允许所有操作。

@var AccessControl::$readonlyFields ?=[] (影响add/set) 字段列表,添加/更新时为这些字段填值无效(但不报错)。

@var AccessControl::$readonlyFields2 ?=[] (影响set操作) 字段列表,更新时对这些字段填值无效。

@var AccessControl::$hiddenFields ?= [] (for get/query) 隐藏字段列表。默认表中所有字段都可返回。一些敏感字段不希望返回的可在此设置。

@var AccessControl::$requiredFields ?=[] (for add/set) 字段列表。添加时必须填值;更新时不允许置空。

@var AccessControl::$requiredFields2 ?=[] (for set) 字段列表。更新时不允许设置空。

@fn AccessControl::onQuery () (for get/query) 用于对查询条件进行设定。

@fn AccessControl::onValidate () (for add/set). 验证添加和更新时的字段,或做自动补全(AutoComplete)工作。

@fn AccessControl::onValidateId () (for get/set/del) 用于对id字段进行检查。比如在del时检查用户是否有权操作该记录。

上节例子中,用户可以操作系统的所有订单。

现在我们到设计文档中,将接口API设计如下:

== 订单接口 ==

添加订单:
Ordr.add()(amount) -> id

查看订单:
Ordr.query() -> tbl(id, userId, status, amount)
Ordr.get(id)

权限:AUTH_GUEST

应用逻辑
- 用户只能添加(add)、查看(get/query)订单,不可修改(set)、删除(del)订单
- 用户只能查看(get/query)属于自己的订单。
- 用户在添加订单时,必须设置amount字段,不必(也不允许)设置id, userId, status这些字段。
  服务器应将userId字段自动设置为该用户编号,status字段自动设置为"CR"(已创建)

为实现以下逻辑,上面例子中代码可修改为:

class AC1_Ordr extends AccessControl
{
    protected $allowedAc = ["get", "query", "add"];
    protected $requiredFields = ["amount"];
    protected $readonlyFields = ["status", "userId"];

    protected function onQuery()
    {
        $userId = $_SESSION["uid"];
        $this->addCond("t0.userId={$userId}");
    }

    protected function onValidate()
    {
        if ($this->ac == "add") {
            $userId = $_SESSION["uid"];
            $_POST["userId"] = $userId;
            $_POST["status"] = "CR";
        }
    }
}

说明:

2 虚拟字段

@var AccessControl::$vcolDefs (for get/query) 定义虚拟字段

常用于展示关联表字段、统计字段等。
在query,get操作中可以通过res参数指定需要返回的每个字段,这些字段可能是普通列名(col)/虚拟列名(vcol)/子对象(subobj)名。

2.1 关联字段

例如,在订单列表中需要展示用户名字段。设计文档中定义接口:

Ordr.query() -> tbl(id, dscr, ..., userName?, userPhone?, createTm?)

query接口的"..."之后就是虚拟字段。后缀"?"表示是非缺省字段,即必须在"res"参数中指定才会返回,如:

Ordr.query(res="*,userName")

在cond中可以直接使用虚拟字段,不管它是否在res中指定,如

Ordr.query(cond="userName LIKE 'jian%'", res="id,dscr")

通过设置$vcolDefs实现这些关联字段:

class AC1_Ordr extends AccessControl
{
    protected $vcolDefs = [
        [
            "res" => ["u.name AS userName", "u.phone AS userPhone"],
            "join" => "INNER JOIN User u ON u.id=t0.userId",
            // "default" => false, // 指定true表示Ordr.query在不指定res时默认会返回该字段。一般不建议设置为true.
        ],
        [
            "res" => ["log_cr.tm AS createTm"],
            "join" => "LEFT JOIN OrderLog log_cr ON log_cr.action='CR' AND log_cr.orderId=t0.id",
        ]
    ]
}
2.2 关联字段依赖

假设设计有“订单评价”对象,它会与“订单对象”相关联:

@Rating: id, orderId, content

表间的关系为:

订单评价Rating(orderId) <-> 订单Ordr(userId) <-> 用户User

现在要为Rating表增加关联字段 "Ordr.dscr AS orderDscr", 以及"User.name AS userName", 设计接口为:

Rating.query() -> tbl(id, orderId, content, ..., orderDscr?, userName?)
注意:userName字段不直接与Rating表关联,而是通过Ordr表桥接。

实现时,只需在vcolDefs中使用require指定依赖字段:

class AC1_Rating extends AccessControl
{
    protected $vcolDefs = [
        [
            "res" => ["o.dscr AS orderDscr", "o.userId"],
            "join" => "INNER JOIN Ordr o ON o.id=t0.orderId",
        ],
        [
            "res" => ["u.name AS userName"],
            "join" => "INNER JOIN User u ON o.userId=u.id",
            "require" => "userId", // *** 定义依赖,如果要用到res中的字段如userName,则自动添加userId字段引入的表关联。
            // 这里指向orderDscr也可以,一般习惯上指向关联的字段。
        ],
    ];
}

使用require, 框架可自动将Ordr表作为中间表关联进来。
如果没有require定义,以下调用

Rating.query(res="*,orderDscr,userName")

也不会出问题,因为在userName前指定了orderDscr,框架可自动引入相关表。而以下查询就会出问题:

Rating.query(res="*,userName")
或
Rating.query(res="*,userName,orderDscr")
2.3 计算字段

示例:管理端应用在查询订单时,需要订单对象上有一个原价字段:

Ordr.query() -> tbl(..., amount2)
amount2:: 原价,通过OrderItem中每个项目重新计算累加得到,不考虑打折优惠。

可实现为:

class AC0_Ordr extends AccessControl
{
    protected $vcolDefs = [
        [
            "res" => ["(SELECT SUM(qty*ifnull(price2,0)) FROM OrderItem WHERE orderId=t0.id) AS amount2"],
        ]
    ];
}
2.4 子表压缩字段

除了使用子表, 对于简单的情况,也可以设计为将子表压缩成一个虚拟字段,在Query操作时直接返回。

示例:OrderItem是Ordr对象的一个子表,现在想在查询Ordr对象列表时,返回OrderItem的相关信息。
这就要把一张子表压缩成一个字段。我们使用List来描述这种压缩字段的格式:表中每行以","分隔,行中每个字段以":"分隔。
利用List,可将接口设计为:

Ordr.query() -> tbl(..., itemsInfo)
itemsInfo:: List(name, price, qty). 例如"洗车:25:1,换轮胎:380:2", 表示两行记录,每行3个字段。注意字段内容中不可出现":", ","这些分隔符。

子表压缩是一种特殊的计算字段,可实现如下:

class AC1_Ordr extends AccessControl
{
    protected $vcolDefs = [
        [
            "res" => ["(SELECT group_concat(concat(oi.name, ':', oi.price, ':', oi.qty)) FROM OrderItem oi WHERE oi.orderId=t0.id) itemsInfo"] 
        ],
        ...
    ]
}

注意:计算字段,包括子表压缩字段都是很消耗性能的。

2.5 自定义字段

假设有张虚拟表Task, 它没有存储在数据库中, 另一张表UserTaskLog关联到它。在设计文档中定义如下:

@UserTaskLog: id, userId, taskId
@Conf::$taskTable: id, type, name
(关联: UserTaskLog(taskId) <-> Conf::$taskTable )

提供查询接口:
UserTaskLog.query() -> tbl(id, taskId, ..., taskName)
taskName:: 由关联表的taskTable.name字段得到。

实现中,在代码中直接定义Task表:

class Conf
{
    static $taskTable = [
        ["id" => 1, "type"=>"invite", "name" => "邀请5个用户注册"],
        ["id" => 2, "type"=>"invite", "name" => "邀请10个用户注册"],
    ];
}

通过在vcolDefs的join属性指定一个函数,可以实现返回taskName字段:

function getTaskName(&$row)
{
    foreach (Conf::$taskTable as $task) {
        if ($row["taskId"] == $task["id"]) {
            $row["taskName"] = $task["name"];
        }
    }
}

class AC1_UserTaskLog extends AccessControl
{
    protected $vcolDefs = [
        [
            "res" => ["taskName"],
            "join" => getTaskName
        ]
    ];
}

注意:

3 子表

@var AccessControl::$subobj (for get/query) 定义子表

设计接口:

Ordr.get() -> {id, ..., @orderLog}
orderLog:: {id, tm, dscr, ..., empName} 订单日志子表。

实现:

class AC1_Ordr extends AccessControl
{
    protected $subobj = [
        "orderLog" => ["sql"=>"SELECT ol.*, e.name AS empName FROM OrderLog ol LEFT JOIN Employee e ON ol.empId=e.id WHERE orderId=%d", "wantOne"=>false],
    ];
}

子表一般通过get操作来获取,执行指定的SQL语句作为结果。结果以一个数组返回[{id, tm, ...}],如果指定wantOne=>true, 则结果以一个对象返回即 {id, tm, ...}, 适用于主表与子表一对一的情况。

通过在Query操作上指定参数{wantArray:1}也可以返回子表,但目前不支持分页等操作。

4 操作完成回调

@fn AccessControl::onAfter (&$ret) (for all) 操作完成时的回调。可修改操作结果ret。

如果要对get/query结果中的每行字段进行设置,应重写回调 onHandleRow.
有时使用 onAfterActions 就近添加逻辑更加方便。

@var AccessControl::$onAfterActions =[]. onAfter的替代方案,更易使用,便于与接近的逻辑写在一起。

@var AccessControl::$id get/set/del时指定的id, 或add后返回的id.

例如,添加订单时,自动添加一条日志,可以用:

protected function onValidate()
{
    if ($this->ac == "add") {
        ... 

        $this->onAfterActions[] = function () use ($logAction) {
            $orderId = $this->id;
            $sql = sprintf("INSERT INTO OrderLog (orderId, action, tm) VALUES ({$orderId},'CR','%s')", date('c'));
            execOne($sql);
        };
    }
}

@fn AccessControl::onHandleRow (&$rowData) (for get/query) 在onAfter之前运行,用于修改行中字段。

5 其它

5.1 编号自定义生成

@fn AccessControl::onGenId () (for add) 指定添加对象时生成的id. 缺省返回0表示自动生成.

5.2 缺省排序

@fn AccessControl::getDefaultSort () (for query)取缺省排序.

@var AccessControl::$defaultSort ?= "t0.id" (for query)指定缺省排序.

示例:Video对象默认按id倒序排列:

class AC_Video extends AccessControl 
{
    protected $defaultSort = "t0.id DESC";
    ...
}
5.3 缺省输出字段列表

@var AccessControl::$defaultRes (for query)指定缺省输出字段列表. 如果不指定,则为 "t0.*" 加 default=true的虚拟字段

5.4 最大每页数据条数

@fn AccessControl::getMaxPageSz () (for query) 取最大每页数据条数。为非负整数。

@var AccessControl::$maxPageSz ?= 100 (for query) 指定最大每页数据条数。值为负数表示取PAGE_SZ_LIMIT值.

前端通过 {obj}.query(_pagesz)来指定每页返回多少条数据,缺省是20条,最高不可超过100条。当指定为负数时,表示按最大允许值=min($maxPageSz, PAGE_SZ_LIMIT)返回。
PAGE_SZ_LIMIT目前定为10000条。如果还不够,一定是应用设计有问题。

如果想返回每页超过100条数据,必须在后端设置,如:

class MyObj extends AccessControl
{
    protected $maxPageSz = 1000; // 最大允许返回1000条
    // protected $maxPageSz = -1; // 最大允许返回 PAGE_SZ_LIMIT 条
}

@var PAGE_SZ_LIMIT =10000

5.5 虚拟表和视图

假如要对ApiLog进行过滤,只查询管理端的写操作。实现以下接口:

EmpLog.query() -> tbl(id, tm, userId, ac, req, res, reqsz, ressz, empName?, empPhone?)

一种办法可以在后台定义一个视图,如:

CREATE VIEW EmpLog AS
SELECT t0.id, tm, userId, ac, req, res, reqsz, ressz, e.name empName, e.phone empPhone
FROM ApiLog t0
LEFT JOIN Employee e ON e.id=t0.userId
WHERE t0.app='emp-adm' AND t0.userId IS NOT NULL
ORDER BY t0.id DESC

然后可将该视图当作表一样查询(但不可更新),如:

class AC2_EmpLog extends AccessControl 
{
    protected $allowedAc = ["query"];
}

这样就可以实现上述接口了。

另一种办法是直接使用AccessControl创建虚拟表,代码如下:

class AC2_EmpLog extends AccessControl 
{
    protected $allowedAc = ["query"];
    protected $table = 'ApiLog';
    protected $defaultSort = "t0.id DESC";
    protected $defaultRes = "id, tm, userId, ac, req, res, reqsz, ressz, empName, empPhone";
    protected $vcolDefs = [
        [
            "res" => ["e.name AS empName", "e.phone AS empPhone"],
            "join" => "LEFT JOIN Employee e ON e.id=t0.userId"
        ]
    ];

    protected function onQuery() {
        $this->addCond("t0.app='emp-adm' and t0.userId IS NOT NULL");
    }
}

与上例相比,它不仅无须在数据库中创建视图,还也可以进行更新。
其要点是:


@fn AccessControl::addRes($res, $analyzeCol=true)

添加列或计算列.

注意:

@see AccessControl::addCond 其中有示例

@see AccessControl::addVCol 添加已定义的虚拟列。


@fn AccessControl::addCond($cond, $prepend=false)

@param $prepend 为true时将条件排到前面。

调用多次addCond时,多个条件会依次用"AND"连接起来。

添加查询条件。
示例:假如设计有接口:

Ordr.query(q?) -> tbl(..., payTm?)
参数:
q:: 查询条件,值为"paid"时,查询10天内已付款的订单。且结果会多返回payTm/付款时间字段。

实现时,在onQuery中检查参数"q"并定制查询条件:

protected function onQuery()
{
    // 限制只能看用户自己的订单
    $uid = $_SESSION["uid"];
    $this->addCond("t0.userId=$uid");

    $q = param("q");
    if (isset($q) && $q == "paid") {
        $validDate = date("Y-m-d", strtotime("-9 day"));
        $this->addRes("olpay.tm payTm");
        $this->addJoin("INNER JOIN OrderLog olpay ON olpay.orderId=t0.id");
        $this->addCond("olpay.action='PA' AND olpay.tm>'$validDate'");
    }
}

@see AccessControl::addRes

@see AccessControl::addJoin


@fn AccessControl::addJoin(joinCond)

添加Join条件.

@see AccessControl::addCond 其中有示例


@fn AccessControl::addVCol($col, $ignoreError=false, $alias=null)

@param $col 必须是一个英文词, 不允许"col as col1"形式; 该列必须在 vcolDefs 中已定义.

@param $alias 列的别名。可以中文. 特殊字符"-"表示不加到最终res中(只添加join/cond等定义), 由addVColDef内部调用时使用.

@return Boolean T/F

用于AccessControl子类添加已在vcolDefs中定义的vcol. 一般应先考虑调用addRes(col)函数.

@see AccessControl::addRes


@fn callSvc($ac?, $urlParam?, $postParam?, $cleanCall?=false, $hideResult?=false)

直接调用接口,返回数据。如果出错,将调用$GLOBALS['errorFn'] (缺省为errQuit).

@param $cleanCall Boolean. 如果为true, 则不使用现有的$_GET, $_POST等变量中的值。

@param $hideResult Boolean. 如果为true, 不输出结果。


Generated by jdcloud-gendoc @ 2016-12-01T17:32:13+08:00