AccessControl::$allowedAc (var)
AccessControl::$defaultRes (var)
AccessControl::$defaultSort (var)
AccessControl::$hiddenFields (var)
AccessControl::$maxPageSz (var)
AccessControl::$onAfterActions (var)
AccessControl::$readonlyFields (var)
AccessControl::$readonlyFields2 (var)
AccessControl::$requiredFields (var)
AccessControl::$requiredFields2 (var)
AccessControl::$vcolDefs (var)
AccessControl::getDefaultSort (fn)
AccessControl::getMaxPageSz (fn)
AccessControl::onHandleRow (fn)
AccessControl::onValidate (fn)
command-line interface. e.g. run "php x.php"
php built-in web server e.g. run "php -S 0.0.0.0:8080"
筋斗云服务端通用应用框架。
获得指定类型参数
数据库连接及操作
@see MyException errQuit
app_fw框架自动包含 $BASE_DIR/conf.php, $BASE_DIR/php/conf.user.php。
前者定义代码中易变的逻辑;后者为项目配置,一般用于定义环境变量、全局变量等。
@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
@var TEST_MODE Integer/Boolean. 0-生产模式;1-测试模式;2-自动化回归测试模式(RTEST_MODE)
@var DBG_LEVEL Integer. 调试等级。值范围0-9.
测试模式特点:
用于在测试模式下输出调试信息。
@see addLog
@var MOCK_MODE Boolean. 模拟模式. 值:0/1.
@key CFG_MOCK_MODE 符号文件,如文件存在则应用运行于模拟模式。
@key CFG_MOCK_T_MODE 符号文件,如文件存在且在测试模式下,应用运行于模拟模式。
对第三方系统依赖(如微信认证、支付宝支付、发送短信等),可通过设计Mock接口来模拟。
@see ExtMock
@key P_SESSION_DIR ?= $BASE_DIR/session 环境变量,定义session文件存放路径。
@key P_URL_PATH 环境变量。项目的URL路径,如"/jdcloud", 用于定义cookie生效的作用域,也用于拼接相对URL路径。
@see getBaseUrl
继承AppBase类,可实现提供符合BQP协议接口的模块。api_fw框架就是使用它的一个典型例子。
@see AppBase
包含app_fw.php的主文件(如api.php)所在目录。常用于拼接子目录名。
最后不带"/".
客户端应用标识,默认为"user".
根据URL参数"_app"确定值。
type的格式如"i:n:b?:dt:tm?".
@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=>"打蜡"]
]
@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
设置参数,其实是模拟客户端传入的参数。以便供tableCRUD等函数使用。
@see tableCRUD
将objarr格式转为table格式, 如:
objarr2table(
[
["id"=>100, "name"=>"A"],
["id"=>101, "name"=>"B"]
]
) ->
[
"h"=>["id", "name"],
"d"=>[
[100,"A"],
[101,"B"]
]
]
注意:
每行中列数可以不一样,这时可指定最少固定列数 $fixedColCnt, 而该列以后,将自动检查所有行决定是否加到header中。例:
objarr2table(
[
["id"=>100, "name"=>"A"],
["name"=>"B", "id"=>101, "flag_v"=>1],
["id"=>102, "name"=>"C", "flag_r"=>1]
], 2 // 2列固定
) ->
[
"h"=>["id", "name", "flag_v", "flag_r"],
"d"=>[
[100,"A", null,null],
[101,"B", 1, null],
[102,"C", null, 1]
]
]
@see table2objarr
@see varr2objarr
将table格式转为 objarr, 如:
table2objarr(
[
"h"=>["id", "name"],
"d"=>[
[100,"A"],
[101,"B"]
]
]
) -> [ ["id"=>100, "name"=>"A"], ["id"=>101, "name"=>"B"] ]
将类型 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"] ]
$cred为"{user}:{pwd}"格式,支持使用base64编码。
示例:
list($user, $pwd) = getCred(getenv("P_ADMIN_CRED"));
if (! isset($user)) {
// 未设置用户名密码
}
@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).
quote string
一般是把字符串如"abc"转成加单引号的形式"'abc'". 适用于根据用户输入串拼接成SQL语句时,对输入串处理,避免SQL注入。
示例:
$sql = sprintf("SELECT id FROM User WHERE uname=%s AND pwd=%s", Q(param("uname")), Q(param("pwd")));
@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);
执行查询语句,只返回一行数据,如果行中只有一列,则直接返回该列数值。
如果执行失败,返回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
执行查询语句,返回数组。
如果查询失败,返回空数组。
默认返回值数组(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
返回 $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
记录日志。
默认到日志文件 $BASE_DIR/trace.log. 如果指定type=secure, 则写到 $BASE_DIR/secure.log.
可通过在线日志工具 tool/log.php 来查看日志。也可直接打开日志文件查看。
@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
生成html格式的错误信息并中止执行。
默认地,只显示中文错误,双击可显示详细信息。
例:
errQuit(E_PARAM, "接口错误", "Unknown ac=`$ac`");
输出调试信息到前端。调试信息将出现在最终的JSON返回串中。
如果只想输出调试信息到文件,不想让前端看到,应使用logit.
@see logit
根据应用标识($APP)获取应用类型(AppType)。注意:应用标识一般由前端应用通过URL参数"_app"传递给后端。
不同的应用标识可以对应相同的应用类型,如应用标识"emp", "emp2", "emp-adm" 都表示应用类型"emp",即 应用类型=应用标识自动去除尾部的数字或"-xx"部分。
不同的应用标识会使用不同的cookie名,因而即使用户同时操作多个应用,其session不会相互干扰。
同样的应用类型将以相同的方式登录系统。
@see $APP
检查应用根目录下($BASE_DIR)下是否存在标志文件。标志文件一般命名为"CFG_XXX", 如"CFG_MOCK_MODE"等。
@param $internalMsg String. 内部错误信息,前端不应处理。
@param $outMsg String. 错误信息。如果为空,则会自动根据$code填上相应的错误信息。
抛出错误,中断执行:
throw new MyException(E_PARAM, "Bad Request - numeric param `$name`=`$ret`.", "需要数值型参数");
抛出该异常,可以中断执行直接返回,不显示任何错误。
例:API返回非BPQ协议标准数据,可以跳出setRet而直接返回:
echo "return data";
throw new DirectReturn();
例:返回指定数据后立即中断处理:
setRet(0, ["id"=>1]);
throw new DirectReturn();
应用框架,用于提供符合BQP协议的接口。
在onExec中返回协议数据;在onAfter中建议及时关闭DB.
用于单件类,提供getInstance方法,例:
class PluginCore
{
use JDSingleton;
}
则可以调用
$pluginCore = PluginCore::getInstance();
提供事件监听(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)
{
}
返回最后次调用的返回值,false表示中止之后事件调用
如果想在事件处理函数中返回复杂值,可使用$args传递,如下面返回一个数组:
$obj->on('getResult', 'onGetResult');
$out = new stdclass();
$out->result = [];
$obj->trigger('getArray', [$out]);
function onGetResult($out)
{
$out->result[] = 100;
}
调用外部系统(如短信集成、微信集成等)将引入依赖,给开发和测试带来复杂性。
筋斗云框架通过使用“模拟模式”(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
判断是否模拟某外部扩展模块。如果$extType为null,则只要处于MOCK_MODE就返回true.
@see getExt
获取外部依赖对象。一般用getExt替代更简单。
示例:
$sms = ExtFactory::instance()->getObj(Ext_SmsSupport);
@see getExt
用于取外部接口对象,如:
$sms = getExt(Ext_SmsSupport);
写日志到ext.log中,可在线打开tool/init.php查看。
(logit默认写日志到trace.log中)
@see logit
服务接口实现框架。
服务接口包含:
假设在文档有定义以下接口
用户修改密码
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 取可选参数,可指定缺省值。
@see AccessControl 对象型接口框架。
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
错误处理
@see MyException
中断执行,直接返回
@see DirectReturn
调试日志
可使用addLog输出调试信息而不破坏协议输出格式。
@see addLog
@see logit
@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
插件可以在构造函数__construct中初始化自身,例:
// 可选的初始化过程, 注意使用private,保持单例特性(Singleton)
private function __construct()
{
$this->trigger('init');
}
在应用初始化时(apiMain中),会创建所有插件类的实例(如果有的话)。
@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 ()
根据全局变量"SERVER_REV"或应用根目录下的文件"revision.txt", 来设置HTTP响应头"X-Daca-Server-Rev"表示服务端版本信息(最多6位)。
客户端框架可本地缓存该版本信息,一旦发现不一致,可刷新应用。
在conf.php中定义Conf类并继承ConfBase, 实现代码配置:
class Conf extends ConfBase
{
}
设置为false可关闭ApiLog. 例:
static $enableApiLog = false;
所有API执行时都会先走这里。
例:对所有API调用检查ios版本:
static function onApiInit()
{
$iosVer = getIosVersion();
if ($iosVer !== false && $iosVer<=15) {
throw new MyException(E_FORBIDDEN, "unsupport ios client version", "您使用的版本太低,请升级后使用!");
}
}
客户端初始化应用时会调用initClient接口,返回plugins等信息。若要加上其它信息,可在这里扩展。
例:假如定义应用初始化接口为(plugins是框架默认返回的):
initClient(app) -> {plugins, appName}
实现:
static function onInitClient(&$ret)
{
$app = mparam('app');
$ret['appName'] = 'my-app';
}
插件内部接口应继承该类, 它具有以下方法:
static function getInstance();
funcion on($eventName, $eventHandler);
funcion trigger($eventName, array $args = []);
@see JDSingleton JDEvent
@see plugin/index.php 插件配置
@see plugin.php 插件定义
%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)
@see plugin/index.php
@param $plugins ={ pluginName => {js, php, getInterface} }
获取插件类的实例,用于调用插件API。
假设有插件coupon, 一般可定义插件接口类 PluginCoupon如:
class PluginCoupon extends PluginBase
{
}
在插件内部,应直接调用 PluginCoupon::getInstance() 来获得插件实例。
在插件外部才调用本函数。
注意:应通过在plugin.php最后返回的插件配置中指定插件类:
[ 'class' => 'PluginCoupon' ]
主应用作为特殊模块,名称为'core', 对应类为 PluginCore.
对象型接口的入口。
也可直接被调用,常与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
对象型接口框架。
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
@var AccessControl::$allowedAc ?=["add", "get", "set", "del", "query"] 设定允许的操作,如不指定,则允许所有操作。
@var AccessControl::$readonlyFields ?=[] (影响add/set) 字段列表,添加/更新时为这些字段填值无效(但不报错)。
@var AccessControl::$readonlyFields2 ?=[] (影响set操作) 字段列表,更新时对这些字段填值无效。
@var
?= [] (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";
}
}
}
说明:
@var AccessControl::$vcolDefs (for get/query) 定义虚拟字段
常用于展示关联表字段、统计字段等。
在query,get操作中可以通过res参数指定需要返回的每个字段,这些字段可能是普通列名(col)/虚拟列名(vcol)/子对象(subobj)名。
例如,在订单列表中需要展示用户名字段。设计文档中定义接口:
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",
]
]
}
假设设计有“订单评价”对象,它会与“订单对象”相关联:
@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")
示例:管理端应用在查询订单时,需要订单对象上有一个原价字段:
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"],
]
];
}
除了使用子表, 对于简单的情况,也可以设计为将子表压缩成一个虚拟字段,在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"]
],
...
]
}
注意:计算字段,包括子表压缩字段都是很消耗性能的。
假设有张虚拟表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
]
];
}
注意:
@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}也可以返回子表,但目前不支持分页等操作。
@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之前运行,用于修改行中字段。
@fn AccessControl::onGenId () (for add) 指定添加对象时生成的id. 缺省返回0表示自动生成.
@fn AccessControl::getDefaultSort () (for query)取缺省排序.
@var AccessControl::$defaultSort ?= "t0.id" (for query)指定缺省排序.
示例:Video对象默认按id倒序排列:
class AC_Video extends AccessControl
{
protected $defaultSort = "t0.id DESC";
...
}
@var AccessControl::$defaultRes (for query)指定缺省输出字段列表. 如果不指定,则为 "t0.*" 加 default=true的虚拟字段
@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
假如要对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");
}
}
与上例相比,它不仅无须在数据库中创建视图,还也可以进行更新。
其要点是:
添加列或计算列.
注意:
@see AccessControl::addCond 其中有示例
@see AccessControl::addVCol 添加已定义的虚拟列。
@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'");
}
}
添加Join条件.
@see AccessControl::addCond 其中有示例
@param $col 必须是一个英文词, 不允许"col as col1"形式; 该列必须在 vcolDefs 中已定义.
@param $alias 列的别名。可以中文. 特殊字符"-"表示不加到最终res中(只添加join/cond等定义), 由addVColDef内部调用时使用.
@return Boolean T/F
用于AccessControl子类添加已在vcolDefs中定义的vcol. 一般应先考虑调用addRes(col)函数.
直接调用接口,返回数据。如果出错,将调用$GLOBALS['errorFn'] (缺省为errQuit).
@param $cleanCall Boolean. 如果为true, 则不使用现有的$_GET, $_POST等变量中的值。
@param $hideResult Boolean. 如果为true, 不输出结果。