官方文档 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

PhalApi
  • 社区
  • 下载
  • 文档
  • 书籍
  • 体验
  • 关于
  • 贡献
  • English

前言

  • 首页
  • 接口，从简单开始！

一、基础入门

开篇

  • [1.1] 下载与安装
  • [1.2] 创建一个自己的项目(含四个脚本命令的使用说明)
  • [1.3] 在线体验
  • [1.4] 文档、帮助和官网
  • [1.5] 对PhalApi框架的抉择

开发手册

  • [1.11] 快速入门: 接口开发示例(源码+图文)
  • [1.12] 参数规则：接口参数规则配置
  • [1.13] 统一的接口请求方式：?sevice=XXX.XXX
  • [1.14] 统一的返回格式和结构：ret data msg
  • [1.15] 数据库操作：基于NotORM的使用及优化
  • [1.16] 配置读取：内外网环境配置的完美切换
  • [1.17] 日记纪录：简化版的日记接口
  • [1.18] 快速函数：人性化的关怀
  • [1.19] DI服务速查：各资源服务一览表
  • [1.20] DB操作：数据库基本操作速查

更多技艺

  • [1.21] 类的自动加载：遵循PEAR包的命名规范
  • [1.22] 签名验证：自定义签名规则
  • [1.23] 请求和响应：GET和POST两者皆可得及超越JSON格式返回
  • [1.24] 缓存策略：更灵活地可配置化的多级缓存
  • [1.25] 国际化翻译：为走向国际化提前做好翻译准备
  • [1.26] 数据安全：数据对称加密方案
  • [1.27] 精益开发：更富表现力的Model层和重量级数据获取的应对方案
  • [1.28] COOKIE：对COOKIE原生态的支持及记忆加密升级版
  • [1.29] 开放与封闭：多入口和统一初始化
  • [1.30] 保持的力量：接口开发最佳实践
  • [1.31] 新型计划任务：以接口形式实现的计划任务
  • [1.32] 在线接口文档：注释规范
  • [1.33]-开发者工具：实用函数

二、高级专题

思想与应用

  • [2.11] 核心思想：Di依赖注入 让资源更可控
  • [2.12] 海量数据：可配置的分库分表
  • [2.13] 接口调试：在线sql语句查看与性能优化
  • [2.14] 测试驱动开发：意图导向编程下的接口开发
  • [2.15] 演进：新型计划任务续篇
  • [2.16] 领域驱动设计：应对复杂领域业务的Domain层
  • [2.17] 微服务：Api接口服务层
  • [2.18] 定制化：资源服务的再实现
  • [2.19] 扩展库：可重用的扩展类库
  • [2.20] 约定编程：架构明显的编程风格

工具与实践

  • [2.21] 服务器统一部署方案简明版
  • [2.22] 更多工具：精英项目和团队建设

三、扩展类库

  • [3.1] 扩展类库：微信开发
  • [3.2] 扩展类库：代理模式下phprpc协议的轻松支持
  • [3.3] 扩展类库：基于PHPMailer的邮件发送
  • [3.4] 扩展类库：优酷开放平台接口调用
  • [3.5] 扩展类库：七牛云存储接口调用
  • [3.6] 扩展类库：新型计划任务
  • [3.7]-扩展类库：微信小程序开发
  • [3.8] 扩展类库：用户、会话和第三方登录集成
  • [3.9] 扩展类库：swoole支持下的长链接和异步任务实现
  • [3.10] 扩展类库：Auth 权限扩展 (由@黄苗笋提供)
  • [3.11] 扩展类库：基于FastRoute的快速路由（支持RESTful）
  • [3.12] 扩展类库：基于PhalApi的DB集群拓展Cluster (由@喵了个咪提供)
  • [3.13] 扩展类库：基于PhalApi的Redis拓展 (由@喵了个咪提供)
  • [3.14] 基于PhalApi的图片上传拓展 (由@Summer)
  • [3.15] 基于PhalApi的第三方支付拓展 (由@Summer)
  • [3.16] PhalApi-Image -- 图像处理 (由@喵了个咪提供)

四、项目实践

  • [4.1] 开发实战1：利用扩展库进行微信服务号开发
  • [4.2] 开发实战2：模拟优酷开放平台接口项目开发
  • [4.3] 开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动）
  • [4.4] 开发实战4：一个可能的大型项目开发（多终端多版本）
  • [4.5] 落地应用框架：freeApi服务于互联网开发者的免费接口供应平台（由@转身.满目疮痍 提供，并正在致力开发完善中）

五、其他

  • [5.1] 架构与思想：Phal Api核心设计和思想解读
  • [5.2] 杂谈：扯一些phal Api的前世和今生
  • [5.3] 框架总结：术语表和PHP开发建议
  • [5.4] 许可
  • [5.5] 联系和加入我们
  • [5.6] 更新日记
  • [5.8] 致框架贡献者：加入PhalApi开源指南
  • 附录1：接口文档参考模板

六、SDK包

  • [6.1] 基于接口查询语言的SDK包
  • [6.2] SDK包（JAVA版）
  • [6.3] SDK包（PHP版）
  • [6.4] SDK包（object c版） 由@Aevit 提供
  • [6.5] SDK包（javascript版） 由@喵了个咪 提供
  • [6.6] SDK包（Ruby版）

七、教程

教程-入门篇

  • [7.1]-简单介绍以及环境搭建
  • [7.2]-把它玩起来
  • [7.3]-请求和返回
  • [7.4]-国际化高可用和自动生成文档
  • [7.5]-数据库操作和Model层
  • [7.6]-小技巧和浅谈API适用范围以及入门篇总结

教程-进阶篇

  • [7.7]-三层结构Api,Domain,和Model
  • [7.8]-DI依赖注入和单例模式
  • [7.9]-自动加载和拦截器
  • [7.10]-notrom进阶以及事务操作
  • [7.11]-数据库读写分离
  • [7.12]-解决大量数据存储数据库分表分库拓展
  • [7.13]-使用缓存以及用redis拓展解决实际问题
  • [7.14]-PhalApi能带来什么和进阶篇总结

教程-实战篇

  • [7.15]-Redis队列处理异步任务

八、视频教程

  • [8.1]-PhalApi视频教程

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号
Fork me on GitHub


接口，从简单开始！ | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

  • 文档
  • 书籍
  • 体验
  • 关于
  • 贡献
  • English

文档首页 下一章

编程于我们，犹如著书而存世。表达，从简单开始；代码，亦然。 -- 暗夜在火星

接口，从简单开始！

一个隐喻

假设我们有这样的表达式：

1 + 1 = 2

显然，这是简单明了而易于理解的。但倘若我们这样：

1 + (96 - 867 + 700 - 6 + 7 - 30/10 + 100  - 27) +  1 = 2

同样可以获得相同的结果，但表达却羞涩难懂。

你可能会偷笑：怎么可能会有人这样编写复杂又没用的代码呢？然而，如果你回顾一下曾经做过的项目或留意一下周围的代码，你会发现，这种情况是真实存在的。

当然，在不同领域的项目开发中，因各自需求不同各代码的编写也不尽相同。纵使这样，即使我们不能简化到最理想的状态，但至少我们可以通过努力以达到“编写人容易理解的代码”。一如这样：

1 + (0) +  1 = 2

PhalApi框架所做的

使用PhalApi框架进行项目开发，我们不能保证项目编写的代码一定会“短而美”，因为更多的代码编写来自于你双手的输入、来自你自己切身的思考和设计。但我们希望PhalApi可以在支持接口快速开发的基础上，为你和更多开发团队提供关于项目开发的一些技艺、参考和帮助。

所以，与其他很多关注服务器性能的框架不同， PhalApi更加关注的是人的心情、开发效率和团队合作 ，而这些正是通过测试驱动开发、自动化工具、持续集成和敏捷开发等途径来达成的 。

然而，这一切一切，都要从代码的编写开始。毕竟我们作为专业人士，代码是我们连接世界的媒介。而接口代码的编写，又应从简单开始。

泡一杯咖啡，让我们开始吧。

本框架和文档适用于

  1. 工匠：基于PHP语言的后台接口项目开发，特别是创业项目的快速迭代
  2. 探险者：进行定制和扩展，打造成公司内部产品簇的接口应用框架
  3. 架构师：专注框架的设计、思想和开源

文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.1]-下载与安装 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

  • 关于
  • 贡献
  • English

上一章 文档首页 下一章

我们的时代是一个问题时代，一个问题很大的时代。在科技的神奇影响下，一切都在经历着变革。如果我们愿意在生活中睁大双眼，每天都有问题等着我们去研究，去解决。
-- 教皇保罗六世，1969年5月18日 摘自《全球通史-从史前史到21世纪》

1.1.1 温馨提示

  • 请从release分支拉取稳定的代码
  • 推荐在Linux服务器上进行开发
  • 建议PHP >= 5.3.3

1.1.2 下载

你可以选择下面你喜欢的方式之一来下载 。

(1) 网页下载

进入 Git@OSC - PhalApi ，然后点击下载。
0403

(2)使用git

命令下，使用git命令快速拉取：

$ git clone https://git.oschina.net/dogstar/PhalApi.git

1.1.3 安装

安装如同其他的框架一样，将代码上传到到服务器后解压即可。下面是不同环境的参考：

(1)Windowns系统下的XAMPP

将PhalApi框架下载后，解压到xampp的htdocs目录下，如：

访问：

http://localhost/phalapi/Public/demo/

即可看到： QQ截图20150403225621

(2)Linux系统下的Nginx

将PhalApi框架下载并解压到指定目录，假设为： /home/apps/projects/ ，则先配置nginx：

server {
    listen 80;
    server_name dev.phalapi.com;
    root /home/apps/projects/PhalApi/Public;
    charset utf-8;
    access_log logs/dev.phalapi.com.access.log;
    error_log logs/dev.phalapi.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

然后，检测并重启nginx：

$ nginx -t
$ nginx -s reload

在本地绑定HOST：

#PhalApi小牛试刀
127.0.0.1 dev.phalapi.com

最后，访问默认接口服务： http://dev.phalapi.com/demo/ ，正常时会返回类如：

{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1422779027
    },
    "msg": ""
}

T_FUNCTION错误的处理方案

如果你当前的PHP环境不支持匿名函数，会报T_FUNCTION这个错误。这时，可这样处理：

先把匿名函数的写法换成直接new的方式，如：

 //数据操作 - 基于NotORM
DI()->notorm = function() {
    $debug = isset($_GET['debug']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

换成：

 //数据操作 - 基于NotORM
$debug = isset($_GET['debug']) ? true : false;
DI()->notorm = new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);

然后，在init.php文件最后添加以下代码：

 if (!class_exists('Closure')) {
    class Closure {
    }
}

即可解决。

但由于匿名函数是DI中所需要的重要语言特性，所以建议还是将PHP版本更新到5.3以上。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.2]-创建一个自己的项目 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

有梦想的人学习能力特别强，而且不拒绝任何新东西。 -- 《理想丰满》

1.2.1 可视化安装

将代码上传到服务器并解压后，访问： ./Public/install/ 下的安装向导，如在浏览器打开：

http://dev.phalapi.com/install/

然后按照提示，一步步操作即可：

(0)第零步：同意安装

(1)第一步：环境检查

(2)第二步：系统配置

(3)第三步：接口请求

(4)运行效果

1.2.2 手动创建

在Windows系统下，可以手动创建一个新的项目。

假设目录为： F:\xampp\htdocs\PhalApi
以下是简单的创建操作：

(1)新建项目目录

在./PhalApi目录下新建一个自己的项目目录，通常以项目名称命名，如项目MyApp对应：

然后，再分别在里面添加接口目录Api、领域目录Domain、数据源目录Model和通用目录Common，即：

(2)准备一个接口

全部的接口类文件需要放置到./Api 目录下，你可以尝试编写一个简单的Default.Index接口，也可以直接将./Demo/Api/Default.php文件复制过来。

(3)创建访问入口

将./Public/demo目录复制一份为对应项目名称小写的目录，如这里的myapp：

注意！这里需要修改./Public/myapp/index.php和./Public/myapp/checkApiParams.php中的目录：

(4)浏览器访问

在浏览器输入以下链接：

http://localhost/phalapi/Public/myapp/

应该可以看到：

1.2.3 自动生成

在Linux系统下，我们可以使用脚本快速生成一个项目结构，此过程只需要1分钟左右。

简明操作如下：

(1)使用phalapi-buildapp脚本

phalapi-buildapp脚本用于创建项目，进入 ./PhalApi/PhalApi/ 后，可以看到这个脚本。

不过，首先，需要为这个脚本添加执行权限：

$ chmod +x ./phalapi-buildapp

然后，调用脚本创建一个MyApp的项目：

$ ./phalapi-buildapp MyApp

OK!

(2)查看生成的项目及访问

回到 /path/to/PhalApi 目录，你会发现生成了一个MyApp目录，并且里面的文件如下：

$ cd MyApp/
$ tree
.
├── Api
│   └── Default.php
├── Common
├── Domain
├── Model
└── Tests
    ├── Api
    │   └── Api_Default_Test.php
    ├── Common
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php

9 directories, 4 files

在Public目录下，则会有myapp的访问入口：

└── myapp
    ├── checkApiParams.php
    └── index.php

在浏览器访问：

#根目录设置为./Public
http://dev.phalapi.com/myapp/

#或没设置根目录为./Public时访问
http://dev.phalapi.com/Public/myapp/

可以看到接口结果输出。

(3)执行单元测试

如果安装了PHPUnit，执行一下单元测试，也是通过的！

$ phpunit ./Tests/
PHPUnit 4.3.4 by Sebastian Bergmann.

...

Time: 7 ms, Memory: 5.75Mb

OK (3 tests, 13 assertions)

1.2.4 更多技巧

(1)日记目录需要0777权限

请确保日记目录Runtime拥有写入的权限，可以给0777 。

(2)脚本加执行权限和软链

对于脚本命令，需要添加执行权限和软链，便于日后使用：

$ cd /pah/to/PhalApi/PhalApi

$ chmod +x ./phalapi-buildapp
$ sudo ln -s /path/to/phalapi-buildapp /usr/bin/phalapi-buildapp

$ chmod +x ./phalapi-buildsqls
$ sudo ln -s /path/to/phalapi-buildsqls /usr/bin/phalapi-buildsqls

$ chmod +x ./phalapi-buildtest
$ sudo ln -s /path/to/phalapi-buildtest /usr/bin/phalapi-buildtest

(3)No such file or directory的解决方案

解决方案一：增加 /usr/bin/php软链

可以将php的执行路径追加到/usr/bin下，即类似：

ln -s /usr/local/php/bin/php /usr/bin/php

解决方案二：使用alias起别名

或者通过使用别名 alias 的方式来注册脚本命令：

# vim /etc/bashrc

#PhalApi - 自动生成SQL语句
alias phalapi-buildsqls='/usr/local/php/bin/php /path/to/PhalApi/phalapi-buildsqls'

需要注意的是，保存后，需要source一下，即：

# source /etc/bashrc

$ source /etc/bashrc

1.2.5 四个脚本命令使用说明

(1) 创建应用项目

如前文所述，可以通过phalapi-buildapp脚本来创建一个新的项目，其使用说明如下：

(2)生成单元测试骨架代码

当需要对某个类进行单元测试时，可使用phalapi-buildtest脚本生成对应的单元测试代码，其使用如下：

其中，

  • 第一个参数file_path：是待测试的源文件相对/绝对路径
  • 第二个参数class_name：是待测试的类名
  • 第三个参数bootstrap：是测试启动文件，通常是./test_env.php文件
  • 第四个参数author：你的名字，默认是dogstar

通常，可以先写好类名以及相应的接口，然后再使用此脚本生成单元测试骨架代码。
以Default.Index默认接口服务为例，需要为此生成测试骨架代码，可以依次这样操作：

$ cd /path/to/PhalApi
$ cd ./Demo/Tests
$ ../../PhalApi/phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php > ./Api/Api_Default_Test.php

最后，需要将生成好的代码，重定向保存到你要保存的位置。通常与产品代码对齐，并以“{类名} + _Test.php”方式命名。

(3)生成数据库建表SQL

当需要创建数据库表时，可以使用phalapi-buildsqls脚本结合配置文件dbs.php生成建表SQL，这个工具在创建分表时尤其有用，其使用如下：

其中，

  • 第一个参数dbs_config：是指向数据库配置文件./Config/dbs.php的路径，可以使用相对路径
  • 第二个参数table：是需要创建sql的表名，每次生成只支持一个
  • 第三个参数engine：（可选）是指数据库表的引擎，可以是：Innodb或者MyISAM

温馨提示：需要提前先将建表的SQL语句（除主键id和ext_data字段外）放置到./Data/目录下，文件名为：{表名}.sql。

如，我们需要生成10用户user_session表的的建表语句，那么需要添加数据文件./Data/user_session.sql（除主键id和ext_data字段外）：

      `user_id` bigint(20) DEFAULT '0' COMMENT '用户id',
      `token` varchar(64) DEFAULT '' COMMENT '登录token',
      `client` varchar(32) DEFAULT '' COMMENT '客户端来源',
      `times` int(6) DEFAULT '0' COMMENT '登录次数',
      `login_time` int(11) DEFAULT '0' COMMENT '登录时间',
      `expires_time` int(11) DEFAULT '0' COMMENT '过期时间',

然后，进入到项目根目录，执行命令：

$ php ./PhalApi/phalapi-buildsqls ./Config/dbs.php user_session

就会看到生成好的SQL语句了，类似：

CREATE TABLE `phalapi_user_session_0` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_1` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_2` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_3` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_4` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_5` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_6` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_7` (
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_8` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_9` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

最后，便可把生成好的SQL语句，导入到数据库，进行建表操作。

(4)生成接口代码 - V1.3.4及以上版本支持

当需要编写开发一个接口时，可以使用phalapi-buildcode脚本生成基本的Api、Domain和Model代码。此脚本不是很强悍的，项目可以根据自己的喜欢使用，或者修改定制自己的模板。其使用如下：

其中，

  • 第一个参数app_path：是指项目根目录到你的项目的相对路径
  • 第二个参数api_path：是需要创建接口的相对项目的相对路径，支持多级目录，可不带.php后缀
  • 第三个参数author：（可选）你的名字，默认为空
  • 第四个参数overwrite：（可选）是否覆盖已有的代码文件，默认为否

例如，我们要为Demo项目生成一个新的接口文件./AA/BB/CC.php，则可以：

$ cd /path/to/PhalApi
$ ./PhalApi/phalapi-buildcode Demo AA/BB/CC dogstar
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Api/AA/BB ...
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Domain/AA/BB ...
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Model/AA/BB ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Api/AA/BB/CC.php ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Domain/AA/BB/CC.php ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Model/AA/BB/CC.php ...

OK! AA/BB/CC has been created successfully!

可以看到生成的代码有：

访问接口：

最后，在线接口列表，可以看到：

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.3]-在线体验 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

片刻之欢愉，不如须臾之宁静。

1.3.1 在线体验

//默认的接口服务
http://demo.phalapi.net/
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.2.0",
        "time": 1443278491
    },
    "msg": ""
}

//带参数的示例接口
http://demo.phalapi.net/?service=Default.Index&username=oschina
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "oschina您好，欢迎使用PhalApi！",
        "version": "1.2.0",
        "time": 1443278475
    },
    "msg": ""
}

//故意请求一个非法的服务
http://demo.phalapi.net/?service=Demo.None
{
    "ret": 400,
    "data": [],
    "msg": "非法请求：服务Demo.None不存在"
}

1.3.2 在线接口调试

这里说的接口调试，更多是针对接口调用过程中所执行的SQL语句查看，如：

//带&__sql__=1可打印SQL语句
http://demo.phalapi.net/?service=User.getMultiBaseInfo&user_ids=1,2,3&__sql__=1

返回：
[1 - 0.00032s]SELECT * FROM tbl_user WHERE (id = ?); -- 1
[2 - 0.00019s]SELECT * FROM tbl_user WHERE (id = ?); -- 2
[3 - 0.00016s]SELECT * FROM tbl_user WHERE (id = ?); -- 3
{"ret":200,"data":{"code":0,"msg":"","list":[{"id":"1","name":"dogstar","note":"oschina"},{"id":"2","name":"aevit","note":"fami"},{"id":"3","name":"test","note":"phalapi"}]},"msg":""}

1.3.3 [酷！]接口参数在线查询

为了方便客户端查看最新的接口参数，特别提供此在线工具，根据接口代码实时生成接口参数报表，完全不需要后台开发编写维护额外的文档。我觉得，这很符合敏捷开发之道。

//接口参数在线查询工具链接
http://demo.phalapi.net/checkApiParams.php

如：http://demo.phalapi.net/checkApiParams.php ，访问效果如下：

因此，接口所需要的参数，对于接口开发人员，也只是简单配置一下参数规则，便可以轻松获取。
关于具体的参数规则配置和接口结果返回格式定义，后期会详细讲到。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.4]-文档、帮助和官网 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

创造性源于专注并且持久的思考，直至灵感来临。 -- 《快速软件开发(珍藏版)》

1.4.1 开发文档

开发文档，是由PhalApi团队精心编写的，意在给广大开发同学提供开发指导和使用帮助。最全和最新的开发文档，请查看这里：

http://www.phalapi.net/wikis/

1.4.2 在线类参考手册

此部分在线类参考手册，是使用phpdoc自动生成，支持移动端访问：

1.4.3 问题与反馈

我们建议使用issue来纪录来共享问题，你也可以通过其他方式来与我们进行沟通，以获得更多帮助。

1.4.4 官网

官网链接为：

http://www.phalapi.net/

支持移动端显示：

1.4.4 框架升级

最后，补充说明一下，大家较为关心的框架升级问题。

由于PhalApi框架的核心代码，已经在前期有意识地单独隔离了出来，即在 ./PhalApi/PhalApi 目录下。

所以，当后期项目需要升级框架时，直接替换此目录下全部的文件即可。为了保险起见，在升级前，你可以做好备份工作。
我们会争取100%做到框架的向前兼容。如果升级后有什么 问题，可随时与我们联系。

对此，项目不应该直接针对核心部分进行改动，而是在其他地方放置，以免影响框架升级。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.10]-对PhalApi框架的抉择 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

人类一思考，上帝就偷笑了。 -- 《下一个倒下的会不会是华为》

1.10.1 对PhalApi框架的抉择

能使用框架来进行项目开发，和知道为何使用此框架进行项目开发明显不同。

对框架的选择，名义上是架构师的职责，但对于充满好奇心和有着更强求知欲的开发人员来说，同样应该给予关注。

之所以选择一个框架进行项目开发，明显可以减少很多不必要的重复编码，但更深层次，则是可以减少开发周期、统一开发规范、降低项目风险，不致于项目失控。

所以，在决定使用PhalApi框架前，对于此框架是否适用于即将启动的项目进行一番思索，是大有裨益的。通过 推定框架 ，有助于避免因采用不当的框架而对项目造成不必要的阻力。

1、适用的场景和项目

海量数据和移动App

此框架特别适用于现在各种 移动App项目的后台接口开发，以及服务器间的后台接口开发，同时具有应对 海量数据 的能力。并且还可以挂靠多个项目，也可以很好地支持多个终端、开放不同的入口。

同时提供了可以很好应对海量数据的解决方案：如没有提供图片上传的代码或工具而希望开发人员将图片上传到CDN，支持大数据的存储，以及后台计划任务。

HTTP协议和JSON格式

基于此，我们采用了主流设计，即将框架设计成使用HTTP协议并JSON格式返回结果的接口请求，因为这能为大众熟悉并潜意识接受。 通过规范的接口调用和返回，有利于客户端和后台开发人员的关系融洽。

2、敏捷开发和快速交付

虽然框架和工程实践间没有必然的联系，但这其中有一些微妙的关系。

一种代码编写的方式会形成一种开发的风格；一种开发风格会奠定一个团队的合作氛围；一个团队的合作氛围会决定项目交付的质量。

而PhalApi所提倡和希望做到的正是通过让后台接口开发更简单以让后台开发人员心情更愉悦，从而为客户端提供高质量稳定的接口以支撑更多优质App的开发。并且，结合重构、测试驱动开发和持续集成等，可以让你的项目如虎添翼，在快速交付的同时，体验编码开发的乐趣。

3、约束和关注

架构约束程序。

约束有时会让开发者处处受阻。但好的约束能够统一规范而不致于项目代码凌乱不堪，也不会轻易地允许低级开发新手犯下一些本可避免的BUG。正如，我们都讨厌等待红绿灯，但我们必须肯定它对交通和生命安全保障的作用。

4、复杂领域业务的应对和解决方案

正如前面说到的，我们关注在海量数据下为移动App提供稳定的接口，我们提倡敏捷开发下的快速交付。所以我们去掉与接口开发无关的功能，没有提供视图渲染和模板解析的操作。

但只是这样而已吗？

不！我们还关注对复杂领域业务的应对和解决方案。

如果我们PhalApi框架所关注的，也是你们项目所关注的，那么我们有理由相信PhalApi能为你的项目带来很多友好的约束和贴心的帮助。

5、框架的性能

很多框架都要强调其能提供的性能，然后PHP本身就是动态的脚本语言，要想提高项目的运行速度，就是要进行减法，即减少不必要的PHP代码。但是前面强调性能的框架则做了与其承诺矛盾的做法：为框架增添了很多项目可能不需要用到的功能。明显地就是一系列既定的执行流程和侦听事件、回调、调度等等。
而这些，势必会对性能有所影响，特别当应用项目不需要这些特性却又不能定制简化时，框架所谓的强悍功能会适得其反。

对于这一块，我们则提供了极大的空间。因为，入口的文件，由你指定。除了一些必要的加载外，很多都可以支持自定义和定制化。

而且我们也使用autobench进行了压力测试和通过xhprof进行了性能剖析，证明PhalApi框架在性能上确实如我们预料的那样 -- 快！

6、PhalApi的成熟度、部署与学习成本

成就度

不可否认，PhalApi还是太年轻了。

PhalApi正式开源于2015年1月，但是我们在努力完善中并致力于有助于快速开发的后台接口。我们“减少不必要的创新”，我们坚持恒星一般的接口并很好地支持框架升级， 我们尽量提供优秀的文档和及时的沟通帮助。

更为重要的是，这是一个不只为框架而框架的PhalApi框架。

如果在开发过程中对所遇到的问题而框架不能很好解决和支持时，可以考虑改写框架，将某一目标或属性提升至架构，其他团队成员则无须再编写重复代码去重复实现。因为，你会发现，在PhalApi下，你可以轻松做到这一点： 小步快跑下的浮现式设计 。

部署

在Nginx下，你可以快速部署此框架。

以下是为初学者参考的Nginx配置：

server {
    listen       80;
    server_name  demo.phalapi.com;

    root   /home/apps/projects/demo.phalapi.com/Public;

    charset utf-8;

    access_log  logs/demo.phalapi.com.access.log;
    error_log  logs/demo.phalapi.com.error.log;

    location / {
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include        fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

除此之前，我们已经对框架的结构目录进行了很好地划分。包括配置文件、日记文件、框架代码、多个项目挂靠、不同的入口开放以及多App的支持等等。

在结合Phing进行一键发布时，你会发现，项目的部署发布同样方便快捷。

学习成本

这块学习成本是不可避免的，毕竟PhalApi是一个新的框架，很多开发同学之前都没有接触过。

但我们尽量站在后台开发同学的角度，用心地设计每一个接口，编写每一行代码。在源代码里面倾注了必要的注释和使用示例，并同步提供了详细的文档说明和实战代码，以便快速入门上手。

更为重要的是，我们都是尽可能按照主流惯例来设计接口规范，以便各位同学可以快速上手，就仿佛这个框架是出自你的设计一样。

1.10.2 何时可以采用PhalApi？

我们致力于提供可以快速开发后台接口的框架，并希望能对你的项目有实际的帮助。所以，我们不会鼓吹PhalApi框架，因为没有可以适用于任何领域、任何实际项目的框架。也就是没有所谓的银弹，也没有一种放之四海而皆准的方法，起码你可能需要作一些调整。

而且，我们也不会赞同你不加考虑就采用PhalApi框架，因为如果此框架不利于你项目的开发，甚至有反作用，这显然有悖于我们的初心：为你的项目提供实质的帮助。因此，在采用PhalApi框架前，你应该稍微查看一下我们的文档，和相关的源代码。以下是一些初步的建议。

 1、架构无关：如果你的只是小项目，无需要过多考虑涉及大规模决策、各模块及元素、属性间的微妙关系，即架构无关时。
PhalApi和其他框架一样，也能满足小项目的开发需求。

 2、专注架构：如果你需要专注框架，则更需要理解你项目将面临的约束、风险和需要满足的质量属性。
然后评估PhalApi是否能统一这此约束、降低风险并满足这些质量属性。若能，则用之；若不能，则另取之。

 3、提升架构：最后，如果你发现PhalApi拥有不断演进的能力并有助于浮现式设计（我们也正是如此设计和研发的），并且愿意
将PhalApi框架提升到你们产品线的后台接口框架，那么你将能为你的团队、项目乃至公司提供一个更优质的框架，并会为之感到兴奋和骄傲。
当然，“能力越大，责任也就越大”。但我们作为程序员，不正是希望能以已之所学，服务于人吗？

1.10.3 何时不应该使用PhalApi？

正如其他负责任的开源框架一样，我们在说明PhlaApi框架适用场景的同时，也应该指明不应该使用PhalApi的时机。其中包括但不限于：

  • 需要开发CLI项目时（但已提供支持命令行项目开发的CLI扩展类库）；
  • 需要开发网站项目，即有界面展示和视图渲染（但已提供支持视图渲染的View扩展类库）；
  • 对数据严谨性要求高，如金融行业的相关项目，毕竟PHP是弱类型语言；

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.11]-快速入门(backup) | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

一个真正的强者，不是摆平了多少人，而是他能帮助到多少人。 --开源中国源创会分享广州站 @海洋之心-悟空

1.11.1 模拟开发：获取开源中国用户信息接口

首次使用此接口开发框架时，可以先查看此开发示例。

假设，我们需要为开源中国打造一个平放平台，其中有一个接口是可以根据用户ID来获取用户的基本信息。

本文，就以模拟获取开源中国用户信息接口开发（即：从数据库获取用户的基本信息并以JSON格式返回给客户端）为例，简明的接口开发中的流程，以及用到的基本功能和操作，主要包括有：统一入口文件、参数规则配置、接口层/领域层/模型持久层、日记纪录、数据库操作、配置读取、国际化翻译等。

以给大家一个感观的认识。

最终接口的调用与返回结果如下：

//接口请求格式
http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=帐号ID

//返回结果格式
{
    "ret": 200,
    "data": {
        "code": 0,    //状态码，0表示正常获取，1表示用户不存在
        "msg": "",
        "info": {      //用户信息
            "id": "1",    //用户ID
            "name": "dogstar",   //帐号
            "note": "oschina"   //来源
        }
    },
    "msg": ""
}

1.11.2 开发流程

(1) 统一入口文件

为了更好保护我们的项目代码，建议将接口的访问路径设置在：./PhalApi/Pubic目录，并且各套接口（如按版本分：v1/v2/v3等等；按不同终端分：ios/android/pc等）各自独立入口。所以本示例中将./PhalApi/Pubic/demo/index.php下。

参数入口文件的写法，我们可以快速得到基本的入口文件如下：

// $ vim ./Public/demo/index.php

<?php
/**
 * Demo 统一入口
 */

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()->loader->addDirs('Demo');

/** ---------------- 响应接口请求 ---------------- **/

$server = new PhalApi();
$rs = $server->response();
$rs->output();

此外，我们还需要一个公共的初始化文件：

//$ vim ./Public/init.php

<?php
/**
 * 统一初始化
 */

/** ---------------- 根目录定义，自动加载 ---------------- **/

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

/** ---------------- 注册&初始化服务组件 ---------------- **/

//自动加载
DI()->loader = $loader;

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

//调试模式，$_GET['__debug__']可自行改名
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

//翻译语言包设定
SL('zh_cn');

(2)TDD测试驱动开发

遵循最佳实践，我们在编写代码前先编写单元测试。但同时为了减少编写测试代码的痛苦，我们可以先定义接口函数签名，再通过脚本自动生成测试代码骨架来提高我们的开发效率。

//$vim ./Demo/Api/User.php

<?php

class Api_User extends PhalApi_Api
{
    public function getBaseInfo()
    {
    }
}

通过脚本生成测试骨架：

$ mkdir -p ./Demo/Tests/Api/
$ cd ./Demo/Tests/Api
$ php ../../../PhalApi/build_phpunit_test_tpl.php ../../Api/User.php Api_User ./../test_env.php

然后，根据/Public/demo/index.php入口文件，也搭建一个测试环境的入口文件：

vim ./Demo/Tests/test_env.php

修正一下Api_Examples_User_Test.php里，测试环境test_env.php的包含路径：

//手动调用test_env.php的路径
require_once dirname(__FILE__) . '/../test_env.php';

并且，修改测试，以符合我们通过userId=1获取基本信息（名字为dogstar，来源为oschina）：

//$vim ./Demo/Tests/Api/Api_User_Test.php

    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        $str = 'service=User.GetBaseInfo&user_id=1';
        parse_str($str, $params);

        DI()->request = new PhalApi_Request($params);

        $api = new Api_User();
        //自己进行初始化
        $api->init();
        $rs = $api->getBaseInfo();

        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('msg', $rs);
        $this->assertArrayHasKey('info', $rs);

        $this->assertEquals(0, $rs['code']);

        $this->assertEquals('dogstar', $rs['info']['name']);
        $this->assertEquals('oschina', $rs['info']['note']);
    }

此时，单元测试是预期失败的：

$ phpunit ./Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

F

Time: 3 ms, Memory: 5.25Mb

There was 1 failure:

1) PhpUnderControl_ApiUser_Test::testGetBaseInfo
Failed asserting that a NULL is not empty.

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

(3)Api接口层

此接口层，主要是负责响应客户端的请求，调用需要的领域层进行必要的服务功能提供。

配置参数规则

为了获取到用户ID，我们可以在getRules()函数里面定义参数规则，以便框架自动帮我们过滤获取需要的参数。

温馨提示：
接口层的全部类成员函数都应以小写开头。
但对外，函数首字母不区分大小写，因为框架会将请求的函数强制转换成小写再执行。

原因在于：
1、我们坚持驼峰法的代码风格；
2、对外界我们可以统一使用大写来提供服务名称，如：User.Login，这样更显专业。

//$vim ./Demo/Api/User.php

<?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'getBaseInfo' => array(
                'userId' => array('name' => 'user_id', 'type' => 'int', 'min' => 1, 'require' => true),
            ),
        );
    }

    //...

如上，我们就定义了getBaseInfo接口中的user_id参数，对应的变量名字为userId，整型，最小值为1，必须。

接口实现

//$vim ./Demo/Api/User.php

    public function getBaseInfo()
    {
        $rs = array('code' => 0, 'msg' => '', 'info' => array());

        $domain = new Domain_User();
        $info = $domain->getBaseInfo($this->userId);

        if (empty($info)) {
            DI()->logger->debug('user not found', $this->userId);

            $rs['code'] = 1;
            $rs['msg'] = T('user not exists');
            return $rs;
        }

        $rs['info'] = $info;

        return $rs;
    }

(4)Domain领域层

领域层主要是关注复杂业务的处理，以及缓存的处理、耗时操作后台异步处理等，并调用Model持久层获取需要的数据。因此，是Api与Model层之间的桥梁。

在此示例中，我们只需要简单地调用Model层获取用户的信息即可，再加强一下用户ID的合法性判断。

//$ vim ./Demo/Domain/User.php

<?php

class Domain_User
{
    public function getBaseInfo($userId)
    {
        $rs = array();

        $userId = intval($userId);
        if ($userId <= 0) {
            return $rs;
        }

        $model = new Model_User();
        $rs = $model->getByUserId($userId);

        return $rs;
    }
}

(5)Model持久层

此一层主要关注数据从持久存储的获取，特别是针对数据库的操作，但不排除其他媒介，如文件、缓存等。

首先，先准备一下我们需要的表和数据：

CREATE TABLE `phalapi_test`.`tbl_user` (
  `id` INT NOT NULL,
  `name` VARCHAR(45) NULL,
  `note` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

INSERT INTO `phalapi_test`.`tbl_user` (`id`, `name`, `note`) VALUES ('1', 'dogstar', 'oschina');

然后，编写需要的Model代码，即利用NotORm实现对数据的操作：

//$ vim ./Demo/Model/User.php

<?php

class Model_User
{
    public function getByUserId($userId)
    {
        return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
    }
}

(6)单元测试通过啦！

在完成上面简单的开发后，我们即可以实现接口的开发，运行一下刚才的单元测试，完美通过！

$ phpunit ./Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

SELECT * FROM tbl_user WHERE (id = ?); -- 1

.SELECT * FROM tbl_user WHERE (id = ?); -- 1<br />

Time: 34 ms, Memory: 6.50Mb

OK (2 tests, 7 assertions)

在单元测试的保证下，我们便可以放心大胆地将我们的代码发布到外网，提供给更多的开发者，和终端用户使用。

(7)数据库配置

因为是单元测试，所以我们配置搭建了新的一个测试环境，特别对于数据库的配置，如下：

//$ vim ./Config/dbs.php

<?php
/**
 * examples配置
 */

return array(
    /**
     * DB数据库服务器集群
     */
    'servers' => array(
        'db_demo' => array(
            'host'      => '192.168.0.104',         //数据库域名
            'name'      => 'phalapi_test',          //数据库名字
            'user'      => 'root',                  //数据库用户名
            'password'  => '123456',                //数据库密码
            'port'      => '3306',                  //数据库端口
        ),
    ),

    /**
     * 自定义路由表
     */
    'tables' => array(
        '__default__' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
            ),
        ),
    ),
);

温馨提示：
为了方便在单元测试时进行调试，和查看日记，对于全部查询、执行的SQL语句都会显示出来，全部的日记改用控制台输出。

(8)最终接口调用

接口调用的链接，这时已经相当明了了，即：域名 + 路径（/demo） + 参数（可从单元测试那直接获取）

如本示例的是：

http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=1

返回的结果是：

{
    "ret": 200,
    "data": {
        "code": 0,
        "msg": "",
        "info": {
            "id": "1",
            "name": "dogstar",
            "note": "oschina"
        }
    },
    "msg": ""
}

截图效果：
show

温馨提示：
如果提示日记写入失败，请确保./Runtime目录具有写入权限，即0777。

1.11.3 更多简明的使用

(1)日记纪录

当我们访问一个不存在的用户时，将会触发日记纪录：

DI()->logger->debug('user not found', $this->userId);

如访问：

http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=2

然后，可以在Runtime下看到按天分目录的日记：

$ tailf ./Runtime/log/201501/20150128.log
2015-01-28 00:37:34|DEBUG|user not found|2

温馨提示：
外网环境上，请把Runtime目录软链到磁盘空间很大的路径。

(2)国际化翻译

当需要翻译时，可以使用人性化的函数T()，如：

$rs['msg'] = T('user not exists');

对应地需要补充翻译的内容：

//$ vim ./Language/zh_cn/common.php

    'user not exists' => '用户不存在',

还是以上面的用户不存在为例，看下运行的截图效果： show

(3)配置读取

配置的读取，使用方便，直接通过以下方式便可以获取，以点号分割：

DI()->config->get('dbs')

第一段，必须为文件名，后面的为用点号相连的数组下标，不限级。

(4)[酷！]接口参数在线查询

为了方便客户端实时查看最新的接口参数，这里提供了一个快速的在线工具：

http://demo.phalapi.net/checkApiParams.php?service=User.GetBaseInfo

用浏览器打开后，即可以看到最新的接口参数说明， 不需要后台接口开发人员编写文档维护，直接从代码中生成参数报表：

1.11.4 Demo相关代码文件

从上面可以得到，此示例相关的代码如下：

Demo$ tree
.
├── Api
│   ├── Default.php
│   └── User.php
├── Domain
│   └── User.php
├── Model
│   └── User.php
└── Tests
    ├── Api
    │   ├── Api_Default_Test.php
    │   └── Api_User_Test.php
    └── test_env.php

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.12]-参数规则：接口参数规则配置 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

“播下一种思想，收获一种行为；播下一种行为，收获一种习惯；播下一种习惯，收获一种性格；播下一种性格，收获一种命运。” --《成君忆：水煮三国》

1.12.1 参数解析

参数，对于接口来说，是非常重要的输入。对于外部调用来说，同等重要。

因此，对于参数这块，我们是希望能够既减轻后台开发对接口参数获取、判断、验证、文档编写的痛苦；又便于客户端方便的、自由的调用；既利已又利他。

由此，我们引入了 参数解析 这一概念，即：通过配置参数的规则，即可自动实现参数的获取和验证。

1.12.2 参数解析的配置规则

熟悉Yii的同学，对于以下的规则配置应该倍感亲切，但是不熟悉的同学也可以同样快速上手。因为，你会慢慢发现，这样的规则很符合我们PHP开发的规范，如果没有，我们继续努力改进。

格式如下：

array(
    '参数名' => array('name' => '接口参数名称', 'type' => '类型', 'default' => '默认值', ...),
    ... ...
)

1.12.3 示例

(1)简单的示例

假设这样的业务场景，我们需要提供一个用户登录的接口，其中需要用户名和密码，因此：

<?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'login' => array(
                'username' => array('name' => 'username'),
                'password' => array('name' => 'password'),
            ),
        );
    }

    public function login()
    {
        return array('username' => $this->username, 'password' => $this->password);
    }
}

当我们这样调用接口时：

/?service=User.Login&username=test&password=123456

就可以获取到需要的参数：

{"ret":0,"data":{"username":"test","password":"123456"},"msg":""}

从中，可以很容易理解：参数规则需要统一配置在接口实现类里面的 getRules() 函数，随后即可以通过类成员属性方式获取，如： $this->username 。

(2)更完善的示例

很多时候我们都会对用户名和密码作一些验证，如是否必须、长度、最值，以及默认值等。

继续上面的业务场景，我们登录下用户名和密码必须，且密码长度至少为6个字符，则可以调整参数规则：

'login' => array(
   'username' => array('name' => 'username', 'require' => true),
   'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),

尝试一下非法的参数请求，如无任何参数的情况下，访问/?service=User.Login，返回：

{"ret":400,"data":[],"msg":"Illegal Param: wrong param: username"}

再尝试一下密码长不对的情况，访问/?service=User.Login&username=test&password=123，返回：

{"ret":400,"data":[],"msg":"Illegal Param: password.len should >= 6, but now password.len = 3"}

1.12.4 三级参数

(1)系统参数

已被系统固定占有的参数，目前只有一个，即：service，为需要调用的服务，类型为字符串，格式为：XXX.XXX，首字母不区分大小写，建议统一以大写开头。

以下是一些示例：

#推荐写法
/?service=User.GetBaseInfo

#正确写法（开头小写）
/?service=user.getBaseInfo
#正确写法（方法名小写，但类名只能开头小写，否则会导致linux系统下文件加载失败）
/?service=user.getbaseinfo

#错误写法（缺少方法名）
/?service=User
#错误写法（缺少点号分割）
/?service=UserGetBaseInfo
#错误写法（默认只支持点号分割）
/?service=User|GetBaseInfo

(2)应用参数

应用参数是指在一个项目中，全部接口都需要的参数，或者通用的参数规则。假如我们的项目中全部需要签名sign参数，且必须；以及非必须的版本号，则可以在./Config/app.php中的apiCommonRules配置：

//$vim ./Config/app.php
<?php

return array(
    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' => array(
        //签名
        'sign' => array(
            'name' => 'sign', 'require' => true,
        ),
        //客户端App版本号，如：1.0.1
        'version' => array(
            'name' => 'version', 'default' => '',
        ),
    ),

    ... ...

(3)接口参数

接口参数即为上面在各个接口子类中配置的规则，为特定接口所持有。同时，为了方便同一套接口的规则重用，可以使用下标为 '*' 表示是本接口通用规则，如我们为了加强安全性，为全部的用户接口操作都加上4位的验证码：

    public function getRules()
    {
        return array(
            '*' => array(
                'code' => array('name' => 'code', 'require' => true, 'min' => 4, 'max' => 4),
                ),
            'login' => array(
                'username' => array('name' => 'username', 'require' => true),
                'password' => array('name' => 'password', 'require' => true, 'min' => 6),
            ),
        );
    }

在完成对上面的应用参数规则、接口通用规则和指定规则的参数进行配置后，对用户登录的接口进行请求时就需要这样访问：

/?service=User.login&sign=77f81c17d512302383e5f26b99dae4d9&username=test&password=123456&code=abcd

温馨提示： 在Api类里面配置规则时，下标不区分大小写。因为框架会自动将请求的函数名和全部的规则下标转换成小写进行匹配。

这里，再小结一下，接口参数可以分为两种： 通用接口参数 和 指定接口参数 。前者用 * 号下标表示，后者则用函数名作为下标表示。

(4)多个参数规则的优先级

当同一个参数规则分别在应用参数、接口通用参数及特定接口参数出现时，后面的规则会覆盖前面的，即具体化的规则会替换通用的规则，以保证接口在特定场合的定制性。

简而言之，多个参数规则的优先级从高到下，分别是（正如你想到的那样）：

  • 1、指定接口参数
  • 2、通用接口参数
  • 3、应用参数
  • 4、系统参数（通常忽略，因为只有service）

1.12.5 在线接口参数查询工具

为了便于理解上面全部的参数规则，对于具体接口调用的要求，这里可以使用在线接口参数查询工具在浏览器访问查看：

/demo/checkApiParams.php?service=User.Login

可以看到：
show

此工具同时也可以方便客户端实时查看接口文档时，进行辅助的接口规则说明。

自描述数据

这里值得一提的是，我们这里所定义的参数规则实际上也是自描述数据。即配置的代码真实同步反映了参数的相关属性。

1.12.6 参数传递的方式

系统下GET和POST皆可，但是推荐：

  • 1、service参数以GET方式传递，接口统一以/?service=XXX.XXX链接请求，便于交流，更重要的是当接口发生问题时，可以快速在服务器上通过nginx日记定位问题；

  • 2、其他参数以POST方式传递，特别对于敏感数据，如密码，以相对保护数据安全；

  • 3、在编写文档，或者进行调试时，可以全部临时使用GET方式，如本文档的写法，同时在浏览器时也可以使用GET；

  • 4、如果需要对数据包进行加密或者压缩、自定义参数格式，可以重载PhalApi_Request::genData()，然后再继续使用参数规则解析；

1.12.7 参数规则

类型type | 参数名称 name | 是否必须require | 默认值default | 最小值&最大值min&max | 更多
字符串 | string | true/false，默认false | 应为字符串 | 可选 | regex下标为正则匹配的规则；format下标可用于定义字符编码的类型，如utf8、gbk,gb2312
整数 | int | true/false，默认false | 应为整数 | 可选 | ---
浮点数 | float | true/false，默认false | 应为浮点数 | 可选 | ---
布尔值 | boolean | true/false，默认false | true/false | --- | 以下值会转换为true： ok, true, success, on, yes, 1
时间戳/日期 | date | true/false，默认false | 会按格式转换 | 可选，仅当为timestamp时才判断 | 格式：format 为timestamp时会将字符串的日期转换
数组 | array | true/false，默认false | 为非数组会自动转换/解析成数组 | 可选，判断数组元素个数 | 格式：format 为explode时，会根据separator将字符串分割成数组, 为json时，会json解析
枚举 | enum | true/false，默认false | 应为range中的某个元素 | --- | 必须，range,以数组指定枚举的范围
文件 | file | true/false，默认false | 数组类型 | min和max表示文件大小范围 | range下标表示允许上传的文件类型，ext表示需要过滤的文件扩展名
回调 | callable/callback | true/false，默认false | --- | --- | callable/callback设置回调函数，params为回调函数的第三个参数，第一个为参数值，第二个为所配置的规则

温馨提示：
全部的参数规则，都可以配置desc下标，对应在线接口文档的”说明“部分。
如： array('name' => 'username', 'desc' => '用户名')

使用source配置为单个参数指定数据源

此外，还有一个特殊的配置，可以用于指定当前单个参数的数据来源。即：source配置。如指定用户在登录时，用户名使用$_GET、密码使用$_POST。

public function getRules() {
    return array(
        'login' => array(
            'username' => array('name' => 'username', 'source' => 'get'),
            'password' => array('name' => 'password', 'source' => 'post'),
        ),
    );
}

这样，PhalApi框架就会从$_GET中提取username参数，从$_POST中提取password参数。

目前支持的source与对应的数据源映射关系如下：

source | 对应的数据源
post | $_POST
get | $_GET
cookie | $_COOKIE
server | $_SERVER
request | $_REQUEST
header | $_SERVER['HTTP_X']

温馨提示：source配置需要PhalApi 1.4.0及以上版本支持。

下面是对各类型的示例说明。

(1)字符串 string

当一个参数规则 未指定类型时，默认为string。一个完整的写法可以为：

array('name' => 'username', 'type' => 'string', 'require' => true, 'default' => 'nobody', 'min' => 1, 'max' => 10)

若传递的参数长度过长，如&username=alonglonglonglongname，则会异常失败返回：

{"ret":400,"data":[],"msg":"Illegal Param: username.len should <= 10, but now username.len = 21"}

但是当需要验证的是类型是中文的话会出现一点问题一个中文字符会占用3个字节所以在min和max验证的时候会出现一些问题,PhalApi提供了format方式对你需要验证长度的string进行指定格式可以排除此问题

array('name' => 'username', 'type' => 'string','format' => 'utf8', 'min' => 1, 'max' => 10)

对于正则表达式的验证，一个邮箱的例子是：

'email' => array(
    'name' => 'email',
    'require' => true,
    'min' => '1',
    'regex' => "/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i",
    'desc' => '邮箱',
),

(2)整型 int

如通常数据库中的id，即可配置成：

array('name' => 'id', 'type' => 'int', 'require' => true, 'min' => 1 )

当传递的参数，不在其配置的范围内时，如&id=0，则会异常失败返回：

{"ret":400,"data":[],"msg":"Illegal Param: id should >= 1, but now id = 0"}

(3)浮点 float

浮点型，类似整型的配置，此处略。

(4)布尔值 boolean

布尔值，主要是可以对一些字符串转换成布尔值，如ok, true, success, on, yes, 以及会被PHP解析成true的字符串，都会转换成true，方便调用。如通常的是否记住我：

array('name' => 'isRememberMe', 'type' => 'boolean', 'default' => true)

(5)日期 date

日期可以按自己约定的格式传递，可以这样配置：

array('name' => 'registerData', 'type' => 'date')

对应地，risterData=2015-01-31 10:00:00则会被获取到为："2015-01-31 10:00:00"。

当需要将字符串的日期转换成timestamp时，则配置成：

array('name' => 'registerData', 'type' => 'date', 'format' => 'timestamp')

则上面的参数再请求时，则会被转换成：1422669600。

(6)数组 array

很多时候在接口进行批量获取时，都需要提供一组参数，所以这时可以使用数组来进行配置。如：

array('name' => 'uids', 'type' => 'array', 'format' => 'explode', 'separator' => ',')

对应&uids=1,2,3则会被转换成：

array ( 0 => '1', 1 => '2', 2 => '3', )

又如接口需要使用JSON来传递整块参数时，可以这样配置：

array('name' => 'params', 'type' => 'array', 'format' => 'json')

对应&params={"username":"test","password":"123456"}则会被转换成：

array ( 'username' => 'test', 'password' => '123456', )

特别地，当配置成了数组，却未指定格式format时，会转换成一个元素的数组，如：&name=test，会转换成：array('test')。

(7)枚举 enum

在需要对接口参数进行范围限制时，可以使用此枚举型。如对于性别的参数，可以这样配置：

array('name' => 'sex', 'type' => 'enum', 'range' => array('female', 'male'))

当传递的参数不合法时，如&sex=unknow，则会被拦截，返回失败：

{"ret":400,"data":[],"msg":"Illegal Param: sex should be in female\/male, but now sex = unknow"}

关于枚举类型的配置，这里需要特别注意配置时，应尽量使用字符串的值。
因为通常而言，接口通过GET/POST方式获取到的参数都是字符串的，而如果配置规则时指定范围用了整型，会导致底层规则验证时误判。如：

//接口参数为： &type=N

//接口参数规则为：
array('name' => 'type', 'type' => 'enum', 'range' => array(0, 1, 2))

//误判，因为：
var_dump(in_array('N', array(0, 1, 2))); //结果为true，因为 'N' == 0

为了避免这类情况发生，应该这样配置：

//接口参数规则为（使用字符串）：
array('name' => '&type', 'type' => 'enum', 'range' => array(`0`, `1`, `2`))

(8)文件 file

在需要对上传的文件进行过滤、接收和处理时，可以使用文件类型，如：

array(
    'name' => 'upfile',
    'type' => 'file',
    'min' => 0,
    'max' => 1024 * 1024,
    'range' => array('image/jpeg', 'image/png') ,
    'ext' => array('jpeg', 'png')
)

其中，min和max分别对应文件大小的范围，单位为字节；range为允许的文件类型，使用数组配置，且不区分大小写。

如果成功，返回的值对应的是$_FILES["upfile"]，即会返回：

array(
     'name' => '',
     'type' => '',
     'size' => '',
     'tmp_name' => '',
)

对应的是：

  • $_FILES["upfile"]["name"] - 被上传文件的名称
  • $_FILES["upfile"]["type"] - 被上传文件的类型
  • $_FILES["upfile"]["size"] - 被上传文件的大小，以字节计
  • $_FILES["upfile"]["tmp_name"] - 存储在服务器的文件的临时副本的名称
  • $_FILES["upfile"]["error"] - 由文件上传导致的错误代码

若需要配置默认值default选项，则也应为一数组，且其格式应类似如上。

其中,ext是对文件后缀名进行验证,当如果上传文件后缀名不匹配时将抛出异常。文件扩展名的过滤可以类似这样进行配置：

//单个后缀名 - 数组形式
'ext' => array('jpg')

//单个后缀名 - 字符串形式
'ext' => 'jpg'

//多个后缀名 - 数组形式
'ext' => array('jpg', 'jpeg', 'png', 'bmp')

//多个后缀名 - 字符串形式（以英文逗号分割）
'ext' => 'jpg,jpeg,png,bmp'

(9)回调 callable / callback

当需要利用已有函数进行自定义验证时，可采用回调参数规则，如：

//配置规则
array('name' => 'version', 'type' => 'callable', 'callback' => array('Common_MyVersion', 'formatVersion'))

然后，回调时将调用下面这个函数：

//新增一个自定义的版本检测函数
class Common_MyVersion {

    public static function formatVersion($value, $rule) {
        if (count(explode('.', $value)) < 3) {
            throw new PhalApi_Exception_BadRequest('版本号格式错误');
        }

        return $value; // 最后要返回参数值
    }
}

温馨提示：回调函数的签名为：function format($value, $rule, $params)， 第一个为参数值，第二个为所配置的规则，第三个参数为配置规则中的params（可忽略）。通过验证后，最后必须返回参数值。

由于自 PHP 5.4 起可用 callable 类型指定回调类型 callback。所以，为了减轻记忆的负担，这里使用 callable或者callback均可，即可以配置成：'type' => 'callable',，也可以配置成：'type' => 'callback',，回调函数的选项也一样。

温馨提示：关于回调类型，更多请参考Callback / Callable 类型。

1.12.8 关于参数设计的原则

(1)通配的$_REQUEST

使用$_REQUEST获取参数，便于在不同场合下GET/POST之间的切换，同时在初始化DI()->request服务时，可以指定传递的参数，以便于灵活的单元测试；

(2)更自由的名称映射

之所以没把规则配置的下标默认成与客户端传递的name一致，是为了更自由的名称映射；
如可能我们PHP后台开发喜欢用驼峰法来表示，但客户端想用下划线来分割，则通过这样配置：

array(
    'isRememberMe' => array('name' => 'is_remember_me', 'type' => 'boolean', 'default' => true),
)

更重要的是，有时我们希望能缩短客户端请求的参数名称以节省流量时，可以这样配置：

array(
    'isRememberMe' => array('name' => 're', 'type' => 'boolean', 'default' => true),
)

(3)异常返回

对于客户端参数不合法时，以异常失败返回，而不是隐性地转换，是因为后台接口往往需要手动对传递的参数进行人工的验证，而不是希望得到隐性转换的值。即当客户端参数传递不对时，我们需要明确提示说：参数非法。

1.12.9 扩展你的参数

当PhalApi提供的参数规则不能满足接口参数的规则验证时，除了使用callable类型进行扩展外，还可以扩展PhalApi_Request_Formatter接口来定制项目需要的类型。

一如既往，分两步：

  • 1、扩展实现PhalApi_Request_Formatter接口
  • 2、在DI注册你的类型

下面以大家所熟悉的邮件类型为例，说明扩展的步骤。

首先，我们需要一个实现了邮件类型验证的功能类：

<?php

class Common_MyFormatter_Email implements PhalApi_Request_Formatter {

    public function parse($value, $rule) {
        if (!preg_match('/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/', $value)) {
            throw new PhalApi_Exception_BadRequest('邮箱地址格式错误');
        }

        return $value;
    }
}

然后，注册一下：

DI()->_formatterEmail = 'Common_MyFormatter_Email';

温馨提示： 在DI中手动注册服务时，名称的格式为： 下划线("_") + 统一前缀（"formatter") + 参数类型（全部小写后，首字母大写）；
若需要实现自动注册，扩展的类名格式须为：

class PhalApi_Request_Formatter_{类型名称} implements PhalApi_Request_Formatter { ...

系统已自动注册的格式化服务有：

  • _formatterArray 数组格式化服务
  • _formatterBoolean 布尔值格式化服务
  • _formatterCallable 回调格式化服务
  • _formatterDate 日期格式化服务
  • _formatterEnum 枚举格式化服务
  • _formatterFile 上传文件格式化服务
  • _formatterFloat 浮点数格式化服务
  • _formatterInt 整数格式化服务
  • _formatterString 字符串格式化服务

至此，便可使用自己定制的类型规则了，

array('name' => 'user_email', 'type' => 'email')

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.13]-统一的接口请求方式：_sevice=XXX.XXX | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

有句话说得好，最可怕的事情不是别人比你优秀，而是优秀的人竟然还比你更努力。 --《考拉小巫的留学成长日记》

此篇章主要是讲述接口统一请求的方式，以及提供一个PHP实现的简单客户端。

1.13.1 指定接口服务：?service=XXX.XXX

我们统一固定用service参数来表示需要请求获得的服务，并通过GET方式传递，即请求的URI格式为：

接口域名 + 入口路径 + ?service=XXX.XXX

如：
http://dev.phalapi.com   +   /demo/  +  ?service=User.GetBaseInfo

当我们在浏览器以GET方式请求时，可以在nignx看到这样的日记：

127.0.0.1 - - [07/Feb/2015:22:46:46 -0800] "GET /demo/?service=User.GetBaseInfo&sign=&user_id=1 HTTP/1.1" 200 107 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:11.0) Gecko/20100101 Firefox/11.0"

如果通过接口用POST方式请求，则会看到：

127.0.0.1 - - [07/Feb/2015:19:32:05 -0800] "POST /demo/?service=User.GetBaseInfo&sign= HTTP/1.1" 200 135 "-" "-"

这里service的名称，开头不区分大小写，建议统一以大写开头，以显得专业。对应的接口是：

class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }
}

其他传递接口服务名称的方式

虽然在这里，我们统一使用?service=XXX.XXX的格式来传递接口服务名称，但如果项目有需要，也可以采用其他方式来传递，如使用熟悉的斜杠分割：?service=XXX/XXX，或者使用省略方法名的方式而在服务端自动补全默认方法，又或者使用其他参数名称，如使用?s=XXX.XXX。

如果需要采用其他传递接口服务名称的方式，则可以重载PhalApi_Request::getService()方法。以下是改用斜杠分割、并换用r参数名字的实现示例：

<?php
class Common_Request extends PhalApi_Request {

    public function getService() {
        // 优先返回自定义格式的接口服务名称
        $servcie = $this->get('r');
        if (!empty($servcie)) {
            return str_replace('/', '.', $servcie);
        }

        return parent::getService();
    }
}

实现好自定义的请求类，需要在入口文件./Public/项目/index.php进行注册：

//装载你的接口
DI()->loader->addDirs('Demo');

DI()->request = new Common_Request();

这样，请求接口服务的方式便可以变成这样了。

原来的方式 | 现在的方式
?service=Default.Index | ?r=Default/Index
?servcie=User.GetBaseInfo | ?r=User/GetBaseInfo

这里有几个注意事项：

  • 1、需要转换为原始的接口服务格式，即：XXX.XXX
  • 2、为保持兼容性，子类需兼容父类的实现。即在取不到自定义的接口服务名称参数时，应该返回原来的。

温馨提示：以上定制功能，需要PhalApi 1.4.0及以上版本，方可支持。

1.13.2 统一参数用GET

在一个项目中，会有很多公共的接口参数，如客户端、版本号、密钥等。这些同样可以纳入GET参数里面，或者也可以放到POST里面。

温馨提示：
这样要求是有目的的，因为这样的话可以在nginx的access日记里面查看来自客户端的快照信息，以便统计或者定位问题。

1.13.3 接口参数用POST

特别地，接口参数我们建议统一使用POST方式传递，理由很简单：

  • 1、相对保护上传的数据，一如密码；
  • 2、避免特殊字符或者过大数据包在GET下的限制；

1.13.4 获取Header头信息

每个请求都有自己的Header头,获取Header头信息可以判断请求来源或者是把一些敏感的内容存放到Header头信息中(如:加密后的参数),框架也提供了方便快捷获取Header的方法:

DI()->request->getHeader("Host");

1.13.5 测试下的模拟参数

默认地，PhalApi框架会将$_REQUEST作为接口参数的来源：

DI()->request = 'PhalApi_Request';

当我们需要统一强制用$_GET，可以在init.php文件中这样简单定制：

DI()->request = new PhalApi_Request($_GET);

同样，也可以在init.php文件中强制用$_POST：

DI()->request = new PhalApi_Request($_POST);

在测试环境下，为了模拟接口请求，我们需要人工提供接口参数，因此可以这样轻松模拟：

$str = 'service=User.GetBaseInfo&user_id=1';

parse_str($str, $params);
DI()->request = new PhalApi_Request($params);

1.13.6 PHP接口客户端示例

先看下调用和使用的代码示例：

<?php
require_once './PhalApiClient.php';

$config = array(
    'host' => 'http://dev.phalapi.com/demo',
    'secrect' => '******'
);

$client = new PhalApiClient($config);
$rs = $client->request('User.GetBaseInfo', array('userId' => 1));

if ($client->getRet() == PhalApiClient::RET_OK) {
    var_dump($rs);
} else {
    var_dump($client->getMsg());
    var_dump($client->getUrl());
}

附调用接口的客户端源代码：

//$ vim ./PhalApiClient.php
<?php

class PhalApiClient
{
    protected $host;
    protected $secrect = '';

    protected $params = array();

    protected $moreParams = array();

    protected $url;
    protected $ret;
    protected $msg;
    protected $data = array();

    const RET_OK = 'OK';
    const RET_WRONG = 'WRONG';
    const RET_ERROR = 'ERROR';

    public function __construct($config)
    {
        $this->host = rtrim($config['host'], '/') . '/';
        $this->secrect = $config['secrect'];
    }

    public function request($service, $params = array(), $timeoutMs = 3000)
    {
        if (!empty($service)) {
            $this->params['service'] = $service;
        }
        $this->params['sign'] = $this->encryptAppKey($params, $this->secrect);
        $this->url = $this->host . '?' . http_build_query($this->params);
        $this->moreParams = $params;

        $rs = $this->doRequest($this->url, $params, $timeoutMs);

        if ($rs === false) {
            $this->ret = self::RET_ERROR;
            $this->msg = '后台接口请求超时';
            return $this->getData();
        }

        $rs = json_decode($rs, true);

        if (isset($rs['data']['code']) && $rs['data']['code'] != 0) {
            $this->ret = self::RET_WRONG;
            $this->msg = '接口调用失败[code =' . $rs['data']['code'] . ']' . ', 错误>信息：' . isset($rs['data']['msg']) ? $rs['data']['msg'] : '无';
            return $this->getData();
        }

        $this->ret = intval($rs['ret']) == 200 ? self::RET_OK : self::RET_WRONG;
        $this->data = $rs['data'];
        $this->msg = $rs['msg'];

        return $this->getData();
    }

    public function getRet()
    {
        return $this->ret;
    }

    public function getData()
    {
        return $this->data;
    }

    public function getMsg()
    {
        return $this->msg;
    }

    public function getUrl()
    {
        return $this->url . '&' . http_build_query($this->moreParams);
    }

    protected function encryptAppKey($params, $secrect)
    {
        return '';
    }

    protected function doRequest($url, $data, $timeoutMs = 3000)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $timeoutMs);

        if (!empty($data)) {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }

        $rs = curl_exec($ch);

        curl_close($ch);

        return $rs;
    }
}

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.14]-统一的返回格式和结构：ret-data-msg | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

表达，从简单开始。--《Robin Williams：写给大家看的设计书》

1.14.1 统一返回的格式

很明显地，默认情况下，我们选择了 JSON 作为统一的格式返回接口结果。这里简单说明一下选取JSON统一返回的原因：

  • JSON当前很流行，且普通接口都采用此格式返回
  • JSON在绝大部分开发语言中都支持，跨语言

  • JSON在浏览器浏览时，有可视化插件支持，如FF下：

1.14.2 统一返回结构

通常，我们正常情况下请求接口会返回类似：

{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1423142802
    },
    "msg": ""
}

其中，ret表示为返回状态码，200表示成功；data为领域业务数据，由接口自定义；最后msg为错误的提示信息。下面分别解释之。

(1)返回状态码 ret

参照HTTP的状态码，特约定：

  • 200：接口正常请求并返回
  • 4XX：客户端非法请求
  • 5XX：服务器运行错误

200 正常返回

当返回200时，需要同时返回data部分数据，以便客户端实现所需要的业务功能。

4XX 客户端非法请求

此类请求是由客户端不正确调用引起的，如请求的接口服务不存在，或者接口参数不对，验证失败等等。当这种情况发生时，客户端同学只需要调整修正调用即可。

对于此系统的状态码，在进行接口开发时，可由项目自已定义约定。 通常地，我们需要告知客户端签名失败时，可以这样：

throw new PhalApi_Exception_BadRequest('wrong sign', 1);

即抛出PhalApi_Exception_BadRequest异常即可，错误信息会返回客户端，对应msg字段；状态为1，系统对此类的异常会在400基础上相加的，即： 401 = 400 + 1 。

5XX 服务器运行错误

此类错误是应该避免的，但当客户端发现有这种情况时，应该知会后台接口开发人员进行修正。
如当配置的参数规则不符合要求时，或者获取了不存在的参数等即会触发此类异常错误，通常由框架抛出。

(2)业务数据 data

data为接口和客户端主要沟通对接的数据部分，可以为任何类型，由接口自定义。但为了更好地扩展、向后兼容，建议都使用array。

返回格式的定义与在线查看

当我们在开发接口时，可以通过为接口添加注释的方式来定义接口的返回格式，然后就可以为外部提供在线文档的实时查看了。

如：

<?php

class Api_User extends PhalApi_Api {

    /**
     * 获取用户基本信息
     * @desc 用于获取单个用户基本信息
     * @return int code 操作码，0表示成功，1表示用户不存在
     * @return object info 用户信息对象
     * @return int info.id 用户ID
     * @return string info.name 用户名字
     * @return string info.note 用户来源
     * @return string msg 提示信息
     */
    public function getBaseInfo() {
        // ... ...
    }

然后在浏览器访问：
http://demo.phalapi.net/checkApiParams.php?service=User.getBaseInfo

可以看到：
apic

注释格式

格式是以docs的 return 注释来标明的，其格式为：

@return  返回的类型  字段名字路径(以点号连接)  字段名字及解析

其中，返回的类型可以为：

关键字 | 说明
string | 字符串
int | 整型
float | 浮点型
boolean | 布尔型
date | 日期
array | 数组
fixed | 固定值
enum | 枚举类型
object | 对象

温馨提示：array与object的区别
array是指没有下标的一个数组集合，或者有下标但下标是连续的自然数，且各元素的结构相同；object则是指一个结构体，类似字典。

此外，为了明确数组与对象间的返回格式，我们也推荐如果是元素来自数组，则在返回字段的后面添加方括号来表明，以提醒客户端在接收到此类返回时需要循环处理。如：

     * @return array list 用户列表
     * @return int list[].id 用户ID
     * @return string list[].name 用户名字
     * @return string list[].note 用户来源

当需要对接口进行更多说明时，可使用@desc注释，即：

     * @desc 用于获取单个用户基本信息

(3)错误信息 msg

当返回状态码不为200时，此字段不为空。即当有异常（如上面所说的客户端非法请求和服务端运行错误两大类）触发时，会自动将异常的错误信息作为错误信息msg返回。

但对于服务端的异常，出于对接口隐私的保护，框架在错误信息时没有过于具体地描述；相反，对于客户端的异常，由会进行必要的说明，以提醒客户端该如何进行调用调整。

此外，我们根据需要可以考虑是否需要进行国际化的翻译。如果项目在可预见的范围内需要部署到国外时，提前做好翻译的准备是很有帮助的。如下，开发时可以这样返回异常错误信息：

throw new PhalApi_Exception_BadRequest(T('wrong sign'), 1);

(4)头部信息 header

当使用的是HTTP/HTTPS协议时，并且需要设置头部header时，可以使用PhalApi_Response::addHeaders()进行设置。对于JSON格式的返回默认设置的头部有：

Content-Type:"application/json;charset=utf-8"

若需要其他的头部设置，可类似这样，在init.php初始化文件进行设置跨域访问（但通常不建议允许全部跨域，这里仅作演示）：

DI()->response->addHeaders('Access-Control-Allow-Origin', '*');

后面相同的头部信息会覆盖前面的。

(5)将ret状态码转换成相应的Http状态码输出

默认情况下，PhalApi返回的Http状态码都为200，不管ret值为多少。当需要将Http状态码与ret结果状态码时对应时，可以这样使用：

$api = new PhalApi();
$rs = $api->response();

// 在输出前，调整Http状态码
$rs->adjustHttpStatus()->output();

然后，PhalApi将会根据ret值自动转换。

当抛出4xx系列异常时，如：

throw new PhalApi_Exception_BadRequest('无访问权限', 3); // 403 = 400 + 3

会得到以下输出：

HTTP/1.1 403 Forbidden

{
    "ret": 403,
    "data": [],
    "msg": "非法请求：无访问权限"
}

若希望在状态码不为200情况下也返回业务数据data，或者不想使用抛出异常的方式但同样也能返回业务数据data，这时，可以手动设置ret状态码，例如：

class Api_Default extends PhalApi_Api {
    public function index() {
        DI()->response->setRet(403);
        return array('name' => 'PhalApi');
    }
}

会得到以下输出：

HTTP/1.1 403 Forbidden

{
    "ret": 403,
    "data": {
        "name": "PhalApi"
    },
    "msg": ""
}

若还需要追加msg错误提示信息，可以使用setMsg()方法，如这样：

DI()->response->setRet(403)->setMsg('缺少访问权限');

请注意，手动设置的状态码和错误信息，当抛出异常时，PhalApi会以所抛出异常的错误码和错误信息为准。

温馨提示：根据ret调整Http状态，需要PhalApi 1.4.1 及以上版本支持。

1.14.3 关于Exception类异常没捕捉的原因

我们没有对Exception类的异常进行捕捉，封装返回非200的形式，是因为我们出于以下的考虑：

  • 一来为了方便开发过程中快速发现及定位具体出错的位置；
  • 二来为了便于线上环境中nginx服务器对错误的捕捉和纪录；

1.14.4 JsonP格式和其他的返回

在部分H5页面异步请求的情况下，客户端需要我们返回JSONP格式的结果，则可以这样在入口文件重新注册response：

if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

但是在测试环境中，我们是不希望有内容输出的，所以我们可以测试时这样注册response：

DI()->response = 'PhalApi_Response_Explorer';

1.14.5 扩展你的返回格式

当你的项目需要返回其他格式时，如返回XML，则可以先这样实现你的格式类：

class MyResponse_XML extends PhalApi_Response {

    protected function formatResult($result) {
        //TODO：把数组$result格式化成XML ...
    }
}

随后，也是简单重新注册一下即可：

DI()->response = new MyResponse_XML();

1.14.6 各状态码产生的时机

1.14.7 更好地建议

很多时候，很多业务场景，客户端在完成一个接口请求并获取到所需要的数据后，需要进行不同的处理的。

  • 就登录来说，当登录失败时，可能需要知道：
  • 是否用户名不存在？
  • 是否密码错误？
  • 是否已被系统屏蔽？
  • 是否密码错误次数超过了最大的重试次数？
  • ...

显然，这里也有一个返回状态码，更准备来说，是业务操作状态码。并且，此类的状态依接口不同而不同，很难做到统一。

SO？

我们建议的是，项目接口在业务数据data里面统一再定义一个状态码，通常为code字段，完整路径即： data.code ，同时为0时表示操作成功，非0时为不同的失败场景。如上面的登录：

  • code = 0 登录成功
  • code = 1 用户名不存在
  • code = 2 密码错误
  • code = 3 系统已屏蔽此账号
  • code = 4 密码错误次数超过了最大的重试次数
  • ...

最后，客户端在获取到接口返回的数据后，先统一判断ret是否正常请求并正常返回，即ret = 200；若是，则再各自判断操作状态码code是否为0，如果不为0，则提示相应的文案并进行相应的引导，如果为0，则走正常流程！

1.14.8 领域特定设计与Fiat标准

在《RESTful Web APIs》一书中提及到，标准可以划归到4个分类，分别是：fiat标准、个人标准、公司标准以及开放标准。

显然，我们这里推荐的是 JSON + ret-data-msg 返回格式既不是个人标准，也不是公司标准（就笔者观察的范筹而言，未发现某个公司定义了此格式）。而且，也不属于开放标准，因为也还没达到此程度。更多的，它是fiat标准。
我们很容易发现，身边的应用、系统以及周围项目都在使用诸如此类的返回结构格式，如一些AJAX的接口。

当然，我们可希望可以消除语义上的鸿沟，以便在后台接口开发上有一个很好地共识。

同时，JSON + ret-data-msg 返回格式也是一种领域特定的格式，它更多是为app多端获取业务数据而制作的规范。虽然它不是很完美，不具备自描述消息，也没有资源链接的能力，但我们认为它是一种恰到好处的格式。
在基于JSON通用格式的基础上，加以 ret-data-msg 的约束，它很好地具备了统一性，可能门槛低，容易理解。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.15]-数据库操作：基于NotORM的使用及优化 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

有些裁缝只会塞塞衬里，修修补补，而有些裁缝却能做出新东西--他们之间隔着一条深渊。 -- 《外套》

1.15.1 NotORM官网

这里使用了NotORM进行DB操作，具体的数据库操作使用文档请见NotORM官网： http://www.notorm.com

(1) 基本CURD

//查询
$row = DI()->notorm->user->where('id', 1)->fetch();

//更新
$data = array('name' => 'test', 'update_time' => time());
DI()->notorm->user->where('id', 1)->update($data);

//插入（须是同一个对象才能正确获取插入的ID）
$data = array('name' => 'phalapi');
$userORM = DI()->notorm->user;
$userORM->insert($data);
$id = $userORM->insert_id();

//删除
DI()->notorm->user->where('id', 1)->delete();

(2)update相同的数据的判断

在使用update操作时，如果更新的数据和原来的一样，则会返回0（影响0行）。这时，会和更新失败（同样影响0行）混淆。

但NotORM是一个优秀的类库，所以提供了优秀的解决文案。我们在使用update时，只须了解这两者返回的结果的微妙区别即可。
因为失败异常时，返回false；而相同数据更新会返回0。即：

  • 1、update相同的数据时，返回0，严格来说是：int(0)
  • 2、update失败时，如更新一个不存在的字段，返回false，即：bool(false)

用代码表示，就是：

$rs = DI()->notorm->user->where('id', $userId)->update($data);

if ($rs >= 1) {
    //成功
} else if ($rs === 0) {
    //相同数据，无更新
} else if ($rs === false) {
    //更新失败
}

以下单元测试代码，可以验证上面的判断：

    public function testUpdateOk()
    {
        $userId = 87;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('reg_time' => time()));

        $this->assertSame(1, $rs);
    }

    public function testUpdateZero()
    {
        $userId = 1;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('username' => 'aevit'));

        $this->assertSame(0, $rs);
    }

    public function testUpdateFail()
    {
        $userId = 1;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('wrong_username' => 'aevit'));

        $this->assertSame(FALSE, $rs);
    }

(3)简单的关联查询

如果是简单的关联查询，可以使用NotORM支持的写法，这样的好处在于我们使用了一致的开发，并且能让PhalApi框架保持分布式的操作方式（注意，关联的表仍然需要在同一个数据库）。

以下是一个简单的示例。

假设我们有这样的数据：

INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');
INSERT INTO `phalapi_user_session_0` VALUES ('1', '1', 'ABC', '', '0', '0', '0', null);

那么对应关联查询的代码如下面：

    public function testLeftJoin()
    {
        $rs = DI()->notorm->user_session_0
            ->select('expires_time, user.username, user.nickname')
            ->where('token', 'ABC')
            ->fetchRow();

        var_dump($rs);
    }

运行一下，我们可以看到这样的输出：

SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;

.[1 - 0.06318s]SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;<br>
array(3) {
  ["expires_time"]=>
  string(1) "0"
  ["username"]=>
  string(35) "wx_edebc877070133c65161d00799e00544"
  ["nickname"]=>
  string(10) "weixinName"
}

这样，我们就可以实现关联查询的操作。按照NotORM官网的说法，则是：

If the dot notation is used for a column anywhere in the query ("$table.$column") then NotORM automatically creates left join to the referenced table. Even references across several tables are possible ("$table1.$table2.$column"). Referencing tables can be accessed by colon: $applications->select("COUNT(application_tag:tag_id)").

->select('expires_time, user.username, user.nickname')这一行调用将会【自动产生关联操作】，而ON 的字段，则是这个字段关联你配置的【表结构】，外键默认为： 表名_id 。

(4)加1操作

NotORM已提供了NotORM_Literal，其用法如下：

DI()->notorm->user->where('id', 1)->update(array('age' => new NotORM_Literal("age + 1")));

当需要更新为当前时间，可以：

$array = array(
    "title" => "NotORM",
    "author_id" => null,
    "created" => new NotORM_Literal("NOW()"),
);

1.15.2 NotORM的优化

但为了更符合项目的开发，这里对NotORM的底层作了升级修改，以下为主要修改点和新的使用：

(1)将原来返回的结果全部从对象改成数组

对原来的大部分使用无特别影响，可按原来的方式开发。主要目的是为了更方面处理返回的数据，以及简化对结果的再解析，简单明了。
如：

DI()->notorm->user->where('username = ?', 'dogstar')->fetch();

返回的将是一个数组：

array(7) {
  ["id"]=>
  string(3) "180"
  ["username"]=>
  string(17) "dogstar"
  ["regtime"]=>
  string(10) "1414811954"
  //...
}

(2)提供获取全部结果的接口 - fetchAll() / fetchRows()

如：

$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchAll();

或：

$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchRows();

即可获取全部的数据，不再受限于分页。
这里提供了fetchAll()和fetchRows()两种等效的操作，是为了减少记忆的痛苦，下同。

(3)提供更灵活的查询方式 - queryAll() / queryRows()

当需要进行复杂的SQL查询时，可以使用此接口，如：
(注意:limit替换值:start和:num必须使用int类型)

$sql = 'select * from example AS ep LEFT JOIN user AS u ON ep.ui
d = u.id  where ep.touid = :userId ORDER BY dateline desc LIMIT :start,:num';
$params = array(':userId' => $userId, ':start' => $start, ':num' => $num);
$rs= DI()->notorm->example->queryAll($sql, $params);

或：

$rs= DI()->notorm->example->queryRows($sql, $params);

(4)limit 操作的调整

取消了NotORM中对OFFSET关键字的使用，改用逗号的写法，修改后正确的使用方法应该是：

$table->limit(10);  // limit 10   # 查询前10个

$table->limit(5, 10); // limit 5,10   # 从第5个位置开始，查询前10个

(5)禁止全表删除，防止误删

出于对数据的保护，当执行删除操作却又没有任何where条件时，将会禁止进行全表操作。如：

    public function testDeleteAll()
    {
        DI()->notorm->user->delete();
    }

可以看到：

$ phpunit --filter testDeleteAll ./Api/Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

E

Time: 315 ms, Memory: 6.25Mb

There was 1 error:

1) PhpUnderControl_ApiUser_Test::testDeleteAll
Exception: sorry, you can not delete the whole table --dogstar

(6)添加& __sql__ =1请求参数，可开启HTTP调试模式

当处于debug模式时，可以输入执行的全部SQL语句，以便调试。

如：

SELECT times FROM tpl_user_session_10 WHERE (user_id = ?); -- '74110'
{"ret":0,"data":{"code":0},"msg":""}

(7)关于NotORM中fetch()操作没有limit 1的处理方案 - fetchOne() / fetchRow()

之前，有开发同学提及到，为什么notorm的基类fetch为啥没用limit(1)呢。后来，我去发了下NotORM的写法，确实做得很微妙。
其实NotORM之所以没有在fetch()里面自动limit 1是因为，你可以循环地获取数据，如：

$user = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->limit(3);
while(($row = $user->fetch())) {
    var_dump($row);
 }

但是，更多情况下，我们只需要获取某一行的数据，上面的做法会造成不必要的SQL查询。为了保留原来的写法，我特意添加扩展了一个新的操作：fetchRow()，用法同fetch()，但只会取第一条。
以下是使用示例：

$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow());

var_dump($rs);

//结果输出类如：
array(3) {
  ["id"]=>
  string(1) "1"
  ["username"]=>
  string(5) "aevit"
  ["nickname"]=>
  string(4) "test"
}

//对应执行的SQL语句：
[2 - 0.06544s]SELECT id, username, nickname FROM fami_user WHERE (id > ?) LIMIT 1; -- 0<br>

如果，我们只需要获取这一行的某个字段，也可以像fetch()那样使用，即：

$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow('nickname'));

var_dump($rs);

//结果输出类如：
string(4) "test"

//纪录不存在时，返回 bool(false)

(8)显式的SQL语法异常提示

很多时候，在开发时，我们对数据库操作一开始会存在一些SQL语法的问题，PDO会返回false,且原来NotORM也是使用 静默方式 来处理这类错误，从而使得开发人员有时难以发现这些问题，除非将调试的SQL手动放到数据库执行才能发现问题所在。

为了能给开发同学更早、更直观的方式查看问题的所在，这里对NotORM底层进行了调整，使用了 显式方式 的策略来处理，即：直接抛出PDO异常。

如：

$userId = 1;

//OK
$rs = DI()->notorm->user->select('username')->where('id', $userId)->fetchOne();

//WRONG
$rs = DI()->notorm->user->select('wrong_username')->where('id', $userId)->fetchOne();

将会看到：

[1 - 0.06437s]SELECT username FROM fami_user WHERE (id = 1) LIMIT 1;<br>
[2 - 0.06496s]SELECT wrong_username FROM fami_user WHERE (id = 1) LIMIT 1;<br>

PDOException: Unknown column 'wrong_username' in 'field list'

(9)复杂的关联查询

如果是复杂的关联查询，则是建议使用原生态的SQL语句，但我们仍然可以保持很好的写法，如这样一个示例：

        $sql = 'SELECT t.id, t.team_name, v.vote_num '
            . 'FROM phalapi_team AS t LEFT JOIN phalapi_vote AS v '
            . 'ON t.id = v.team_id '
            . 'ORDER BY v.vote_num DESC';
        $rows = $this->getORM()->queryAll($sql, array());

注意，此时的表需要使用全名，即自带前缀。这样也可以实现更自由的关联查询。

(10)事务操作

关于事务操作，可以参考 NotORM官网 的说明：

$db->transaction = $command Assign 'BEGIN', 'COMMIT' or 'ROLLBACK' to start or stop transaction

即：

//第一步：先指定待进行事务的数据库（通过获取一个notorm表实例来指定；否则会提示：PDO There is no active transaction）
$user = DI()->notorm->user;

//第二步：开启事务开关（此开关会将当前全部打开的数据库都进行此设置）
DI()->notorm->transaction = 'BEGIN';

//第三步：进行数据库操作
$user->insert(array('name' => 'test1',));
$user->insert(array('name' => 'test2',));

//第四：提交/回滚
DI()->notorm->transaction = 'COMMIT';
//DI()->notorm->transaction = 'ROLLBACK';

推荐使用PhalApi的事务操作方式

PhalApi一开始对事务这块考虑不周，后来发现很多同学、很多项目都需要用到数据库事务操作。
基于此，在不破坏原来的代码基础上，我们决定在PhalApi_DB_NotORM上添加对数据库维度的事务操作支持。

示例简单如下：

    public function testTransactionCommit()
    {
        //Step 1: 开启事务
        DI()->notorm->beginTransaction('db_demo');

        //Step 2: 数据库操作
        DI()->notorm->user>insert(array('name' => 'test1'));
        DI()->notorm->user>insert(array('name' => 'test2'));

        //Step 3: 提交事务
        DI()->notorm->commit('db_demo');
        // DI()->notorm->rollback('db_demo'); // 回滚

    }

温馨提示： 以上操作，须PhalApi 1.3.1 及以上版本才能支持。

(11)扩展对非MySQL数据库的支持

PhalApi使用的是NotORM来进行数据库操作，而NotORM底层则是采用了PDO。目前，NotORM支持： MySQL, SQLite, PostgreSQL, MS SQL, Oracle (Dibi support is obsolete)。

但需要注意的是，PhalApi本身对NotORM进行了修改，需要调整一下代码才能更好地支持除MySQL外的数据库。 即使NotORM不支持的数据库，你也可以轻松通过添加扩展的方式来支持。如：

首先，定制自己的数据库连接的PDO。

class Common_MyDB extends PhalApi_DB_NotORM {

    protected function createPDOBy($dbCfg) {
        /* Connect to an ODBC database using driver invocation */
    $dsn = 'uri:file:///usr/local/dbconnect';
    return new PDO($dsn, $dbCfg['user'], $dbCfg['password']);
    }
}

随后，在初始化文件init.php中重新注册DI()->notorm即可，如：

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new Common_MyDB(DI()->config->get('dbs'), $debug);
};

1.15.3 可选的Model基类

(1)表数据入口模式

我们一直在考虑，是否应该提供数据库的基本操作支持，以减少开发人员重复手工编写基本的数据操作。

最后，我们认为是需要的。然后就引发了新的问题：是以继承还是以委托来支持？

委托有助于降低继承的层级，但仍然需要编写同类的操作然后再次委托。所以，这里提供了基于NotORM的Model基类：PhalApi_Model_NotORM。

然而提供这个基类还是会遇到一些问题，例如：如何界定基本操作？如何处理分表存储？如何支持定制化？

由于我们这里的Model使用了 “表数据入口” 模式，而不是“行数据入口”，也不是“活动纪录”，也不是复杂的“数据映射器”。所以在使用时可以考虑是否需要此基类。即使这样，你也可以很轻松转换到“行数据入口”和“活动纪录”模式。这里，PhalApi中的Model是更广义上的数据源层（后面会有更多说明），因此对应地PhalApi_Model_NotORM基类充当了数据库表访问入口的对象，处理表中所有的行。

(2)规约层的CURD

在明白了Model基类的背景后，再来了解其具体的操作和如何继承会更有意义。

而具体的操作则与数据表的结构相关，在“约定编程”下：即每一个表都有一个主键（通常为id，也可以自由配置）以及一个序列化LOB字段ext_data。我们很容易想到Model接口的定义（注释已移除，感兴趣的同学可查看源码）：

interface PhalApi_Model {

    public function get($id, $fields = '*');

    public function insert($data, $id = NULL);

    public function update($id, $data);

    public function delete($id);
}

上面的接口在规约层上提供了基于表主键的CURD基本操作，在具体实现时，需要注意两点：一是分表的处理；另一点则是LOB字段的序列化。

(3)不使用Model基类的写法

由于我们使用了NotORM进行数据库的操作，所以这里也提供了基于NotORM的基类：PhalApi_Model_NotORM。下面以我们熟悉的获取用户的基本信息为例，说明此基类的使用。

为唤醒记忆，下面贴上Model_User类原来的代码：

// $ vim ./Demo/Model/User.php

<?php

class Model_User {

    public function getByUserId($userId) {
        return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
    }
}

对应的调用：

$model = new Model_User();
$rs = $model->getByUserId($userId);

(4)继承Model基类的写法

若继承于PhalApi_Model_NotORM，则是：

// $ vim ./Demo/Model/User.php

<?php

class Model_User extends PhalApi_Model_NotORM {

    protected function getTableName($id) {
        return 'user';
    }
}

从上面的代码可以看出，基类已经提供了基于主键的CURD操作，但我们需要钩子函数以返回对应的表名。相应地，外部调用则调整为：

$model = new Model_User();
$rs = $model->get($userId);

再进一步，我们可以得到其他的基本操作：

$model = new Model_User();

//查询
$row = $model->get(1);
$row = $model->get(1, 'id, name'); //取指定的字段
$row = $model->get(1, array('id', 'name')); //可以数组取指定要获取的字段

//更新
$data = array('name' => 'test', 'update_time' => time());
$model->update(1, $data); //基于主键的快速更新

//插入
$data = array('name' => 'phalapi');
$id = $model->insert($data);
//$id = $model->insert($data, 5); //如果是分表，可以这样指定

//删除
$model->delete(1);

1.15.4 定制化你的Model基类

正如上面提及到的两个问题：LOB序列化和分表处理。所以，如果PhalApi现有就此两问题的解决方案不能满足项目的需求，可作定制化处理。

(1)LOB序列化

先是LOB序列化，考虑到有分表的存在，当发生数据库变更时（特别在线上环境）会有一定的难度和风险，因此引入了扩展字段ext_data。当然，此字段也应对数据库变更的同时，也可以作为简单明了的值对象的大对象。序列化LOB首先要考虑的问题是使用二进制（BLOB）还是文本（CLOB），出于通用性、易读性和测试性，我们目前使用了json格式的文本序列化。所以，如果考虑到空间或性能问题（在少量数据下我认为问题不大，如果数据量大，应该及时重新调整数据库表结构），可以重写formatExtData() & parseExtData()。

如改成serialize序列化：

abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    /**
     * 对LOB的ext_data字段进行格式化(序列化)
     */
    protected function formatExtData(&$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = serialize($data['ext_data']);
        }
    }

    /**
     * 对LOB的ext_data字段进行解析(反序列化)
     */
    protected function parseExtData(&$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = unserialize($data['ext_data'], true);
        }
    }

    // ...
}

将Model类继承于Common_Model_NotORM后，

// $ vim ./Demo/Model/User.php

<?php
class Model_User extends Common_Model_NotORM {
   //...
}

就可以轻松切换到序列化，如：

$model = new Model_User();

//带有ext_data的更新
$extData = array('level' => 3, 'coins' => 256);
$data = array('name' => 'test', 'update_time' => time(), 'ext_data' => $extData);
$model->update(1, $data); //基于主键的快速更新

(2)分表处理

其次是分表处理，同样考虑到分表的情况，以及不同的表可能配置不同的主键表，而基于主键的CURD又必须要先知道表的主键名才能进行SQL查询。所以，问题就演变成了如何找到表的主键名。这里可以自动匹配，也可以手工指定。自动匹配是智能的，因为当我们更改表的主键时，可以自动同步更新而不需要担心遗漏（虽然这种情况很少发生）。手工指定可以大大减少系统不必要的匹配操作，因为我们开发人员也知道数据库的主键名是什么，但需要手工编写一些代码。在这里，提供了可选的手工指定，即可重写getTableKey($table)来指定你的主键名。

如，当我们的表的主键都固定为id时：

abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    protected function getTableKey($table) {
        return 'id';
    }
}

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.16]-配置读取：内外网环境配置的完美切换 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

Experience by itself teaches nothing... Without theory, experience has no meaning. Without theory, one has no questions to ask. Hence, without theory, there is no learning. – Edwards Deming

1.16.1 配置的简单读取

通常，我们会有以下三个配置文件：

dogstar@ubuntu:Config$ tree
.
├── app.php
├── dbs.php
└── sys.php

其中app.php为项目应用配置；dbs.php为分布式存储的数据库配置；sys.php为不同环境下的系统配置。

在入口文件，注册配置组件服务后：

//$ vim ./Public/init.php

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

假设有这样的app.php配置：

return array(

    'version' => '1.1.1',

    'email' => array(
        'address' => 'chanzonghuang@gmail.com',
    );
);

我们就可以分别这样根据需要获取配置：

//app.php里面的全部配置
DI()->config->get('app');

//app.php里面的单个配置
DI()->config->get('app.version');  //返回：1.1.1

//app.php里面的多级配置
DI()->config->get('app.email.address');  //返回：'chanzonghuang@gmail.com'

其他配置文件类似，你也可以随意添加新的配置文件。

1.16.2 内外环境配置管理的策略

可以说，在项目开发中，除了我们的代码、数据和文档外，配置也是一块相当重要的组成部分，而且占据着非常重要的位置。最为明显的是，如果配置一旦出错，就很可能影响到整个系统的运行，并且远比修改代码再上线发布速度快（假设我们不用文件存储配置而是以分布式存储在各服务器内存中）。

但这里将讨论另外一个同样重要的问题，即： 不同环境下不同配置的管理和切换 。

现在将不同的策略分说如下。

(1)支持整包发布的环境变量配置

此种策略的主要方法就是在PHP代码中读取php-fpm中的ENV环境配置，再对应到linux环境下的profile环境变量，即：

PHP代码  -->    $_ENV环境配置  -->   Linux服务器环境变量/etc/profile

这样的好处莫过于可以支持项目代码的整包发布，而不需要在各个环境（开发环境、测试环境、回归环境、预发布环境、生产环境）来回修改切换配置，同时运维可以更好地保护服务器的账号和密码而不让开发知道。

而这样的不足则是，在对项目进行初次部署时，需要添加以上一系列的配置，而且后期维护也比较复杂麻烦，特别当机器多时。这时可以通过pupple/stacksalt这些运维工具进行自动化管理。但对于开发来说，依然会觉得有点烦锁。

(2)不同环境，不同入口

当服务器的账号和密码也是由开发来掌控时，则可以使用这种在代码层次控制的策略。

我们可以在Public下提供不同的入口，一如：

$ mkdir ./Public/myapi
$ myapi$ tree
.
├── index.php
└── test.php

我们有这样不同的入口，客户端在测试时，只需要将入口路径改成：/myapi/test.php?service=XXX.XXX，而在打包发布时只需要将入口路径改成：/myapi/?service=XXX.XXX 即可。也就是将test.php去掉。

而在服务端，仅需要在这些不同的入口文件，修改一下配置文件目录路径即可：

//$ vim ./Public/myapi/test.php
DI()->config = new PhalApi_Config_File(API_ROOT . '/ConfigTest');

另外，也可以通过客户端在请求时带参数来作区分，如带上&env=test或者&env=prod。

(3)持续集成下的配置管理

但个人最为建议的还是在 持续集成 下作配置管理。

因为首先，持续集成中的发布应该是经常性的，且应该是自动化的。所以，既然有自动化的支持，我们也应该及早地将配置纳入其中管理。

配置文件不同只要是环境不同，而环境不同所影响的配置文件通常只有sys.php和dbs.php；为此，我们为测试环境和生产环境准备了各自的配置文件，而在发布时自动选择所需要的配置文件。一般地，我们建议生产环境的配置文件以 .prod 结尾。所以，这时我们的配置文件可能会是这样：

dogstar@ubuntu:Config$ tree
.
├── app.php
├── dbs.php
├── dbs.php.prod
├── sys.php
└── sys.php.prod

多了生产环境的配置文件：dbs.php.prod和sys.php.prod。

再通过发布工具，我们就可以对不同环境的配置文件进行快速选择了。这里以phing为例，说明一下相关的配置和效果。

如下，在Phing的配置文件build.xml中，在生产环境发布过程中，我们将配置文件进行了替换。

//$ vim ./build.xml

        <copy
            file="./Config/dbs.php.prod"
            tofile="./Config/dbs.php"
            overwrite="true" />
        <copy
            file="./Config/sys.php.prod"
            tofile="./Config/sys.php"
            overwrite="true" />

执行phing发布后，将会看到对应的这样提示：

     [copy] Copying 1 file to /home/phapapi/Config
     [copy] Copying 1 file to /home/phapapi/Config

1.16.3 主从数据库的配置

通常一般而言，数据库支持分表分库配置，且只有一份，即：

./Config/dbs.php

然后，我们这样指定数据库的配置：

DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

如果需要从库时，可以参考./Config/dbs.php，创建一个从库的配置拷贝，即：

cp ./Config/dbs.php ./Config/dbs_slave.php

然后，注册一个从库的notorm：

DI()->notormSlave = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs_slave'), $debug); //注意：配置不同
};

最后使用此从库的服务DI()->notormSlave即可完成对从库的读取操作，用法同DI()->notorm，这里不再赘述。

1.16.4 扩展你的配置读取

虽然上面有不同的配置文件管理策略，但很多实际情况下，我们的配置需要可以随时更改、下发和调整。并且在海量访问，性能要求高的情况下快速读取配置。

这就要求我们的项目既可以方便维护即时修改，又需要能够快速同步一致更新下发和读取。这样就涉及到了配置更高层的管理： 统一集中式的配置管理，还是分布式的配置管理？ 文件存储，还是DB存储，还是MC缓存，还是进驻内存？

个人认为，性能最优的莫过于配置进驻内存并分布式存储。毕竟作为前端机的PHP内存这块还是相对充裕且可以利用的。但需要把这一条线打通，有点难度。

这里不过多地谈论配置更多的内容，但在PhalApi框架中，当你根据自己项目的需要实现了自己的配置读取方式后，再次简单的在入口文件重新注册即可。

1.16.5 使用Yaconf扩展快速读取配置

首先，非常感谢 @豹 同学的提议：使用Yaconf扩展快速读取配置。

虽然现在是冬天，有点冷（特别是在北京的同学），而且现在还是凌晨1点25分。但我们希望能继续将开源的热情继续下去。

以下是刚刚这几个小时努力的成果。

(1)前期准备：PHP7 的安装

具体可以参考：Linux环境PHP7.0安装

(2)前期准备：Yaconf扩展的安装

具体可以参考：laruence/yaconf

(3)使用

和正常的配置一样，简单如下：

DI()->config = new PhalApi_Config_Yaconf();

var_dump(DI()->config->get('foo')); //相当于var_dump(Yaconf::get("foo"));

var_dump(DI()->config->has('foo')); //相当于var_dump(Yaconf::has("foo"));

以上，假设我们已经有了这样的配置文件：

$ vim ./test.ini
name="PhalApi"
version="1.3.1"

需要注意的是，如果需要使用Yaconf扩展，需要PHP7的版本支持，但使用基本上和原来PhalApi的思路是完全一样的。
但还有所区别的是，配置文件的目录路径（当然也可以配置成和PhalApi的配置目录一致）、以及配置文件的格式。

Just have fun!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.17]-日记纪录：简化版的日记接口 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

PHP日记接口规范

关于日记系统，PHP规范给出了相当好的说明和定义，请见： Logger Interface

1.17.1 简化版

但在用过这么多开源框架进行各种项目开发，以及内部框架的开发后，发现实际上日记的分类并没有使用得这么丰富，而通常只是频繁地几类。因此，为了减少不必要的复杂性，这里特地精简为三种，只有：

  • error 系统异常类
  • info 业务纪录类
  • debug 开发调试类

现分说如下。

(1)error 系统异常类

此类是后台开发同学认为 不应该发生却发生的事情 ，即我们通常所说的系统异常。
如：调用app store支付的接口失败了，我们需要纪录一下当时的场景，以便复查和给用户补尝其损失。
又如：写入一条纪录到数据纪录，但失败了，我们需要纪录一下，以便排查。

用法很简单，如：

//只有描述
DI()->logger->error('fail to insert DB');

//描述 + 简单的信息
DI()->logger->error('fail to insert DB', 'try to register user dogstar');

//描述 + 当时的上下文数据
$data = array('name' => 'dogstar', 'password' => '123456');
DI()->logger->error('fail to insert DB', $data);

上面三条纪录，效果类如：

dogstar@ubuntu:dev.phalapi.com$ tailf ./Runtime/log/201502/20150207.log
2015-02-07 20:37:55|ERROR|fail to insert DB
2015-02-07 20:37:55|ERROR|fail to insert DB|try to register user dogstar
2015-02-07 20:37:55|ERROR|fail to insert DB|{"name":"dogstar","password":"123456"}

截图效果： apic

(2)info 业务纪录类

此类通常为业务需要进行必要的操作纪录，以便发生系统问题后的回滚处理、问题排查以及数据统计。
如在有缓存的情况下，可能数据没及时写入数据库而导致数据丢失或者回档，这里可以通过日记简单查看是否可以恢复。以及说明一下操作发生的背景或原由，如通常游戏中用户的经验值添加：

//假设：10 + 2 = 12
DI()->logger->info('add user exp', array('name' => 'dogstar', 'before' => 10, 'addExp' => 2, 'after' => 12, 'reason' => 'help one more phper'));

//对应的LOG
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}

但当哪天我们看到以下的LOG是就会发现系统存在隐藏的BUG：

//WTF? 10 + 2 = 11 ???
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":11,"reason":"help one more phper"}

而当用户玩家来投诉客服时，客服妹子来找到我们后台开发时，我们可以证明得了是系统原因造成了用户丢失1点经验值。
特别地，若我们看到以下的LOG时，不难看出有人在用非法的渠道刷经验：

2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}
2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":12,"addExp":2,"after":14,"reason":"help one more phper"}

....

2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":998,"addExp":2,"after":1000,"reason":"help one more phper"}

所幸我们有日记并及时发现了，随后如何处理就视具体的项目而定了。但当产品来追问时，我们可以及时给出反馈和做出处理。

还有更为重要的是 数据统计 。这块就App数据分析和统计这块已经有了很好的第三方服务支持，一如：友盟。但我们仍然可以轻松实现自己的数据统计，以便二次确认和定制化。毕竟，总是依赖第三方不是那么轻便，而且存在敏感数据安全问题。
这里简单提供一个上报接口，如：

    public function getRules() {
        return array(
            'report' => array(
                'username' => array('name' => 'username', 'require' => true),
                'msg' => array('name' => 'msg', 'require' => true),
            ),
        );
    }

    public function report() {
        DI()->logger->info($this->username, $this->msg);
    }

客户端在需要的场景，如用户打开应用，请求：

http://dev.phalapi.com/demo/?service=DogstarTest.Report&username=dogstar&msg=enter%20app

即可看到：

2015-02-07 21:01:13|INFO|dogstar|enter app

到后期，若我们需要统计用户的登录情况时，可以这样统计：

$ cat ./Runtime/log/201502/20150207.log | grep "enter app" | awk -F '|' '{print $3}' | sort | uniq -c
     11 dogstar
      5 King
      2 Tom

(3)debug 开发调试类

此类为开发调试用，用法如上，这里不再赘述。以下是一些简单的示例：

DI()->logger->debug('just for test');

DI()->logger->debug('just for test', '一些其他的描述 ...');

DI()->logger->debug('just for test', array('name' => 'dogstar', 'password' => '******'));

1.17.2 使用DI()->logger->log()进行更灵活的分类

若上面的error/info/debug都不能满足项目的需求时，可以这样更灵活地进行日记纪录：

DI()->logger->log('demo', 'add user exp', array('name' => 'dogstar', 'after' => 12));
DI()->logger->log('test', 'add user exp', array('name' => 'dogstar', 'after' => 12));

//对应的日记
2015-02-07 21:13:27|DEMO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}
2015-02-07 21:15:39|TEST|add user exp|{"name":"dogstar","after":12}

注意到，DI()->logger->log()第一个参数为日记分类的名称，在写入日记时会自动转换为大写。其接口函数签名为：

    /**
     * 日记纪录
     *
     * 可根据不同需要，将日记写入不同的媒介
     *
     * @param string $type 日记类型，如：info/debug/error, etc
     * @param string $msg 日记关键描述
     * @param string/array $data 场景上下文信息
     * @return NULL
     */
    abstract public function log($type, $msg, $data);

1.17.3 别忘记了日记的级别设置

上面的三类日记分别对应的标识为：

  • error 系统异常类：PhalApi_Logger::LOG_LEVEL_ERROR
  • info 业务纪录类：PhalApi_Logger::LOG_LEVEL_INFO
  • debug 开发调试类：PhalApi_Logger::LOG_LEVEL_DEBUG

根据不同的项目在不同环境下的需要，我们可以这样有选择性地将需要的日记分类写入保存：

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

多个日记级别需要使用或运算进行组合。

1.17.4 扩展你的日记存储

普遍情况下，我们认为将日记存放在文件是比较合理的，因为便于查看、管理和统计。当然，如果你的项目需要将日记纪录在数据库，也可以快速扩展实现。如实现数据库的存储思路：

//$ vim ./Apps/Common/Logger/DB.php

class Common_Logger_DB extends PhalApi_Logger {

    public function log($type, $msg, $data) {
        //TODO 数据库的日记写入 ...
    }

随后，利用DI注册一下即可：

//日记纪录 - DB
DI()->logger = new Common_Logger_DB($dbConfig,
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.18]-快速函数：人性化的关怀 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.18.1 提供快速函数的缘由

很多人，很多框架都在说遵循的是OOP，实际上很多都是面向过程的开发，或者是很呆板的面向对象。

而且你也会发现，面向对象和面向过程在某方面是相互补充的。而且，面向对象里面很痛苦的改动在面向过程中可以很容易实现，反之亦然。如面向对象中添加一个类成员变量（新数据）很简单，但在抽象父类中添加一个抽象方法（新方法）却很痛苦；而在面向过程中添加一个全局变量（新数据）很痛苦，但要添加一个新方法却很简单。

所以，没有绝对的好与不好，关键在于如何设计和如何使用。而PhalApi则遵循敏捷开发的思想，为了更多的接口开发人员减少代码编写和记忆的痛苦、体验开发的乐趣，我们思虑衡量再三，提供了以下的快速函数。

1.18.2 目前已有的快速函数

(1)获取全局DI实例的函数：DI()

为了定位获得全部服务资源，我们需要一个统一的入口，在这里即为PhalApi_DI的全局实例。
我们可以这样原始获取：

$di = PhalApi_DI::one();

也可以这样快速获取：

$di = DI();

也就是说：

function DI() {
    return PhalApi_DI::one();
}

(2)国际化翻译函数：T()

在真正需要进行翻译的项目中，翻译函数的调用显然会到处可见。为了方便编写代码，这里取Translator的首写字母T作为快速函数的名字，在需要进行翻译时只需要调用T()，即可：

$msg = T('hello world');

$msg = T('hello {name}', array('name' => 'dogstar'));

对应地：

function T($msg, $params = array()) {
    return PhalApi_Translator::get($msg, $params);
}

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.19]-DI服务速查：各资源服务一览表 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

“完成，好过完美。” -- 脸谱书文化之一

1.19.1 一览表

变量名称 | 是否启动时自动注册 | 是否必须 | 接口/类 | 作用说明
loader | 否 | 是 | PhalApi_Loader | 自动加载：负责PEAR风格下类的自动加载，需要手动注册，指定项目路径
config | 否 | 是 | PhalApi_Config | 配置：负责项目配置的读取，需要手动注册，指定存储媒介，默认是PhalApi_Config_File
logger | 否 | 是 | PhalApi_Logger | 日记纪录：负责日记的写入，需要手动注册，指定日记级别和存储媒介，默认是PhalApi_Logger_File
request | 是 | 是 | PhalApi_Request | 接口参数请求：用于收集接口请求的参数
response | 是 | 是 | PhalApi_Response | 结果响应：用于输出返回给客户端的结果，默认为PhalApi_Response_Json
notorm | 否 | 推荐 | PhalApi_DB_NotORM | 数据操作：基于NotORM的DB操作，需要手动注册，指定数据库配置
cache | 否 | 推荐 | PhalApi_Cache | 缓存：实现缓存读写，需要手动注册，指定缓存
filter | 否 | 推荐 | PhalApi_Filter | 拦截器：实现签名验证、权限控制等操作
crypt | 否 | 否 | PhalApi_Crypt | 对称加密：实现对称加密和解密，需要手动注册
curl | 否 | 否 | PhalApi_CUrl | CURL请求类：通过curl实现的快捷方便的接口请求类，需要手动注册
cookie | 否 | 否 | PhalApi_Cookie | COOKIE的操作
tracer | 是 | 是 | PhalApi_Helper_Tracer | 内置的全球追踪器，支持自定义节点标识（1.4.0及上以版本支持）
debug | 否 | 否 | boolean | 应用级的调试开关，通常可从配置读取，为true时开启调试模式
_formatterArray | 否 | 否 | PhalApi_Request_Formatter_Array | 数组格式化服务（系统内部使用）
_formatterBoolean | 否 | 否 | PhalApi_Request_Formatter_Boolean | 布尔值格式化服务（系统内部使用）
_formatterCallable | 否 | 否 | PhalApi_Request_Formatter_Callable | 回调格式化服务（系统内部使用）
_formatterDate | 否 | 否 | PhalApi_Request_Formatter_Date | 日期格式化服务（系统内部使用）
_formatterEnum | 否 | 否 | PhalApi_Request_Formatter_Enum | 枚举格式化服务（系统内部使用）
_formatterFile | 否 | 否 | PhalApi_Request_Formatter_File | 上传文件格式化服务（系统内部使用）
_formatterFloat | 否 | 否 | PhalApi_Request_Formatter_Float | 浮点数格式化服务（系统内部使用）
_formatterInt | 否 | 否 | PhalApi_Request_Formatter_Int | 整数格式化服务（系统内部使用）
_formatterString | 否 | 否 | PhalApi_Request_Formatter_String | 字符串格式化服务（系统内部使用）

1.19.2 DI服务是否已注册的判断误区

(1)错误的判断方法

当需要判断一个DI服务是否已被注册，出于常识会这样判断：

if (isset(DI()->cache)) {

但这样的判断永远为false，不管注册与否。

追其原因在于，DI类使用了魔法方法的方式来提供类成员属性，并存放于PhalApi_DI::$data中。

这就导致了如果直接使用isset(DI()->cache)的话，首先不会触发魔法方法 PhalApi_DI::__get($name)的调用，其次也确实没有PhalApi_DI::$cache这个成员属性，最终判断是否存在时都为false。

简单来说，以下两种判断，永远都为false：

//永远为false
var_dump(isset(DI()->XXX));
var_dump(!empty(DI()->XXX));

(2)正确判断的写法：先获取，再判断

正确的用法应该是：

//先获取，再判断
$XXX = DI()->XXX;
var_dump(isset($XXX));
var_dump(!empty($XXX));

这里，可能会对大家带来一定的困惑，所以在这里特别进行说明。也对框架中这一点不合理的设计深感抱歉。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.20]-DB操作：数据库基本操作速查 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

一花一世界，
一沙一天国，
君掌盛无边，
刹那含永劫。

-- 《天真的预兆》

1.20.1 前提

为了让大家更为明确数据库NotORM的使用，我们假设有以下数据库表：

-- ----------------------------
-- Table structure for `tbl_user`
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user`;
CREATE TABLE `tbl_user` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  `age` int(3) DEFAULT NULL,
  `note` varchar(45) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tbl_user
-- ----------------------------
INSERT INTO `tbl_user` VALUES ('1', 'dogstar', '18', 'oschina', '2015-12-01 09:42:31');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', '21', 'USA', '2015-12-08 09:42:38');
INSERT INTO `tbl_user` VALUES ('3', 'King', '100', 'game', '2015-12-23 09:42:42');

并且，假设我们已获得了tbl_user表对应的notorm实例$user，此NotORM表实例可从两种方式获得：

  • 1、使用原生态的notorm，即：$user = DI()->notorm->user
  • 2、使用PhalApi_Model_NotORM基类的方式，即：$user = $this->getORM()，如：
<?php
class Model_User extends PhalApi_Model_NotORM {
    protected function getTableName($id) {
        return 'user';
    }

    public function doSth() {
        $user = $this->getORM(); //获取NotORM表实例
    }
}

写在前面的话

NotORM的实例是有内部状态的，因为在开发过程中，需要特别注意何时需要保留状态（使用同一个实例）、何时不需要保留状态。即：

保留状态的写法：

$user = $notorm->user;  //获取一个新的实例
$user->where('age > ?', 18);
$user->where('name LIKE ?', '%dog%');  //相当于age > 18 AND name LIKE '%dog%'

不保留状态的写法：

$user = $notorm->user;  //获取一个新的实例
$user->where('age > ?', 18);

$user = $notorm->user;  //重新获取新的实例
$user->where('name LIKE ?', '%dog%');  //此时只有 name LIKE '%dog%'

关于这两者的使用场景，下面在进行说明时会特别提及。
下面，就让我们结合实例来尝试一下数据库的操作吧！

调试模式

(1)打印SQL语句

在请求接口时，当需要查看当前接口所执行的SQL语句时，可以添加参数&__sql__=1。如请求：

http://demo.phalapi.net/?service=User.getBaseInfo&user_id=1&__sql__=1

会返回类似这样：

[1 - 0.00035s]SELECT * FROM tbl_user WHERE (id = ?); -- 1<br>
{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}

(2)显示数据库连接错误的详细信息

普通情况下，数据库连接失败时会这样提示：

{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: 数据库db_demo连接失败"
}

考虑到生产环境不方便爆露服务器的相关信息，故这样简化提示。当在开发过程中，需要定位数据库连接失败的原因时，可使用debug调试模式。
开启调试模式的方法有两种：

  • 1、在请求接口时添加 &__debug__=1，开启本次调试
  • 2、将配置文件 ./Config/sys.php中的debug设置为true，开启全局调试

开启后，当再次失败时，会看到类似这样的提示：

{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: 数据库db_demo连接失败，异常码：1045，错误原因：SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost' (using password: NO)"
}

然后，便可根据具体的错误提示进行排查解决。

默认表名与指定表名

请注意：PhalApi v1.3.4 版本以上有效。

当继承PhalApi_Model_NotORM时，如编写Model_User类：

<?php
class Model_User extends PhalApi_Model_NotORM {
}

则默认的表名为：user。默认表名的自动匹配规则是：取Model_后面部分的字符全部转小写。转化后框架会另外加上表前缀。

在以下场景下，需要重写此方法以指定表名：

  • 自动匹配的表名与实际表名不符
  • 存在分表
  • Model类名不含有Model_

如，当Model_User类对应的表名为：my_user表时，可使用getTableName($id)重新指定表名：

<?php
class Model_User extends PhalApi_Model_NotORM {
    protected function getTableName($id) {
        return 'my_user';
    }
}

1.20.2 基本操作

(1)SELECT

单个字段：

// SELECT id FROM `tbl_user`
$user->select('id')

多个字段获取：

// SELECT id, name, age FROM `tbl_user`
$user->select('id, name, age')

字段别名获取：

// SELECT id, name, MAX(age) AS max_age FROM `tbl_user`
$user->select('id, name, MAX(age) AS max_age')

全部字段（缺省）获取：

// SELECT * FROM `tbl_user`
$user->select('*')

(2)WHERE

单个字段查询：

// WHERE id = 1
$user->where('id', 1)
$user->where('id = ?', 1)
$user->where(array('id', 1))

多个字段查询：

// WHERE id > 1 AND age > 18
$user->where('id > ?', 1)->where('age > ?', 18)
$user->and('id > ?', 1)->and('age > ?', 18)
$user->where('id > ? AND age > ?', 1, 18)
$user->where(array('id > ?' => 1, 'age > ?' => 10))

// WHERE name = 'dogstar' AND age = 18
$user->where(array('name' => 'dogstar', 'age' => 18))

// WHERE name = 'dogstar' OR age = 18
$user->or('name', 'dogstar')->or('age', 18)

// WHERE ((name = ? OR id = ?)) AND (note = ?) -- 'dogstar', '1', 'xxx'
#实现方式1：使用and拼接
$user->where('(name = ? OR id = ?)', 'dogstar', '1')->and('note = ?', 'xxx')
#实现方式2：只使用where，不用数组传参
$user->where('(name = ? OR id = ?) AND note = ?', 'dogstar', '1', 'xxx')
#实现方式3：只使用where，用下标为顺序数字的数组传参
$user->where('(name = ? OR id = ?) AND note = ?', array('dogstar', '1', 'xxx'))
#实现方式4：只使用where，用下标为标识符的数组传参
$user->where('(name = :name OR id = :id) AND note = :note', array(':name' => 'dogstar', ':id' => '1', ':note' => 'xxx'))

IN查询：

// WHERE id IN (1, 2, 3)
$user->where('id', array(1, 2, 3))

// WHERE id NOT IN (1, 2, 3)
$user->where('NOT id', array(1, 2, 3))

// WHERE (id, age) IN ((1, 18), (2, 20))
$user->where('(id, age)', array(array(1, 18), array(2, 20)))

模糊匹配查询：

// WHERE name LIKE '%dog%'
$user->where('name LIKE ?', '%dog%')

// WHERE name NOT LIKE '%dog%'
$user->where('name NOT LIKE ?', '%dog%')

NULL判断查询：

// WHERE (name IS NULL)
$user->where('name', null)

对于非空的判断，则可以：

// WHERE (name IS NOT ?) LIMIT 1; -- NULL
$user->where('name IS NOT ?', null)

(3)ORDER BY

单个字段排序：

// ORDER BY age
$user->order('age')

// ORDER BY age DESC
$user->order('age DESC')

多个字段排序：

// ORDER BY id, age DESC
$user->order('id')->order('age DESC')
$user->order('id, age DESC')

(4)LIMIT

按数量限制：

// LIMIT 10
$user->limit(10)

按数量和偏移量限制（请注意：先偏移量、再数量）：

// LIMIT 2,10
$user->limit(2, 10)

(5)GROUP BY和HAVING

不带HAVING：

// GROUP BY note
$user->group('note')

带HAVING：

// GROUP BY note HAVING age > 10
$user->group('note', 'age > 10')

1.20.3 CURD之查询类（Retrieve）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
fetch() | 循环获取每一行 | while($row = $user->fetch()) { //... } | 否
fetchOne() | 只获取第一行 | $row = $user->where('id', 1)->fetchOne(); | 等效于fetchRow() | 是
fetchRow() | 只获取第一行 | $row = $user->where('id', 1)->fetchRow(); | 等效于fetchOne() | 是
fetchPairs() | 获取键值对 | $row = $user->fetchPairs('id', 'name'); | 第二个参数为空时，可取多个值，并且多条纪录 | 否
fetchAll() | 获取全部的行 | $rows = $user->where('id', array(1, 2, 3))->fetchAll(); | 等效于fetchRows() | 是
fetchRows() | 获取全部的行 | $rows = $user->where('id', array(1, 2, 3))->fetchRows(); | 等效于fetchAll() | 是
queryAll() | 复杂查询下获取全部的行，默认下以主键为下标 | $rows = $user->queryAll($sql, $parmas); | 等效于queryRows() | 是
queryRows() | 复杂查询下获取全部的行，默认下以主键为下标 | $rows = $user->queryRows($sql, $parmas); | 等效于queryAll() | 是
count() | 查询总数 | $total = $user->count('id'); | 第一参数可省略 | 否
min() | 取最小值 | $minId = $user->min('id'); | 否
max() | 取最大值 | $maxId = $user->max('id'); | 否
sum() | 计算总和 | $sum = $user->sum('age'); | 否

循环获取每一行（多个字段）：

// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $notorm->user->select('id, name')->where('age > 18');
while($row = $user->fetch()) {
     var_dump($row);
}

// 输出
array(2) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(3) "Tom"
}
array(2) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(4) "King"
}

循环获取每一行（单个字段）：

// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $notorm->user->select('id, name')->where('age > 18');
while($row = $user->fetch('name')) { // 指定获取name这列，但此字段须在select里
     var_dump($row);
}

// 输出
string(3) "Tom"
string(4) "King"

循环获取每一行（错误的用法，注意！会死循环）：

while($row = $notorm->user->select('id, name')->where('age > 18')->fetch('name')) {
     var_dump($row);
}

只获取第一行（多个字段）：

// SELECT id, name FROM tbl_user WHERE (age > 18) LIMIT 1;
$rs = $user->select('id, name')->where('age > 18')->fetchOne();  //等同fetchRow()
var_dump($rs);

// 输出
array(2) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(3) "Tom"
}

只获取第一行（单个字段）：

var_dump($user->fetchOne('name'));  // 输出 string(3) "Tom"
var_dump($user->fetchRow('name'));  // 等同输出 string(3) "Tom"

获取键值对（多个字段）：

// SELECT id, name, age FROM tbl_user LIMIT 2;
$rs = $user->select('name, age')->limit(2)->fetchPairs('id'); //指定以ID为KEY
var_dump($rs);

// 输出
array(2) {
  [1]=>
  array(3) {
    ["id"]=>
    string(1) "1"
    ["name"]=>
    string(7) "dogstar"
    ["age"]=>
    string(2) "18"
  }
  [2]=>
  array(3) {
    ["id"]=>
    string(1) "2"
    ["name"]=>
    string(3) "Tom"
    ["age"]=>
    string(2) "21"
  }
}

获取键值对（单个字段）：

// SELECT id, name FROM tbl_user LIMIT 2
var_dump($user->limit(2)->fetchPairs('id', 'name')); //通过第二个参数，指定VALUE的列

// 输出
array(2) {
  [1]=>
  string(7) "dogstar"
  [2]=>
  string(3) "Tom"
}

获取全部的行：

// SELECT * FROM tbl_user
var_dump($user->fetchAll());  //全部表数据输出，输出结果略，相当于$user->fetchRows()

复杂查询下获取全部的行（1.3.1及以前版本默认下以主键为下标，1.3.1以后的版本则默认采用数组方式）：

// SELECT name FROM tbl_user WHERE age > :age LIMIT 1
$sql = 'SELECT name FROM tbl_user WHERE age > :age LIMIT 1';
$params = array(':age' => 18);
$rs = $user->queryAll($sql, $params);
var_dump($rs);

// 输出
array(1) {
  [0]=>
  array(1) {
    ["name"]=>
    string(3) "Tom"
  }
}

// 也可以这样。。。
$sql = 'SELECT name FROM tbl_user WHERE age > ? LIMIT 1';  //使用问号表示变量
$params = array(18);
$rs = $user->queryRows($sql, $params); //使用queryRows()别名
var_dump($rs);

请注意：使用上面这种方式进行查询，需要手动填写完整的表名字，并且需要通过某个表的实例来运行。

取最小值：

// SELECT MIN(age) FROM tbl_user
var_dump($user->min('age'));

// 输出
string(2) "18"

取最大值：

// SELECT MAX(age) FROM tbl_user
var_dump($user->max('age'));

// 输出
string(3) "100"

计算总和：

// SELECT SUM(age) FROM tbl_user
var_dump($user->sum('age'));

// 输出
string(3) "139"

1.20.4 CURD之插入类（Create）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
insert() | 插入数据 | $user->insert($data); | 原生态操作需要再调用insert_id()获取插入的ID | 否
insert_multi() | 批量插入 | $user->insert_multi($rows); | 可批量插入 | 否
insert_update() | 插入/更新 | 接口签名：insert_update(array $unique, array $insert, array $update = array() | 不存时插入，存在时更新 | 否

插入数据

// INSERT INTO tbl_user (id, name, age, note) VALUES (4, 'PhalApi', 1, 'framework')
$data = array('id' => 4, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$user->insert($data);
$id = $user->insert_id(); //必须是同一个实例，方能获取到新插入的行ID，且表必须设置了自增
var_dump($id); //新增的ID

//使用Model的写法
$model = new Model_User();
var_dump($model->insert($data)); //输出新增的ID

批量插入：

// INSERT INTO tbl_user (name, age, note) VALUES ('A君', 12, 'AA'), ('B君', 14, 'BB'), ('C君', 16, 'CC')
$rows = array(
    array('name' => 'A君', 'age' => 12, 'note' => 'AA'),
    array('name' => 'B君', 'age' => 14, 'note' => 'BB'),
    array('name' => 'C君', 'age' => 16, 'note' => 'CC'),
);
$rs = $user->insert_multi($rows);
var_dump($rs);

// 输出
int(3) //成功插入的条数

插入/更新：

// INSERT INTO tbl_user (id, name, age, note) VALUES (8, 'PhalApi', 1, 'framework') ON DUPLICATE KEY UPDATE age = 2
$unique = array('id' => 8);
$insert = array('id' => 8, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$update = array('age' => 2);
$rs = $user->insert_update($unique, $insert, $update);
var_dump($rs); //输出影响的行数

1.20.5 CURD之更新类（Update）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
update() | 更新数据 | $user->where('id', 1)->update($data); | 更新异常时返回false，数据无变化时返回0，成功更新返回1 | 否

更新数据：

// UPDATE tbl_user SET age = 2 WHERE (name = 'PhalApi');
$data = array('age' => 2);
$rs = $user->where('name', 'PhalApi')->update($data);
var_dump($rs);

// 输出
int(1) //正常影响的行数
int(0) //无更新，或者数据没变化
boolean(false) //更新异常、失败

更新数据（+1）：

// UPDATE tbl_user SET age = age + 1 WHERE (name = 'PhalApi')
$rs = $user->where('name', 'PhalApi')->update(array('age' => new NotORM_Literal("age + 1")));
var_dump($rs); //输出影响的行数

1.20.6 CURD之删除类（Delete）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
delete() | 删除 | $user->where('id', 1)->delete(); | 禁止无where条件的删除操作 | 否

按条件删除，返回影响的行数：

// DELETE FROM tbl_user WHERE (id = 404);
$user->where('id', 404)->delete();

注意： 如果是全表删除，框架将会禁止，并抛出异常。如：

// Exception: sorry, you can not delete the whole table
$user->delete();

参考

更多请参考 NotORM官网接口说明

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.21]-类的自动加载：遵循PEAR包的命名规范 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.21.1 PhalApi自动加载的机制

首先，PhalApi的自动加载是简单的；其次，PhalApi不强制只使用我们框架的机制。

有些框架，单单在类文件的自动加载这一块就弄得非常复杂，以致开发同学需要在这些框架添加一些自己的类代码时，往往不知要怎么引入！甚至明明用引入了却又不见得生效！

而在PhalApi，我们则是： 简单、统一、规范。

1.21.2 PEAR包命名规范

PEAR包的类文件路径和类名映射非常简单，如下图：

0215
或者查看（里面有很好的代码风格编写说明）： Autoloading Standard

而PhalApi则更简单，我们暂时还不使用命名空间，所以namespace这一块省去。例如：

Api_User
Domain_User
Model_User

对应以下路径的文件：

.
|-- Api
|   `-- User.php
|-- Domain
|   `-- User.php
|-- Model
|   `-- User.php

再举一个模拟的示例，如类Api_Game_User_Equitment对应以下文件路径：

./Api/Game/User/Equitment.php

此外，还需注意区分大小写。

1.21.3 挂靠式自动加载

然后在我们准备了这些类和文件后，我们怎么才能让这些类被框架所自动加载呢？

我们提供的方式是： 挂靠式自动加载 。

熟悉Linux系统的同学可能很容易明白，还没接触到Linux的同学也是可以很快理解的。这里稍微说明一下。

所谓的 挂靠 就是把你代码的目录放在项目的目录下（或者其他任何一个地方），随后简单地将此目录添加到自动加载器即可。如上面的：

DI()->loader->addDirs('Demo');

当有多个目录时，可以这样：

DI()->loader->addDirs(array('Demo', 'Demo1', 'Demo2'));

需要注意的是，上面相对路径的都需要放置在应用项目的目录API_ROOT下面。如果是单个文件的引入，则可以使用绝对路径。下面分别简单说明。

(1)相对路径的目录路径挂靠

通过PhalApi_Loader::addDirs()方式挂靠的路径，都是强制在目录API_ROOT下面，即下面的三种方式是等效的（在Linux系统上）：

//路径：API_ROOT/Demo
DI()->loader->addDirs('Demo');

//路径：API_ROOT/./Demo
DI()->loader->addDirs('./Demo');

//路径：API_ROOT/Demo
DI()->loader->addDirs('/Demo');

如果需要挂靠的目录不在项目目录下，在Linux可以通过软链来解决。

(2)单个文件允许绝对路径引入

对于单个文件的引入，可以通过PhalApi_Loader::loadFile()来引入。注意以下两种写法的区别：

//路径：API_ROOT/Demo/Tool.php
DI()->loader->loadFile('Demo/Tool.php');

//路径：/path/to/Demo/Tool.php
DI()->loader->loadFile('/path/to/Demo/Tool.php');

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.22]-签名验证：自定义签名规则 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.22.1 过滤器服务

正如我们核心思想DI里面所说的，我们把后台很多功能资源都称为服务，所以在PhalApi框架中我们已经系统规定了DI()->filter为过滤器服务，以实现接口请求时的一些拦截操作，一如现在要说明的签名验证。

在接口进行初始化时，会自动调用已注册的过滤器服务 DI()->filter ，关键的代码如下：

//$vim ./PhalApi/PhalApi/Api.php
    public function init()
    {
        $this->createMemberValue();

        $this->filterCheck();

        $this->checkStatus();
    }

    protected function filterCheck()
    {
        $filter = DI()->filter;

        if (isset($filter)) {
            $filter->check();
        }
    }

默认直接可用的接口验证

基于很多同学对接口签名验证比较陌生，从框架推出签名验证以来，很长一段时间内，很多同学对于如何实现一个接口签名依然不知如何下手。
为了给大家更好的便利性，我们提供了一个基本版的接口验证服务。

主要是基于md5进行的签名生成，这个只能作为一般性的参考。大家可以在此基础上进行调整延伸。

默认情况下，可以去掉注释开启使用PhalApi_Filter_SimpleMD5进行接口验证，即：

//签名验证服务
DI()->filter = 'PhalApi_Filter_SimpleMD5';

其验签的算法如下（如注释所示）：

1、排除签名参数（默认是sign）
2、将剩下的全部参数，按参数名字进行字典排序
3、将排序好的参数，全部用字符串拼接起来
4、进行md5运算

以下面的示例参数为例，即：

1、排除签名参数（默认是sign）
?service=Default.Index&username=dogstar

2、将剩下的全部参数，按参数名字进行字典排序
service=Default.Index
username=dogstar

3、将排序好的参数，全部用字符串拼接起来
"Default.Indexdogstar" = "Default.Index" + "dogstar"

4、进行md5运算
sign = 35321cc43cfc1e4008bf6f1bf9b7e3b8 = md5("Default.Indexdogstar")

5、请求时，加上签名参数
?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8

下面是两个调用示例，错误请求下（即签名失败）：

http://localhost/phalapi/Public/demo/?service=Default.Index&username=dogstar

返回：
{
    "ret": 406,
    "data": [],
    "msg": "非法请求：签名错误"
}

温馨提示：
签名错误情况下，可以查看日记获得正确的sign，如：

2015-10-23 23:16:16|DEBUG|Wrong Sign|{"needSign":"35321cc43cfc1e4008bf6f1bf9b7e3b8"}

正常请求下（带sign签名）：

http://localhost/phalapi/public/demo/?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8

如果不想使用sign作为关键的签名参数，可以在注册时指定，如使用缩写s：

DI()->filter = new PhalApi_Filter_SimpleMD5('s');

1.22.2 微信签名示例

所以，如果我们需要实现签名验证，只需要简单的两步即可：

  • 1、实现过滤器接口 PhalApi_Filter::check()；
  • 2、注册过滤器服务 DI()->filter；

下面以大家熟悉的 微信验签 为例，进行示例说明。

(1)实现过滤器接口 PhalApi_Filter::check()

通常我们约定返回ret = 402表示验证失败，所以当签名失败时，我们可以返回ret = 402以告知客户端签名不对。根据微信的检验signature的PHP示例代码，我们可以快速实现自定义签名规则，如：

//$ vim ./Demo/Common/SignFilter.php
<?php

class Common_SignFilter implements PhalApi_Filter
{
    public function check()
    {
        $signature = DI()->request->get('signature');
        $timestamp = DI()->request->get('timestamp');
        $nonce = DI()->request->get('nonce');

        $token = 'Your Token Here ...';
        $tmpArr = array($token, $timestamp, $nonce);
        sort($tmpArr, SORT_STRING);
        $tmpStr = implode( $tmpArr );
        $tmpStr = sha1( $tmpStr );

        if ($tmpStr != $signature) {
            throw new PhalApi_Exception_BadRequest('wrong sign', 1);
        }
    }
}

(2)注册过滤器服务 DI()->filter

随后，我们只需要再简单地注册一下过滤器服务即可，在init.php初始化文件最后追加：

//$ vim ./Public/init.php
//签名验证服务
DI()->filter = 'Common_SignFilter';

(3)运行效果

当我们再次请求接口时，如默认的服务：/demo/?service=Default.Index，即会出现以下的错误： apic

即：

{
    "ret": 401,
    "data": [
    ],
    "msg": "非法请求：wrong sign"
}

如果符合接口签名的验证，则会正常返回我们熟悉的内容，如：

/demo/?service=Default.Index&signature=b75e0a1b574d4e111a1d6ed3c9cfbe2ccdc09404&timestamp=123&nonce=123

会返回：

{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1423055188
    },
    "msg": ""
}

1.22.3 特殊的场景

(1)个别接口不需要验签？

在注册好统一的接口验签的过滤器拦截服务后，是会存在这样一种情况：即个别的接口不需要签名。

而这种情况，我们也是有考虑到的。所以在提供了公共的功能的情况下，我们是可以快速灵活地进行定制化和扩展。

当我们个别的接口不需要签名验证时，只需要简单地在接口子类里面重定义过滤器的检测即可，如在我们熟悉的默认服务器取消签名验证：

//vim ./Demo/Api/Default.php
class Api_Default extends PhalApi_Api
{
    //....

    protected function filterCheck()
    {
    }
}

1.22.4 接口服务白名单配置

除了可以像上面这样编码实现，排除个别接口服务的接口验证，还可以使用接口服务白名单配置，通过框架自身实现对指定配置的接口服务排除。即调用的接口服务，如果配置了白名单，则不调用过滤器。

接口服务白名单配置是：app.service_whitelist，即配置文件./Config/app.php里面的service_whitelist配置，其默认值是：

    'service_whitelist' => array(
        'Default.Index',
    ),

如源代码里的注释所示，配置的格式有以下四种。

类型 | 配置格式 | 匹配规则 | 示例及说明
全部 | *.* | 匹配全部接口服务（慎用！） | 如果配置了此规则，即全部的接口服务都不触发过滤器。
方法通配 | Default.* | 匹配某个类的任何方法 | 即Api_Default接口类的全部方法
类通配 | *.Index | 匹配全部接口类的某个方法 | 即全部接口类的Index方法
具体匹配 | Default.Index | 匹配指定某个接口服务 | 即Api_Default::Index()

如果有多个生效的规则，按短路判断原则，即有任何一个白名单规则匹配后就跳过验证，不触发过滤器。

以下是更多的示例：

    'service_whitelist' => array(
        '*.Index',           // 全部的Index方法
        'Test.*',            // Api_Test的全部方法
        'User.GetBaseInfo',  // Api_User::GetBaseInfo()方法
    ),

配置好上面的白名单后，以下这些接口服务全部不会触发过滤器：

// 全部的Index方法
?service=Default.Index
?service=User.Index

// Api_Test的全部方法
?service=Test.DoSth
?service=Test.Hello
?service=Test.GOGOGO

// Api_User::GetBaseInfo()方法
?service=User.GetBaseInfo

温馨揭示：以上白名单配置，需要PhalApi 1.4.0及以上版本，方可支持。

1.22.5 更好地建议

通常关于接口签名这块，我们还需要：

  • 1、为不同的接入方定义不同的密钥和私钥；

  • 2、如果业务需要，为各个接口、各个接入方分配调用权限；

  • 3、统一签名参数的规则，可以配置在./Config/app.php中的，如上面的签名需要的参数，我们可以追加统一的参数规则：
    /**
 * 应用接口层的统一参数
 */
'apiCommonRules' => array(
    'signature' => array('name' => 'signature', 'require' => true),
    'timestamp' => array('name' => 'timestamp', 'require' => true),
    'nonce' => array('name' => 'nonce', 'require' => true),
),

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.23]-请求和响应：GET和POST两者皆可得及超越JSON格式返回 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.23.1 请求

把我们后台的API想象成一个函数，那么请求的参数就是我们的参数列表；而接口响应的数据则对应函数返回的结果。

对于请求，正如前面所看到的，我们可以使用$_GET，也可以使用$_POST，也可以两者都使用，还可以在测试时自己指定模拟的请求数据包。

或者，在实际项目开发中，我们还需要根据自身的需求，跟我们自己的客户端作一些约定。如通常地，我们会要求客户端 service参数必须用GET方式 ，以便服务器返回500时定位接口服务位置。对此，简单的我们把$_POST['service']去掉即可，如在入口文件前面添加：

unset($_POST['service']);   //只接收GET方式的service参数

(1) 在index.php入口处指定数据源

很多时间，不同的项目对数据接收有不同的需求。如简单地，强制统一使用$_POST参数，我们可以这样在入口处进行代码调整：

//vim ./index.php

DI()->request = new PhalApi_Request($_POST);

对于复杂的情况，如需要使用post_raw数据，则可以继承PhalApi_Request实现相应的数据源解析。如：

class My_Request extends PhalApi_Request{
    public function __construct($data = NULL) {
        parent::__construct($data);

        // json处理
        $this->post = json_decode(file_get_contents('php://input'), TRUE);

        // 普通xml处理
        $this->post = simplexml_load_string (
            file_get_contents('php://input'),
            'SimpleXMLElement',
            LIBXML_NOCDATA
        );
        $this->post = json_decode(json_encode($this->post), TRUE);
    }
}

然后在子类实现对各类参数的数据源的准备。可以说，PhalApi_Request::__construct()构造函数用于初始化各类辅助侯选的数据源，而PhalApi_Request::getData()则用于生成主要默认的数据源。

(2) 单元测试时指定数据源

在进行单元测试时，我们需要模拟接口的请求动作，也需要提供接口参数。这时的参数的指定更为灵活。可通过以下代码来实现，即：

//数据源
$data = array(...);

DI()->request = new PhalApi_Request($data);

或者使用PhalApi封装的测试类来快速模拟调用接口：

    public function testIndexByRunner()
    {
        //Step 1. 构建请求URL
        $url = 'service=Default.Index&username=dogstar';
        $params = array();

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url, $params);
    }

(3) 接口数据的加密传送

有时，出于安全性的考虑，项目需要对请求的接口参数进行对称加密传送。这时可以通过重载PhalApi_Request::genData()来轻松实现。
假设，我们现在需要把全部的参数base64编码序列化后通过$_POST['data']来传递，则相应的解析代码如下。

第一步，先定义自己的扩展请求类，在里面完成对称解析的动作：

<?php

class Common_Request extends PhalApi_Request {

    public function genData($data) {
        if (!isset($data) || !is_array($data)) {
            $data = $_POST; //改成只接收POST
        }

        return isset($data['data']) ? base64_decode($data['data']) : array();
    }
}

第二步，在index.php入口文件中重新注册请求类（即添加以下代码）：

//重新注册request
DI()->request = 'Common_Request';

然后，就可以轻松实现了接口参数的对称加密传送。
至此，你也许已经发现：指定数据源和对称加密是可以结合来使用的。

(4) 接口参数级别的数据源

除了可以指定全局的接口数据源外，还可以进行更细致的配置，即为某个接口参数指定使用$GET、$_POST、$_COOKIE、$_SERVER、$_REQUEST或头部等其他数据源。

其使用方式是在配置接口参数规则时，使用source配置来指定当前参数的数据源，如指定用户在登录时，用户名使用$_GET、密码使用$_POST。

public function getRules() {
    return array(
        'login' => array(
            'username' => array('name' => 'username', 'source' => 'get'),
            'password' => array('name' => 'password', 'source' => 'post'),
        ),
    );
}

这样，PhalApi框架就会从$_GET中提取username参数，从$_POST中提取password参数。

目前支持的source与对应的数据源映射关系如下：

source | 对应的数据源
post | $_POST
get | $_GET
cookie | $_COOKIE
server | $_SERVER
requset | $_REQUEST
header | $_SERVER['HTTP_X']

因此，这样就可以轻松、更自由获取不同来源的参数。以下是一些常用的配置示例。

// 获取HTTP请求方法，判断是POST还是GET
'method' => array('name' => 'REQUEST_METHOD', 'source' => 'server'),

// 获取COOKIE中的标识
'is_new_user' => array('name' => 'is_new_user', 'source' => 'cookie'),

// 获取HTTP头部中的编码，判断是否为utf-8
'charset' => array('name' => 'Accept-Charset', 'source' => 'header'),

若需要扩展项目自定义的映射关系，则可以重载PhalApi_Request::getDataBySource($source)方法，如：

class My_Request extends PhalApi_Request {
    protected function &getDataBySource($source) {
        if (strtoupper($source) == 'stream') {
            // TODO 处理二进制流
        }

        return parent::getDataBySource($source);
    }
}

然后，便可在项目中这样配置使用二进制流的数据源。

// 从二进制流中获取密码
'password' => array('name' => 'password', 'source' => 'stream'),

若配置的source为无效或非法时，则会抛出异常。
如故意配置一个错误的数据源source为NOT_FOUND。

'password' => array('name' => 'password', 'source' => 'NOT_FOUND'),

在请求接口时，会遇到这样的异常提示返回。

{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: 参数规则中未知的数据源：NOT_FOUND"
}

温馨提示：以上功能需要PhalApi 1.4.0 及以上版本支持。

1.23.2 响应

当前默认使用JSON的格式返回，但项目需要其他返回格式也是可以的。只需要实现PhalApi_Response抽象中的formatResult($result)格式化返回结果，然后也是重新注册DI()->response服务即可。如：

(1) JSONP的返回

在H5页面中，我们可能会需要用到JSONP的返回，所以这里默认提供了这种格式的支持。

在入口文件中，添加：

//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

在接口访问时再带上&callback=XXX参数即可。

这里在创建响应服务时，可以看到是用了回调函数的名字进行初始化。考虑到会存在XSS（跨站脚本攻击），对回调函数要进行相应的过滤，可以用黑名单或者白名单的方式。黑名单方式暂时还没提供，白名单相对简单但需要项目自己实现，如：

class Common_JsonP extends PhalApi_Response_JsonP {

    protected function clearXss($callback) {
        return in_array($callback, array('fun1', 'func2', 'func3')) ? $callback : '';
    }
}

(2) 其他格式返回

如上面所述，当需要返回一种当前PhalApi没提供的格式时，可以：

  • 1、实现PhalApi_Response抽象中的formatResult($result)格式化返回结果
  • 2、重新注册DI()->response服务

这里以扩展XML返回格式为例，简单说明。

首先，添加实现一个XML响应子类：

<?php

class Common_Response_XML extends PhalApi_Response {

    protected function formatResult($result) {
        return ArrayToXML::toXml($result);
    }
}

关于ArrayToXML，请查看： 将PHP数组转成XML

然后，重新注册：

DI()->response = new Common_Response_XML();

done!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.24]-缓存策略：更灵活地可配置化的多级缓存 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.24.1 论缓存的重要性

在很多情况下，我们都需要缓存。如对重复获取但变化不大的数据进行缓存以提供服务器的响应能力，又如当需要为数据库服务器减少QPS时等等。但同时缓存也因为有延时导致数据不能实时更新，或者在需要更新时不能被更新。如在接口调试、单元测试或者预览时，但这些都可以通过一些技巧来获得。这里，将从简单的缓存、再到高速缓存、多级缓存逐步进行说明。

1.24.2 简单缓存

这里所指的简单缓存，是指文件缓存。因为实现简单，且部署方便。但其缺点也是明显的，如文件I/O读写导致性能低，不能支持分布式。所以在没有集群服务器下是适用的。

当需要使用文件缓存时，先对文件缓存进行DI注册，便可使用。

//$ vim ./Public/init.php
DI()->cache = new PhalApi_Cache_File(array('path' => API_ROOT . '/Runtime', 'prefix' => 'demo'));

// 设置
DI()->cache->set('thisYear', 2015, 600);

// 获取
echo DI()->cache->get('thisYear');

// 删除
DI()->cache->delete('thisYear');

1.24.3 高速缓存

高速缓存通常具备分布式的能力，并能进驻内存，因此性能高。但同样此优点所带来的则是需要另外部署缓存扩展，如常见的Memcached、Redis。若需要考虑缓存落地，还要进一点筛选和配置。

如使用Memcached：

DI()->cache = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211, 'prefix' => 'demo_'));

// 设置
DI()->cache->set('thisYear', 2015, 600);

// 获取
echo DI()->cache->get('thisYear');

// 删除
DI()->cache->delete('thisYear');

从上面示例代码注意到，我们使用了前缀配置，以防同一台MC服务器同一端口下key名冲突。当然，更好是使用配置文件来配置缓存的地址和端口，以及前缀。

如何配置多个Memcache/Memcached实例？

实际项目开发中，当需要连接多个Memcache/Memcached实例，可以在单个实例配置基础上采用以下配置：

$config = array(
    'host'    => '192.168.1.1, 192.168.1.2',  //多个用英文逗号分割
    'port'    => '11211, 11212',              //多个用英文逗号分割
    'weight'  => '20, 80',                    //（可选）多个用英文逗号分割
);

DI()->cache = new PhalApi_Cache_Memcached($config);

上面配置了两个MC实例，分别是：

  • 192.168.1.1，端口为11211，权重为20
  • 192.168.1.2，端口为11212，权重为80

其中，权重是可选的。并且以host域名的数量为基准，即最终MC实例数量以host的个数为准。端口数量不足时取默认值11211，多出的端口会被忽略；同样，权重数量不足时取默认值0，多出的权重会被忽略。

如下，是一份稀疏配置：

$config = array(
    'host'    => '192.168.1.1, 192.168.1.2, 192.168.1.3',
    'port'    => '11210',
);

相当于：

  • 192.168.1.1，端口为11210，权重为0（默认值）
  • 192.168.1.2，端口为11211（默认值），权重为0（默认值）
  • 192.168.1.3，端口为11211（默认值），权重为0（默认值）

请注意，通常不建议在权重weight使用稀疏配置，即要么全部不配置权重，要么全部配置权重，以免部分使用默认权重为0的MC实例不生效。

温馨提示：以上多个MC实例的配置，须更新到PhalApi 1.4.0及版本以上。

1.24.4 多级缓存

然而，很多时候，我们需要多级缓存来承载更大的访问量。以便结合本地低速缓存和分布式高速缓存来获得更好的用户体验和服务器吞吐率。这时，则可以使用多级缓存策略。

在切换到多级缓存时，我们明显希望原有的代码调用不需要同步调整便能获得升级后的多级缓存，并且我们后台开发人员也希望可以轻便在原有的单点缓存的基础上进行扩展。所以这就引出了一个有趣的问题：我们该如何组织多级缓存？

作为一个框架，除了考虑上述的原有调用、单点缓存复用外，还需要考虑到多级缓存的组装。部分框架，一如我最喜欢的Phalcon则是使用了配置的形式来实现。但仍然，我们需要了解其配置的各个格式要求才能更好掌握和使用，这点带来了学习的成本。因此，PhalApi则是使用了简单的组合模式来实现。

正如你在源代码中看到的PhalApi_Cache_Multi类，通过此类的实例可以利用PhalApi_Cache_Multi::addCache()快速添加一个缓存节点，而节点的优先级则按开发同学添加的顺序来确定。显然在添加时，我们应该先添加分布式的高速缓存，再添加本地的低速缓存（希望不会有人先添加文件缓存再添加MC缓存）。而各个节点的初始化，则是我们之前所熟悉的。只是简单添加即可。

以下是结合文件缓存和MC缓存的多级缓存示例：

$cache = PhalApi_Cache_Multi();

$mcCache = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211, 'prefix' => 'demo_'));
$cache->addCache($mcCache);

$fileCache = new PhalApi_Cache_File(array('path' => API_ROOT . '/Runtime', 'prefix' => 'demo'));
$cache->addCache($fileCache);

DI()->cache = $cache;

然后，就可像之前那样设置、获取和删除缓存，而不需考虑是单点缓存，还是多级缓存：

// 设置
DI()->cache->set('thisYear', 2015, 600);

// 获取
echo DI()->cache->get('thisYear');

// 删除
DI()->cache->delete('thisYear');

1.24.5 静态结构图

对应地，我们可以得出清晰明了的UML静态结构图：

结构层次非常简单，但主要分为三大类：左边是多级缓存；中间突出的是特殊情况，即：空对象模式下的空缓存；右边是目前已提供或者后期扩展的具体缓存实现。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.25]-国际化翻译：为走向国际化提前做好翻译准备 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

Your new understanding has made you powerful. Please use your new powers for good.

1.25.1 固定的中文写法

一直以来，我们都是硬编码方式返回中文的文案或者提示，如：

$rs['msg'] = '用户不存在';

这种写法在项目根本不需要考虑国际化翻译时，是完全没问题的。

1.25.2 通用的翻译写法

当我们需要进行翻译时，可以这样进行调整：

$rs['msg'] = T('user not exists');

然后在对应的翻译文件中（如中文对应文件是：./Language/zh_cn/common.php）添加对应的翻译即可：

// $vim ./Language/zh_cn/common.php
return array(
    'user not exists' => '用户不存在',
);

(1)当存在动态变量时？

有时，我们需要动态返回一些值，这里可以用 大括号 将需要动态替换的值包住并提供替换的参数即可，一如：

echo T('hello {name}', array('name' => 'dogstar'));

如果对应的中文翻译是：

'hello {name}' => '您好，{name}',

将会看到输出：

您好，dogstar

(2)当多个动态变量时，还可以这样省略变量名

如果要简化动态变量的传递，可以这样：

echo T('{0} I love you because {1}', array('PhalApi', 'no reasons'));

如果对应的中文翻译是：

'{0} I love you because {1}' => '{0} 我爱你因为{1}',

将会看到输出：

PhalApi 我爱你因为no reasons

(3)当翻译不存在时？

翻译不存在，有两种场景：一是指定的语言包不存在；二是语言包存在但翻译不存在。无论何种情况，找不到翻译时，都会用代码硬编码的内容返回。

1.25.3 语言的设定

当我们拥有了多种语言时，则可以在入口/初始化文件中，选择设定需要的语言。

SL('zh_cn');    //SL()函数等效于PhalApi_Translator::setLanguage()

参数即为语言包的路径名，如下面的en, zh_cn：

.
|-- en
|   `-- common.php
`-- zh_cn
    `-- common.php

此处，也可以通过客户端传递参数来自行选择语言。简单地：

SL($_GET['lan']);

1.25.4 添加语言翻译包

有时需要手动添加语言翻译包，因为对应的翻译不一定在项目的./Language下，如扩展类库。

当需要添加额外的语言翻译包时，可以这样添加：

        PhalApi_Translator::addMessage(API_ROOT . '/Library/User');

在User这个目录下的翻译包类似地，为：

./Language/
└── zh_cn
    └── common.php

1 directory, 1 file

相当于追加一个额外的项目路径，其他的加载过程和原来的一样。

1.25.5 建议

所以，你可以轻松看到，所谓的翻译也只是通过数组下标找一下对应的内容而已，没有太多的技术性，也没有过多的性能问题。

但正是有这样提前周到的国际化准备，我们可以对外（如像产品、BOSS和外界）传送这样一个隐喻： 我们的项目可以快速支持国际翻译 。
这听起来多么高大尚啊！因为那些不懂技术的人，根本不在乎是用PHP的数组来存放还是什么技术，而在于能不能走向国际化。

SO？既然翻译”无伤大雅“（指对性能的影响和对代码编写的阻碍），统一使用翻译的写法是值得推荐的。

即使项目没有机会用到真正的翻译，但至少有两点我认为也是有用的：

  • 1、便于产品维护接口返回的提示文案；
  • 2、被同行问到时，你们有支持i18n吗？我们也可以笑着回答：有 ^_^ ；

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.26]-数据安全：数据对称加密方案 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.26.1 对称加密和非对称加密

关于数据加密和安全这一块，只作简单说明。

首先，对称加密是指数据可以加密成密文也可以解密还原，共用同一个密钥，而非对称是则公钥和私钥，两者皆使用可逆加密算法。不可逆加密不存在密钥，只有salt，用来增加可变性和随机性，如md5。而在加密里面，又数块算法加密的方式最为完善，随附图一张：

1.26.2 PHP的mcrypt加密扩展

在PhalApi中，同样也是使用了mcrypt作为底层的数据加密技术方案。请查看： PHP 手册 函数参考 加密扩展

1.26.3 加解密的使用

在单元测试中，我们可以快速找到加密和解密的使用，这里再简单举一例：

$mcrypt = new PhalApi_Crypt_Mcrypt('12345678');

$data = 'The Best Day of My Life';
$key = 'phalapi';

$encryptData = $mcrypt->encrypt($data, $key);
var_dump($encryptData);

$decryptData = $mcrypt->decrypt($encryptData, $key);
var_dump($decryptData);

上面将会输出(有乱码)：

1.26.4 更富弹性和便于存储的加密方案

上面看到，mcrypt下的加密在两点不足：

  • 1、有乱码，不能很好地永久化存储；
  • 2、只针对文本字符串的加密，不支持数组等，且无法还原类型；

为此， 我们提供了更富弹性和便于存储的加密方案，即：序列化 + base64 + mcrypt的多重加密方案。

以下是上面的示例-多重加密版：

$mcrypt = new PhalApi_Crypt_MultiMcrypt('12345678');

$data = 'The Best Day of My Life';
$key = 'phalapi';

$encryptData = $mcrypt->encrypt($data, $key);
var_dump($encryptData);

$decryptData = $mcrypt->decrypt($encryptData, $key);
var_dump($decryptData);

对应的输出（这里使用了文字结果输出，是因为没了乱码）：

string(44) "rmFMdhvszAkHhOdzwt/APBACk/Mn/SqhV1Ahp1xT0Gk="
string(23) "The Best Day of My Life"

1.26.5 RSA的支持与超长字符串的应对方案

基于项目有使用RSA进行加密和解密的需求，这里特扩展对RSA的支持。同时针对到RSA对字符串长度的限制，提供了分段处理的方案。RSA加密模块的静态类结构UML如下：
rsa-PhalApi

(1)原生态的通信加密和解密

此部分只是简单地封装了openssl相关函数的操作，可以实现与其他语言和客户端下RSA的加密通信。
唯一需要注意的是，对于 “私钥加密，公钥解密” 和 “公钥加密，私钥解密” 这两种情况下key的互换和对应问题。不要混淆。

(2)超长字符串的分段处理

这里重点说明一下超长字符串通信加密的问题。
解决方案主要涉及两点：一是分段的处理，二是中间层转换。分段是指将待加密的字符串分割成允许最大长度117（有用户反馈说是127）内的数组，再各自处理；中间层转换是为了稳定性、通用性和方便落地存储，使用了json和base64的结合编码。

虽然此方案解决了超长字符串的问题，但需要特别指出的是， 不能与其他语言、或者PHP其他框架和客户端进行原生态的RSA通信 。
我们突破了长度的限制，但失去了通用性。这里罗列一下各个场景和对应的处理方式：

支持：PhalApi项目A <--> PhalApi项目A

支持：PhalApi项目A <--> PhalApi项目B，PhalApi项目C，PhalApi项目D，...

不支持：PhalApi项目 <--> 非PhalApi项目的PHP项目

解决方案：将以下RSA模块相关的代码以包的形式拷贝到需要的PHP项目。

.
├── Crypt
│   └── RSA
│       ├── KeyGenerator.php
│       ├── MultiBase.php
│       ├── MultiPri2Pub.php
│       ├── MultiPub2Pri.php
│       ├── Pri2Pub.php
│       └── Pub2Pri.php
└── Crypt.php

不支持：PhalApi项目 <--> 非PHP语言的项目

解决方案：参考PhalApi对RSA超长字符串的处理，同步实现。

不支持：PhalApi项目 <--> 客户端（iOS/Android/Windows Phone, etc）

解决方案：参考PhalApi对RSA超长字符串的处理，同步实现。

(3)使用示例：2块2毛2和故事

这是一个有趣的故事，发生在我一个很好的朋友身上。

在去年暑假的时候，有位师妹通过几层关系找到我朋友，想他帮她支付回家的火车票。本着“调试”（调戏）的精神，我朋友爽快地答应了并让她把支付宝帐号发过来。然后，不一会，我朋友收到了一条有支付宝帐号的短信，立马打了2.22元过去，并附言： 别问我是谁，请叫我雷锋！ 后来证实师妹没发来过短信，原来收到的那条是诈骗的短信。。。。你能想象得到骗子收到这2.22元和看到这条留言时的表情么，哈哈~~

以下测试代码，以上面故事为背景，并演示了RSA的使用示例：

    public function testDecryptAfterEncrypt()
    {
        $keyG = new PhalApi_Crypt_RSA_KeyGenerator();
        $privkey = $keyG->getPriKey();
        $pubkey = $keyG->getPubKey();

        DI()->crypt = new PhalApi_Crypt_RSA_MultiPri2Pub();

        $data = 'AHA! I have $2.22 dollars!';

        $encryptData = DI()->crypt->encrypt($data, $privkey);

        $decryptData = DI()->crypt->decrypt($encryptData, $pubkey);

        $this->assertEquals($data, $decryptData);
    }

1.26.6 建议

在上面的加密中，接口项目在开发时，需要自定义两个值：加密向量和私钥。

为了提高数据加密的安全度，建议：

  • 加密向量项目统一在./Config/app.php中配置；
  • 各模块业务数据加密所用的Key则由各业务点自定义；

这样，可以对不同的数据使用不同的加密私钥，即使破解了某一个用户的数据，也难以破解其他用户的。

1.26.7 扩展你的对称加密

尤其对于加密方案和算法，我们在项目开发决策时，更应该优先考虑使用现在行业内成熟公认的加密方案和算法，而不是自己去从头研发。

但如果你项目确实有此需要，或者需要在mcrypt的基础上再作一些变通，也是可以很快地实现和注册使用。

首先，请先实现下面的加密接口：

// $vim ./PhalApi/PhalApi/Crypt.php

interface PhalApi_Crypt {

    public function encrypt($data, $key);

    public function decrypt($data, $key);
}

然后，重新注册加密服务即可。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.27]-精益开发：更富表现力的Model层和重量级数据获取的应对方案 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

我们能够有意识地推断我们想要在哪一条想法的溪流中遨游，然而此后与那些想法的接触会潜在地塑造我们的习惯和信仰。 -- 《智慧社会》

1.27.1 写在前面的话

此篇章有点长，但我认为是值得一读的。因为这里我将逐步讲述如何在已有的基础上演变扩展出更高层次的代码结构和系统架构，而不致于因目前频繁的需求变更而导致代码凌乱不堪。更为重要的是，你将能从中发现，如何在一个框架中持续演变，最终体验浮现式设计的乐趣。如果你的项目亦能如此，我相信你会找到编程如同搭建积木般轻便明了的感觉。

1.27.2 更富表现力的Model层

接口不尽相同，主要区别在于领域业务数据的处理。而数据的来源则更为广泛，可能是来自数据库，可能来自第三方平台接口，可能存放于内存。所以，PhalApi这里的Model层，则是 广义上的数据源层 ，用于获取原始的业务数据，而不管来自何方，何种存储媒介。这也是为什么我们没有将Model层打造成活动纪录或者数据映射器的原因。当然，如果你确实需要，也可以自行调整。

如果数据来源于数据库，我们则需要考虑到数据库服务器的感受，保证不会有过载的请求而导致它罢工。对此，我们可以结合缓存来进行性能优化。

如，一般地：

    // 版本1：简单的获取
        $model = new Model_User();
        $rs = $model->getByUserId($userId);

这种是没有缓存的情况，当发现有性能问题并且可以通过缓存来解决时，我们可以在调用时简单引入缓存：

        // 版本2：使用单点缓存/多级缓存 (应该移至Model层中)
        $key = 'userbaseinfo_' . $userId;
        $rs = DI()->cache->get($key);
        if ($rs === NULL) {
                $rs = $model->getByUserId($userId);
                DI()->cache->set($key, $rs, 600);
        }

但不建议在领域Domain层中引入缓存，因为会导致混淆和不便进行测试。更好是将缓存的处理移至Model，保持数据获取的透明性：

class Model_User extends PhalApi_Model_NotORM {

    public function getByUserIdWithCache($userId) {
        $key = 'userbaseinfo_' . $userId;
        $rs = DI()->cache->get($key);
        if ($rs === NULL) {
            $rs = $this->getByUserId($userId);
            DI()->cache->set($key, $rs, 600);
        }
            return $rs;
    }

对应地，外部的调用调整成：

    // 版本3：使用单点缓存/多级缓存
        $model = new Model_User();
        $rs = $model->getByUserIdWithCache($userId);

至此，Model层对于上层如Domain来说，负责获取源数据，而不管此数据来自于数据库，还是远程接口，抑或是缓存包装下的数据。这正是我们使用数组在Model层和Domain层通讯的原因，因为数组更加通用，不需要额外添加实体。

1.27.3 重量级数据获取的应对方案

纵使更富表现力的Model很好地封装了源数据的获取，但是仍然会遇到一些尴尬的问题。特别地，当我们大量地进行缓存读取判断时，会出现很多重复的代码，这样既不雅观也难以管理，甚至会出现一些简单的人为编写错误而导致的BUG。另外，当我们需要进行预览、调试或测试时，我们是不希望看到缓存的，即我们能够手工指定是否需要缓存。

这里再稍微简单回顾总结一下我们现在的问题：我们希望通过缓存策略来优化Model层的源数据获取，特别当源数据获取的成本非常大时。但我们又希望我们可以轻易控制何时需要缓存，何时不需要，并且希望原有的代码能在OCP的原则下不需要修改，但又能很好地传递源数据获取的复杂参数。归纳一下，则可分为三点：缓存的控制、源数据的获取、复杂参数的传递。

(1)缓存的控制

不管是单点缓存，还是多级缓存，都希望使用原有已经注册的cache组件服务。所以，应该使用委托。委托的另一个好处在于使用外部依赖注入可以获得更好的测试性。

(2)源数据的获取

源数据的获取，作为源数据获取的主要过程和主要实现，需要进行缓存的控制（可细分为：是否允许读缓存、和是否允许写缓存）、 获取缓存的key值和有效时间，以及最终原始数据的获取。明显，这里应该使用模板方法，然后提供钩子函数给具体子类。

这里，我们提供了Model代理抽象类PhalApi_ModelProxy。

之所以使用代理模式，是因为实际上并不一定会真正调用到最终源数据的获取，因为往往源数据的获取成本非常高，故而我们希望通过缓存来拦截数据的获取。

由于Model代理被上层的Domain领域层调用，但又依赖于下层Model层获得原始数据，所以处于Domain和Model之间。为了保持良好的项目代码层级，如果需要创建PhalApi_ModelProxy子类，建议新建一个ModelProxy目录。

如对用户基本信息的获取，我们添加了一个代理：

class ModelProxy_UserBaseInfo extends PhalApi_ModelProxy {

    protected function doGetData($query) {
        $model = new Model_User();

        return $model->getByUserId($query->id);
    }

    protected function getKey($query) {
        return 'userbaseinfo_' . $query->id;
    }

    protected function getExpire($query) {
        return 600;
    }
}

其中，doGetData($query)方法由具体子类实现，委托给Model_User的实例进行源数据获取。另外，实现钩子函数以返回缓存唯一key，和缓存的有效时间。

这里只是作为简单的示例，更好的建议是应该将缓存的时间纳入配置中管理，如 配置四个缓存级别：低(5 min)、中(10 min)、高(30 min)、超(1 h) ，然后根据不同的业务数据使用不同的缓存级别。这样，即便于团队交流，也便于缓存时间的统一调整。

(3)复杂参数的传递

敏锐的读者会发现，上面有一个$query查询对象，这就是我们即将谈到的复杂参数的传递。

$query是查询对象PhalApi_ModelQuery的实例。我们强烈建议此类实例应当被作为 值对象 对待。虽然我们出于便利将此类对象设计成了结构化的使用。但你可以轻松通过new PhalApi_ModelQuery($query->toArray())来拷贝一个新的查询对象。

此查询对象，目前包括了四个成员变量：是否读缓存、 是否写缓存、主键id、时间戳。

很多时候，这四个基本的变量是满足不了各项目的实际需求的，因此你可以定义你的查询子类， 以支持丰富的数据获取。如调用优酷平台接口获取用户最近上传发布的视频时，需要用户昵称、获取的数量、排序种类等。

(4)最终的调用

在完成了上面的工作后，让我们看下最终呈现的效果：

        // 版本3：缓存 + 代理
        $query = new PhalApi_ModelQuery();
        $query->id = $userId;
        $modelProxy = new ModelProxy_UserBaseInfo();
        $rs = $modelProxy->getData($query);

在领域层中，我们切换到了Model代理获取数据，而不再是原来的Model直接获取。其中新增的是代理具体类 ModelProxy_UserBaseInfo，和可选的查询类。

(5)UML静态图

至此，我们很好地在源数据的获取基础上，统一结合缓存策略。你会发现： 缓存节点可变、具体的源数据可变、复杂的查询亦可变 。

将此图简化一下，可得到：

这样的设计是合理的，因为缓存节点我们希望能在项目内共享，而不管是哪块的业务数据；对于具体的源数据获取明显也是不尽相同，所以也需要各自实现，同时对于同一类业务数据（如用户基本信息）则使用一样的缓存有效时间和指定格式的缓存key（通常结合不同的id组成唯一key）；最后在前面的缓存共享和同类数据的基础上，还需要支持不同数据的具体获取，因此需要查询对象。也就是说，你可以在不同的层级不同的范畴内进行自由的控制和定制。

如果退回到最初的版本，我们可以对比发现，Model Proxy就是Domain和Model间的桥梁，即：中间层。因为每次直接通过Model获取源数据的成本较大，我们可以通过Model Proxy模型代理来缓存获取的数据来减轻服务器的压力。

1.27.4 细粒度和可测试性

这无疑是细粒度的划分，但对于支撑复杂的领域业务却发挥着重要的作用。一来是如此清楚明了，二来则是带来了可测试性。

正如前面提及到的，我们在预览、调试、单元测试或者后台计划任务时，不希望有缓存的干扰。在细粒度划分的基础上，可轻松用以下方法实现而不必担心会破坏代码的简洁性。

(1)取消缓存的方法1： 外部注入模拟缓存

在构造Model代理时，默认情况下使用了DI()->cache作为缓存，当需要进行单元测试时，我们可以两种途径在外部注入模拟的缓存而达到测试的目的：替换全局的DI()->cache，或单次构造注入。对于计划任务则可以在统一的后台任务启动文件将DI()->cache设置成空对象。

(2)取消缓存的方法2： 查询中的缓存控制

在项目层次，我们可以统一构造自己的查询基类，以实现对缓存的控制。

如：

class Common_ModelQuery extends PhalApi_ModelQuery {

    public function __construct($queryArr = array()) {
        parent::__construct($queryArr);

        if (DI()->debug) {
            $this->readCache = FALSE;
            $this->writeCache = FALSE;
        }
    }
}

至于DI()->debug的设置，则可以在入口文件中根据约定的接口参数设定，简单地如：

if (isset($_GET['debug']) && $_GET['debug'] == 1) {
    DI()->debug = true;
}

这样便可以获得了接口预览和调试的能力。

1.27.5 何时使用此方案？

可以看到，此方案是在缓存策略（包括单点缓存、低高速缓存、多级缓存）和广义Model层基础上扩展的，以便应对重量级的业务数据获取。此方案有一定的优势，但作为代价则是额外的代码编写以及层级复杂性。并且，我们还没谈及到数据变更时的处理。

所以，请在确切需要统一封装高成本的数据获取时，才使用此方案。

1.27.6 扩展：多接口参数传递的优雅处理方案

当接口的查询参数过多时，我们需要手工重复地将接口参数从Api层传递到Domain层，再通过Query对象传递到Model层，这中间任何一个环节的缺失或遗漏都会造成一个BUG。

为此，项目可以考虑使用一种更为优雅的方案来进行整合，并实现自动化参数获取，但又保留接口原来的参数验证。

假设，我们需要以下多个接口参数：

function getRules() {
    return array(
        'getList' => array(
            'keyword' => array(...),
            'filed' => array(...),
            'page' => array(...),
            'perpage' => array(...),
            'order' => array(...),
        ),
    );
}

为避免出现以下这样的手工调用（而且也不符合值对象的特征）：

$query = new Query_Demo();
$query->keyword = $this->keyword;
$query->filed = $this->filed;
$query->page = $this->page;
$query->perpage = $this->perpage;
$query->order = $this->order;

$domain = new Domain_Demo();
$list = $domain->getList($query);

我们首先需要提取出一个层超类：

class Query_Demo extends PhalApi_ModelQuery {
    public $keyWord;
    public $filed;
    public $page;
    public $perpage;
    public $order;

    public function __construct($api) {
        //按需获取，自动初始化
        $vars = get_object_vars($api);
        foreach ($vars as $key => $var) {
            if (isset($api->$key)) {
                $this->$key = $api->$key;
            }
        }
    }
}

然后，在接口Api中对Domain层的调用就会简化成：

$query = new Query_Demo($this); //自动初始化

$domain = new Domain_Demo();
$list = $domain->getList($query); //通过查询对象传递众多参数

这样的好处在于：

  • 1、更方便职能的划分
  • 2、易于测试
  • 3、实现简单（可提取一个Query的层超类来完成自动填充）
  • 4、便于IDE时的参数提示，同时可以提供默认值

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.28]-COOKIE：对COOKIE原生态的支持及记忆加密升级版 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

只要你能把握自己，你就能把握任何东西。 -- 古谚语

1.28.1 原生态的支持

应开发同学的需求，这里补充对COOKIE的支持。

因为，确实在项目的开发过程中，有时是需要对COOKIE进行处理的。所以，这里提供了简单的原生态支持。

下面通过代码示例来简明如何使用。

(1)注册COOKIE服务

如同其他的服务一样，我们在使用前需要对COOKIE进行注册。不需要用到COOKIE的同学，可以忽略此服务的注册。

在初始化文件init.php中添加如下注册代码：

//COOKIE
DI()->cookie = 'PhalApi_Cookie';

(2)使用

查看PhalApi_Cookie提供的接口，可以得到针对COOKIE的三种操作：

//设置COOKIE服务
DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

//获取
echo DI()->cookie->get('name');  //输出 phalapi

//删除
DI()->cookie->delete('name');

1.28.2 记忆加密升级版

实际情况，项目的使用的情况更为复杂。

比如，需要对数据进行加解密，或者需要突破COOKIE的限制（设置后需要下一次请求才能获取，不过这也正是COOKIE的特点），想记住这一次设置的内容。所以，额外提供了这个升级版本。

(1)注册COOKIE服务

和原生态的初始化，有些许不同，在于需要配置指定加解密的服务。如下：

$config = array('crypt' => $crypt, 'key' => 'a secrect');
DI()->cookie = new PhalApi_Cookie_Multi($config);

上面中的crypt即为加密解服务，未指定时使用DI()->crypt，需要实现PhalApi_Crypt接口；key则为对应的密钥。

(2)使用COOKIE服务

使用同原生态一样，客户端感知不到区别，这里不再赘述。

(3)一个简单的实现示例

假设我们有这样一个简单的加解密类（真的很简单的）：

class Cookie_Crypt_Mock implements PhalApi_Crypt {

    public function encrypt($data, $key) {
        return base64_encode($data);
    }

    public function decrypt($data, $key) {
        return base64_decode($data);
    }
}

随后，写一个简单的接口测试一下（因为涉及到COOKIE的操作，需要与浏览器可视化测试，所以没用单元测试）：

    public function cookieTest() {
        $rs = array();

        $config = array('crypt' => new Cookie_Crypt_Mock(), 'key' => 'a secrect');
        DI()->cookie = new PhalApi_Cookie_Multi($config);

        $rs['aEKey'] = DI()->cookie->get('name');
        DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

        return $rs;

    }

运行效果如下：

从上图中可以看出，COOKIE在浏览器已经被我们做了“加密”，哈哈~

1.28.3 一如其他框架的建议

  • 1、敏感数据不要存到COOKIE，以保证数据安全
  • 2、大的数据不要存到COOKIE，以保证瘦的客户端

1.28.4 扩展你的COOKIE

如需扩展，请重载PhalApi_Cookie基类。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.29]-开放与封闭：多入口和统一初始化 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

美国著名棒球运动员约格.贝拉说过：“通过仔细地观察，你可以了解很多事情。”

1.29.1 绝对的开放，还是绝对的固化？

框架应该都会考虑这样一个问题：到底是应该给应用项目提供统一固定的入口和初始化流程呢，还是应该给他们完全自由的空间？

但我发现，很多PHP的框架都提供了一个绝对的固化流程。也就是你能作出改动的地方很少，虽然这样应用不需要过多地去考虑。

然而我觉得，这样做，弊大于利。特别在现在项目需求背景各有各的不同时。所以，我为PhalApi框架选择了开放式的初始化做法。考虑到若开放的度太大，项目可能会迷茫，所以又结合了统一的初始化。

下面分别说明这两点：开放式入口和封闭式的初始化。

1.29.2 开放式的入口

作为一个接口系统，是需要为不同的终端、不同的开放人群，甚至不同的版本提供不同的服务的。

如：

  • 按不同的终端：iOS设备、Android设备、PC版、网页版
  • 按不同的开放人群：手机客户端、内部管理后台、内部其他系统、公共开放平台
  • 按不同的版本：版本从V1、V2到VN
  • 按不同的项目：同一个应用下可能会存在多个项目，当然更推荐每个项目单独一个系统

基于此，为不同的维度提供不同的入口就很有现实实用场景了。

更为重要的是，这些不同的入口都应该尽可能简单，并能统一共享公共的组件资源、数据库、日记系统这些。一个可能的入口示例如下：

<?php
/**
 * Demo 统一入口
 */

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()->loader->addDirs('Demo');

/** ---------------- 响应接口请求 ---------------- **/

$api = new PhalApi();
$rs = $api->response();
$rs->output();

简单解读一下上面的代码，首先要加载统一初始化文件，其实装载挂靠的接口项目（对应你的项目目录的名称），最后创建一个PhalApi接口实例进行响应、输出结果。

下面来看下统一初始化文件需要做的事情。

1.29.3 封闭式的初始化

不言而喻，尽管我们有按不同维度划分入口的需要，但统一初始化的过程更是必不可少的。

不同的入口提供了各维度定制的机会，统一的初始化则为应用提供了统一定制的机会。

(1)常规入口代码

常规的入口，需要以下初始化操作：

<?php
/**
 * 统一初始化
 */

/** ---------------- 根目录定义，自动加载 ---------------- **/

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

/** ---------------- 注册&初始化服务组件 ---------------- **/

//自动加载
DI()->loader = $loader;

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

//调试模式，$_GET['__debug__']可自行改名
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

//翻译语言包设定
SL('zh_cn');

(2)常规代码解读

上面是框架执行所需的基础服务注册和配置，一般直接可用，但也可以根据需要作些细微的调整。如日记的级别设定、调试的参数修改（改成一个只有自己知道的参数名字，别让外界知道！）等。

出于让大家对初始化过程有一个更理性的认识，这里补充一下各代码的作用。

最开始是利用了PHP原生态的时区设置和宏定义：

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

接着，便开始引入PhalApi框架的类自动加载器：

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

这样，我们就可以从原生态的PHP开发，切入到了PhalApi接口开发模式。但在捲起袖口准备大干一场前，我们还需要注册一些必备的服务：

//自动加载
DI()->loader = $loader;

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

上面的自动加载、配置、日记和数据库操作通常而言，对于一个项目都是必须的。
但配置文件的路径可自行指定，日记的存储类型也可以自由组合（多个类型采用或运算），还可以选择你合适的数据库配置。 注意到，PhalApi_DB_NotORM初始化时，除了配置文件外，还有一个debug参数，此参数的作用是用于控制是否打印显示执行的SQL语句及对应消耗的时间。

至此，我们已经为项目完成了绝大部分的基础服务注册，且上面的初始化顺序建议保留不变。因为，前后有依赖关系。

但为了让我们的项目更有活力、更具生气、更国际化，我们还可以多加这么两行代码：

//调试模式，$_GET['__debug__']可自行改名
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

//翻译语言包设定
SL('zh_cn');

这里，也有一个debug参数，之所以和数据库的分开，是因为如果混在一起会导致返回结果解析失败（如不再是JSON格式）。
此debug的来源，默认来自环境的系统配置文件（如区分生产环境和测试环境）；也可以来自某个请求的手动设置，这样，开发同学便可以快速进行在线调试了。 而这个参数，则是框架代码、项目代码以及扩展类库所共用的调试开关，至于各个场景使用的效果，视各环节而定。

SL()是一个快速函数，作用是设定翻译语言包。如果觉得中文下的UTF-8查看不直观，可以自行加个参数修改，如：

SL(isset($_GET['__lan__']) ? $_GET['__lan__'] : 'zh_cn');

(3)更多定制注册代码

上面的操作，涵盖了大部分项目的需要。除此之外，还有一些额外的服务，可根据自身的情况，定制处理：

/** ---------------- 以下服务组件就根据需要定制注册 ---------------- **/

//缓存 - MC
/**
DI()->cache = function() {
    $mc = new PhalApi_Cache_Memcached(DI()->config->get('sys.mc'));
    return $mc;
};
 */

//签名验证服务
//DI()->filter = 'Common_SignFilter';

//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

(4)更多定制解读

此部分的注册，非项目必须部分。可根据需要，自行定制。

如上面出现的第一个，即缓存服务，使用的是Memcached：

DI()->cache = function() {
    $mc = new PhalApi_Cache_Memcached(DI()->config->get('sys.mc'));
    return $mc;
};

接下来的，是重要的接口签名验证服务。之所以没有提供这个服务的实现，是出于更高安全性考虑而建议项目各自制定签名规则并实现。然后这样简单注册即可被框架自动调用：

//签名验证服务
//DI()->filter = 'Common_SignFilter';

在有些需要支持JsonP返回的场景，可以使用PhalApi_Response_JsonP返回格式开放callback操作：

//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

1.29.4 用一张图来表示

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.30]-保持的力量：接口开发最佳实践 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

神啊，求你赐给我平静的心，去接受我无法改变的事；赐给我勇气，去做我能改变的事；赐给我智慧，去分辨两者的不同。 --平静之祷

1.30.1 论保持的力量

追到一个心仪的女生不难，难于如何保持和培养一份真挚的感情；获得一时的财富也不难，难于如何长久保持收益；创业的公司很容易博得一时媒体的关注以及某次天使的投资，但难于如何排除各种障碍、充分利用各方资源发展成中企业及至上市公司。

同样，提供一时的接口很容易，但当我们需要不断为接口提供升级，以及当我们维护提供一整套接口时，面临的困难和问题会越来越大。
所以，这是一场持久的战役。需要我们用稳重的心态、专业的能力在背后持久支撑、推动。

值得庆幸的是，这些都是问题而不是限制，都是可以被解决的。
以下是结合 @郭了个浩浩 同学提供的apigee.web_api.pdf文档，以及我们多年来的项目实际开发经验为新手提供的一些建议，对老同学相信也会有所帮助。
每个建议通常会包括三部分： 现在主流的做法、PhalApi的做法以及项目的选取。

1.30.2 最佳实践建议

为了大家查阅和翻看，这里先罗列本章的全部建议：

  • (1)接口风格和协议的选择 - HTTP
  • (2)接口域名 - 使用api单独域名
  • (3)异常处理 - 200/400/500三大接口结果状态码
  • (4)对外的命名规则 - 使用小写加下划线
  • (5)对内的命名规则 - 使用驼峰法和遵循PEAR命名
  • (6)安全与验证 - 使用接口签名和token登录态双重机制
  • (7)返回结果格式 - JSON
  • (8)URL规则与路由映射 - 统一service接口服务，可一个文件一个接口
  • (9)SDK包 - 给客户端自由的调用空间和自由
  • (10)接口文档 - 使用markdown快速编写
  • (11)测试驱动开发 - 坚持单元测试

1.30.3 建议细说

(1)接口风格和协议的选择 - HTTP

目前，后台接口开发可以用RESTFull风格，也可以用Web Service；可以用SOAP协议、RPC协议，也可以用HTTP协议；可以用短链接，也可以使用长链接。如果我们希望继续进行划分，还可以分为同步或异步、单个或批量、是否有SDK包、内部接口还是开放接口平台等。

主流的做法

现在看来，大部分大型的企业以及大多数的小公司使用的都是HTTP协议下的接口开发，部分使用RESTFull，但Web Service较少。如：

  • 优酷开放平台，示例请点击
  • 微信公众号-服务器接口协议，示例请点击
  • 新浪微博开放平台，使用REST风格，示例请点击
  • Amazon ，采用Web Service、SOAP、REST等多种风格和协议提供服务，示例请点击

PhalApi的做法

我们选取了HTTP的协议，在于其无论是客户端接入、开发调试，还是部署构建上都很容易实现，而且也符合主流，因为大家都比较熟悉。
这一点是非常重要的：因为简单，后台接口开发的同学才会更容易上手；因为容易，客户端接入才会更加无压力而不用担心处处受挫。

项目的选取

根据项目不同的项目背景和需求，可以选择你合适的风格或者协议。但是即使出于安全、性能或者其他技术或非技术的原因而不采用HTTP协议的情况下，你也可以在PhalApi原有的接口开发实现时，轻松扩展你需要的协议。如使用SOAP，PHPRpc或者swoole下的TCP协议。其中，部分协议已有扩展类库提供支持。

(2)接口域名 - 使用api单独域名

首先，有一点是可以肯定的。
接口系统应该有自己单独的域名，而不应该附属于网站或者管理后台。

主流的做法

显然，主流做法也是这样做的。如：

  • 优酷开放平台：https://openapi.youku.com
  • 微信公众号： https://api.weixin.qq.com
  • 新浪微博： https://api.weibo.com

项目的选取

如果可以，尽量让接口系统使用独立的域名，并且使用api作为一级域名。如：

//你的网站为：
http://www.demo.com

//则对应的接口为：
http://api.demo.com

(3)异常处理 - 200/400/500三大接口结果状态码

对于接口的异常处理，在使用HTTP协议下，可以通过HTTP本身的响应状态码来进行区分。 在非HTTP协议并有SDK包的情况下，异常的处理手段则会更为多样。

主流的做法

优酷接口采用了HTTP响应状态码加结果返回的形式，如：

Request URL:https://openapi.youku.com/v2/videos/show_basic.json
Request Method:GET
Status Code:400 Bad Request

{"error":{"code":1004,"type":"SystemException","description":"Client id null"}}

新浪微博也一样：

Request URL:https://api.weibo.com/2/statuses/mentions/ids.json
Request Method:GET
Status Code:403 Forbidden

{"error":"auth by Null spi!","error_code":21301,"request":"/2/statuses/mentions/ids.json"}

微信接口则采用了统一200的形式，如：

Request URL:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=
Request Method:GET
Status Code:200 OK

{"errcode":41002,"errmsg":"appid missing"}

PhalApi的做法

为了与HTTP保持一致性，同时降低不必要的复杂性，我们采用了200/400/500三大接口结果状态码。

注意，这里所说的三大状态码，是指接口返回结果中的状态码，而不是HTTP的响应状态。
也就是说接口全部的结果返回都应该是200，除非接口服务有内部未捕获的异常，即：

Status Code:200 OK

返回结果状态码刚是以下几种：

//正常返回
{
    "ret": 200,
    "data": {
        //...
    },
    "msg": ""
}

//客户端非法请求
{
    "ret": 400,
    "data": [],
    "msg": "非法请求：接口服务Default.Test不存在"
}

//服务端内部错误
{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: can not connect to database db_demo"
}

####项目的选取
你可以根据你的需要，扩展400和500这两系列的错误，如401表示登录失败等。
此外，在data里面，你也可以添加一个code来表示业务级的操作码，以及客户端根据不同的业务场景做出不同和反应、交互或引导提示。

(4)对外的命名规则 - 使用小写加下划线

对外的命名，是指外部看得到的命名，如接口参数的名字，接口返回的结果节点名字，以及数据库的表名、字段名。

主流的做法

新浪微博采用了小写加下划线的做法，如：

//URL
https://c.api.weibo.com/2/friendships/followers/trend_count.json

//请求参数
source
access_token

//返回结果
{
    "uid": 10438,
    "result": [
        {
            "days": "2012-04-04",
            "follower_count_online":"15",  //粉丝数
            "active_follower":"14", //活跃粉丝数
            "loyal_follower":"0"   //互动粉丝数
        },
        ....
    ]
}

Amazon采用了首字母大写且无下划线的做法，如：

//Responses
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2009 12:00:00 GMT
Connection: close
Server: AmazonS3

<?xml version="1.0" encoding="UTF-8"?>
<BucketLoggingStatus xmlns="http://doc.s3.amazonaws.com/2006-03-01">
  <LoggingEnabled>
    <TargetBucket>mybucketlogs</TargetBucket>
    <TargetPrefix>mybucket-access_log-/</TargetPrefix>
    <TargetGrants>
      <Grant>
        <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:type="AmazonCustomerByEmail">
          <EmailAddress>user@company.com</EmailAddress>
        </Grantee>
        <Permission>READ</Permission>
      </Grant>
    </TargetGrants>
  </LoggingEnabled>
</BucketLoggingStatus>

PhalApi的做法

我们提倡使用全部小写加下划线的命名，因为这样更符合客户端的使用，如： 接口参数：

//正确的
&user_id=888

//错误的
&userId=888

返回字段：

//正确的
"device_type": "cube",

//错误的
"deviceType": "cube",

数据库字段：

//正确的
`user_id` bigint(20) DEFAULT '0' COMMENT '创建者的用户ID',

//错误的
`userId` bigint(20) DEFAULT '0' COMMENT '创建者的用户ID',

项目的选取

不管是使用全部小写，还是全部大写，项目都应该保持一致的命名风格，而不是混合凌乱的风格。

(5)对内的命名规则 - 使用驼峰法和遵循PEAR命名

与对外命名对应的则是对内的命名规则，这里又回归到了老生常谈的PHP代码风格。
这里不作过多的说明，只是稍作提及。

PhalApi的做法

我们建议使用PEAR包的命名风格，和驼峰法，如下为一个接口示例：

$ vim ./Api/Default.php
<?php
/**
 * 默认接口服务类
 *
 * @author: dogstar <chanzonghuang@gmail.com> 2014-10-04
 */

class Api_Default extends PhalApi_Api {

    public function getRules() {
        return array(
            'index' => array(
                'username'  => array('name' => 'username', 'default' => 'PHPer', ),
            ),
        );
    }

    public function index() {
        return array(
            'title' => 'Default Api',
            'content' => T('Hello {name}, Welcome to use PhalApi!', array('name' => $this->username)),
            'version' => PHALAPI_VERSION,
            'time' => $_SERVER['REQUEST_TIME'],
        );
    }
}

项目的选取

你可以选择你喜欢的风格，但团队应该保持一致。

即便你不喜欢PhalApi约定的PEAR命名，你也可以自行实现内部的类加载机制。
当有多个项目或者多个模块并存时，可以添加模块名前缀来作区分，如：

$ tree
.
├── Demo
│   └── Api
│       └── DUser.php
├── MyApp
│   └── Api
│       └── MUser.php
└── Task
    └── Api
        └── TUser.php

$ head */*/*
==> Demo/Api/DUser.php <==
<?php
class Api_DUser extends PhalApi_Api {
}

==> MyApp/Api/MUser.php <==
<?php
class Api_MUser extends PhalApi_Api {
}

==> Task/Api/TUser.php <==
<?php
class Api_TUser extends PhalApi_Api {
}

其它的Domain层和Model层等也类似，这样可以避免类名冲突，或者IDE开发环境下的混淆。

(6)安全与验证 - 使用接口签名和token登录态双重机制

既然采用HTTP协议，那么安全方面就需要接口自身进行保证。

所幸，现在可用的加密手段有多种选择。
对于接口签名，我们可以使用非对称的验签方式，如md5；也可以用对称的方式，如RSA。
最后，为每一个接入的客户端分配app_key和app_secrect即可。

当然，更好的安全是接口系统再提供登录态的验证，即通常所说的token。 这两者的相合，会为接口增加更好的安全保障。

主流的做法

  • 七牛云存储，采用Access Key/Secret Key，并且在需要时添加相应的凭证
  • 微信公众号，采用由AppID(应用ID)和AppSecret(应用密钥)生成的ACCESS_TOKEN
  • 优酷开放平台，采用应用Key client_id

PhalApi的做法

我们不提供具体的接口签名方案，是因为把这种决策移交给项目应用本身进行定制。
而定制也是非常简单的，只需要简单的两步即可：

  • 1、实现过滤器接口 PhalApi_Filter::check()；
  • 2、注册过滤器服务 DI()->filter；

对于token，虽然框架没有提供内置的实现，但可以从PhalApi的扩展类库寻找这种支持，这一点已经User扩展类库支持。

项目的选取

正如PhalApi提供的自由空间，项目可以自行实现接口签名，和根据需要是否采用User扩展类库，或者自行实现token的处理。

(7)返回结果格式 - JSON

在 [1.14.1 统一返回的格式]一节中，已经对JSON的返回格式作了说明，这里不再赘述，也只是稍作提及。

主流的做法

目前采用了JSON的格式返回的有：

  • 新浪微博
  • 优酷开放平台
  • 腾讯开放平台
  • 微信接口

采用了XML格式返回的有：

  • Amazon

PhalApi的做法

我们默认采了JSON的格式返回。

项目的选取

项目可以轻松扩展成其他格式的返回。

(8)URL规则与路由映射 - 统一service接口服务，可一个文件一个接口

先从项目内部的文件划分说起，通常最为常见的情况是，很多开发人员都喜欢把很多很多很多接口都塞到一个接口文件里面。

这样的文件，通常会有2K到3K左右。
我觉得这是一种极端，而且是一种不好的极端。因为文件过大的话，会带来很多问题。

但与之对立的有另一种做法，即一个文件，一个接口。
这一点，在我之前就职的一家出名的游戏公司中得到了广泛的认可和遵循。如：

//?service=UserInfo.Go
<?php
class Api_UserInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}

//?service=GroupInfo.Go
<?php
class Api_GroupInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}

虽然也是一种极端，但却很好地做到了接口隔离，即不用担心修改此接口的实现而影响到其他接口服务。

最后，我们再来聊URL规则，就更顺畅了。如果我们采用一个文件对应一个接口，则我们可以省略Action（全部都为go()方法），简写成：?service=XXX。
再进一步，我们可以利用接口服务器（如Nginx）的规则Rewrite来提供更好的URL规则，同时尽量隐藏我们的接口内部实现细节，如：

//原始地
http://api.demo.com/?service=UserInfo.Go

//简化地
http://api.demo.com/?service=UserInfo

//再进一步
http://api.demo.com/UserInfo

//或者
http://api.demo.com/UserInfo.json

还有一点需要关注的就是接口的版本，当有v1，v2，v3等不同的版本时，我们也需要在接口URL中体现这些版本的不同。

主流的做法

  • 新浪微博的一个接口URL：https://c.api.weibo.com/2/friendships/followers/trend_count.json
  • 优酷平台的一个接口URL：https://openapi.youku.com/v2/videos/show_basic.json
  • 微信的一个接口URL：https://api.weixin.qq.com/device/get_stat

PhalApi的做法

目前而言，PhalApi在URL规则和路由这块还比较欠缺，没有像其他网站一样提供强大的路由支持。
但我们在代码实现的层面，可以提供不同的入口，以开放给不同的终端（内部的或者外部的）， 以及不同的版本支持。如：

$ tree Public/
Public/
├── v1
│   └── index.php
├── v2
│   └── index.php
└── v3
    └── index.php

3 directories, 3 files

则对应的版本URL则可以为：

//v1版本
http://api.demo.com/v1/?service=Default.Index

//v2版本
http://api.demo.com/v2/?service=Default.Index

//v3版本
http://api.demo.com/v3/?service=Default.Index

项目的选取

项目可以结合不同的入口，以及接口服务器的URL规则Rewrite作一些自定的URL路由。

(9)SDK包 - 给客户端自由的调用空间和自由

目前移动开发主要有iOS、Android、Windowns Phone、网站等不同的终端，各种终端又有不同的语言，如果我们需要提供SDK包，不仅仅需要考虑到纵向的版本升级，还需要维护横向的多样性。
而且，如果我们使用的是HTTP协议，则不必要担心这些维护的成本，同时给客户端提供一个自由的空间进行调用 -- 即客户端可以自己编写本身的接口客户端。

主流的做法

很多国内的开放平台接口都是不提供SDK包的，但有些安全度高的则会，如支持宝。

以下是一些提供了SDK的平台 ：

  • 支付宝SDK
  • Yahoo! SDKs
  • Amazon丰富的开发工具包

PhalApi的做法

我们暂时没有提供SDK包，但对于PHP，有一个简单的客户端类，可见： [1.13]-统一的接口请求方式

项目的选取

出于公司产品簇的项目考虑，项目可以内部提供SDK给同类的客户端使用，如分为iOS版的客户端SDK，以及Android版的客户端SDK。

小故事：与SDK包的一个真实的痛苦经历

有一点是非常重要的，千万不要让不懂PHP语言的人去开发提供PHP的SDK包，更不要使用所谓的工具自动转换生成SDK包代码。
在我曾经做过的一个项目中，因为需要接入一个接口系统，而这个接口是由专业的JAVA团队维护的，但他们对PHP语言则是非常薄弱，以致他们使用了工具来生成PHP语言的SDK包。

这就导致了我在接入一个简单的接口时，却开发联调耗费了两天、测试联调时耗费了在接口调用超时问题排查上。
而最后找到的原因却是因为app_key不对而导致服务端异常，而在SDK包却隐藏了这一异常错误信息，反而给出了time out超时的提示，严重误导了排查的方向！
而当我尝试深入去调试SDK时，得到却又是既没有code又没有message的异常！最让人难以忍受的是，他们提供的SDK包竟然和JAVA的企业系统一样复杂的结构（正如他们是使用工具来生成转换的）！

想象一下，PHP代码下有\com\sina\webo\sdk\Constants.php这样类似JAVA的文件结构，PHP的同学会作何感想？用JAVA的世界的方式来开发PHP，显然是走不通的啊！
而执意要走的话，到最后就是各种接入的痛苦，稍微按奈不住的同学难免就会因为情绪问题而大开争论了。而这一切，只是因为非PHP人员使用了自动生成工具。
我觉得，这是一种不负责任的做法，希望大家不要效仿。

(10)接口文档 - 使用markdown快速编写

（场外音：通过沐浴法理清了头绪，继续回来执笔编写）。
就我个人经历而言，markdown就是一个开始你会拒绝，接着你会越来越喜欢，到最后会爱不释手的一个工具。

如果你或者你的团队还在使用邮件或者work文档来传递共享接口文档，那就太不应该了；如果你正在使用某个WIKI系统进行文档的维护但却不喜欢它的编辑或者展示方式时，你可以尝试使用一下markdown。
正如你现在正在查看的文档也是通过markdown编写的。

主流的做法

作为开放接口平台，文档肯定是以网站的形式提供。但很多时候，对于我们内部的接口或者小项目来说，显然这样的成本太大了。
接口，从简单开始。
我们理应一直坚持这一点，所以文档也是一样，我们应该寻求一种在内部快速共享最新接口文档的途径。如：

  • 1、使用内部WIKI
  • 2、使用开源中国或者其他站点的WIKI（这时可以通过在线编辑或者GIT更新）

项目的选取

你可以根据项目的需要，或者公司以往的做法，但至少不要再使用邮件或者word文档。

(11)测试驱动开发 - 坚持单元测试

单元测试，在PhalApi里面不只一次提到了，这里再次进行说明，是希望能引起大家的关注，去尝试体验一下。

我们都知道，在开发一个新功能时、新接口时，修复一个BUG或者作一些大的调整或者重构工作，我们是毫无压力的，而且这时的成本很低，仅在于开发人员本身的时间和精力的消耗。
当提测后进入测试阶段，测试人员发现一个BUG后，有些团队会以禅道或者Bugzilla或其他方式来纪录和追踪BUG。这时我们开发会觉得一个这么小的问题还需要去纪录、去登记很不值得。然后，我们应当注意到这时修复一个BUG会涉及到测试人员资源的开销。
当进入了回归测试阶段，特别是多系统交互、跨团队合作时，一个BUG就会从一个人传到另一个人，从这个团队流到那个团队，这时成本就会逐渐增大。

最后，上线后，当一个奇怪的问题出现后，我们需要定位原因就更加困难重重了。
我曾经就经历这样一番：有用户发现游戏的道具减少了。我们一开始以为是某些运营配置、或者数据以及用户的等级限制所引发的，但在排除了各种业务的问题后，到最后却发现是PHP中array使用“+”运算而引发的血案！
在正常情况下，我们都知道array_merge()函数对于数值的下标则会追加并重新生成下标序列，即会合并；而数组+则会去掉相同下标的元素。

但实际情况下，线上BUG所产生的影响不在于排查和修复的时间成本，而在于在这段时间内所损失的金额、数据等成本。

当然，从测试的角度上看，测试并不能保证我们的系统没有BUG，只能说暂时未发现BUG。
单元测试也一样，作为开发人员，我们应当在最低成本的时期就及时发现我们直觉觉得可能会出现的问题并进行修复。
对我们亲手所编写的代码负责，并且用客观的方式来证明我们的代码目前未发现问题，而不是主观认为“我写的代码没有问题”。更不应该一次又一次地犯下各种低级或者重复的错误，而让团队其他成员对我们丧失信任。

PhalApi一直很注重单元测试，也很注重自动化，为了减轻大家重复编写单元测试骨架代码的痛苦，我们提供了一个可以生成单元测试代码的脚本。
假设我们有这么一个类：

<?php

class Api_Default extends PhalApi_Api {

        public function index() {
             //TODO
        }
}

那么，我们可以这样生成测试代码：

$ cd .//Demo/Tests
$ phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php
<?php
/**
 * PhpUnderControl_ApiDefault_Test
 *
 * 针对 ../Api/Default.php Api_Default 类的PHPUnit单元测试
 *
 * @author: dogstar 20150514
 */

require_once dirname(__FILE__) . '/test_env.php';

if (!class_exists('Api_Default')) {
    require dirname(__FILE__) . '/../Api/Default.php';
}

class PhpUnderControl_ApiDefault_Test extends PHPUnit_Framework_TestCase
{
    public $apiDefault;

    protected function setUp()
    {
        parent::setUp();

        $this->apiDefault = new Api_Default();
    }

    protected function tearDown()
    {
    }

    /**
     * @group testGetRules
     */
    public function testGetRules()
    {
        $rs = $this->apiDefault->getRules();
    }

    /**
     * @group testIndex
     */
    public function testIndex()
    {
        $rs = $this->apiDefault->index();
    }

}

温馨提示：

  1. 可以先执行：ln -s /path/to/PhalApi/phalapi-buildtest /usr/bin/phalapi-buildtest
  2. test_env.php为测试环境初始化文件，可以在里面引用init.php文件，并作一些调整
  3. 输出的测试代码可以重定向到./Demo/Tests/Api/Api_Default_Test.php，让测试代码与产品代码对齐

最后，我们就可以这样执行单元测试了：

$ phpunit ./Api_Default_Test.php

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.31]-新型计划任务：以接口形式实现的计划任务 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.31.1 这里所说的计划任务

计划任务主要负责处理一些耗时的操作，或者非用户触发的作业。

有些人会称它为后台任务，或者推送作业，又或者定时任务。这时则统称为：计划任务。

例如，当你发布一条微信朋友圈后需要通知上百个好友时；当一条后台的推荐资讯需要推送到每个用户的客户端时；当需要将本地的静态资源如图片同步到CDN时。
显然这些动则需要分钟级别的操作，不应该在客户端调用接口时同步处理（但让我惊讶的是现实真的有人会这么做！），又或者非用户触发而需要后台处理（但更让我惊讶的是竟然也有系统是在用户请求时附带进行处理，而且还是国内某个知名的会员中心！）。

这里不仅仅是提供实现计划任务的约束和机制，更多的是引导大家更好地应对此类问题。

1.31.2 计划任务的关键环节

(1)触发

首先，是何时何地由何用户产生一条待执行的计划任务，我们可以把这个场景点称为一个触发点。
通常的做法，我们会先纪录下此触发点的场景信息，并放入到一个队列里面，以便等待计划任务消费。

(2)调度

其次，是通过何种机制进行计划任务的调度。
这里不仅有技术层面的问题，还有业务的问题，如每次批量处理多少，间隔多少，是否需要失败重试等等？

(3)消费

最后，则是具体的计划任务执行，以完成必要的操作，也称为消费。
很多传统的做法，都是把这些操作和接口混在一起的，而这里，PhalApi则会以一种更为明朗的方式来实现，从而自底而上，支持更多的调度方式和触发机制。

1.31.3 传统的计划任务

如果以一图而鳖之，上图虽然简化，但可以很好地说明传统计划任务的结构体系。
即：很多项目都是使用内嵌的方式来包含计划任务，这样明显会把接口服务系统和后台计划任务混在一起，增加了系统间的耦合性。
虽然小项目可以忍受或者适合这种混合，但是出于长远考虑，进行有意识地分解还是很有好处的。

而且这种混合潜意识下又让开发人员不加判断就进行调用，这会严重增加接口的反应时间。
我曾目睹一个接口耗时了近36秒之久，在对这个旧系统的接口进行一番排查后，原来是这个接口在发布后对上百个好友做了通知推送导致产生了上百条insert语句。

(1)传统的调度方式

我们重点关注一下传统计划任务的调度方式，在过去，我们通常会有两种方式：一种是启动死循环的进程，另一种是启动一个crontab之类的定时任务。
当然，上述的在接口请求时同步进行调度也算一种方式，但不是正规的做法。

如果采用死循环的方式，我们还需要考虑代码更新升级后，对脚本的重启，以便载入新的代码。如果是sh循环调用PHP脚本，则可以忽略。

1.31.4 新型的计划任务

(1)以接口的形式提供计划任务服务

PhalApi中最具特色的做法是，将计划任务的执行消费实现，以接口形式来提供。
这样的好处在于，我们作为接口开发人员，可以以熟悉的方式来进行计划任务的开发。
但更大的得益在于，将计划任务通过接口的形式提供后，我们会看到更为广阔的使用场景：我们可以使用MQ队列消费，可以同步请求也可以异步请求。

(2)系统架构

我们所做的，不仅仅只是把原来混合型的代码作简单分解，如下： a pic

而是以一种更为正统的做法，为此我们添加了一些必要的节点来设计此构架。新的实现方式下的体系结构如下：
a pic

节点说明

在上图中，应用节点还是我们的接口系统；MQ队列则是用于存放待消费的场景信息，同其他的MQ一样；计划任务则可以分为两部分，API接口实现和任务调度。
计划任务这两部分，物理部署上可以合在一起，也可以分开，这取决于应用系统是采用分布式的做法，还是单一的服务器。

执行流程

由上图可以看出，一个完整的计划任务流程为：

  • 1、应用产生一条新的计划任务，并存放于MQ队列
  • 2、计划任务定时或者不停扫描新的计划任务；若有，则进行调度
  • 3、计划任务API完成需要的工作，并将结果返回调度器

(3)单个添加，批量处理

这里只支持单个MQ添加，而处理则是批量的，且每批处理的数据可指定配置。

(4)MQ共享

无论是分布式还是本地一体化，MQ队列都应该是可以共享访问的，以便为应用节点、计划任务调度节点所访问，如下图所示： a pic

首选redis MQ

因为MQ作为频繁读写的媒介，应该优先使用高效缓存来提高系统的吞吐率以及增加并发的能力。此外，作为临时一次性的数据，使用高效缓存也是大有好处的（但我们也需要考虑到数据丢失的情况）。
而且，为了支持 单个添加，批量处理，第三方缓存应该很好地支持队列的操作。
所以，redis是一个不错的选择。

如下，是redis简单的队列操作：

$redis = new Redis();
$redis->connect('127.0.0.1', 6300);

$redis->lpush('test_key', 'www');
$redis->lpush('test_key', 'phalapi');
$redis->lpush('test_key', 'net');

echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";

数据库MQ

如果考虑到redis扩展不好安装，或者应用喜欢使用数据库来存放MQ，也是可以的。只需要用SQL的一些基本的操作语句便可做到FIFO。

文件MQ

文件MQ也是一种方式，但很少使用。

(5)更丰富的调度方式

接口同步调度

虽然也是同步调度，但是我们将计划任务隔离后，便于日后发现此同步的计划任务影响到接口的响应时间时，可以及时轻松地切换到后台异步处理的方式。

回归传统的调度

我们也可以沿用传统的做法，即使用死循环的脚本调度，或者crontab类的定时任务。

MQ队列消费

既然我们以接口服务的形式提供计划任务的操作，那么可以把同一接口的调度放置到同一队列中进行维护和消费。

接口异步调度

当计划任务以接口服务提供后，我们可以使用另一种免MQ的做法，即使用接口的异步调度。如下：
a pic

这样既可以避免死循环带来的性能负载问题，也可以避免定时任务带来的延时问题，可以说异步调度是一种折中完美的做法。
但这也可能是一种不负责任或者不安全的做法，因为我们无法跟进异步计划任务的结果。

本地调度和远程调度

本地调度是指在执行过程中构建模拟接口的调用而无须经过网络请求，远程调度则是通过远程接口请求来实现。
如果把本地调度和远程调度，跟同步/异步组合起来，我们可以得到以下三种有意义的组合：

  • 本地同步调度
  • 远程同步调度
  • 远程异步调度

(6)计划任务的划分

service即类型

明显地，接口服务名称service即可作为计划任务划分的依据。

不同的service作为不同的队列，不同类型的计划任务；而相同的service则作为相同的队列相同的计划任务。

接口参数即参数

接口参数即可计划任务执行时所需要的上下文信息。

1.31.5 PhalApi中计划任务的核心设计解读

(1)桥接模式 - 数据与行为独立变化

为了给计划任务一个执行的环境，我们提供了 计划任务调度器 ，即：Task_Runner。
每个计划任务需要调度的接口是不一样的，即不同的接口服务决定不同的行为；每个行为需要的数据也不一样，即不同的接口参数决定不同的数据。

自然而言的，Task_Runner按照桥接模式，其充当的角色如下：
a pic

然后，我们就可以这样各自实现： a pic

(2)适配器模式 - 对象适配器和类适配器

在对MQ进行实现时，我们提供的Redis MQ队列、文件MQ队列和DB MQ队列，都使用了适配器模式，以重用框架已有的功能。
其中，Redis MQ队列和文件MQ队列是属于对象适配器，DB MQ队列是类适配器。 对于对象适配器，我们也提供了外部注入，以便客户端在使用时可以轻松定制扩展，当然也可以使用默认的缓存。

如下： a pic

这样以后，我们可以这样根据创建不同的MQ队列：

//Redis MQ队列
$mq = Task_MQ_Redis();
//或
$mq = Task_MQ_Redis(new PhalApi_Cache_Redis(array('host' => '127.0.0.1', 'port' => 6379)));

//文件MQ队列
$mq = new Task_MQ_File();
//或
$mq = new Task_MQ_File(new PhalApi_Cache_File(array('path' => '/tmp/cache')));

//DB MQ队列
$mq = new Task_MQ_DB();

//Array MQ队列
$mq = new Task_MQ_Array();

(3)模板方法 - 本地和远程两种调度策略

在完成底层的实现后，我们可以再来关注如何调度的问题，目前可以有本地调度和远程调度两种方式。

  • 本地调度：是指本地模拟接口的请求，以实现接口的调度
  • 远程调度：是指通过计划任务充当接口客户端，通过请求远程服务器的接口以完成接口的调度

为此，我们的设计演进成了这样： a pic

上图多了两个调度器的实现类，并且远程调度器会将远程的接口请求功能委托给连接器来完成。

(4)设计审视

好了！让我们再回头审视这样的设计。

首先，我们在高层，也就是规约层得到了很好的约定。
不必过多地深入理解计划任务内部的实现细节，我们也可以轻松得到这样的概念流程：
计划任务调度器(Task_Runner)从MQ队列(Task_MQ)中不断取出计划任务接口服务(PhalApi_Api)进行消费。

再下一层，则是具体的实现，即我们所说的实现层。
客户可以根据自己的需要进行选取使用，他们也可以扩展他们需要的MQ。重要的是，他们需要自己实现计划任务的接口服务。

根据爱因斯坦说的，要保持简单，但不要过于简单。
所以，为了更好地理解计划任务的运行过程，我们提供了简单的时序图：
a pic

上图主要体现了两个操作流程：加入MQ和MQ消费。
其中，注意这两个流程是共享同一个MQ的，否则不能共享数据。同时调度是会进行循环式的调度，并且穷极之。

(5)没有引入工厂方法的原因

我们在考虑是否需要提供工厂方法来创建计划任务调度器，或者MQ。
但我们发现，设计是如此明了，不必要再引入工厂方法来增加使用的复杂性，因为存在组合的情况。而且，对于后期客户端进行扩展也不利。

当我们需要启动一个计划任务时，可以这样写：

$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);

$runner->go('MyTask.DoSth');

上面简单的组合可以有：4种MQ * 2种调度 = 8种组合。

所以，我们最后决定不使用工厂方法，而是把这种自由组合的权利交给客户端。

(6)失败重试与并发问题

除了对计划任务使用什么模式进行探讨外，我们还需要关注计划任务其他运行时的问题。

一个考虑的是失败重试，这一点会发生在远程调度中，因为接口请求可能会超时。这时我们采用的是失败轮循重试。
即，把失败的任务放到MQ的最后，等待下一批次的尝试。连接器在进行请求时，也会进行一定次数的超时重试。这里主要是为了预防接口服务器崩溃后的计划任务丢失。

另一个则是并发的问题。这里并没有过多地进行加锁策略。
而是把这种需要的实现移交给了客户端。因为加锁会使得计划任务更为复杂，而且有时不一定需要使用，如一个计划任务只有一个进程时，也就是单个死循环的脚本进程的情况。

(7)客户端的使用

最后，客户端的使用就很简单了：

$mq = new Task_MQ_Redis();
$taskLite = new Task_Lite();

$taskLite->add('MyTask.DoSth', array('id' => 888));

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.11]-核心思想：DI依赖注入-让资源更可控 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

一个没有绝对答案的世界，却拥有绝对的丰富。 --《沈奇岚：我愿生命从容》

2.11.1 定义

(1) 关于依赖注入

即控制反转，目的是了减少耦合性，简单来说就是使用开放式来获取需要的资源。

(2) 关于资源

这里说的资源主要是在开发过程中使用到的资源，包括配置项；数据库连接、Memcache、接口请求等系统级的服务；以及业务级使用到的实例等。

引入依赖注入的目的不仅是为了增加一个类，而是为了更好的对资源进行初始化、管理和维护。下面将进行详细的说明。

2.11.2 一个简单的例子

很多时候，类之间会存在依赖、引用、委托的关系，如A依赖B时，可以这样使用：

class A {
    protected $b;

    public function __construct()
    {
        $this->b = new B();
    }
}

这种方式在A内限制约束了B的实例对象，当改用B的子类或者改变B的构建方式时，A需要作出调整。这时可以通过依赖来改善这种关系：

class A {
    protected $b;

    public function __construct($b)
    {
        $this->b = $b;
    }
}

再进一步，可以使用DI对B的对象进行管理：

class A {
    public function __construct()
    {
    }

    public function doSth()
    {
        //当你需要使用B时
        $b = $di->get('B');
    }
}

这样的好处？

一方面，对于使用A的客户（指开发人员），不需要再添加一个B的成员变量，特别不是全部类的成员函数都需要使用B类服务时。另一方面在外部多次初始化A实例时，可以统一对B的构建。

2.11.3 依赖注入的使用示例

为方便使用，调用的方式有：set/get函数、魔法方法setX/getX、类变量$di->X、数组$di['X']，初始化的途径有：直接赋值、类名、匿名函数。

/** ------------------ 创建与设置 ------------------ **/
//获取DI
$di = DI();
//演示的key
$key = 'demoKey';

/** ------------------ 设置 ------------------ **/
//可赋值的类型：直接赋值、类名赋值、匿名函数
$di->set($key, 'Hello DI!');
$di->set($key, 'Simple');
$di->set($key, function(){
    return new Simple();
});
//设置途径：除了上面的set()，你还可以这样赋值
$di->setDemoKey('Hello DI!');
$di->demoKey = 'Hello DI!';
$di['demoKey'] = 'Hello DI!';

/** ------------------ 获取 ------------------ **/
//你可以这样取值
echo $di->get('demoKey'), "\n";
echo $di->getDemoKey(), "\n";
echo $di->demoKey, "\n";
echo $di['demoKey']. "\n";

/**
 * 演示类
 */
class Simple
{
    public function __construct()
    {
    }
}

2.11.4 依赖注入的好处

(1)减少对各个类编写工厂方法以单例获取的开发量

DI相当于一个容器，里面可以放置基本的变量，也可以放置某类服务，甚至是像文件句柄这些的资源。在这容器里面，各个被注册的资源只会存在一份，也就是当被注册的资源为一个实例对象时，其效果就等于单例模式。

因此，保存在DI里面的类，不需要再编写获取单例的代码，直接通过DI获取即可。

例如很多API的服务组件以及其他的一些类，都实现了单例获取的方式。分别如：

微博接口调用：

<?php
class Weibo_Api
{
    protected static $_instance = null;

    public static function getInstance()
    {
        if (!isset(self::$_instance)) {
            self::$_instance = new Weibo_Api();
        }
        return self::$_instance;
    }

    //....
}

七牛云存储接口调用：

class Qiniu_Api {
    private static $_instance = null; //实例对象

    public static function getInstance()
    {
        if (self::$_instance ===null) {
            self::$_instance = new Qiniu_Api();
        }
        return self::$_instance;
    }
}

QQ开放平台接口调用：

class QQ_Api {
    private static $_instance = null; //实例对象

    public static function getInstance()
    {
        if (self::$_instance ===null) {
            self::$_instance = new QQ_Api();
        }
        return self::$_instance;
    }
}

如果使用DI对上面这些服务进行管理，则上面三个类乃至其他的类对于单例这块的代码都可以忽略不写。注册代码如下：

$di->sStockApi = 'Weibo_Api';
$di->sDioAopi = 'Qiniu_Api';
$di->sShopApi = 'QQ_Api';

上面是通过类名来进行延迟加载，但需要各个类提供public的无参数的构造函数。如果各个服务需要进行初始化，可以将初始化的工作放置在onInitialize()函数内，DI在对类实例化时会回调此函数进行初始化。

(2)统一资源注册，便于后期维护管理

这里引入DI，更多是为了“一处创建，多处使用”， 而不是各自创建，各自使用。

创建和使用分离

考虑以下场景：假设有这样的业务数据需要缓存机制，所以可注册一个实现缓存机制的实例：

$di->set('cache', new FileCache());

然后提供给多个客户端使用：

$di['cache']->set('indexHtml', $indexContent);   //缓存页面
$di['cache']->set('config', $config);  //缓存公共配置
$di['cache']->set('artistList', $artistList);   //缓存数据

当需要切换到MC或者Redis缓存或者多层缓存时，只需要修改对缓存机制的注入即可，如：

$di->set('cache', new RedisCache());

依赖注入的一个很大的优势就在于可以推迟决策，当需要用到某个对象时，才对其实例化。可以让开发人员在一开始时不必要关注过多的细节实现，同时也给后期的扩展和维护带来极大的方便。

再上一层，假设未来我们需要更高级的缓存服务，那么我们可以在不影响客户端使用的情况下，轻松升级。
未来的可配置化的多级缓存策略

以下是一个模拟的使用场景，但依然对现在的项目有一定的帮助。假设我们现在有一个MC集群的缓存且引入了DI，使用如下：

<?php

//初始化
$di = PhalApi_DI::one();
$di->cache = new Memcache();
$di->cache->connect('localhost', 11211);

//不同文件的多处使用 ...
echo $di->cache->get('key');
echo $di->cache->get('key2');
echo $di->cache->get('key3');
...

假设现在发现一层缓存存在穿透情况，为保证服务器的稳定性，我们已开发实现了多层缓存策略，并且可以通过简单配置即可实现，只需要对DI容器里面的cache实例进行升级，其他客户端的调用即可马上享受到缓存升级的优质服务。升级涉及改动的代码如下：

<?php

//初始化
$di = new PhalApi_DI();
$di->cache = function () {
$ultraFastFrontend = new DataFrontend(array(
    "lifetime" => 3600
));

$fastFrontend = new DataFrontend(array(
    "lifetime" => 86400
));

$slowFrontend = new DataFrontend(array(
    "lifetime" => 604800
));

return new Multiple(array(
    new ApcCache($ultraFastFrontend, array(
        "prefix" => 'cache',
    )),
    new MemcacheCache($fastFrontend, array(
        "prefix" => 'cache',
        "host" => "localhost",
        "port" => "11211"
    )),
    new FileCache($slowFrontend, array(
        "prefix" => 'cache',
        "cacheDir" => "../app/cache/"
    ))
));
};

备注：关于多级缓存策略，后续会提供源代码和重用库，或者期待读者的分享。

(3)延迟式加载，提高性能

延迟加载可以通过DI中的类名初始化、匿名函数和参数配置（未实现）三种方式来实现。

延迟加载有时候是非常有必要的，如在初始化项目的配置时，随着配置项的数据增加，服务器的性能也将逐渐受到影响，因为配置的内容可能是硬编码，可能来自于数据库，甚至需要通过接口从后台调用获取， 特别当很多配置项不需要使用时。而此时，支持延时加载将可以达到很好的优化，而不用担心在需要使用的时候忘记了初始化。从而很好的提高服务器性能，提高响应速度。

如对一些耗时的资源先进行匿名函数的初始化：

$di['hightResource'] = function() {
    //获取返回耗性能的资源
    //return $resource;
}

(4)以优雅的方式取代滥用的全局变量

在我看来，PHP里面是不应该使用全局变量（global和$_GLOBALS），更不应该到处使用。

用了DI来管理，即可这样注册：

$di->set('debug', true);

然后这样使用：

$debug = $di->get('debug');

也许有人会想：仅仅是换个地方存放变量而已吗？其实是换一种思想使用资源。

以此延伸，DI还可用于改善优化另外两个地方：通过include文件途径对变量的使用和变量的多层传递。

变量的多层传递，通俗来说就是漂洋过海的变量。

2.11.5 DI思想的来源与推荐参考

Dependency Injection/Service Location

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.12]-海量数据：可配置的分库分表 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

2.12.1 背景

为了应对产品海量用户的愿景需求，这里将设计一个分布式的数据库存储方案，以便能满足数据量的骤增、云服务的横向扩展、后台接口开发的兼容性，以及数据迁移等问题，避免日后因为全部数据都存放在单台服务器上的限制。

2.12.2 主要思想

  • 1、分库分表
  • 2、路由规则
  • 3、扩展字段
  • 4、可配置
  • 5、SQL语句自动生成

(1)分库分表

是指将不需要进行必要关联查询的表分开存放，如存放事件推送的weili_event_pushto和存放标签的weili_tag；同时，对于 同一个表，因为存放的数据量是可预见式的暴增，如上述的weili_event_pushto，每时每刻都会产生大量的来自用户发布的事件，因此为了突破 MySQL单表的限制以及其他问题，需要将此表同时创建N份。

(2)路由规则

在上面进行了分库分表后，开发人员在读取时，就需要根据相应的规则找到对应 的数据库和数据库表，这里建议每个表都需要有int(11)类型的id字段，以便作为分表的参考。

(3)扩展字段

在完成了分库分表和制定路由规则后，考虑到日后有数据库的DB变更，为减少DB变更对现有数据库表的影响，这里建议每个表都增加text类型的extra_data字段，并且使用json格式进行转换存储。

(4)可配置

在有了N台数据库服务器以及每个表都拆分成M张表后，为减少后台接口开发人员的压力，有必须在后台接口框架提供可配置 的支持。即：数据库的变更不应影响开发人员现有的开发，也不需要开发人员作出代码层面的改动，只需要稍微配置一下即可。关于这块，请见下面的框架实现部 分。

(5)SQL语句自动生成

对于相同表的建表语句，可以通过脚本来自动生成，然后直接导入数据即可。

2.12.3 PhalApi框架的实现方案

PhalApi框架主要需要实现的是路由这一层的映射，并且通过可配置的方式进行控制，同时还应支持生产环境和测试环境的异同，如在测试环境我们明显不需要1000张数据库的表。为此，需要提供一种 表名 + id 映射到 数据库服务器 + 具体哪张表 的规则。 show 如上图所示，表名会统一加上前缀，并且将id按一定的表总数进行取模，最后再根据得到的具体表名，通过映射表查找到对应 的数据库服务器进行操作。其中，model层为开发实现，数据库表的映射由接口框架实现支持。

2.12.4 使用示例

(1)配置数据库的路由配置

修改./Config/dbs.php文件，以下是参考的示例配置。其中servers为DB服务器，包括数据库的账号信息等，tables为数据库表的映射关系，其中default下标为缺省的数据库路由。

在每个数据库表里面，可以配置多个数据库表，通过开始的下标start和结束的下标end来对表进行分布式存放，并且如果没有start和end的，则视为不需要拆分存放，同时也是当找不到合适时的拆分表时所采用的默认配置。

return array(
    /**
     * avaiable db servers
     */
    'servers' => array(
        'db_demo' => array(
            'host'      => 'localhost',             //数据库域名
            'name'      => 'test',                  //数据库名字
            'user'      => 'root',                  //数据库用户名
            'password'  => '123456',                //数据库密码
            'port'      => '3306',                  //数据库端口
        ),
    ),

    /**
     * custom table map
     */
    'tables' => array(
        '__default__' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
            ),
        ),
        'demo' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 2, 'db' => 'db_demo'),
            ),
        ),
    ),
);

上面示例配置的意思是：

表名   DB服务器
  tbl_demo    db_demo
  tbl_demo_0      db_demo
  tbl_demo_1      db_demo
  tbl_demo_2      db_demo

(2)准备需要创建表的基本SQL语句

这里说的基本SQL语句是指：仅是这个表所特有的字段，排除已固定公共有的自增主键id，和扩展字段ext_data。下面是一个示例：

`name` varchar(11) DEFAULT NULL,

(3)生成并导入SQL语句

由于拆分后的数据库表数量众多，这里提供了一个快捷的脚本工具来生成所需要创建的数据库表。

$ php ./build_sqls.php
Usage: ./build_sqls.php <dbs_config> <table> [engine=InnoDB]

执行上面的脚本，输入数据库表参数后：

 php ./build_sqls.php ./Config/dbs.php demo

将会从配置文件 里面寻找所需要创建的表，并生成类似以下的SQL语句：

/**
 * DB: localhost  db_demo
 */

CREATE TABLE `demo` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/**
 * DB: localhost  db_demo
 */

CREATE TABLE `tpl_demo_0` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_1` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_2` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(4)使用与代码开发

在将上面的SQL语句导入数据库后，即可以像之前那样操作数据库。下面是一些示例：

DI()->notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()->config->get('dbs'), true);

DI()->notorm->demo->where('id', '1')->fetch();

用到了拆分表的代码示例，假设demo表被拆分成了3个表，则客户端在调用里，需要根据（id % 3 ）来拼接合适的数据库表名，其他使用不变。

DI()->notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()->config->get('dbs'), true);

$row = DI()->notorm->demo_0->where('id', '3')->fetch();
$row = DI()->notorm->demo_1->where('id', '10')->fetch();
$row = DI()->notorm->demo_2->where('id', '2')->fetch();

使用Model基类的情况

更好的写法，应该是继承于PhalApi_Model_NotORM，并统一实现分表的操作，如：

<?php

class Model_Demo extends PhalApi_Model_NotORM {

    protected function getTableName($id) {
        $tableName = 'demo';
        if ($id !== null) {
            $tableName .= '_' . ($id % 3);
        }
        return $tableName;
    }
}

然后，上面的查询分别对应：

$model = new Model_Demo();

$row = $model->get('3', 'id');
$row = $model->get('10', 'id');
$row = $model->get('2', 'id');

更进一步，我们可以通过$this->getORM($id)来获取分表的实例进行分表的操作，如：

<?php

class Model_Demo extends PhalApi_Model_NotORM {

    //... ...

    public function getNameById($id) {
        $row = $this->getORM($id)->select('name')->fetchRow(); //假设$id为3，则 $this->getORM($id) 等效于 DI()->notorm->demo_0
    return !empty($row) ? $row['name'] : '';
    }

}

2.12.5 多个数据库的配置方式

当需要使用多个数据库时，可以先在servers中可以配置多组数据库的信息，然后在tables为不同的数据库表指定不同的数据库服务器。

假设我们有两台数据库服务器，分别叫做db_A、db_B，即：

return array(
    /**
     * DB数据库服务器集群
     */
    'servers' => array(
        'db_A' => array(                              //db_A
            'host'      => '192.168.0.1',             //数据库域名
            // ... ...
        ),
        'db_B' => array(                              //db_B
            'host'      => '192.168.0.2',             //数据库域名
            // ... ...
        ),
    ),

    //... ...

若db_A服务器中的数据库有表a_table_user、a_table_friends，而db_B服务器中的数据库有表b_table_article、b_table_comments，则：

<?php
return array(

    //... ...

    /**
     * 自定义路由表
     */
    'tables' => array(
        //通用路由
        '__default__' => array(
            'prefix' => 'a_',  //以 a_ 为表前缀
            'key' => 'id',
            'map' => array(
                array('db' => 'db_A'),  //默认，使用db_A数据库
            ),
        ),

        'table_article' => array(                                     //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_article表使用db_B数据库
            ),
        ),

        'table_comments' => array(                                    //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_comments表使用db_B数据库
            ),
        ),
    ),

如果项目存在分表的情况，可结合上述的分表的说明进行配置。
这里为了让大家更为明了，假设db_A服务器中的数据库有表a_table_user、a_table_friends_0到a_table_friends_9（共10张表）， 而db_B服务器中的数据库有表b_table_article、b_table_comments_0到b_table_comments_19（共20张表），则结合起来的完整配置为：

<?php
return array(
    /**
     * DB数据库服务器集群
     */
    'servers' => array(
        'db_A' => array(                              //db_A
            'host'      => '192.168.0.1',             //数据库域名
            // ... ...
        ),
        'db_B' => array(                              //db_B
            'host'      => '192.168.0.2',             //数据库域名
            // ... ...
        ),
    ),

    /**
     * 自定义路由表
     */
    'tables' => array(
        //通用路由
        '__default__' => array(
            'prefix' => 'a_',  //以 a_ 为表前缀
            'key' => 'id',
            'map' => array(
                array('db' => 'db_A'),  //默认，使用db_A数据库
            ),
        ),

        'table_friends' => array(                                     //分表配置
            'prefix' => 'a_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_A'),                                // b_table_comments表使用db_B数据库
                array('start' => 0, 'end' => 9, 'db' => 'db_A'),      //分表配置（共10张表）
            ),
        ),

        'table_article' => array(                                     //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_article表使用db_B数据库
            ),
        ),

        'table_comments' => array(                                    //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_comments表使用db_B数据库
                array('start' => 0, 'end' => 19, 'db' => 'db_B'),     //分表配置（共20张表）
            ),
        ),
    ),
);

2.12.6 与主从数据库的有机结合

虽然这是专门为海量数据设计的存储方案，但也是可以结合主从配置来获得更庞大强壮的方案，当然为之付出的是复杂性的引入。

简单地，可以将dbs.php复制一份dbs_slave.php出来给从库使用，然后注册一个从库的服务：

DI()->slaveNotorm = new PhalApi_DB_NotORM(DI()->config->get('slave_dbs'));

最后，在需要使用从库来读取时，使用slaveNotorm 服务即可。

2.12.7 不足与注意点

这样的设计是有明显的灵活性的，因为在后期如果需要迁移数据库服务器，我们可以在框架支持的情况下轻松应对，但依然需要考虑到一些问题和不足。

(1)DB变更

DB变更，这块是必不可少的，但一旦数据库表被拆分后，表数量的骤增导致变更执行困难，所以这里暂时使用了一个折中的方案，即提供了一个ext_data 扩展字段用于存放后期可能需要的字段信息，建议采用json格式，因为通用且长度比序列化的短。但各开发可以根据自己的需要决定格式。即使如此，扩展字段 明显做不到一些SQL的查询及其他操作。

(2)表之间的关联查询

表之间的关联查询，这个是分拆后的最大问题。虽然这样的代价是我们可以得到更庞大的存储设计， 而且很多表之间不需要必须的关联的查询，即使我们需要，我们也可以通过其他手段如缓存和分开查询来实现。这对开发人员有一定的约束，但是对于可预见性的海 量数量，这又是必须的。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.13]-接口调试：在线SQL语句查看与性能优化 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

本章主要讲解调试模式的相关操作，如何快速排查和定位开发过程中所遇到的问题和线上发生的故障。

温馨提示：以下内容基于PhalApi 1.4.0 版本及以上。

2.13.1 开启调试调试

开启调试模式很简单，主要有两种方式：

  • 单次请求开启调试：默认添加请求参数&__debug__=1
  • 全部请求开启调试：把配置文件./Config/sys.php文件中的配置改成'debug' => true,

请特别注意，在实际项目中，调试参数不应使用默认的调试参数，而应各自定义，使用更复杂的参数，从而减少暴露敏感或者调试信息的风险。例如：

  • 不推荐的做法：&__debug__=1
  • 一般的做法：&__phalapi_debug__=1
  • 更好的做法：&__phalapi_debug__=202cb962ac59075b964b07152d234b70

2.13.2 调试信息有哪些？

温馨提示：调试信息仅有当在开启调试模式后，才会返回。

正常响应的情况下，当开启调试模式后，会返回多一个debug字段，里面有相关的调试信息。如下所示：

{
    "ret": 200,
    "data": {
    },
    "msg": "",
    "debug": {
        "stack": [  // 自定义埋点信息
        ],
        "sqls": [  // 全部执行的SQL语句
        ]
    }
}

在发生异常时，最初的框架的处理方式是直接报500错误。现在调整为，当开启调试模式后，会将发生的异常转换为对应的结果按结果格式返回，即其结构会变成以下这样：

{
    "ret": 0,  // 异常时的错误码
    "data": [],
    "msg": "", // 异常时的错误信息
    "debug": {
        "exception": [  // 异常时的详细堆栈信息
        ],
        "stack": [  // 自定义埋点信息
        ],
        "sqls": [  // 全部执行的SQL语句
        ]
    }
}

(1) debug.sqls全部执行的SQL语句

所执行的全部SQL语句，会由框架自动搜集并统计。最后显示的信息格式是：

[序号 - 当前SQL的执行时间ms]所执行的SQL语句及参数列表

示例：

[1 - 0.32ms]SELECT * FROM tbl_user WHERE (id = ?); -- 1

表示是第一条执行的SQL语句，消耗了0.32毫秒，SQL语句是SELECT * FROM tbl_user WHERE (id = ?);，其中参数是1。

又如，假设我们编写一个这样获取用户总人数的示例接口。

// $ vim ./Demo/Api/User.php
class Api_User extends PhalApi_Api {
    public function amount() {
        return DI()->notorm->user->count('id');
    }
}

然后通过/?service=User.Amount&__debug__=1请求该接口，可以看到类似这样的返回结果。

{
    "ret": 200,
    "data": "49", // 共49个用户
    "msg": "",
    "debug": {
        "stack": [
            "[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)"
        ],
        "sqls": [
            "[1 - 0.3ms]SELECT COUNT(id) FROM tbl_user;" // 本次所执行的SQL语句
        ]
    }
}

温馨提示：这只是一个示例。实际项目中，不推荐直接在Api直接操作数据库，也不推荐返回非数组格式的data。

(2) debug.stack自定义埋点信息

埋点信息的格式如下：

[#序号 - 距离最初节点的执行时间ms - 节点标识]代码文件路径(文件行号)

示例：

[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)

表示，这是第一个埋点（由框架自行添加），执行时间为0毫秒，所在位置是文件/home/apps/projects/PhalApi/Public/index.php的第6行。即第一条的埋点发生在框架初始化时：

// $ vim ./Public/init.php

if (DI()->debug) {
    // 启动追踪器
    DI()->tracer->mark();
}

与SQL语句的调试信息不同的是，自定义埋点则需要开发人员根据需要自行纪录，可以使用全球追踪器DI()->tracer进行纪录，其使用如下：

// 添加纪录埋点
DI()->tracer->mark();

// 添加纪录埋点，并指定节点标识
DI()->tracer->mark('DO_SOMETHING');

通过上面方法，可以对执行经过的路径作标记。你可以指定节点标识，也可以不指定。对一些复杂的接口，可以在业务代码中添加这样的埋点，追踪接口的响应时间，以便进一步优化性能。当然，更专业的性能分析工具推荐使用XHprof。

参考资料：XHprof扩展类库

继续上面的示例，在进行数据库操作前后，我们添加相应的操作埋点。

    public function amount() {
        DI()->tracer->mark('开始读取数据库');

        $rs = DI()->notorm->user->count('id');

        DI()->tracer->mark('读取完毕');

        return $rs;
    }

再次请求，会看到类似以下的返回结果。

{
    "ret": 200,
    "data": "49",
    "msg": "",
    "debug": {
        "stack": [
            "[#0 - 0ms]/home/apps/projects/PhalApi/Public/index.php(6)",
            "[#1 - 5.5ms - 开始读取数据库]/home/apps/projects/PhalApi/Demo/Api/User.php(74)",
            "[#2 - 6.4ms - 读取完毕]/home/apps/projects/PhalApi/Demo/Api/User.php(78)"
        ],
        "sqls": [
            "[1 - 0.3ms]SELECT COUNT(id) FROM tbl_user;"
        ]
    }
}

可以看出，在“开始读取数据库”前消耗了5.5毫秒，以及相关的代码位置。

(3) debug.exception异常堆栈信息

当有未能捕捉的接口异常时，开启调试模式后，框架会把对应的异常转换成对应的返回结果，而不是像最初那样直接500，页面空白。这些是由框架自动处理的。

继续上面的示例，让我们故意制造一些麻烦，手动抛出一个异常。

    public function amount() {
        ... ...

        throw new Exception('这是一个演示异常调试的示例', 501);

        return $rs;
    }

再次请求后，除了上面的SQL语句和自定义埋点信息外，还会看到这样的异常堆栈信息。

{
    "ret": 501,
    "data": [],
    "msg": "这是一个演示异常调试的示例",
    "debug": {
        "exception": [
            {
                "function": "amount",
                "class": "Api_User",
                "type": "-&gt;",
                "args": []
            },
            ... ...
        ],
        "stack": [
            ... ...
        ],
        "sqls": [
            ... ...
        ]
    }
}

然后便可根据返回的异常信息进行排查定位问题。

(4) 添加自定义调试信息

当需要添加其他调试信息时，可以使用DI()->response->setDebug()进行添加。

如：

$x = 'this is x';
$y = array('this is y');
DI()->response->setDebug('x', $x);
DI()->response->setDebug('y', $y);

请求后，可以看到：

    "debug": {
        "x": "this is x",
        "y": [
            "this is y"
        ]
    }

2.13.3 一个错误的接口开发

有时，在进行接口开发时，会需要进行批量获取的功能，如列表。但很多开发的同学可能会因为时间赶或者没有意识去对SQL查询进行优化，或者甚至不知道自己的接口背后隐藏着多少问题。下面是一个错误的开发示例。

(1)新增的批量获取接口

假设我们在开发一个国际的项目，并且运行良好，BOSS说因业务需要，要加多一个接口以支持批量获取用户的基本信息，提供给国外某知名的社交平台调用。

于是乎，我们很快就根据原来的单个获取接口实现了新的接口：

//$vim ./Demo/Api/User.php
<?php

class Api_User extends PhalApi_Api {

    public function getRules() {
        return array(
            //...
            'getMultiBaseInfo' => array(
                'user_ids' => array('name' => 'user_ids', 'type' => 'array', 'format' => 'explode', 'require' => true),
            ),
        );
    }

    //...

    public function getMultiBaseInfo() {
        $rs = array('code' => 0, 'msg' => '', 'list' => array());

        $domain = new Domain_User();
        foreach ($this->user_ids as $userId) {
            $rs['list'][] = $domain->getBaseInfo($userId);
        }

        return $rs;
    }
}

(2)运行调用一下

显然，我们可以很清楚地调用新增的接口：

http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3

可返回：

{
    "ret": 200,
    "data": {
        "code": 0,
        "msg": "",
        "list": [
            {
                "id": "1",
                "name": "dogstar",
                "note": "oschina"
            },
            {
                "id": "2",
                "name": "Tom",
                "note": "USA"
            },
            {
                "id": "3",
                "name": "King",
                "note": "game"
            }
        ]
    },
    "msg": ""
}

假设我们已经有了这样的数据库表数据：

INSERT INTO `tbl_user` VALUES ('1', 'dogstar', 'oschina');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', 'USA');
INSERT INTO `tbl_user` VALUES ('3', 'King', 'game');

(3)这样的问题？

这样的问题，在对外黑盒调用的客户端同学是发现不了的，对于测试人员来说也是无法感知的。但所犯的错误也是显然易见的，就是没有进行SQL的批量查询优化，造成了很多不必要的重复查询。
这里，根据前面学习的调试方式，则我们可以快速发现存在的问题：

http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3&__debug__=1

如下返回，我们看到了很多重复类似的查询语句。

{
    ... ...
    "debug": {
        .... ...
        "sqls": [
            "[1 - 0.34ms]SELECT * FROM tbl_user WHERE (id = ?); -- 1",
            "[2 - 0.16ms]SELECT * FROM tbl_user WHERE (id = ?); -- 2",
            "[3 - 0.16ms]SELECT * FROM tbl_user WHERE (id = ?); -- 3"
        ]
    }
}

(4)如何改进？

这是一个很基本的问题，当然在实际项目中不会普通存在，这里只是作为一个示例加以说明。但让人失望的是，实际项目确实存在为数不少的这样的情况。可能是新人的技术和意识问题，也有可能是老同学的态度问题。所以，优化这么一个接口的批量SQL查询不难，难的是如何才能让新、老同学都注重这块的SQL查询优化呢？而不是等到线上服务器异常崩溃后再来推托责任。
具体的代码改进，留给读者自己实践了。毕竟，看了，实践了，才会真正深刻地掌握。

2.13.4 由此引申

  • 这里不专门讲述SQL的优化，但也顺便提供一些SQL查询优化的建议：
  • 使用批量查询，而不是N次循环查询！
  • 重复的数据，不要重复获取；
  • 根据需要，按需要获取表字段，而不是SELECT *；
  • 针对频繁的搜索字段，建立必要的索引，以加快查询速度；
  • 使用关联查询，而不是粗暴地类似：where uid IN (... 这里是成千上W个用户ID ...)；
  • 针对单条SQL语句执行时间超过1秒的，重点优化；

搞定，收工，开饭！

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.14]-测试驱动开发：意图导向编程下的接口开发 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

有的时候宁愿付钱让你一周在床上待着，也不想让你用这周剩下的时间去调试你在周一所写的代码。 --丹·所罗门

2.14.1 测试驱动开发

做正确的事，比把事情做正确更为重要。

当明确需要做何事后，再通过事先编写单元测试来准确表达我们将要实现的功能，是相当具有指导意义的。你会发现接下来你的开发历程就是：单元测试-设计-重构，而且这种正向循环是很有创造性的，并且进行到一定程度后会慢慢体会到浮现式设计的乐趣。

关于测试驱动开发TDD，有很多资料已进行了说明，这里不再赘述。

如果还没了解PHPUnit，可先阅读：PHPUnit 手册
如果还没了解PHPUnit，可先阅读：PHPUnit 手册
如果还没了解PHPUnit，可先阅读：PHPUnit 手册

2.14.2 意图导向编程

在编写代码前，先写测试代码，更容易提高 关注点 。

因为，在开发过程中， 大多时候会被外界打断（如需求沟通、线上问题处理、临时会议等），而通过单元测试则可以让你“几乎忘却需要做什么”的情况下重新让你回到之前的状态，特别在并行开发多个不同项目的需求时尤其重要。

除此之外，遵循“红-绿-重构”这样的流程，我们可以在更高的层面关注需要实现的功能需求，并自顶而下地进行设计优化，精益代码。

2.14.3 编写测试的原则、模式和指导

首先应该意识到，测试代码和生产代码一样重要。其次，测试代码也应该和生产代码一样被同步维护更新，这样才能保持生气，更大地发挥作用。只有当不断地对测试的代码进行修修补被，我们才能保持自动化测试这张“安全网”常新。

2.14.4 F.I.R.S.T.原则

  • 快速 Fast
  • 独立 Independent
  • 可重复 Repeatable
  • 自足验证 Self-validating
  • 及时 Timely

2.14.5 构造-操作-检验（BUILD-OPERATE-CHECK）模式

这个模式也可以理解成：“当... 做...应该...”。其中，构造包括测试环境的搭建、测试数据前期的准备；操作是指对被测试对象的调用， 以及被测试对象之间的通信和协助交互；最后检验则是对业务规则的断言、对功能需求的验证。

2.14.6 如何编写高效测试代码

  • 1、与产品代码分开，与测试代码对齐
  • 2、利用测试骨架（phpunit-skelgen或者自定义生成器）自动生成测试代码
  • 3、使用测试替身、测试桩构建昂贵资源、制造异常情况
  • 4、每个测试一个概念

2.14.7 PhalApi开发下的单元测试

我们推荐在各自的项目代码中平行编写单元测试，并逐渐完善、保持同步。以下是进行单元测试的参考。

(1)Api接口层的单元测试

Api接口层，是我们后台开发的主要切入点，也是直接对外提供服务的入口，属于更高层次的概念并拥有指定的业务功能，更是后台开发的关注点。所以在对新接口进行开发前，编写单元测试是非常有意义的。

为了可以自动生成测试代码，我们可以先简单定义好接口的函数签名（以获取用户基本信息接口为例）：

//$ vim ./Demo/Api/User.php
<?php

class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }

}

随后，自动生成测试代码骨架：

$ mkdir ./Demo/Tests/Api -p
$ cd ./Demo/Tests/Api
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php > ./Demo/Tests/Api/Api_User_Test.php

根据接口的需要，验证接口返回的格式，以及业务数据的正确性。

//$ vim ./Demo/Tests/Api/Api_User_Test.php
    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&user_id=1';

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url);

        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('msg', $rs);
        $this->assertArrayHasKey('info', $rs);

        $this->assertEquals(0, $rs['code']);

        $this->assertEquals('dogstar', $rs['info']['name']);
        $this->assertEquals('oschina', $rs['info']['note']);
    }

上面的验证意思简单明了，结合 构造-操作-检验（BUILD-OPERATE-CHECK）模式 加以说明一下。

构造：构建请求URL

        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&user_id=1';

此参数即对应接口请求的URL参数，我们将此参数追加在接口入口并在浏览器打开可以得到同样的接口执行效果。但这样的好处更在于通过单元测试帮我们记住了各种接口测试的业务场景。而不再是像以前那样打开N个浏览器窗口人工进行调试，也不用像以前那样苦苦寻找浏览器记录。

如果接口需要POST数据，或者其他更多参数，可以使用$params来传递更多参数，一如：

        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&user_id=1';
        $params = array(); //更多参数

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url, $params); //通过第二个参数，传送更多参数

操作：执行请求

这里的操作，显然就是对应我们接口的调用。简单地如：

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url);

这样，便可以在服务端模拟进行一次接口的请求调度，注意这里是在服务端进行的接口请求，而不是客户端。
此外，如果需要传递更多参数，可以参考前面的示例。这里简单补充一下PhalApi_Helper_TestRunner测试辅助类的接口签名说明：

<?php
class PhalApi_Helper_TestRunner {

    /**
     * @param string $url 请求的链接
     * @param array $param 额外POST的数据
     * @return array 接口的返回结果
     */
    public static function go($url, $params = array()) {

        ... ...

检验：验证

在对接口返回的结果中，我们可以这样依次进行正确性的验证：

  • 1、先验证接口返回的格式是否正确，有无字段遗漏；
  • 2、返回的业务数据是否正确；
        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('msg', $rs);
        $this->assertArrayHasKey('info', $rs);

        $this->assertEquals(0, $rs['code']);

        $this->assertEquals('dogstar', $rs['info']['name']);
        $this->assertEquals('oschina', $rs['info']['note']);

由于测试环境的数据变动频繁，所以我们可以针对个别的接口进行更精确的验证，而对类似列表获取这样的大批量的数据，则校验其结构格式。
除此之外，还有一种情况也是需要纳入检验，即除了上面的正常请求情况下的 异常请求 。

接下来的即是之前文档里面所说的单元测试执行和接口开发，此处略。

(2)Domain层和Model层的单元测试

下面继续简单补充一下之前没谈及到的Domain层和Model层的单元测试。

显然，这两层的开发，已经在前面的接口测试驱动开发的指导下很好地完成了。现在可以快速追加对这两层的单元测试。得益于我们的生成测试骨架的脚本，操作如下：

$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Domain/User.php Domain_User > ./Demo/Tests/Domain/Domain_User_Test.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Model/User.php Model_User > ./Demo/Tests/Model/Model_User_Test.php

接着，修改一下测试环境 test_env.php的引用路径：

//$ vim ./Demo/Tests/Domain/Domain_User_Test.php
//$ vim ./Demo/Tests/Model/Model_User_Test.php

require_once dirname(__FILE__) . '/../test_env.php';

各自完善一下单元测试：

//$ vim ./Demo/Tests/Domain/Domain_User_Test.php

    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        $userId = '1';

        $rs = $this->domainUser->getBaseInfo($userId);

        $this->assertArrayHasKey('id', $rs);
        $this->assertArrayHasKey('name', $rs);
        $this->assertArrayHasKey('note', $rs);

        $this->assertEquals('dogstar', $rs['name']);
    }

执行一下：

$ phpunit ./Demo/Tests/Domain/Domain_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

.

Time: 49 ms, Memory: 6.25Mb

OK (1 test, 4 assertions)

(3)Model层的单元测试

Model层的单元测试类似，不再赘述。

2.14.7 更进一步的单元测试套件

到目前为止，我们有了如下的产品代码：

dogstar@ubuntu:Demo$ tree
.
├── Api
│   └── User.php
├── Domain
│   └── User.php
├── Model
│   └── User.php

并拥有了与之平行对应的单元测试：

dogstar@ubuntu:Tests$ tree
.
├── Api
│   └── Api_User_Test.php
├── Domain
│   └── Domain_User_Test.php
├── Model
│   └── Model_User_Test.php
└── test_env.php

这样是一个很好的开始，但若我们每次测试都分别调用三次这些不同层次的单元测试，显然有点不科学。所以，利用PHPUnit的配置文件，我们可以轻松管理我们的测试套件，如：

<!-- dogstar@ubuntu:Tests$ vim ./phpunit_user_getbaseinfo.xml -->

<?xml version="1.0" encoding="UTF-8"?>

<phpunit backupGlobals="false"
    ...

    <testsuites>
        <testsuite name="Test Suite">
            <file>./Api/Api_User_Test.php</file>
            <file>./Domain/Domain_User_Test.php</file>
            <file>./Model/Model_User_Test.php</file>
        </testsuite>
    </testsuites>
</phpunit>

啊哈！终于，当需要调用这些分布在不同目录位置的单元测试时，只需要这么简单的一行命令：

dogstar@ubuntu:Tests$ phpunit -c ./phpunit_user_getbaseinfo.xml
PHPUnit 4.3.4 by Sebastian Bergmann.

.....

Time: 54 ms, Memory: 7.25Mb

OK (5 tests, 28 assertions)

2.14.8 这样的好处？

上面的过程，细节较多，而且需要实际操作的部分也比较多。对于之前没有接触过单元测试这块的同学，可能会有点迷茫，对于不愿意接受单元测试的同学来说更加枯燥。

然而，然而。 当我们把越痛苦的事情越早完成后，我们后面就顺畅多了。正如在某一次培训中的某一位敏捷开发的专家所说的： 要逐步对小问题做优化，而不是要等到大问题到来时再做变革 。

那这样的好处在于哪里呢？

这里不就理论回答，而是以我个人的经历来简单说明。

首先，正如上面所说的，单元测试帮你很好地记住并整理了各种接口测试的场景，而不用再像以前那样打开N个浏览器窗口逐个人工校对。
其次，在单元测试的论证下我们可以更有信心地跟测试说、跟产品说、跟发布说我们的代码没问题，因为我们通过严格的单元测试，而不是人为主观上的想当然应该不会有问题吧。
最后，也是最重要的，在后期的接口升级、改动和维护中，单元测试再一次为我们提供了保护，犹如一张安全网，涵盖我们改动的每一处代码。与此同时，对于重构也亦然。

但单元测试所带给你的，不仅仅是上面所说的简单这几点。更多地完全不一样的开发历程，而其中滋味和令人兴奋的体现，只有当你亲自去尝试才会明白其中滋味。So, try it by yourself.

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.15]-演进：新型计划任务续篇 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

计算机科学中的任何问题都可以用另外的间接层解决，但是这通常会引发另一个问题。 -- David Wheeler

2.15.1 新型计划任务回顾

在 [1.31]-新型计划任务：以接口形式实现的计划任务 一章中，我们讨论了PhalApi中对计划任务的设计和底层实现。

但对于很多应用，很多项目，或者很多同学来说，仍然比较广泛，不能直接使用。
这一章则专门为此而进行演进，并提供最终可用的计划任务调度，同时我们也会阐明如何进行扩展定制。

也就是说，这一章将提供Task扩展类库的统一调度方式，以便在启动crontab任务后，可以通过数据库简单配置，即可执行各种任务。

2.15.2 最终调度的方式：crontab

出于对业务的考虑，我们首先需要明确此crontab调度方式所支持的功能，它应该包括但不限于：

  • 1、通过简单的数据库配置，即可启动一个新的任务
  • 2、具备循环调度的能力，并能初步防止并发调度
  • 3、可以对异常的任务进行修复
  • 4、优先执行太远未执行的任务
  • 5、支持本地和远程两种调度方式、三种MQ类型，以及扩展的能力

2.15.3 核心时序图与分层

在原来的时序图基础上，我们可以进行演进的设计，追加了统一的调度后如下所示：
a pic

通过上面详细的时序图，我们可以发现里面的设计是出于这样的分层考虑：

序号 | 层 | 关键操作 | 说明 | 如何使用
1 | 启动脚本 | crontab.php | 操作crontab执行的脚本 | 客户端可以进行必要的初始化工作
2 | 进程级 | Task_Progress::run() | 根据进程配置的数据库表，进行循环调度 | 不需要改动，直接使用
3 | 触发器 | Task_Trigger::fire() | 进行计划任务调度的上下文环境，用于指定runner和mq类型 | 客户端也可进行定制扩展，进行必要的操作
4 | MQ消费与调度 | Task_MQ::pop()和Task_Runner::go() | 不断消费MQ队列，并依次进行调度 | 不需要改动，直接使用，也可扩展
5 | 计划任务服务 | PhalApi_Api::doSth() | 执行计划任务服务 | 由客户端按接口形式实现

虽然上面的层级，初看起来有点多，但我们再次验证了计算机那个伟大的定论：计算机的任何问题都可以通过一个中间层来解决。

由此看出，上面的层级其实相当于：

客户端初始化 --> 直接使用 --> 自由组合与操作 --> 直接使用 --> 任务服务实现

2.16.4 进程配置的数据库表设计

CREATE TABLE `phalapi_task_progress` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(200) DEFAULT '' COMMENT '任务标题',
  `trigger_class` varchar(50) DEFAULT '' COMMENT '触发器类名',
  `fire_params` varchar(255) DEFAULT '' COMMENT '需要传递的参数，格式自定',
  `interval_time` int(11) DEFAULT '0' COMMENT '执行间隔，单位：秒',
  `enable` tinyint(1) DEFAULT '1' COMMENT '是否启动，1启动，0禁止',
  `result` varchar(255) DEFAULT '' COMMENT '运行的结果，以json格式保存',
  `state` tinyint(1) DEFAULT '0' COMMENT '进程状态，0空闲，1运行中，-1异常退出',
  `last_fire_time` int(11) DEFAULT '0' COMMENT '上一次运行时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

对此表的关键字段说明如下：

字段 | 说明 | 示例
trigger_class | 触发器的类名 | 须实现Task_Progress_Trigger::fire($params)接口
fire_params | 触发器的参数 | 加传给Task_Progress_Trigger::fire()函数的参数，格式为：service&MQ类名&runner类名
interval_time | 执行间隔 | 单位为秒
enable | 是否启动 | 此字段禁止时，将不再执行
state | 进程状态 | 当此状态一直为异常或者运行且超过1天时，系统会进行修复，即重置为空闲状态

其中，对于fire_params参数，MQ类名和runner类名可选，以下是一些示例：

//示例1：完整的配置
//fire_params=Task_Demo.DoSth&Task_MQ_DB&Task_Runner_Local
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq);
$runner->go('Task_Demo.DoSth');

//示例2：使用默认的Runner
//fire_params=Task_Demo.DoSth&Task_MQ_DB
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq);  //默认使用本地Runner
$runner->go('Task_Demo.DoSth');

//示例3:使用默认的MQ和默认的Runner
//fire_params=Task_Demo.DoSth
$mq = new Task_MQ_Redis(); //默认使用redis的MQ
$runner = new Task_Runner_Local($mq);  //默认使用本地Runner
$runner->go('Task_Demo.DoSth');

//示例4：使用自定义的MQ和Runner
//fire_params=Task_Demo.DoSth&My_MQ&My_Runner
class My_MQ implements Task_MQ {
     // ...
}
class My_Runner extends Task_Runner {
     // ...
}
$mq = new My_MQ();
$runner = new My_Runner($mq);
$runner->go('Task_Demo.DoSth');

2.15.5 运行效果

最终的效果就是，我们通过这样两行简单的代码，即可实现一系列复杂的任务调度：

$progress = new Task_Progress();
$progress->run();

让我们来看下这样设计的运行效果吧！看下这两行代码背后所产生的魔力。

首先，我们先添加两条计划任务：

INSERT INTO `phalapi_task_progress` VALUES ('1', 'test demo', 'Task_Progress_Trigger_Common', 'Task_Demo.DoSth&Task_MQ_File&Task_Runner_Local', '300', '1', '', '0', '0');
INSERT INTO `phalapi_task_progress` VALUES ('2', 'test ok', 'Task_Progress_Trigger_Common', 'Default.Index&Task_MQ_DB&Task_Runner_Local', '100', '1', '', '0', '0');

然后，伪造一些MQ：

INSERT INTO `phalapi_task_mq_0` VALUES ('8', 'Default.Index', '', '0', '');

最后，生成单元测试：

<?php
class PhpUnderControl_TaskProgress_Test extends PHPUnit_Framework_TestCase
{
    public $taskProgress;

    protected function setUp()
    {
        parent::setUp();

        $this->taskProgress = new Task_Progress();
    }

    /**
     * @group testRun
     */
    public function testRun()
    {
        $rs = $this->taskProgress->run();
    }

}

并执行之：

$ phpunit ./Task_Progress_Test.php

[1 - 0.06666s]SELECT id, title FROM phalapi_task_progress WHERE (state != ?) AND (last_fire_time < ?) AND (enable = ?) ORDER BY last_fire_time ASC; -- 0, 1431965153, 1<br>
[2 - 0.07002s]SELECT id, title, trigger_class, fire_params FROM phalapi_task_progress WHERE (state = 0) AND (interval_time + last_fire_time < ?) AND (enable = ?); -- 1432051553, 1<br>
[3 - 0.06549s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '1');<br>
[4 - 0.07432s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '1');<br>
[5 - 0.06469s]UPDATE phalapi_task_progress SET result = '{\"total\":0,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '1');<br>
[6 - 0.06746s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '2');<br>
[7 - 0.07043s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '2');<br>
[8 - 0.06673s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;<br>
[9 - 0.48185s]DELETE FROM phalapi_task_mq_0 WHERE (id IN ('8'));<br>
[10 - 0.06514s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;<br>
[11 - 0.50694s]UPDATE phalapi_task_progress SET result = '{\"total\":1,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '2');<br>

Time: 1.98 seconds, Memory: 6.50Mb

OK (1 test, 0 assertions)

查看对比一下数据库，目前发现运行良好！

提交代码，保存文档，收工睡觉！

2.15.6 演进的乐趣

得益于前期良好的设计以及底层支持，我们发现，在提供这样一种统一的调度方式是非常方便的。

不仅如此，如果你明白了其中的设计，需要进行定制和扩展也是非常方便的。也就是说，我们不仅提供了一种具体实际可用的方式，也提供了广阔自由的扩展空间。具体与抽象，两者仍可得。

然而，这一切不仅依赖于良好的设计，还依赖于测试驱动开发下的浮现式设计。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.16]-领域驱动设计：应对复杂领域业务的Domain层 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

温馨提示：此篇章需要比较长的时间才能最终定稿，因为我还要寻找最合适的方式和语言来表述。

2.16.1 领域驱动设计

很多框架关心性能，而不关心人文；很多项目关心技术，而不关注业务。

就这造成了复杂的领域业务在项目中得不到很好地体现和描述，也没有统一的规则，更没有释意的接口。最终导致了在“纯面向对象”框架里面凌乱的代码编写，为后期的维护扩展、升级优化带来很大的阻碍。这就变成了，框架只关注性能，项目只关心技术，而项目却可怜地失去了演进的权利，慢慢地步履维艰，最终牵一发而动全身。

很多人都不知道该如何真正应对和处理领域的业务 ，尽管领域业务和单元测试都是如此重要并被广泛推崇。正如同表面上我们都知道单元测试却没有具体真实地接触过，并且一旦到真正需要编写一行单元测试的代码时就傻眼了。

这里不是发明一些新技术，也不是提供一些新的模式，而是继续将前人、大神和顶级大师关于领域驱动设计这方面的思想结合真实后台接口开发进行分享，进而推广之。

2.16.2 讲述故事

很多人，都喜欢听故事。像我以前中学的时候，就很喜欢看《故事会》。

如果，我们能让代码也像小说一样，在讲述某个故事时，将会更加吸引“读者”（也就是其他开发同学），从而易于理解和维护。

最近，我在做一个项目时，再一次发现了这种讲述故事的威力。

(1)一个第三方登录的写法

我们先以F项目来命名这个项目，在F项目中，我们跟其他App一样，需要接入第三方登录，其中包括：微信登录、微博登录和QQ登录、邮箱登录等。

以下，则是我根据 讲述故事 的方式，为微信登录编写的代码：

<?php

class Api_User_Login extends PhalApi_Api {

    public function getRules() {
        return array(
            'weixin' => array(
                'openId' => array('name' => 'wx_openid', 'require' => true, 'min' => 1, 'max' => 28),
                'token' => array('name' => 'wx_token', 'require' => true, 'min' => 1, 'max' => 150),
                'expiresIn' => array('name' => 'wx_expires_in', 'require' => true, 'min' => 1),
                'nickname' => array('name' => 'name', 'default' => '',),
                'avatar' => array('name' => 'avatar', 'default' => '',),
            ),
        );
    }

    public function weixin()
    {
        $rs = array('code' => 0, 'info' => array(), 'msg' => '');

        $domain = new Domain_User_Login_Weixin();
        $isFirstBind = $domain->isFirstBind($this->openId);

        $userId = 0;
        if ($isFirstBind) {
            $userId = Domain_User_Generator::createUserForWeixin(
                   $this->openId, $this->nickname, $this->avatar);

            $domain->bindUser($userId, $this->openId, $this->token, $this->expiresIn);
        } else {
            $userId = $domain->getUserIdByWxOpenId($this->openId);
        }

        $token = Domain_User_Session::generate($userId, $this->client);

        $rs['info']['user_id'] = $userId;
        $rs['info']['token'] = $token;
        $rs['info']['is_new'] = $isFirstBind ? 1 : 0;

        return $rs;
    }
}

温馨提示：
以下代码为我正在参与开发的一个项目的源代码，已征得项目负责人同意。同时出于对项目的尊重，已省去部分代码。

(2)登录场景的故事

细细品读上面的代码，其实就是在描述登录场景的故事：
当用户进行微信登录时，先查看用户是否首次登录；如果是，则为用户自动生成了一个帐号并绑定，如果不是，则获取已绑定的用户ID；最后，生成一个登录态的token。

当然，这里为了突出故事的主线，已去除了很多异常情况的处理。

(3)有趣的开发体验

更为有趣的是，此次参与F项目开发的还有另外一位同学。
这位同学拥有多年资深的iOS开发经验，但对PHP开发还是首次接触，但他在参考微信登录的写法后，很快就交付了微博和QQ登录这两个接口服务。

但令我为之惊讶和兴奋的不是他的速度，而是他所编写的代码是如此的优雅美丽，犹如出自资深PHP开发人员之手。

这让我再一次相信，使用 讲述故事 的方式来开发接口，不仅能让代码更易于传送业务逻辑，也能为更多的同学乃至新手接受并快速上手。

(4)与TDD的结合

讲述故事 有一个很明显的特点就是，全部的操作都是处于同一抽象级别的，即都是释意接口下的领域业务规则和操作。

但对于如何引出这个业务场景，很多人用传统的方式都是写一个接口，然后在浏览器调试。

其实，这并不是最好的开发体验。因为，使用这种传统的开发方式，你难免会落入技术缠绕的纠结中，比如在想使用哪些类型的数据库表字段。也就是说，你在丢失关注点。

而通过测试驱动，则会先引导你做正确的事，再将你的关注引导到领域业务上，最后将自然而然地就知道应用使用什么技术了。

讲故事，是针Domain领域层外部使用的说明。下面，我们将走进Domain层内部，阐明我们应该如何为讲故事做好准备。

2.16.3 表达规则

(1)释意接口

释意接口的作用是很大的，这可以使得后来的同学在看待一个接口时，无须深入内部实现即可明白它的用意和产生的影响。
如一个get系列的操作，我们可以推断出它是无副作用的。但如果当时的开发者不遵守约定，在里面作了一些“手脚”，则会破坏我们这些“望文生义”的推断。

在我曾经就职的一个游戏公司里面，我常根据接口的命名来推断它的作用，但往往会倍受伤害。因为以前的开发人员没有遵守这些约定，当时的team leader还责怪我不能太相信这些接口的命名。然而我想，如果我们都不能相信我们团队其他人员的接口，我们又能相信谁呢？我们是否应该反思，是否应该遵守约定编程所带来的好处？
任何一个问题，都不是个人的问题，而是一个团队的问题。如果我们经常不断地发生一生项目的问题而要去指责某个人时，我们又为何不从一开始就遵守约定而去避免呢？

简单来说，释意接口会将“命令-查询”分离、会将多个操作分解成更小粒度的操作而保持同一层面的处理。根据《领域驱动设计》一书的说法：
类型名、方法名和参数名一起构成了一个释意接口（Intention-Revealing Interface），以解释设计意图，避免开发人员需要考虑内部如何实现，或者猜测。

如下面的家庭组成员领域业务类：

<?php
class Domain_Group_Member {

    public function joinGroup($userId, $groupId) {
        //TODO
    }

    public function hasJoined($userId, $groupId) {
        //TODO
    }

}

我们可以知道，Domain_Group_Member::joinGroup()用于加入家庭组，会产生副作用，是一个命令操作；Domain_Group_Member::hasJoined()用于检测用户是否已加入家庭组，无副作用，则是一个查询操作。

(2)业务规则的描述

规则出现且仅出现一次。

当代码出现重复时，我们都知道会面临维护的高成本。而当规则多次出现时，我们更知道当规则发生变化时所带来的各种严重的问题，这也正是为什么总有一些这样那样的BUG的原因。
系统出现问题，大多数上都是业务的问题。而业务的问题在于我们不能把规则收敛起来，汇集于一处。

在以往的开发中，我都很注意对这些规则统一的重构工作。这使得我可以非常相信我所提供业务的稳定性，以及在给别人解讲时的信心。
如有一次，我们有一个大型的系统中的一个页面跳转链接的生成规则，后来系统进行了调整，需要对URL生成规则作出调整。我跟另一位新来的同事说只需改一处时，他仍然很惊讶地问我怎么可能？！因为他看到是这么多场景，如此多的页面，怕会有所遗漏。然而，事实证明，我们确实只需要改动一处就可以了。

类似这样的URL拼接规则，我们可以这样表示：

<?php
class Domain_Page_Helper {

    public static function createUrl($userId) {
        return DI()->config->get('app.web.host') . '/u/' . $userId;
    }
}

正如你看到了，我们使用了static静态方法，是因为这个规则生成可以当作一个工具方法来使用。我们不反对使用static方法，但推荐只在合适的时候使用。

规则出现且仅出现一次，可以说是一个知易行难的做法，因为我们总会有不经意间重复实现规则。有时我们会忽略已有的规则，有时我们会出于当前紧张开发进度的考虑，有时我们可能懒得去统一。
但把规则的实现统一起来，再重复调用，会让你在今后的项目开发中，长期收益。没错，真的会长期收益。

2.16.4 不可变值与无状态操作

(1)在开源中国上翻译的两点收获

首先，让我们简单来了解一下PHP语言的运行机制。
PHP是一个运行于服务端的脚本解析语言，每一个HTTP请求都会触发一个php-fpm进程来响应，所以不同于其他长时间运行的语言或者系统，不用过多地考虑内存的回收或者对实体的管理和共享。

这样是有明显的好处，作为PHP开发人员，由于每一次请求所消耗的内存都会在本次释放，即使运行错误也不会影响其他的调用。从而，我们可以放心快速地开发。
但我们也应该看到这样的便利给很多开发同学所带来的误导。正因为不用再担心一些传统的问题（如内存管理），他们变得更无限制。当这种无限制日积月累而引发诸多项目的问题时，他们会开始责怪PHP这门语言。

其实，语言本身没有对错，关键在于我们怎么使用。

先前，在开源中国进行翻译时，我从翻译的文章中收获了两点。

  • 第一点是，学习不同的语言，你将会获得不同的灵感。比如，你是OC的开发人员，可以从GO语言中获得OC的开发灵感；反之亦然。
  • 第二点是，不可变值的使用。

这里，我将尝试说明如何在PhalApi现有的分层机制基础上，结合不可变值和无状态，应对复杂的领域业务开发。

(2)不可变值

通常，我们在程序中处理的变量可以分为：值和实体。简单来说，值是一些基本的类型，如整数、布尔值、字符串；实体则是类对象，有自己内部的状态。当一个实体表示一个值的概念时（如坐标、金额、日期等），我们可以称之为值对象。
明显地，系统的复杂性不在于对值的处理，而在于对一系列实体以及与其关联的另一系列实体间的处理。

如同其他语言一样，如果我们也在PHP遵循 不可变值 与 无状态 这两个用法，我们的系统乃至业务都可以从中获益。

不可变值 是指一个实体在创建后，其内部的状态是不可变更的，这样就能在系统内放心地流通使用，而无须担心有副作用。

举个简单的例子，在我们国际交易系统中有一个金额为100RMB的对象，表示用户此次转账的金额。如果此对象是不可变值，那么我们在系统内，无论是计算手费、日记纪录，还是转账事务或其他，我们都能信任此对象放心使用，不用担心哪里作了篡改而导致一个隐藏的致使BUG。

也就说，当你需要修改此类对象时，你需要复制一个再改之。有人会担心new所带来的内存消耗，但实际上，new一个只有一些属性的对象消耗很少很少。

要明白为什么在修改前需要再创建新的对象，也是很容易理解的。首先，我们保持了和基本类型一致的处理方式；其次，我们保持了概念的一致性，如坐标A（1，2）和坐标B（1，3）是两个不同的坐标。
当坐标A发生改变，坐标A就不再是原来的坐标A，而是一个新的坐标。从哲学角度上看，这是两个不同的概念。

在PhalApi中，我们可以看到不可变值在Query对象中的应用：

$query1 = new PhalApi_ModelQuery();
$query1->id = 1;

$query2 = new PhalApi_ModelQuery($query1->toArray());
$query2->id = 2;

这样以后，我们就不再需要小心翼翼维护“漂洋过海”的值对象了，而是可以轻松地逐层传递，这有点像网络协议的逐层组装。

这又让我想起了《领域驱动设计》一书中较为中肯的说法：
把值对象看成是不可变的。不要给它任何标识，这样可以避免实体的维护工作，降低设计的复杂性。

(3)无状态操作

前面提到了PHP的运行机制，不同于长时间运行的语言或系统，PHP很少会在不同的php-fpm进程中共享实体，最多也只是在同一次请求中共享。

这样，当我们在一次请求中需要处理两个或两个以上的用户实体时，可以怎么应对呢？
关于对实体的追踪和识别，可以使用ORM进行实体与关系数据库映射，但PhalApi弱化了这种映射，取而代之的是更明朗的处理方式，即： 无状态操作 。

因为PhalApi都是通过“空洞”的实体来获得数据，即实体无内部属性，对数据库的处理采用了 表数据入口模式 。
当我们需要获取两个用户的信息时，可以这样：

$model = new Model_User();
$user1 = $model->get(1);  //$user1是一个数组
$user2 = $model->get(2);

//而不是
$user1 = new Model_User(1);  //$user1是一个对象
$user2 = new Model_User(2);

//或者可以这样批量获取
$users = $model->multiGet(array(1, 2));  //$users是一个二维数组，下标是用户的ID

这样做，没有绝对的对错，可以根据你的项目应用场景作出调整。但我觉得无状态在PhalApi应用，可以更简单便捷地处理各种数据以及规则的统一，以实现操作的无状态。因为：

  • 1、可以按需取得不同的字段，多个获取时可以使用批量获取
  • 2、在单次请求处理中，简化对实体的追踪和维护
  • 3、换种方式来获得不可变值性的好处，因为既然没有内部状态，就没有改变了

(4)引申到Domain层

Domain层作为ADM（Api-Domain-Model）分层中的桥梁，主要负责处理业务规则。

将值对象与无状态操作引申到Domain层，同样有处于简化我们对数据和业务规则的处理。

我们可以根据上述的家庭组成员领域类来完成类似下面功能场景的业务需求：

$domain = new Domain_Group_Member();

if (!$domain->hasJoined(1, 100)) {
    $domain->joinGroup(1, 100);
}
if (!$domain->hasJoined(2, 100)) {
    $domain->joinGroup(2, 100);
}
if (!$domain->hasJoined(3, 100)) {
    $domain->joinGroup(3, 100);
}

即：如果用户1还没加入过组100，那么就允许他加入。用户2、用户3也以此类推。

当我们把业务规则，划分为更细的维度时，我们可以轻松上在业务层组装不同的功能，讲述不同的故事。

2.16.5 越痛苦的事情，越早做

在有一次敏捷开发分享会上，有位前辈说：要对小问题不断进行优化迭代，而不要等到大问题来了再作变革。
也就是我们常听说的黄金法则： 演变优于剧变 。

同样，在持续集成中，也提倡着类似的理念，即：越痛苦的事情，越早做。

有时，对自己狠一点，是会有所收获的。

我们都赞扬美好的事物，但我们很少也会那样去做，因为我们知道美好需要付出更多的努力，意味着有所牺牲。

如很多女生，都很喜欢苗条的身材，却总忍不住零食的诱惑，也很难坚持锻炼。
我们很多人都喜欢优雅的代码，自已却也会写下一些临时性的代码，而没多及时清理代码的异味，也没有尝试去重构，更没有坚持单元测试。

从另外一个角度说，如果一个项目的问题，我们在前期及时沟通并解决的话，根本不值得过多去关注。但若我们因为团队关系或者心烦意乱有意识去抵触多变的需求时，一个很小的问题，到了上线后，就可能会演变成一个灾难。
到了那时，即使只是一行代码的改变，也会涉及到开发、测试、产品、运维、用户、商务、老板等等一系列的干涉人。为了修复上线，我们还要走一系列的发布流程，事后还需要为这样的故障买单。

既然如此，明明知道当初一个不确定的需求时，为什么没去及时处理呢？
我知道，我相信我们大家也知道，程序员总会有一些很烦很想抵触的时候，这时我们会拒绝改变，拒绝去做一些正确的事情。

但越痛苦的事情，越早做，不仅仅需要我们增强对自身的情商控制，更让我们能很好地应对高价值的系统。
试想，谁会把一个影响到千千万万用户、涉及到动则百万金额的系统交给一个动不动就发脾气的人呢？

所以，对自己“狠”一点吧，明天的你，将会感谢今天努力的你。

2.16.6 收篇

领域驱动设计所提供的思想、概念和设计非常广泛，这里不能一一说明。本章仅仅是摘取其中的部分内容进行再传播，以唤醒入门同学的注意，培养一种约定编程的意识。

更为重要的是，要懂得去学习，学习后应用到自己当前的工作或项目中，慢慢地你将体会到开发编程的乐趣。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.17]-微服务：Api接口服务层 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

我发现，我越是努力，就越发幸运。 -- Thomas Jefferson

2.17.1 微服务

Martin Fowler（我喜欢和敬仰的大师）曾发表了上面这一段话。这段话也出现在了2015年QCon分享会上，并加了一张PPT“什么是微服务”加以说明。

里面提到了 微服务 这个概念，在PhalApi框架中即对应我们的Api接口服务层，只是我们不是称之为微服务，而是接口服务。
不管何种说法，我们都应该关注里面提及到的这几点重要特质：

  • 小，且专注于做一件事情
  • 独立的进程中
  • 轻量级的通信机制
  • 松耦合、独立部署

这里不过多地讨论微服务相关的分享，而是重温接口服务层Api与领域驱动和单元测试之间的关系，以及如何开发一个优雅、稳定又简单的接口。

2.17.2 层级调用的顺序

整体上讲根据从Api接口层、Domain领域层再到Model数据源层的顺序进行开发。

在开发过程中，需要注意不能 越层调用 也不能 逆向调用 ，即不能Api调用Model。而应该是 上层调用下层，或者同层级调用 ，也就是说，我们应该：

  • Api层调用Domain层
  • Domain层调用Domain层
  • Domain层调用Model层
  • Model层调用Model层

如果用一张图来表示，则是：
a pic

为了更明确调用的关系，以下调用是 错误 的：

  • 错误的做法1：Api层直接调用Model层
  • 错误的做法2: Domain层调用Api层，也不应用将Api层对象传递给Domain层
  • 错误的做法3: Model层调用Domain层

这样的约定，便于我们形成统一的开发规范，降低学习维护成本。
比如需要添加缓存，我们知道应该定位到Model层数据源进行扩展；若发现业务规则处理不当，则应该进入Domain层探其究竟；如果需要对接口的参数进行调整，即使是新手也知道应该找到对应的Api文件进行改动。

2.17.3 接口服务的定义

现实项目开发过程中，绝大部分我们编写的接口是给别人使用的，或许给Android客户端同学使用，或者给iOS客户端同学使用，抑或提供给其他后台系统的同学使用。
为了提高并行开发的速度，我们不能等待接口完全开发完成后才提供接口文档，而且他们也不能忍受这么漫长的等待。

所以，客户端同学时常会问我们：什么时候可以提供接口文档？

我们提倡“接口先行”，如果有时不能很好地做到这一点（毕竟多变的需求促发多变的情境），我们可以快速提供接口的定义。
这有点像规约层对接口的定义一样，在PhalApi中定义一个接口，再具体一点即：

  • 1、创建一个接口服务类和声明函数
  • 2、配置接口参数规则
  • 3、提供接口返回格式的注释

简单来说，就是创建一个类，写个函数，定义参数和返回结果。

下面以 开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动） 中的团队参赛接口为例，说明这三步操作的过程。

(1)创建一个接口服务类和声明函数

//$ vim ./Vote/Api/Act.php

<?php
class Api_Act extends PhalApi_Api {

    public function joinIn() {
    }
}

(2)配置接口参数规则

<?php
class Api_Act extends PhalApi_Api {

    public function getRules() {
        return array(
            'joinIn' => array(
                'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
            ),
        );
    }

    public function joinIn() {
    }
}

(3)提供接口返回格式的注释

<?php
class Api_Act extends PhalApi_Api {

    public function getRules() {
        return array(
            'joinIn' => array(
                'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
            ),
        );
    }

    /**
     * 团队参赛接口
     *
     * @return int code 0，参赛成功；1，队名已存在
     * @return int team_id 新建的团队ID
     */
    public function joinIn() {
    }
}

(4)在线查看一下效果

在完成上面的动作后，我们可以通过在线工具来看下实时的效果，在浏览打开后访问：

http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn

可以看到：
a pic

到了这里，即使我们未完成接口的开发，也未提供更完善的接口文档，但接口客户端同学也可以根据这个在线的接口参数进行开发了。

2.17.4 在ATDD下讲述故事

我们一直推崇测试驱动开发，但在对于Api接口开发，更准确来说是ATDD，即：验收测试驱动开发（Acceptance Test Driven Development）。

在前面Domain层文档中，我们提到了Api层是讲述故事的场景。因此，为了验证我们的业务场景是否正确，我们应该事先编写好单元测试，以不断引导我们前往正确的目的地。

我们可以使用脚本来快速生成测试骨架：

$ pwd
$ /path/to/api.vote.phalapi.com/Vote/Tests/Api
$ phalapi-buildtest ../../Api/Act.php Api_Act ../test_env.php > Api_Act_Test.php

然后，稍微修改完善测试场景：

    /**
     * @group testJoinIn
     */
    public function testJoinIn()
    {
        //Step 1. 构建请求URL
        $url = 'service=Act.JoinIn';
        $params = array(
            'sign' => 'phalapi',
            'team_name' => 'test team name',
            'user_id' => '1',
            'token' => '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731',
        );

        DI()->notorm->team->where('team_name', $params['team_name'])->delete();

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url, $params);
        //var_dump($rs);

        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('team_id', $rs);
        $this->assertEquals(0, $rs['code']);
        $this->assertGreaterThan(0, $rs['team_id']);

        //create again
        $rs = PhalApi_Helper_TestRunner::go($url, $params);
        $this->assertEquals(1, $rs['code']);
    }

从上面测试的代码可以看出，我们先后进行了两次报名。明显地，第一次报名应该是成功的，第二次则应该提示不能重复报名。

单元测试的好处，不但在于可以引导我们做正确的事情，还可以提高我们的关注点，不致于在开发过程中被各种事务（如临时性的会议）打断后回来却不知刚才开发到哪了。
然而，更多的是为后期的维护、扩展提供可验证的业务场景。这点是很重要的。因为每一个测试场景，都保存了对应场景的模拟信息，这样不仅仅在后面的扩展，还是突如其来的BUGFIXED，我们都可以快速证明我们的修改是正确的，至少不会影响到原来的业务流程。
试想一下，如果原来可以下单支付的接口，突然被影响到而导致支付不成功，这是何等的损失！

我现在慢慢地，每当需要修改别人的代码时，我都会看下有没有对应的单元测试。如果没有，我会先补回，这样能增强我修改别人代码的信心。

2.17.5 精益接口开发

传统的接口开发，由于没有很好的分层结构，而且热衷于在一个文件里面完成绝大部分事情，最终导致了臃肿漫长的代码，也就是通常所说的意大利面条式的代码。

在PhalApi中，我们针对接口领域开发，提供了新的分层思想：ADM（Api - Domain - Model）。
即便这样，如果项目在实际开发中，仍然使用原来的做法，纵使使用再好的接口开发框架，也还是会退化到原来的局面。

为了能让大家更为明确Api接口层的职责所在，我们建议：

(1)Api接口层应该做：

  • 应该：对用户登录态进行必要的检测
  • 应该：控制业务场景的主流程，创建领域业务实例，并进行调用
  • 应该：进行必要的日记纪录
  • 应该：返回接口结果

(2)Api接口层不应该做：

  • 不应该：进行业务规则的处理或者计算
  • 不应该：关心数据是否使用缓存，或进行缓存相关的直接操作
  • 不应该：直接操作数据库
  • 不应该：将多个接口合并在一起

在明确了上面应该做的和不应该做的，并且也完成了接口的定义，还有验收测序驱动开发的场景准备后，相信这时，即使是新手也可以编写出高质量的接口代码。
因为他会受到约束，他知道他需要做什么，主要他按照限定的开发流程和约定稍加努力即可。

如果真的这样，相信我们也就慢慢能体会到 精益开发 的乐趣。

最后，让我们一起来看下上述团队参赛接口开发的代码实现：

    /**
     * 团队参赛接口
     *
     * @return int code 0，参赛成功；1，队名已存在
     * @return int team_id 新建的团队ID
     */
    public function joinIn() {
        $rs = array('code' => 0, 'team_id' => 0);

        DI()->userLite->check(true);

        $domain = new Domain_Team();
        if ($domain->isExists($this->teamName)) {
            $rs['code'] = 1;
            return $rs;
        }

        $teamId = $domain->joinIn($this->teamName);
        $rs['team_id'] = $teamId;

        return $rs;
    }

可以看出，上面的代码短小达意，简单清晰。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.19]-扩展库：可重用的扩展类库 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

PhalApi框架扩展类库

-- 致力于与开源项目一起提供企业级的解决方案！

此部分类库为 PhalAPi框架 下的扩展类库包，各个扩展包各自独立，可以根据需要自动下载安装。

虽然此部分的扩展很多都是基于已有的第三方开源实现，或者开发同学自己实现分享的，但我们所做的不只是代码的搬运工，更多的是在系统架构上的组件库重用，以及在此引入 防腐层 ，避免进入 供应商锁定（Vendor Lock-In） 。

除此之外，可以在PhalApi框架的基础上，快速引入符合我们国内实际项目开发需要的各种扩展。如时，您会发现，原来编程本来就是一件如此简单的事情，就像搭积木一样。

正如我们一直推荐的：

接口，从简单开始！

目前已提供的扩展类库 - 针对使用者

1、微信开发

此扩展可用于微信的服务号、订阅号、设备号等功能开发，则PhalApi框架下简单配置即可开发使用。

更多请点这里查看使用文档

2、代理模式下phprpc协议的轻松支持

此扩展可用于phprpc协议的调用，服务端只需要简单添加入口即可完美切换。

更多请点这里查看使用文档

3、基于PHPMailer的邮件发送

此扩展可用于发送邮件。

更多请点这里查看使用文档

4、优酷开放平台接口调用

此扩展可用于调用优酷开放平台的接口。

更多请点这里查看使用文档

5、七牛云存储接口调用

此扩展可以用于将图片上传到七牛CDN，或者其他七牛接口的调用。

更多请点这里查看使用文档

6、用户、会话和第三方登录集成

此类库主要特点有：

  • 1、可以和第三方登录集成，包括：微信登录、新浪登录、QQ登录
  • 2、为客户端提供了直接可以调用的登录接口
  • 3、为服务端提供了直接可以检测用户登录态的操作
  • 4、支持token落地、高效缓存和分布式的数据库存储

  • 5、展示了如何开发一个项目级的类库、包括数据库配置、翻译等

    更多请点这里查看使用文档

7、swoole支持下的长链接和异步任务实现

目前，此扩展类库提供了：

  • 长链接的接口调用

  • 异步计划任务的调用

    更多请点这里查看使用文档

8、新型计划任务

此扩展类型用于后台计划任务的调度，主要功能点有：

  • 1、提供了Redis/文件/数据库三种MQ队列
  • 2、提供了本地和远程两种调度方式
  • 3、以接口的形式实现计划任务

更多请点这里查看使用文档

9、Auth 权限扩展 (由@黄苗笋提供)

实现了基于用户与组的权限认证功能，与RBAC权限认证类似，主要用于对服务级别的功能进行权限控制，主要功能点有：

  • 1、提供了接口服务维度的权限验证
  • 2、提供了可配置的组与规则
  • 3、支持免检用户

更多请点这里查看使用文档

11、FastRoute快速路由

此扩展基于 FastRoute 实现，需要 PHP 5.4.0 及以上版本，可以通过配置实现自定义路由配置，从而轻松映射到PhalApi中的service接口服务。，主要有：

  • 1、基于FastRoute实现
  • 2、需要PHP 5.4.0 及以上版本
  • 3、通过配置文件来实现自定义路由，并映射到service

  • 4、可兼容无路由的历史URI

    更多请点这里查看使用文档

12、基于PhalApi的DB集群拓展DB_Cluster (由@喵了个咪提供)

为应对海量数据分析与统计，提供针对分表分库统一封装的数据库操作接口，主要用于解决大量数据写入分析的问题。请注意：V0.1bate版本,很多功能尚不完善,只提供技术交流使用,请不要用于生产环境。主要特点有：

  • 1、适用于大量select和大量的insert的业务场景
  • 2、基于架构思维的实现
  • 3、分表分库算法介绍

  • 4、基准测试对比

    更多请点这里查看使用文档

13、基于PhalApi的Redis_Base拓展 (由@喵了个咪提供)

主要提供更丰富的Redis操作,并且进行了分库处理可以自由搭配

  • 1、适用于对Redis需要其他数据类型操作的业务

  • 2、可以用于队列脚本,封装了队列处理

    更多请点这里查看使用文档

扩展开发指南 - 针对开发者

为了统一扩展类库的风格、便于用户更容易使用，这里建议：

  • 代码：统一放置在Library目录下，各扩展包各自一个目录，尽量Lite.php入口类，遵循PEAR包命名规范；
  • 配置：统一放置在DI()->config->get('app.扩展包名')中，避免配置冲突；
  • 文档：统一提供WIKI文件对扩展类库的功能、安装和配置、使用示例以及运行效果进行说明；

1.19.4 从微架构到扩展类库的演进

在应用项目的实际开发，我们也可以有意识地将一些通用的工具和操作与业务分离，以便可以在项目内更好地重用。当抽离成工具或者通用类后，则可以进一步推广到公司内其他项目，即组件复用。如果觉得可以，则发扬开源精神，分享给社区。这也是符合从微架构到应用构架、系统架构、乃至企业架构的演进之路。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.20]-约定编程：架构明显的编程风格 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

2.20.1 前言

我们一直强调推荐让后台接口开发更简单。

所以，我们提供了PhalApi开发框架和WIKI文档。然后，这仅仅是个开始。

因为，真正发挥作用，实现价值的还是来自项目实际开发中的源代码。

但纵使我们提供了好的框架作为基础，但不得不承认的一个事实是：架构模型和项目代码随着各自的演进逐渐产生分歧。而这种分歧，如果没有注意、管理和约束，则会产生越来越混乱的代码。

在说明如何编写简洁的项目代码之前，让我们先了解一下PhalApi的架构思想以及主要的设计意图。

2.20.2 开发-配置-使用 模式 (develop-config-use pattern)

开发-配置-使用 模式即：开发实现-配置注册-客户使用 模式。

现分说如下。

(1)开发实现

开发实现的主要内容是组件、公共服务或者基础设施的功能实现，此部分主要针对高级开发工程师，或者有经验的PHP同学。

例如对项目的接口签名的验证拦截、一个完成了对七牛云存储接口调用的扩展、又或者是项目内部加密的方案等，这些以包或者接口提供，为外部使用提供了配置说明、使用示例和文档说明。更为重要的是，应该提供了配套的单元测试，并有着很高的代码覆盖率。

此类实现应该是稳定的，即没有明显或者隐藏的BUG。即使有，原作者也可以快速进行定位和解决，以及后期的扩展和升级。

(2)配置注册

一旦上面的接口被实现后，不同的项目都可以轻松引入和使用。这块通常由项目的负责人，或者主程来操作，因为在对项目进行构建部署、组件和环境装配时，需要考虑到哪些组件需要被用到，以何种方式进行初始化和装载。

但使用的方式，应该是简明的。如简明的安装，简明的配置。所以，这里自然而言，就涉及到了 依赖注入 ，DI。

通过DI，项目的负责人，可以轻松地将已通过严格测试的组件/服务注册进来。完成此步骤后，一切都整装待发，剩下的就是使用的问题了。

(3)客户使用

项目会不断有新的需求出来，而团队也会因此同步增加吸纳新开发同学进来负责新模块新功能的开发。而新的同学，往往会是一些开发新手，他们需要使用已有的功能，快速实现一些具体的业务逻辑、规则和功能。

但如果他们还需要实现一些基础重要的功能，又要考虑如何与现在项目整合，会分散他们的关注点。而且，即使放手给他们去做，他们也会常常因为考虑不周或者编程风格各异而产出一些与项目期望不符的代码。

若换一种工作的方式，即如果新手使用已有的组件进行一些特定领域业务的开发，会是怎样？

我想，会有很大的改观。

比如，我们对新来的同学说，你使用DI()->logger就可以写一条日记了，如：

DI()->logger->debug('app enter');

新手可能很喜欢追问一些问题，他可能会问及到，那怎么将一些参数（当时日记的上下文）也进行纪录呢？你可以很骄傲地说：也是可以的，你只需要这样写就可以了：

DI()->logger->debug('app enter', array('device' => 'iOS', 'version' => '1.1.0'));

(4)创建和使用分离

开发-配置-使用 模式 也符合了创建和使用分离的思想。

不同的项目，不同的应用，需要的初始化服务不一样；不同的规模，对不同的技术解决方案也不一样；不同的环境，配置也不一样。

但即使是这样，新手还是可以一如既往地使用之前注册的服务（也就是不需要修改任何调用代码）。也就是上层的调整或者环境变更这些，对新手的使用都是透明的。为了更好地理解这些概念，这里补充一些案例场景。

以日记为例

假设我们有个项目A，分别部署到内网测试环境和外网生产环境，显然内外网环境的配置是不一样的。我们希望在内网环境为日记开启debug模式以方便开发人员进行调试，在外网则希望将其关闭以减少系统的性能开销。在一开始使用文件作为日记存储方案时，对应的内网环境初始化代码如下：

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

在外网，我们只需要去掉PhalApi_Logger::LOG_LEVEL_DEBUG即可：

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

随着项目的不断发展，我们有了一批又一批的新用户。产品经理为此很开心，也请我们开发吃了好几顿大餐。但谨慎的我们发现了现在文件日记的一些限制。如即时文件读写带来了I/O瓶颈，而且不能将分布式的日记文件自动收集起来。所以，我们决定对logger进行更深层次的探索。。。

至于最后是使用了Hive还是Hadoop，还是MC异步后台队列的方式实现，我们这里不具体指定。假设新的logger研发成功后，我们便可以轻松对原有的文件日记组件进行升级，实现完美切换：

//升级后的日记纪录
DI()->logger = new My_Logger(PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

这不仅是几行代码上的区别，而是针对不同问题不同技术解决方案的抉择。这也是有经验的开发和新手之间的区别，因为你选择的技术解决方案要和面临的风险相匹配。例如用牛刀来杀鸡，就是一个不匹配的做法，就如同使用高级的Hive来实现单一小项目的日记存储一样。

这是令人值得兴奋的。在很多旧的项目里面，当遇到瓶颈时，会请一些外部的专家来指导或优化。但即使拥有着各种“法宝”以及知道何时该使用哪种方案的专家，对于这种残留的代码也会步履维艰基于束手无策。因为，各种初始化和调用的代码，分遍在项目的“全国各地，四面八方”。即使你优化了，你会发现还要手动一个个地进行切换升级。更重要的是，很多时候不是你想优化就能优化的。

我曾经遇到过这样一个旧系统。它是在UcHome基础上做二次开发，但对于它的数据库使用，开发人员没有过多地优化，如：没有使用缓存，没有进行批量合并查询优化，重复查询相同的数据，没有建立索引等等，等等。这样的后果就是，请求一次接口，会触发150条到500条SQL语句不等。我后来，在底层添加了在线查看调试SQL语句的功能，尝试进行了一些合并查询，但当我想为数据库的表添加索引时，发现它用的却是虚拟表 -- 视图！

(5)扩展类库对“开发-配置-使用”模式的应用

如果说DI服务是微观上对“开发-配置-使用”模式的使用，那么PhalApi的扩展类库则是宏观上的应用。

扩展类库也是由第三方（可能是PhalApi开发团队、他人或者你）开发实现的，然后再通过简单配置（或者免配置），就可以使用扩展类库的功能了。如邮件发送、phprpc协议。
之所以提供扩展类库的形式，是因为DI服务更适合于单个类以及几个操作接口，而扩展类库则提供更丰富的功能操作和一系列的接口。

这样以后，项目就可以简单快速共享各种扩展类库了。难道这不是一件令人兴奋的事情吗？
因为“哈啊！我又找到了一个可以直接用的代码类库”，而不是“唉，又要写一堆代码，还要测试、联调。。。”。

(6)回顾Yii框架的发现

程序、系统和框架，其作用太多数都体现在动态的功能上，而不是静态有限的功能。而动态的功能则很大程序上依赖于各种配置，如Tomcat下各层级xml配置。有些框架对配置这块提供了丰富的支持，但为此的代码是，配置难以掌控。

就拿Yii框架为例（Yii确实是一个很了不起的框架，这里只是以事论事），当你需要在视图渲染一个数据表格时，你可以使用 CGridView ，并类似这样配置：

$columns = array(
    array('name' => 'mId', 'header' => '序号'),
    array('name'=>'id', 'header'=>'事件ID'),
    array('name'=>'title', 'header'=>'标题'),
    array('name'=>'content', 'header'=>'内容', 'type' => 'html'),
);

$this->widget('bootstrap.widgets.TbGridView', array(
    'type'=>'striped bordered condensed',
    'dataProvider'=>$dataProvider,
    'columns'=> $columns,
));

更为复杂的情况可以是：

$columns = array(
    // ...
    array('class' => 'CDataColumn', 'header' => '内容', 'type' => 'html', 'name' => 'content', 'htmlOptions' => array('width' => '200px')),
    array(
        'class'=>'CButtonColumn',
        'template'=>'{showEvent}<br/><br/>{deleteEvent}',
        'header'=>'操作',
        'buttons'=>array
        (
            'showEvent' => array(
                'label' => '查看',
                'url' => '"?r=DailyOperations/eventManagerShow&user_iduser_id=' . $userId  . '&eventId=". $data["id"];',
                'options' => array('target' => '_blank'),
            ),
            'deleteEvent' => array(
                'label'=>'删除',
                'url'=>'"javascript:void(0)"',
                'imageUrl'=>'/images/delete_24.png',
                'deleteConfirmation'=>"js:'Record with ID '+$(this).parent().parent().children(':first-child').text()+' will be deleted! Continue?'",
                'click'=>'js:function(){if (confirm("此操作将删除：ID = " + $(this).parent().parent().children(\':first-child\').text() + " \n是否确定？")) {deleteEvent($(
this).parent().parent().children(\':first-child\').text());};}',
            ),
        ),
    ),
);

// ...

然后，对于我这么笨的人来说，不管是简单的配置，还是复杂的配置，每次当我需要使用时，我都非常害怕且需要从以下三方便获取帮助：

  • 找曾经写过类似的代码并拷贝过来修改；
  • “耐心”（耐着心）查看官方的文档；
  • 网上搜索相关的例子

因为，每次我都记不住这些配置，但我又不得不承认它的效果很好。然后我觉得其缺点至少有两点：

  • 缺点1：尽管是很简单的功能也需要用配置来实现，从而导致配置羞涩难懂；
  • 缺点2：配置太复杂，对人的记忆要求太高；

这是我对Yii框架配置的体会。

自己工作的回顾

最初，感受到配置式的开发，是在大学的时候做一个OutLook的插件。这个插件需要同步本地和远程服务器的联系人，其中当有冲突时，就有这么几种策略：冲突时以本地为准、冲突时以远程为准、冲突时提醒我、忽略冲突。

这是当时写的博客，感兴趣可以看看 配置编程： 让项目开发从多样到统一

这有点像我们常用的SVN的处理方式。然而当我在尝试开发实现时，我发现过程很复杂，但处理又是如此相似。这里的区别很微妙，特别这些策略又是由外部用户指定时。最后，我惊讶地发现，如果我使用配置来做的话，会非常简单且明了！

但那时，只是初体会。

现在，经过了几年的开发，我才慢慢发现，可以把这种开发模式总结为：开发-配置-使用模式。
不知是否有其他模式和此新发现的模式类似？

2.20.3 框架外延

框架外延是指跨项目，与业务无关的抽象特性与代码实现。

这些从简单到复杂，有：通用功能，定制，扩展类库和产品簇框架。

(1)通用功能

有些功能是常用的，也是通用的。一般以函数或者工具类的形式提供，如一个生成随机数的方法。这些通用的功能，开发人员都可以在项目间流通使用。

(2)定制

当发现PhalApi现在的框架不支持某些接口下具体方案的实现时，或者支持但不满足当前项目的开发需要时，可以进行一些定制化。

之前有一个项目的开发同学，给我提供了一个很好的建议。他说他的项目为了能够获得更高的安全性，对客户端传递的参数进行了整包加密，然后在服务商进行整包解密再通过getRules()获取。所以对应的定制大概实现可以是这样的：

<?php
class My_Request extends  PhalApi_Request {

     protected function genData($data){
         $needData = array();

        if (!isset($data) || !is_array($data)) {
            $data = $_POST;
        }

        $needData['service'] = isset($data['service']) ? $data['service'] : '';
        //整包加密后的数据包
        $params = isset($data['params']) ? $data['params'] : '';
        //TODO: 对称解密 ...

        $needData = array_merge($params, $needData);

        return $needData;
    }
}

(3)扩展类库

有时候，我们在项目中需要使用到一组功能操作，这时可以结合外观模式，将这些需要用到的接口以包的形式都封装到一个扩展里面。因为扩展这个概念是容易理解的，也是开发人员所喜爱的。

在PhalApi的Issues里面，有位同学就提到了对新浪或者百度云空间的支持，就是对应的一个场景。

现在PhalApi提供了部分的扩展类库，但由于个人时间有限，而且也不可能知悉全部项目需要用到的全部扩展。所以当发现需要的扩展类库还没有提供时，简单的方法：自动写一个。

这也是对框架进行外延的一种途径。伴随着这种途径经历的次数越多，你会突然有一天发现你所搭建的这一切结合起来后，已经可以很好地应对了公司内的目前全部项目的开发。

(4)产品簇框架

这个时候，就来了产品簇框架这一阶段。

PhalApi只是提供了一个基础的框架，一个项目实际开发的基础框架，更是一个产品簇框架的铺垫。如果说我们关注项目的快速交付，不如说我们更关注 如何提供一个底层框架以支持不同项目的产品簇框架开发，从而最终支持项目的快速交付 。

也只有这样，你辛苦付出的代码，你才会更加珍惜和不断为之冥想、维护和改进。
也只有这样，你公司里面的其他项目才会更愿意和信赖使用，因为框架是你直接提供的。
也只有这样，代码才得以永恒，因为这种思想在你、我、各个项目团队间不断传递，共存在每个开发人员的脑里和心中，而不是为某个自私的人把持着。

2.20.4 项目内涵

框架外延 是针对特定领域项目以外的问题。然而，在使用PhalApi进行项目开发时，我们如果只关注这些是无用的，我们还要关注项目的实际开发，如何编写代码，即：项目内涵。

项目内涵是指完成特定领域功能所需要的前置条件、基础设施和工作流程。

这里面着重讲解复杂的领域和更广义的数据源这两方面。不是说其他的方面不重要，而是这两方既重要但又常常为人们所误解误用。

(1)复杂的领域业务层

在一个项目架构里面，有三个主要模型：设计模型、领域模型和代码模型。设计模型在选择PhalApi时已大体确定，领域模式则需要项目干系人员消化、理解并表达出来。对于开发人员，代码模型则是他们表达的媒介。

这一层，主要关注的是领域业务规则的处理。所以，我们抛开外界客户端接口调用的签名验证、参数获取、安全性等问题，也不考虑数据从何而来、存放于何处，而是着重关注对领域业务数据的处理上。

根据这么年来的工作、项目开发和学习，这里有一些建议。

规则出现且出现一次

领域之所以复杂，在于规则众多。如果不能很好地把控这些规则，当规则发生变化时，就会出现很大的问题。在开发过程中，要注意对规则进行提炼并且放置在一个指定的位置。如对游戏玩家的经验计算等级时，这样一个规则就要统一好。不要到处都有类型相同的计算接口。

释意接口

领域的逻辑是对现实业务场景的再解释。现实的因素充满变数并且由人为指定，所以不能简单的在计算机中“推导”出领域逻辑。在开发过程中，要特别对这些领域逻辑准确并很好的解释，以便后面接手的同学可以更容易理解和明白这些流程、限制和规则。

其中一个有力的指导就是释意接口。对接口签名甚至是对变量命名的仔细推敲都是很有益处的，因为名字能正名份，不至于混淆或者含糊不清。

代码保持在同一高度

领域层关注的是流程、规则，所以当你进行用户个性化分流和排序时，不应该把底层网络接口请求的细节也放到这里流程里面。把底层技术实现的细节和业务规则的处理分开是很有好处的，这样便于更清晰领域逻辑的表达，也助于单元测试时的测试桩模拟。

(2)更广义的数据源层

领域层固然重要，但如果没有数据源层，领域层就是一个空中楼阁。

但不应把数据源就理所当然地对等成数据库。因为这种观念很常见但也很狭隘。首先，很多项目在对数据存储时，不一定会落地存储，即使落地也不一定使用数据库。我曾经在一家游戏公司任职时，就看到他们使用了文件来存放。相信，你也看到过。其次，在现在多客户端多系统的交互背景下，很多系统都需要进行数据共享和通信，为了提高服务器的性能也会使用到缓存。这些场景下，会导致数据是通过接口来获取，或者来源于缓存。可以看出，如果把数据源就看作是MySql，是非常局限的。

我们在PhalApi中继续使用了Model层，因为受MVC模式的影响，大家都对Model层非常熟悉。但我们却为它赋予了新的诠释和活力。

Model导获取的数据，可以是来自数据库的读取，也可以是通过开放平台接口获取的数据，也可以是不落地直接存放于缓存的数据。

2.20.5 测试先行和真实的测试

框架外延和项目内涵，分别是对 开发-配置-使用 模式 前半部分和后半部分的诠释。

剩下的问题就在于，如何评判我们编写的代码是好的，是美的，并且能够按期望地工作？

如果只是主观地判断，显然是不可靠的也不是完全可信的。例如“我觉得我这样没问题啊！”，即使代码能够正常运行，也不并代表在其他临界或者极端的情况也能如期运行，也不代表这些代码就具备了一些好的品质，更不代表这些代码遵循了我们的架构明显的编程风格：编写人容易理解的代码。

既然主观不可靠，就应该转到客观上的标准。

静态的代码分析，是很有用的，但对于最终需要动态执行的代码，还是需要单元测试才能更好的探知代码内部每个角落的状况。

这里，我们强烈推荐测试先行，也就是测试驱动开发。可能你已对此耳熟能详，也可能你略知一二。

但请相信，测试先行是值得的。关于真实的测试，即编写PHPUnit单元测试，关于PHPUnit的使用，请查看： PHPUnit Manual – 第 1 章 自动化测试 。

2.20.6 PhalApi架构的设计意图

(1)设计意图

PhalApi是开放式的框架，这里不仅仅体现在源代码开放，产品开放，还表现在思想开放。

这样说，可能有些抽象。若落实到代码层次，你会发现PhalApi提供了接口上的约束，并为每块接口提供了很好的扩展机制。也就是说，一旦你发现已提供的功能不满足你项目的需要，你可以轻松定制、扩展和升级。

这就是我们的设计意图。

对于新手，你可以快速使用这个框架；对于老手，你则可以定制它。

(2)协同合作和关注点

开发-配置-使用 模式 并不是为了传递 “君君臣臣、父父子子”这样的封建等级观念。而是为了资源更好的调配，最终更好地完成项目的开发。

架构系统有分层的思想，这里也一样。通过高级开发、开发工程师和新手各尽其才、各施其职，能够更好的提高各自的关注点，并发挥他们应有的能力和价值。

比如对于高级开发，我们希望他们能够抽离业务，并且产出一些公司内可以使用的通用组件、核心技术甚至是产品簇开发框架。而对于他们来说，也许这也正是他们所喜爱的，因为他们在攻关一些技术难点，又或者在解决一些他们觉得有挑战的工作。

对于项目负责人，也就是开发工程师，他们更关注的是整个项目的运行和所能提供的功能。而这一些功能需要各业务规则在代码上的体现，最终又会直接或间接落到某些基础设施的支持上，一如数据库的查询操作。显然，他们不希望每次都为这样通用的技术支持重复开发。如果有已经能够直接拿来使用的代码，那该多好。网上虽然资源众多，但符合公司项目使用的，少之甚少，又或许会有这样那样的限制。如果有公司内部可重用的组件库，这种情况会大为改观。

对于新手，我们要求明显会低很多。简单来说，他们会使用就可以了。当然，我们也推荐新手在熟练使用的情况下，再深化到底层，慢慢过渡到大工。

(3)这只是一个开始

尽管 我们提供了这样架构明显的编程风格，但仍然需要你以及你的团队来遵循。

正如前面所说的，项目的每一行代码和每一个命名都来自你的思考和双手的输入。也正如此，你的付出让你更深刻体会到编程的乐趣，特别是项目发挥和实现了有价值的业务功能时。这时，你会发现维护别人的代码不再是一件痛苦的事件，与团队的合作也变得更加融洽，因为和你一起奋斗努力的是一支精英团队。

请记住，这只是一个开始，一个起点。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.21]-服务器统一部署方案简明版：CentOs---Nginx---php-fpm---MySql-[--Memcached] | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

上一章 文档首页 下一章

2.21.1 前言

目前服务器架构通常采用：CentOs + Nginx + php-fpm + MySql [+ Memcached]，现将部署方案安装方法、遇到的问题和解决方法、启动方式总结如下，以便后期服务器快速部署。

以下文档的是经过几个深夜整理并在阿里云服务器真实部署通过，运行良好，整个过程熟悉后约共半小时，可以进一步考虑做成自动化部署脚本。
如是，请把脚本也邮件发我一份。^_^

2.21.2 统一约定

  • 全部使用编译安装
  • 安装路径路径：/usr/local/

  • 安装包存放位置：/home/apps/web/

    特别注意：请尽量不要使用root账号操作服务器，特别是对外服务器，谢谢。

2.21.3 依赖库和开发工具

yum -y install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers

# nginx
yum -y install pcre-devel zlib-devel

# php
yum -y install gd-devel libjpeg-devel libpng-devel freetype-devel libxml2-devel curl-devel freetype-devel

# mysql
yum -y install bison gcc gcc-c++ autoconf automake zlib* libxml* ncurses-devel libtool-ltdl-devel* mysql-devel

# sz rz
yum install lrzsz

2.21.4 安装Nginx 1.6.0

参考官方网站

http://wiki.nginx.org/Install

(1)安装

./configure --prefix=/usr/local/nginx
make
make install

(2)启动

/usr/local/nginx/sbin/nginx

(3)重启

/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload

(4)更多配置

server {
    listen 80;
    server_name test.wetime.com;
    root /home/apps/projects/test.wetime.com;
    charset utf-8;
    access_log logs/test.wetime.com.access.log;
    error_log logs/test.wetime.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

2.21.5 安装PHP-FPM 5.3.5

(1)参考

http://blog.csdn.net/dc_726/article/details/9519619

(2)安装

./configure --prefix=/usr/local/php --with-mcrypt --with-zlib --with-curl --with-gd --disable-debug --enable-fpm --enable-mbstring --enable-sockets --enable-zip --enable-xml --enable-pdo

温馨提示

如果提示以下错误：

If configure fails try --with-jpeg-dir=<DIR>
configure: error: libpng.(a|so) not found.

则需要执行以下命令：

cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so

yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

(3)启动

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

(4)重启

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini -t
killall php
/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

(5)错误处理与依赖

cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so
yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

我们可以在命令下输入：

yum localinstall –nogpgcheck http://mirrors.ustc.edu.cn/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/free/el/updates/6/x86_64/rpmfusion-free-release-6-1.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/nonfree/el/updates/6/x86_64/rpmfusion-nonfree-release-6-1.noarch.rpm

安装这3个东东后，我们就可以直接使用yum install libmcrypt来安装啦～

(6)php扩展安装

#安装mysql扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysql=/usr/local/mysql/
make
make install

#安装mysqli扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysqli=/usr/local/mysql/bin/mysql_config
make
make install

扩展包位置在./php-5.3.5/ext/mysql，需要使用到 /usr/local/php/bin/phpize，扩展so应该放到：/usr/lib/php/modules/

此外，

#PHP之pdo_mysql扩展安装步骤

# http://www.smallbigcake.com/2012/08/05/1572/
# http://pecl.php.net/get/PDO_MYSQL-1.0.2.tgz

./configure --with-php-config=/usr/local/php/bin/php-config --with-pdo-mysql=/usr/local/mysql

如果提示“libmysqlclient.so.18: cannot open shared object file: No such file or directory in ”错误则参考 ：http://blog.itpub.net/26230597/viewspace-1161322/，
或进行软链：
ln -s /usr/local/mysql/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.18

2.21.6 安装MYSQL 5.6.10

(1)参考

http://www.cnblogs.com/xiongpq/p/3384681.html

(2)mysql-5.6.10.tar.gz下载

http://pan.baidu.com/share/link?shareid=3729862999&uk=1327790029

(3)前期清理和依赖

rpm -e mysql   //普通删除模式
rpm -e --nodeps mysql    // 强力删除模式，如果使用上面命令删除时，提示有依赖的其它文件，则用该命令可以对其进行强力删除
yum -y install make gcc-c++ cmake bison-devel  ncurses-devel

(4)安装

cmake \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc \
-DWITH_MYISAM_STORAGE_ENGINE=1 \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_MEMORY_STORAGE_ENGINE=1 \
-DWITH_READLINE=1 \
-DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock \
-DMYSQL_TCP_PORT=3306 \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DEXTRA_CHARSETS=all \
-DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci

make && make install

(5)ERROR! The server quit without updating PID file

#去掉干扰的配置
mv /etc/my.cnf /etc/my.cnf.bak

#添加日记纪录
vim /usr/local/mysql/my.cnf
log-error=/var/log/mysqld.log
innodb_force_recovery=1

2.21.7 [可选]MEMCACHE 1.4.20

(1)参考官网

http://www.memcached.org/downloads

(2)更多参考

http://crtlib.blog.163.com/blog/static/133158013201392661912837/

(3)先安装依赖包

./configure --prefix=/usr/local/libevent/
make
make install

(4)安装

wget http://memcached.org/latest tar -zxvf memcached-1.x.x.tar.gz cd memcached-1.x.x ./configure && make && make test && sudo make install

(5)最后成功安装后启动

/usr/local/memcached/bin/memcached -d -m 64 -u root -l 127.0.0.1 -p 11211 -c 128 -P /tmp/memcached.pid

(6)memcached 扩展

#http://blog.sina.com.cn/s/blog_66ac09390101drxc.html
#http://www.linuxidc.com/Linux/2013-07/87743.htm

#准备依赖包
./configure --prefix=/usr/local/libmemcached --with-memcached
make
make install

#安装 扩展
/usr/local/php/bin/pecl install memcached
接着输入
libmemcached directory [no] : /usr/local/libmemcached

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[2.22]-更多工具：精益项目和团队建设 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

2.22.1 PHPUnit：自动化单元测试

目前核心框架单元测试的覆盖率（高达90%以上！！！此处应该有掌声~）： a pic

注意，此测试截图并不是最新的，但我们一直都在致力坚持单元测试。
这一点，你可以在单元测试的代码中找到证明。

2.22.2 Phing：一键部署、快速发布

无论如何，都应该走自动化发布流程，避免人工地打包、上传、解压、改生产配置这些重复性的人工操作。

在自动化发布中，Phing是个不错的尝试。

以下是我某个项目中的使用配置，出于对项目的保护，部分数据已删除，但仍然可以参考。

(1)发布配置 - build.xml

主要的操作有：

    1. 备份当前代码，并删除一天前的备份
    2. 从SVN签出最新的发布代码
    3. 执行配置检测脚本
    4. 代码发布切换，并替换使用线上配置文件
    5. 移除单元测试初始化文件，以免误执行
<?xml version="1.0" encoding="UTF-8"?>

<!-- ============================================  -->
<!-- PhalApi                       -->
<!-- @dogstar 20141221                             -->
<!-- ============================================  -->

<project name="demo.phalapi.com" default="build">

    <property
        name="backup_path"
        value="/home/apps/backup/demo.phalapi.com"
        override="true" />
    <property
        name="backup_prefix"
        value="demo.phalapi.com_phing_backup_"
        override="true" />
    <property
        name="svn_todir"
        value="/home/apps/svn/demo.phalapi.com"
        override="true" />

    <!-- ============================================  -->
    <!-- Target: prepare                               -->
    <!-- ============================================  -->
    <target name="prepare">
        <mkdir dir="./Runtime" />
        <mkdir dir="${svn_todir}" />
        <mkdir dir="${backup_path}" />
    </target>

    <!-- ============================================  -->
    <!-- Target: svn checkout                          -->
    <!-- ============================================  -->
    <target name="svncheckout">
        <svncheckout
            repositoryurl="svn://127.0.0.1/PhalApi/demo/release"
            username="test"
            password="123456"
            nocache="true"
            todir="${svn_todir}" />
    </target>

    <!-- ============================================  -->
    <!-- Target: svn update                            -->
    <!-- ============================================  -->
    <target name="svnup" depends="svncheckout">
        <svnupdate
            repositoryurl="svn://127.0.0.1/PhalApi/demo/release"
            username="test"
            password="123456"
            nocache="true"
            todir="${svn_todir}" />
    </target>

    <!-- ============================================  -->
    <!-- Target: check config                          -->
    <!-- ============================================  -->
    <target name="checkconfig">
    <php expression="include('${svn_todir}/Tools/check_config.php')" />
        <php
            function="checkProd"
            class="Tools_Check_Config"
            returnProperty="checkProdErrorMsg" />

        <fail msg="${checkProdErrorMsg}" if="checkProdErrorMsg" />
    </target>

    <!-- ============================================  -->
    <!-- Target: backup                                -->
    <!-- ============================================  -->
    <target name="backup">
        <php
            expression="date('Ymd', strtotime('-1 day'))"
            returnProperty="backup_version_yesterday" />
        <php
            expression="date('Ymd')"
            returnProperty="backup_version_today" />
        <delete>
            <fileset dir="${backup_path}">
                <exclude name="${backup_prefix}${backup_version_yesterday}*" />
                <exclude name="${backup_prefix}${backup_version_today}*" />
            </fileset>
        </delete>

        <php
            expression="date('YmdHis')"
            returnProperty="backup_version" />
        <zip destfile="${backup_path}/${backup_prefix}${backup_version}.zip" basedir="." >
            <fileset dir=".">
                <include name="**/**" />

                <exclude name="./Runtime" />
                <exclude name="./Runtime/**" />
                <exclude name="./.svn" />
                <exclude name="./.svn/**" />
            </fileset>
        </zip>
        <copy
            file="${backup_path}/${backup_prefix}${backup_version}.zip"
            tofile="${backup_path}/${backup_prefix}lastest.zip"
            overwrite="true" />
    </target>

    <!-- ============================================  -->
    <!-- Target: build                                 -->
    <!-- ============================================  -->
    <target name="build" depends="prepare,svnup,checkconfig,backup">
        <copy todir="." overwrite="true" >
            <fileset dir="${svn_todir}">
                <include name="**/**" />

                <exclude name="${svn_todir}/Config/dbs.php" />
                <exclude name="${svn_todir}/Config/sys.php" />
                <exclude name="${svn_todir}/Test" />
                <exclude name="${svn_todir}/Test/**" />
            </fileset>
        </copy>

        <copy
            file="./Config/dbs.php.prod"
            tofile="./Config/dbs.php"
            overwrite="true" />
        <copy
            file="./Config/sys.php.prod"
            tofile="./Config/sys.php"
            overwrite="true" />

        <!-- 避免在生产环境执行PHPUnit，故把测试环境的初始文件移开 -->
        <move
            file="./Test/test_env.php"
            tofile="./Test/test_env.php.bak"
            overwrite="true"/>
    </target>

</project>

(2)回滚配置 - rollback.xml

主要操作：

    1. 回滚到上一个版本
<?xml version="1.0" encoding="UTF-8"?>

<!-- ============================================  -->
<!-- PhalApi                       -->
<!-- @dogstar 20141222 冬至                        -->
<!-- ============================================  -->

<project name="demo.phalapi.com" default="rollback">

    <property
        name="backup_path"
        value="/home/apps/backup/demo.phalapi.com"
        override="true" />
    <property
        name="backup_prefix"
        value="demo.phalapi.com_phing_backup_"
        override="true" />
    <property
        name="svn_todir"
        value="./__svn__"
        override="true" />

    <!-- ============================================  -->
    <!-- Target: rollback                              -->
    <!-- ============================================  -->
    <target name="rollback" >
        <unzip file="${backup_path}/${backup_prefix}lastest.zip" todir="." >
            <fileset dir=".">
                <include name="*.zip"/>
            </fileset>
        </unzip>
    </target>

</project>

2.22.3 autobench：接口压力测试与可视化图表

可以通过以下的脚本来进行，使用示例：

$ ./autobench.sh
Usage: ./autobench.sh <host> <uri>

 - ./autobench.sh www.baidu.com /index.php

参数可以自行调整。

(1)利用bench2graph生成可视化图表

纯PHP访问 - 入口欢迎接口

mysql访问 - 事件获取接口

带MC缓存的访问 - 应用入口 - 带缓存的身份token验证

(2)详细的数据报表

dem_req_rate    req_rate_demo.phalapi.com     con_rate_demo.phalapi.com     min_rep_rate_demo.phalapi.com avg_rep_rate_demo.phalapi.com    max_rep_rate_demo.phalapi.com stddev_rep_rate_demo.phalapi.com      resp_time_demo.phalapi.com       net_io_demo.phalapi.com       errors_demo.phalapi.com
5       5.0     5.0     4.8     5.0     5.2     0.1     22.4    2.9     0
25      25.0    25.0    25.0    25.0    25.0    0.0     21.6    14.3    0
45      45.0    45.0    45.0    45.0    45.0    0.0     22.3    25.7    0
65      64.9    64.9    64.7    64.7    64.7    0.0     25.4    37.0    0
85      84.8    84.8    84.6    84.6    84.6    0.0     28.6    48.3    0
105     104.6   104.6   0.0     0.0     0.0     0.0     34.4    59.7    0
125     124.0   124.0   0.0     0.0     0.0     0.0     42.2    70.7    0
145     143.8   143.8   0.0     0.0     0.0     0.0     59.2    82.0    0
165     147.4   147.4   0.0     0.0     0.0     0.0     262.9   84.1    0
185     151.1   151.1   0.0     0.0     0.0     0.0     429.7   86.2    0

(3)附脚本

#!/bin/bash

if [ $# -eq 0 ]; then
    echo "Usage: $0 <host> <uri>"
    echo ""
    echo " - $0 www.baidu.com /index.php"
    echo ""
    exit
fi

DM=$1
URL=$2

#--signle_host 只测单机
#--host1 测试主机地址
#--uri1 host1 测试URI
#--quiet 安静模式
#--low_rate 测试时最低请求数(指 httperf)
#--hight_rate 测试时最高请求数
#--rate_step 每次测试请求数增加步长
#--num-call 每连接中发起联接数，一般是1
#--num_conn 测试联接数
#--file 测试结果输出的 tsv文件

autobench \
--single_host \
--host1=$DM \
--port1=80 \
--uri1=$URL \
--low_rate=5 \
--high_rate=200 \
--rate_step=20 \
--num_call=1 \
--num_conn=500 \
--timeout=10 \
--file ./$DM.tsv

2.22.4 xhprof：性能分析工具

xhprof是一个不错的内部性能分析工具，这里不过多的讲述此工具的特点和使用，但会以对PhalApi进行的一个性能测试展示它的分析效果。

(1)测试的接口服务

http://api.phalapi.com /demo/?service=Default.Index&username=test

(2)Overall Summary

Total Incl. Wall Time (microsec):     7,873 microsecs
Total Incl. CPU (microsecs):    7,999 microsecs
Total Incl. MemUse (bytes):    304,456 bytes
Total Incl. PeakMemUse (bytes):    306,616 bytes
Number of Function Calls:        338

(3)Top 10耗时

Function Name | Calls | Calls% | Incl. Wall Time(microsec) | IWall%(microsec) | Excl. Wall Time | EWall%
PhalApi_Loader::loadClass | 10 | 3.00% | 3,020 | 38.40% | 1,038 | 13.20%
PhalApi_Loader::loadClass1 | 4 | 1.20% | 785 | 10.00% | 398 | 5.10%
run_init::Public/init.php | 1 | 0.30% | 3,915 | 49.70% | 327 | 4.20%
file_exists | 18 | 5.30% | 249 | 3.20% | 249 | 3.20%
main() | 1 | 0.30% | 7,873 | 100.00% | 248 | 3.20%
load::zh_cn/common.php | 2 | 0.60% | 237 | 3.00% | 237 | 3.00%
PhalApi_Loader::load | 9 | 2.70% | 3,380 | 42.90% | 226 | 2.90%
PhalApi_DI::get | 9 | 2.70% | 1,268 | 16.10% | 223 | 2.80%
PhalApi_Translator::addMessage | 2 | 0.60% | 522 | 6.60% | 136 | 1.70%
DI | 15 | 4.40% | 645 | 8.20% | 134 | 1.70%
class_exists | 11 | 3.30% | 1,383 | 17.60% | 130 | 1.70%
load::Config/sys.php | 1 | 0.30% | 120 | 1.50% | 120 | 1.50%

对应的图表如下：
a pic
从中可以看出，主要的耗时途径在于文件的加载，下面将进一步探讨。

(4)Top 1耗时深入

所加载的文件如下：
a pic

2.22.5 Jenkis和Sonar：持续集成和静态代码分析

PhalApi从来不会隐藏自己的设计，当然，我们也不会隐藏我们内部的各个细节以及存在的问题。
但和其他隐藏了技术债务的框架相比，PhalApi敢于展示自己的静态代码分析报告，并且它也是做得相当出色的。

以下截图来自 GIT@OSC 上的静态代码分析： a pic

当你的项目很重要时，也可以使用Jenkis或者Sonar进行静态代码的分析。
不要个人主观地觉得你的代码风格写得好，而是交由专业的分析工具进行解剖，并理解各个数据报表背后的含义，然后改进之。

2.22.6 Git/SVN：更多的版本控制

2.22.7 WIKI：团队沟通与文档交流

(1)markdown接口模板

#3.2 接口文档模板
##1、功能说明
_请在这里放置简短的接口功能说明。_

##2、接口URL
/?service= _接口服务名称_  + 公共参数(是否免登录态？)

##3、接口参数 [跳转](http://demo.phalapi.com/demo/checkApiParams.php?service=)

参数|必须|默认值|说明
---|---|---|---
user_id|1||用户id

##4、返回结果

###返回字段
参数|类型|说明
---|---|---
data.username|string|用户名

###结果示例
{
    "ret": 200,
    "data": {
       ....  //更多结果的说明

        "msg": ""
    },
    "msg": ""
}

###请求示例
_请放置一个接口请求的链接。_

http://demo.phalapi.com/demo/?service=

2.22.8 静态代码分析工具 - phpmetrics

以下是针对本框架核心代码所做的分析报告：

PS：从报告的评估可以看出，我们的框架明显具有相当 高的可维护性。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.1]-扩展类库：微信开发 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.1.1 扩展类库：微信开发

此扩展可用于微信的服务号、订阅号、企业号等功能开发，在PhalApi框架下简单配置即可开发使用。

更多请参考WechatRobot - PHP微信机器人开发包。

3.1.2 安装和配置

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 Wechat 微信包，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 Wechat 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/Wechat/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(2)扩展包配置

为了让微信扩展包能接收来自微信服务器的信息以及返回信息给用户，我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 插件注册 - 项目级重用
     */
  'Wechat' => array(
    'plugins' => array(
        Wechat_InMessage::MSG_TYPE_TEXT => array('Plugin_Menu',),
        Wechat_InMessage::MSG_TYPE_IMAGE => array(),
        Wechat_InMessage::MSG_TYPE_VOICE => array(),
        Wechat_InMessage::MSG_TYPE_VIDEO => array(),
        Wechat_InMessage::MSG_TYPE_LOCATION => array(),
        Wechat_InMessage::MSG_TYPE_LINK => array(),
        Wechat_InMessage::MSG_TYPE_EVENT => array(),
        Wechat_InMessage::MSG_TYPE_DEVICE_EVENT => array(),
        Wechat_InMessage::MSG_TYPE_DEVICE_TEXT => array(),
    ),
  )

简单说明一下上面配置的作用，很明显，Key为对应的消息类型，如：文本、位置、语音、图片等；然后是各种消息类型对应的处理的类名，可以有多个，从上到下依次处理。
如果觉得配置很多，可以只配置需要用到的消息类型。另外，如果你的配置是其他途径存放的，也亦然。

3.1.3 入门使用

(1)微信入口文件

可以按将下面的入口代码得到你的项目下：

//$ vim ./Public/weixin/index.php

<?php

// echo $_GET['echostr'];
// die();

if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
    die('Access denied!');
}

require_once dirname(__FILE__) . '/../init.php';

//装载项目代码和扩展类库
DI()->loader->addDirs(array('Demo', 'Library'));

/** ---------------- 微信轻聊版 ---------------- **/

$robot = new Wechat_Lite('YourTokenHere...', true);
$rs = $robot->response();
$rs->output();

特别地 ，当首次接入微信时，需要将开头的两句注释去掉，以便通过微信的验证，即：

// echo $_GET['echostr'];
// die();

3.1.4 示例：让红包飞

(1)业务场景

现在，快要过年了，各大企业都在派红包，这里，我们也模拟一下微信服务号上红包的派发。

通常地，当我们需要添加一个新的微信服务号的功能时，可以两步走： 先开发插件，再注册插件 。

(2)开发插件

很简单地：

// $vim ./Demo/Plugin/Money.php

<?php

class Plugin_Money implements Wechat_Plugin_Text {

    public function handleText($inMessage, &$outMessage) {
        $outMessage = new Wechat_OutMessage_News();

        $item = new Wechat_OutMessage_News_Item();
        $item->setTitle('让红包飞~')
            ->setDescription(sprintf('您已领取到一个%d元红包~', rand(1, 100)))
            ->setPicUrl('http://webtools.qiniudn.com/172906_61c8663a_121026.jpeg')
            ->setUrl('http://git.oschina.net/dogstar/PhalApi-Library');

        $outMessage->addItem($item);
    }
}

(3)注册插件

开发好插件后，需要在配置添加对哪种消息开启使用该新的插件。在 ./Config/app.php 配置文件中追加以下配置：

        // 前面配置略 ...

        // 在前面追加红包插件，处理顺序由左到右
        Wechat_InMessage::MSG_TYPE_TEXT => array('Plugin_Money', 'Plugin_Menu', ),

(4)运行效果

3.1.5 微信开发调试

为了便于进行微信开发的调试，我们专门提供了一个快速模拟微信请求的脚本。以下是对上面发红包的模拟请求：

$php ./Library/Wechat/Tests/send_wechat_text.php http://dev.wechat.com/weixin/ demo

<xml>
<ToUserName><![CDATA[oWNXvjipYqRViMpO8GZwXxE43pUY]]></ToUserName>
<FromUserName><![CDATA[gh_43235ff1360f]]></FromUserName>
<CreateTime>1423821388</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>1</ArticleCount>
<Articles>
<item>
<Title><![CDATA[让红包飞~]]></Title>
<Description><![CDATA[您已领取到一个85元红包~]]></Description>
<PicUrl><![CDATA[http://webtools.qiniudn.com/172906_61c8663a_121026.jpeg]]></PicUrl>
<Url><![CDATA[http://git.oschina.net/dogstar/PhalApi-Library]]></Url>
</item>
</Articles>
<FuncFlag>0</FuncFlag>
</xml>

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.2]-扩展类库：代理模式下phprpc协议的轻松支持 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.2.1 扩展类库：代理模式下phprpc协议的轻松支持

我们一直都建议在我们的项目中使用设计模式以便让代码更优雅，但我们更推荐只在合适的场景才使用合适的设计模式，而不是为了“显学”而生硬套用。设计模式又需要更高层的设计原则和工程思想作指导，才能更正确被使用发挥其作用。

在PhalApi，正如前面所说的，我们引入并应用了很多设计原则，一如单一职责原则SPR、开放-封闭原则OCP等。因此，在这里我们可以在phprpc的基础上，利用代理模式优雅地扩展实现phpcpr协议。

在需要使用phprpc协议进行接口调用的项目中，我们可以快速利用此扩展类库。并且你会发现，服务端的接口已有的代码不需要做任何改动，只需要增加此扩展包和添加一个新入口便可完美切换。特别在项目中途BOSS说我们要提高接口调用的安全性而改用phprpc协议时。

3.2.2 安装

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 PHPRPC，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 PHPRPC 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/PHPRPC/ ./PhalApi/Library/ -R

到此安装完毕！

3.2.3 入门使用

(1)phprpc协议入口

可以参考下面的入口代码编写：

//$ vim ./Public/phprpc/index.php

<?php

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()->loader->addDirs(array('Demo', 'Library'));

$server = new PHPRPC_Lite();
$server->response();

3.2.4 示例

这里以?service=User.GetBaseInfo为例进行说明。

(1)浏览器访问失败

在使用了phprpc协议后，我们再以浏览器（HTTP协议）访问调用接口时，会预期地出现以下失败信息：

(2)通过phprpc协议调用

如果通过phprpc协议调用，我们则可以得到接口返回的 源数据 。
假设请求的接口为：?service=User.GetBaseInfo&user_id=1，则可以得到我们熟悉的：

array (
  'ret' => 200,
  'data' =>
  array (
    'code' => 0,
    'msg' => '',
    'info' =>
    array (
      'id' => '1',
      'name' => 'dogstar',
      'note' => 'oschina',
    ),
  ),
  'msg' => '',
)

3.2.5 phprpc协议调试

同样，为了方便进行phprpc协议下接口调用的调试，我们提供了一个脚本，如上面的对?service=User.GetBaseInfo&user_id=1调用，使用脚本即为：

$ php ./Library/PHPRPC/check.php http://dev.phprpc.com/phprpc/?service=User.GetBaseInfo&userId=1
array(3) {
  ["ret"]=>
  int(200)
  ["data"]=>
  array(3) {
    ["code"]=>
    int(0)
    ["msg"]=>
    string(0) ""
    ["info"]=>
    array(3) {
      ["id"]=>
      string(1) "1"
      ["name"]=>
      string(7) "dogstar"
      ["note"]=>
      string(7) "oschina"
    }
  }
  ["msg"]=>
  string(0) ""
}

3.2.6 对客户端的调整

虽然服务端不需要作出太多的改动，但对于客户端来说，仍然需要进行三方面的调整以进行phprpc协议的调用以及参数的传递和返回结果的获取。

现分说如下。

(1)调用方式的改变

首当其冲的就是调用方式的改变，但值得开心的是，phprpc对很多语言都有支持。具体可以查看phprpc官网。

(2)POST参数传递方式的改变

其次对POST参数传递的改变。考虑到phprpc协议中对POST的数据有一定的复杂性，这里统一作了简化。 正如我们下面的代码所示：

    public function response($params = NULL) {
        $paramsArr = json_decode($params, TRUE);
        if ($paramsArr !== FALSE) {
            DI()->request = new PhalApi_Request(array_merge($_GET, $paramsArr));
        }

我们约定： 通过第一个参数用JSON格式来传递全部原来需要POST的数据 。

当POST的数据和GET的数据冲突时，以POST为准。

所以，相应地，当需要传递POST参数时，客户需要这样调整（如PHP下）：

$client->response(json_encode($params)))

如无此POST参数，则可以忽略不传。

(3)返回结果格式的改变

最后，就是返回结果格式的改变。

在phprpc协议下，因为可以更轻松地获取接口返回的源数据，所以这里也同样不再通过字符串流式的序列返回（如原来的JSON或XML），而是直接返回接口的 源数据 。如上面示例中所看到的结果一样。

这点，需要特别注意。

3.2.7 参考

phprpc官网：http://www.phprpc.org/zh_CN/

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.3]-扩展类库：基于PHPMailer的邮件发送 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.3.1 扩展类库：基于PHPMailer的邮件发送

此扩展可用于发送邮件。

3.3.2 安装和配置

从 PhalApi-Library 扩展库中下载获取 PHPMailer 包，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 PHPMailer 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/PHPMailer/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(1)邮箱配置

需要在 ./Config/app.php 配置文件中追加以下配置：

  'PHPMailer' => array(
    'email' => array(
        'host' => 'smtp.gmail.com',
        'username' => 'XXX@gmail.com',
        'password' => '******',
        'from' => 'XXX@gmail.com',
        'fromName' => 'PhalApi团队',
        'sign' => '<br/><br/>请不要回复此邮件，谢谢！<br/><br/>-- PhalApi团队敬上 ',
    ),
  ),

3.3.3 使用

如下代码示例：

require_once './init.php';

DI()->loader->addDirs('Library');

$mailer = new PHPMailer_Lite(true);
$mailer->send('chanzonghuang@gmail.com', 'Test PHPMailer Lite', 'something here ...');

稍候将会收到：

如果需要发送邮件给多个邮箱时，可以使用数组，如：

$addresses = array('chanzonghuang@gmail.com', 'test@phalapi.com');
$mailer->send($addresses, 'Test PHPMailer Lite', 'something here ...');

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.4]-扩展类库：优酷开放平台接口调用 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.4.1 扩展类库：优酷开放平台接口调用

此扩展可用于对优酷开放平台提供的接口进行快速调用，并将失败的情况以debug方式写入日记。

3.4.2 安装和配置

从 PhalApi-Library 扩展库中下载获取 YoukuClient 优酷接口客户端，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 YoukuClient 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/YoukuClient/ ./PhalApi/Library/ -R

到此安装完毕！

3.4.3 使用

可以先在初始化文件init.php中引入扩展类库，并注册一个youkuClient的服务：

//$ vim ./Public/init.php
$loader->addDirs('Library');

DI()->youkuClient = function() {
    return new YoukuClient_Lite('https://openapi.youku.com', 'b043a60fbef8aed0');
};

然后，就可以在项目中使用youkuClient这个服务来实现对优酷平台接口的调用。

通常，我们会在Model层包装这一层数据来源的获取。但这里只是了为演示的效果，全部将代码放到Api里面，如：

//$ vim ./Demo/Api/Youku.php

<?php

class Api_Youku extends PhalApi_Api {

    public function showBasic()
    {
        $uri = '/v2/videos/show_basic.json';
        $params = array(
            'video_url' => 'http://v.youku.com/v_show/id_XOTA4ODU4NjA0.html'
        );
        $timeoutMs = 3000;

        $info = DI()->youkuClient->get($uri, $params, $timeoutMs);

        return array('id' => $info['id'], 'title' => $info['title']);
    }
}

请求一下对应的接口，便可以获得以下的运行效果：

对应的优酷视频是： 【双语字幕】Apple Watch 动手玩 - The Verge

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.5]-扩展类库：七牛云存储接口调用 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.5.1 扩展类库：七牛云存储接口调用

此扩展可用于将图片上传到七牛云存储，或者七牛提供的其他功能。

3.5.2 安装和配置

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 Qiniu 七牛扩展包，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 Qiniu 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/Qiniu/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(2)扩展包配置

我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 七牛相关配置
     */
    'Qiniu' =>  array(
        //统一的key
        'accessKey' => '*****',
        'secretKey' => '****',
        //自定义配置的空间
        'space_bucket' => '自定义配置的空间',
        'space_host' => 'http://XXXXX.qiniudn.com',
    ),

你也可以不使用配置文件，直接在初始化注册DI()->qiniu时指定配置。

3.5.3 入门使用

(1)入口注册

//$ vim ./Public/init.php

// 其他代码....

DI()->qiniu = new Qiniu_Lite();

根据上面所说的，你可以也可以指定配置。

//$ vim ./Public/init.php

// 其他代码....

$qiniuConfig = array( /* 格式参照配置文件 *);
DI()->qiniu = new Qiniu_Lite($qiniuConfig);

3.5.4 示例：图片上传

(1)模拟客户端

先简单写个测试文件：

// vim ./Public/test_qiniu.html

<html>
    <form method="POST" action="/demo/?service=Qiniu_CDN.uploadFile" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit">
    </form>
</html>

效果如下（通常是提供给客户端上传的）：
p

(2)文件上传接口

随后，我们使用此七牛扩展提供的编写文件上传接口，通过查看在线接口文档，可以看到：

(3)运行一下：

缺少上传文件时：

{"ret":200,"data":{"code":1,"url":"","msg":"miss upload file"},"msg":""}

上传失败时：

{"ret":200,"data":{"code":2,"url":"","msg":"fail to upload file"},"msg":""}

可以看到对应的日记（如果不想看到此日记，可通过在初始化日记服务时去掉调试日记）：

2015-03-18 00:07:02|DEBUG|failed to upload file to qiniu|{"Err":"bad token","Reqid":"u1EAAG73DB3PVMwT","Details":"","Code":401}

上传成功时：

{"ret":200,"data":{"code":0,"url":"图片链接","msg":""},"msg":""}

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.6]-扩展类库：新型计划任务 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.6.1 扩展类库：新型计划任务

此扩展类型用于后台计划任务的调度，主要功能点有：

  • 1、提供了Redis/文件/数据库三种MQ队列
  • 2、提供了本地和远程两种调度方式
  • 3、以接口的形式实现计划任务
  • 4、提供统一的crontab调度

更多内容，请查看：[1.31] 新型计划任务：以接口形式实现的计划任务

3.6.2 安装

(1)内置扩展包

此安装略，已内置于PhalApi框架中。

温馨提示：
PhalApi需要PhaApi 1.2.0及以上版本。

(2)配置

我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 计划任务配置
     */
    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'file' => array(
                'path' => API_ROOT . '/Runtime',
                'prefix' => 'phalapi_task',
            ),
            'redis' => array(
                'host' => '127.0.0.1',
                'port' => 6379,
                'prefix' => 'phalapi_task',
                'auth' => '',
            ),
            'mc' => array(
                'host' => '127.0.0.1',
                'port' => 11211,
            ),
        ),

        //Runner设置，如果使用远程调度方式，请加此配置
        'runner' => array(
            'remote' => array(
                'host' => 'http://library.phalapi.net/demo/',
                'timeoutMS' => 3000,
            ),
        ),
    ),

以上内容看情况需要而配置。

(3)数据库表

当需要使用数据库MQ列队时，还需要将以下数据库的配置追加到 ./Config/dbs.php 中：

    'tables' => array(

    //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中，未配置的表将使用默认路由

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'task_mq' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 9, 'db' => 'db_demo'),
            ),
        ),
    )

同时，需要将 ./Data/phalapi_task_mq.sql 文件的SQL建表语句导入到你的数据库。你也可以将利用脚本来生成。

3.6.3 入门使用

(1)加入MQ队列

首先，我们需要在入口文件进行对Task的初始化：

DI()->loader->addDirs('Library');
$mq = new Task_MQ_Redis();  //可以选择你需要的MQ
DI()->taskLite = new Task_Lite($mq);

然后，这样即可添加一个新的计划任务到MQ：

DI()->taskLite->add('Task.DoSth', array('id' => 1));

(2)计划任务的启动

在启动计划任务前，我们需要编写简单的脚本，一如这样：

#!/usr/bin/env php
<?php
require_once '/path/to/Public/init.php';

DI()->loader->addDirs('Demo');

if ($argc < 2) {
    echo "Usage: $argv[0] <service> \n\n";
    exit(1);
}

$service = trim($argv[1]);

$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);
$rs = $runner->go($service);

echo "\nDone:\n", json_encode($rs), "\n\n";

然后使用nohup或者crontab启动即可。

(3)续篇 - 统一的crontab计划任务

上面第二点的启动是通用、自定义、也是自由的启动方式。

这里再提供一种具体的、统一的启动方式，即使用crontab的方式。

首先，创建以下表(或见./Library/Task/Data/phalapi_task_progress.sql文件)：

CREATE TABLE `phalapi_task_progress` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `title` varchar(200) DEFAULT '' COMMENT '任务标题',
      `trigger_class` varchar(50) DEFAULT '' COMMENT '触发器类名',
      `fire_params` varchar(255) DEFAULT '' COMMENT '需要传递的参数，格式自定',
      `interval_time` int(11) DEFAULT '0' COMMENT '执行间隔，单位：秒',
      `enable` tinyint(1) DEFAULT '1' COMMENT '是否启动，1启动，0禁止',
      `result` varchar(255) DEFAULT '' COMMENT '运行的结果，以json格式保存',
      `state` tinyint(1) DEFAULT '0' COMMENT '进程状态，0空闲，1运行中，-1异常退出',
      `last_fire_time` int(11) DEFAULT '0' COMMENT '上一次运行时间',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

接着，添加crontab计划任务：

$ crontab -e

*/1 * * * * /usr/bin/php /path/to/PhalApi/Library/Task/crontab.php >> /tmp/phalapi_task_crontab.log 2>&1

然后，实现你的计划任务服务接口：

<?php
class Api_Task extends PhalApi_Api {

      public function doSth() {
            // ...
      }
}

最后，配置计划任务：

INSERT INTO `phalapi_task_progress`(title, trigger_class, fire_params, interval_time)  VALUES('你的任务名字', 'Task_Progress_Trigger_Common', 'Task_Demo.DoSth&Task_MQ_File&Task_Runner_Local', '300');

注意，在配置时，需要指明MQ和Runner的类型。

3.6.4 更多说明

(1)依赖的资源服务

名称 | 类 | 没有时是否自动创建
curl | PhalApi_CUrl | 是
request | PhalApi_Request | 强制每次初始化，用于本地调度时
response | PhalApi_Response_Json | 强制每次初始化，用于本地调度时

(2)MQ

Redis MQ队列

Redis MQ队列需要的配置为：

    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'redis' => array(
                'host' => '127.0.0.1',
                'port' => 6379,
                'prefix' => 'phalapi_task',
                'auth' => '',
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
host | 否 | 127.0.0.1 | redis的HOST
port | 否 | 6379 | redis的端口
prefix | 否 | phalapi_task | key的前缀
auth | 否 | redis的验证，不为空时执行验证

可以这样创建Redis MQ队列：

//方法一：使用app.Task.mq.redis配置
$mq = new Task_MQ_Redis();

//方法二：外部依赖注入
$redisCache = new PhalApi_Cache_Redis(array('host' => '127.0.0.1'));
$mq = new Task_MQ_Redis($redisCache);

文件MQ队列（通常不能共享，队列大小不限制，有效期一年）

文件MQ需要的配置为：

    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'file' => array(
                'path' => API_ROOT . '/Runtime',
                'prefix' => 'phalapi_task',
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
path | 否 | API_ROOT/Runtime | 缓存的文件目录
prefix | 否 | phalapi_task | key的前缀

可以这样创建文件MQ队列：

//方法一：使用app.Task.mq.file配置
$mq = new Task_MQ_File();

//方法二：外部依赖注入
$fileCache = new PhalApi_Cache_File(array('path' => '/tmp/cache'));
$mq = new Task_MQ_File($fileCache);

Memcached/Memcache MQ队列（通常队列大小不能超过1M，有效期29天）

MC MQ需要的配置为：

    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'mc' => array(
                'host' => '127.0.0.1',
                'port' => 11211,
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
host | 否 | 127.0.0.1 | MC的host
port | 否 | 11211 | MC端口

可以这样创建文件MQ队列：

//方法一：使用app.Task.mq.mc配置
$mq = new Task_MQ_Memcached();

//方法二：外部依赖注入
$mc = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211));
$mq = new Task_MQ_File($mc);

数据库MQ队列

数据库MQ队列需要的配置为：

    'tables' => array(

        //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中，未配置的表将使用默认路由

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'task_mq' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 9, 'db' => 'db_demo'),
            ),
        ),
    )

可以这样创建数据库MQ队列：

$mq = new Task_MQ_DB();

数组MQ队列

数组MQ队列是将MQ存放在PHP的数组里面，用于单元测试或者是一次性、临时性的计划任务调度。

可以这样创建数据库MQ队列：

$mq = new Task_MQ_Array();

(3)本地和远程调度

本地调度的创建

$runner = new Task_Runner_Local($mq, 10);  //10表示每批次弹出10个进行处理

需要注意的是，每次执行一个计划任务，都会重新初始化必要的DI资源服务。
且此调度方式不能用于接口请求时的同步调用。

远程调度的创建

首先需要以下配置：

    /**
     * 计划任务配置
     */
    'Task' => array(
        //Runner设置，如果使用远程调度方式，请加此配置
        'runner' => array(
            'remote' => array(
                'host' => 'http://library.phalapi.net/demo/',
                'timeoutMS' => 3000,
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
host | 是 | 接口域名链接
timeoutMS | 否 | 3000 | 接口超时时间，单位毫秒

可以这样创建：

//使用默认的连接器 - HTTP + POST的方式
$runner = new Task_Runner_Remote($mq, 10); //10表示每批次弹出10个进行处理

//或者，指定连接器
$connector = new Task_Runner_Remote_Connector_Impl();
$runner = new Task_Runner_Remote($mq, 10, $connector); //10表示每批次弹出10个进行处理

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.8]-扩展类库：用户、会话和第三方登录集成 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

“你应该明白，我的朋友，
宇宙从来无所谓起始，
它也永远不会有结局，
因为邦迪，戈尔德，还有我，
直到我们头发掉精光，
对这一点都深信不疑！”

--《物理世界奇遇记》

3.8.1 扩展类库：用户、会话和第三方登录集成

首先，特别感谢@Aevit ，允许我将些扩展类库进行开源。原来此类库的功能只是当前开发项目中的功能，我现将其抽离成可配置使用的扩展类库，以供大家共享。

此类库主要特点有：

  • 1、可以和第三方登录集成，包括：微信登录、新浪登录、QQ登录
  • 2、为客户端提供了直接可以调用的登录接口
  • 3、为服务端提供了直接可以检测用户登录态的操作
  • 4、支持token落地、高效缓存和分布式的数据库存储
  • 5、展示了如何开发一个项目级的类库、包括数据库配置、翻译等

温馨提示：
此扩展类库还在开发完善中，但已有项目在使用，感兴趣的同学可以尝试使用。

3.8.2 安装

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 User 用户包，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 User 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/User ./PhalApi/Library/ -R

(2)数据库表导入

默认下，User类库会使用默认的数据库配置，但有追加一些需要用到的表。因此需要导入以下表：

$ cd /Library/User
$ tree

├── Data
│   ├── user_login_qq.sql
│   ├── user_login_sina.sql
│   ├── user_login_weixin.sql
│   ├── user_session.sql
│   └── user.sql

温馨提示：
导入前，可以自行调整表的前缀。

(3)数据库配置

将 PhalApi-Library/User/Config/dbs.php 里面的配置拷贝到你的项目配置：

    //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'user_session' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 9, 'db' => 'db_demo'),
            ),
        ),

(4)入口注册

//必须显式注册，以便可以让服务自行初始化
DI()->userLite = new User_Lite();

(5)其他可选配置

  • 为了体现高效缓存的好处，建议先注册DI()->cache
  • 为了接口更明朗的参数规则说明，建议将下而规则追加到配置文件./Config/app.php中：
return array(

    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' => array(
        //其他原来的参数配置
        ...

        //登录信息
        'userId' => array(
            'name' => 'user_id', 'type' => 'int', 'default' => 0, 'require' => false,
        ),
        'token' => array(
            'name' => 'token', 'type' => 'string', 'default' => '', 'require' => false,
        ),
    ),

(6)checkApiParams.php的初始化

同样，需要在checkApiParams.php文件显式注册，才能让User这个扩展类库完成自我的必要初始化：

//装载你的接口
DI()->loader->addDirs(array('Demo', 'Library'));
DI()->userLite = new User_Lite();

否则会导致service不存在。

3.8.3 入门使用

(1)对于客户端

(1-1)微信登录

1、功能说明

实现通过微信登录，并且首次登录时，自动创建用户并绑定。

2、接口URL

/?service=User_User_Login.weixin + 公共参数(免登录态)

3、接口参数
参数 | 必须 | 默认值 | 说明
wx_openid | 1 | 微信OPENID
wx_token | 1 | 微信TOKEN
wx_expires_in | 1 | 微信失效时间
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
token | string | 用户TOKEN
is_new | int | 是否为首次登录，0不是，1是
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Login.weixin&wx_openid=wx_122348561111&wx_token=ASDF&wx_expires_in=130000000&name=weixinName&avatar=phpunit.png

(1-2)QQ登录

1、功能说明

实现通过QQ登录，并且首次登录时，自动创建用户并绑定。

2、接口URL

/?service=User_User_Login.qq + 公共参数(免登录态)

3、接口参数
参数 | 必须 | 默认值 | 说明
qq_openid | 1 | QQ的OPENID
qq_token | 1 | QQ的TOKEN
qq_expires_in | 1 | QQ的失效时间
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
token | string | 用户TOKEN
is_new | int | 是否为首次登录，0不是，1是
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Login.qq&qq_openid=xxxx&qq_token=ASDF&qq_expires_in=130000000&name=sinaName&avatar=http://dev.phalapi.com/no_avatar.png

(1-3)新浪微博登录

1、功能说明

实现通过新浪微博登录，并且首次登录时，自动创建用户并绑定。

2、接口URL

/?service=User_User_Login.sina

3、接口参数
参数 | 必须 | 默认值 | 说明
sina_openid | 1 | 新浪微博OPENID
sina_token | 1 | 新浪微博TOKEN
sina_expires_in | 1 | 新浪微博失效时间
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
token | string | 用户TOKEN
is_new | int | 是否为首次登录，0不是，1是
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Login.weixin&wx_openid=wx_122348561111&wx_token=ASDF&wx_expires_in=130000000&name=weixinName&avatar=phpunit.png

(1-4)用户信息

1、功能说明

获取用户个人信息

2、接口URL

/?service=User_User_Info.getUserInfo + 公共参数

3、接口参数
参数 | 必须 | 默认值 | 说明
other_user_id | 1 | 用户id
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
username | string | 用户名
nickname | string | 用户昵称
avatar | string | 用户头像url
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,
        "info": {
            "username": "aevit",
            "nickname": "test",
            "avatar": "",
            "user_id": 1
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Info.getUserInfo&other_user_id=1

(2)对于服务端

(1)温柔式检测登录态

$isLogin = DI()->userLite->check();

(2)强制式检测登录态

DI()->userLite->check(true);

(3)更多操作

当需要对用户进行退出登录时，可以在接口中执行：

DI()->userLite->logout();

当需要为用户新建一个会话时，可以在接口中执行：

DI()->userLite->generateSession(1); //假设用户ID为1

当需要为用户延长登录态有效时间时（默认为一个月），可以在接口进行心跳操作，从而再延长一个月有效时间：

DI()->userLite->heartbeat();

3.8.4 示例

(1)假设在我们的应用的接口中：

<?php

class Api_Test extends PhalApi_Api {

    public function go() {
        Di()->userLite->check(true);

        return 'ok';
    }
}

(2)先调用微信的登录：

/user/?service=User_User_Login.Weixin&wx_openid=wx_122348561111&wx_token=ASDF&wx_expires_in=130000000&name=weixinName&avatar=phpunit.png

返回：

{
    "ret": 200,
    "data": {
        "code": 0,
        "info": {
            "user_id": 94,
            "token": "8509BE9C80B67009C559E6A2C4B88E02C885DA6B9FEFFC2DA080999377D08FBF",
            "is_new": 0
        },
        "msg": ""
    },
    "msg": ""
}

(3)再调用我们应用 的接口：

/user/?service=Test.go&user_id=94&token=CE1218493B3A441FCF630E24E98FD3640133EBA6D734C90A620A11C07CE35323

返回：

{"ret":200,"data":"ok","msg":""}

(4)未登录下，返回：

{"ret":401,"data":[],"msg":"\u975e\u6cd5\u8bf7\u6c42\uff1auser not login"}

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.9]-扩展类库：swoole支持下的长链接和异步任务实现 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

遵守规则可以让你远离选择烦恼，无论是在坐车还是参加鸡尾酒会的时候。 --《选择的悖论》

写在前面的话

此PhalApi扩展类库只是初步开发完成，建议有swoole扩展经验或非保守派的同学使用，也欢迎你来丰富完善此扩展类库。

3.9.1 扩展类库：swoole支持下的长链接和异步任务实现

swoole官网请见： Swoole: PHP的异步、并行、分布式扩展

在这里，首先需要非常感谢swoole。

因为Swoole给我们提供了很多解决以往因PHP本身限制而产生的难题的灵感，但与此同时，也给我们带来了从没遇到过的挑战，特别是并发和长时间运行。

这需要我们更为小心地进行编码，因为以往可以忽略的小问题，在新的解决方案背景下都可能成为一个大问题。此扩展类库更多是作为一种尝试，并且可以作为扩展类库重要的一个转换点。因为在今后的日子里， PhalApi将会争取与其他开源项目一起，提供企业级的解决方案 。

目前，此扩展类库提供了：

  • 长链接的接口调用
  • 异步计划任务的调用

3.9.2 安装

(1)安装swoole扩展类库 安装过程可以参考swoole官网，这里稍以linux系统简单说明一下编译安装。

unzip ./swoole-src-swoole-1.7.16-beta.zip
cd swoole-src-swoole-1.7.16-beta
phpize
./configure
make && make install

安装好后，添加swoole扩展：

extension=swoole.so

重启PHP后，如果看到有swoole扩展，则说明安装成功：

#php -m | grep swoole
swoole

(2)扩展包下载

从 PhalApi-Library 扩展库中下载获取 Swoole 包，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 Swoole 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/Swoole/ ./PhalApi/Library/ -R

到此安装完毕！

温馨提示：
此扩展类库需要PhaApi 1.1.4及以上版本。

(3)配置

将以下配置追加到./Config/app.php：

    /**
     * Swoole扩展类库
     */
    'Swoole' => array(
        //服务
        'server' => array(
            'ip' => '127.0.0.1',
            'port' => 9501,
            'worker_num' => 1,
        ),
        //计划任务
        'task' => array(
            'ip' => '127.0.0.1',
            'port' => 9502,
            'worker_num' => 1,
        ),
    ),

3.9.3 入门使用

(1)长链接入口

在使用长链接入口对外提供接口服务后，由于不再是HTTP协议，所以入口建议放置在新的目录./Server，而不再是./Public。
入口文件的编写，如同我们以往一样，很简单：

//$ vim ./Server/run_server.php
<?php

require_once dirname(__FILE__) . '/../Public/init.php';

DI()->loader->addDirs(array('Library', 'Demo'));

$swooleLite = new Swoole_Lite();
$swooleLite->runServer();

启动、重启和关闭服务

启动可以用：

php ./Server/run_server.php

关闭可以用：

ps -ef | grep run_server | grep -v grep | awk '{print $2}'|xargs kill -9

(2)异步计划任务

异步计划任务是新型的做法，即：也通过接口服务调用的方式来完成计划任务的调度，其启动文件如同长链接入口一样简单：

//$ vim ./Server/run_task.php
<?php

require_once dirname(__FILE__) . '/../Public/init.php';

DI()->loader->addDirs(array('Library', 'Demo'));

$swooleLite = new Swoole_Lite();
$swooleLite->runTask();

启动、重启和关闭服务

启动可以用：

nohup php ./Server/run_task.php > ./Server/run_task.log 2>&1 &

(3)客户端调用

在扩展类库里有一个测试的脚本，可以用来进行PHP客户端的请求。

(1)默认接口调用

$ php ./check.php 127.0.0.1 9501 Default.Index username=swoole
Send: {"service":"Default.Index","username":"swoole"}
Received: {"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430620911},"msg":""}
Connection close

(2)带数据库的调用

$ php ./check.php 127.0.0.1 9501 User.getBaseInfo userId=1
Send: {"service":"User.getBaseInfo","userId":"1"}
Received: {"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","username":"aevit","nickname":"test","password":"DE4CA99150F44B26F0D320DCA6E4B7629C43B6","salt":"wefewfew","reg_time":"0","avatar":"http:\/\/image.famillelab.com\/no_avatar.png","UUID":""}},"msg":""}
Connection close

(3)异步计划任务的调度

$ php ./check.php 127.0.0.1 9502 Default.Index username=swoole
Send: {"service":"Default.Index","username":"swoole"}
Connection close

假设Default.Index为一个计划任务的接口，但目前发现一个问题是，首次请求异步计划任务不会主动结束，而需要工作强制ctrl + c结束，再请求，则正常。

对应的log，可以看到：

2015-05-03 12:24:57|DEBUG|asynctask(0) dispath in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(0) start in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(0) finish in swoole|{"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430627097},"msg":""}
2015-05-03 12:24:57|DEBUG|asynctask(1) dispath in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(1) start in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(1) finish in swoole|{"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430627097},"msg":""}
...

3.9.4 对客户端调整

(1)调用方式的改变

改用长链接。

(2)POST参数传递方式的改变

统一使用json发送数据包。

3.9.5 对服务端的影响

(1)DI资源服务的调整

变量名称 | 是否全局通用 | 是否每次请求新建 | 可否使用 | 备注
loader | 是 | 否 | 可使用
config | 是 | 否 | 可使用
logger | 是 | 否 | 可使用
notorm | 是 | 否 | 可使用 | 每次响应后，关闭此次全部数据库链接
cache | 是 | 否 | 可使用 | 需要查看使用的链接是否支持长链接
filter | 是 | 否 | 可使用
crypt | 是 | 否 | 可使用
curl | 是 | 否 | 可使用
request | 否 | 是 | 可使用
response | 否 | 是 | 可使用
cookie | --- | --- | 不可使用 | 长链接下不应进行COOKIE的操作

(2)关于swoole扩展类库的自问自答

  • 1、数据库使用长链接吗？ -- 不使用，每次响应后手动关闭数据链接
  • 2、内存问题？-- 通过类成员方法的作用域控制内存
  • 3、并发的问题？-- TODO
  • 4、代码更新后如何同步？需要重启服务器吗？ -- 需要重启
  • 5、PhalApi_Response::formatResult()的访问权限，是框架的调整，还是个别自我提高？ -- 扩展自我提高
  • 6、注册错误回调函数？ -- 来自swoole的建议，已注册
  • 7、启动和关闭、重启脚本文件？ -- 提供参考命令
  • 8、一直运行？ -- TODO

    (3)特别注意！

    由于服务在启动后，已经完成了大部分的类加载、配置读取以及PHP文件的解析，所以在对项目代码（包括配置）修改后，需要重启服务，方能生效。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[3.11]-扩展类库：基于FastRoute的快速路由 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.11.1 扩展类库：基于FastRoute的快速路由

此扩展基于 FastRoute 实现，需要 PHP 5.4.0 及以上版本，可以通过配置实现自定义路由配置，从而轻松映射到PhalApi中的service接口服务。

3.11.2 安装和配置

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 FastRoute 扩展包，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 FastRoute 目录复制到 ./PhalApi/Library/ 下，即：

$ cp ./PhalApi-Library/FastRoute/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(2)扩展包配置

我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 扩展类库 - 快速路由配置
     */
    'FastRoute' => array(
         /**
          * 格式：array($method, $routePattern, $handler)
          *
          * @param string/array $method 允许的HTTP请求方式，可以为：GET/POST/HEAD/DELETE 等
          * @param string $routePattern 路由的正则表达式
          * @param string $handler 对应PhalApi中接口服务名称，即：?service=$handler
          */
        'routes' => array(
            array('GET', '/user/get_base_info/{user_id:\d+}', 'User.GetBaseInfo'),
            array('GET', '/user/get_multi_base_info/{user_ids:[0-9,]+}', 'User.GetMultiBaseInfo'),
        ),
    ),

(3)nginx的协助配置（省略index.php）

如果是使用nginx的情况下，需要添加以下配置：

    # 最终交由index.php文件处理
    location / {
        try_files $uri $uri/ $uri/index.php;
    }

    # 匹配未找到的文件路径
    if (!-e $request_filename) {
        rewrite ^/(.*)$ /index.php/$1 last;
    }

然后重启nginx。

3.11.3 入门使用

(1)入口注册

//$ vim ./Public/index.php

$loader->addDirs('Library');

// 其他代码....

//显式初始化，并调用分发
DI()->fastRoute = new FastRoute_Lite();
DI()->fastRoute->dispatch();

/** ---------------- 响应接口请求 ---------------- **/

$api = new PhalApi();
$rs = $api->response();
$rs->output();

3.11.3 调用效果及扩展

(1)通过新的路由正常访问

在完成上面的配置后，我们就可以这样进行页面访问测试：

  http://library.phalapi.com/user/get_base_info/1
  等效于：http://library.phalapi.com/?service=User.GetBaseInfo&user_id=1

  http://library.phalapi.com/user/get_multi_base_info/1,2
  等效于：http://library.phalapi.com/?service=User.GetMultiBaseInfo&user_ids=1,2

(2)非法访问

当请求的HTTP方法与配置的不符合时，就会返回405错误，如我们配置了：

array('POST', '/user/{id:\d+}/{name}', 'handler2'),

但是通过GET方式来访问，即：

http://library.phalapi.com/user/123/name

则会返回：

{
    "ret": 405,
    "data": [],
    "msg": "快速路由的HTTP请求方法错误，应该为：POST"
}

(3)路由配置错误

当在./Config/app.php的文件里配置错误的路由时，会直接抛出FastRoute\BadRouteException异常，以及时提示开发人员修正。

(4)异常错误处理器

我们也可以实现FastRoute_Handler接口来自定义我们自己的错误异常处理回调函数。如：

class FastRoute_Handler_App implements FastRoute_Handler {

    public function excute(PhalApi_Response $response) {
        // ... ...
    }
}

然后，在分发时指定handler：

DI()->fastRoute->dispatch(new FastRoute_Handler_App());

3.11.4 更多路由配置说明

请访问 FastRoute ，查看其官方说明。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[4.2]-开发实战2：模拟优酷开放平台接口项目开发 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

4.2.1 模拟的业务场景

在这个开发实战中，我们将模拟实现优酷的开放平台接口，即： http://open.youku.com/docs

但这里不涉及具体的内部开发（我们也确实不得而知），而是只从外部的角度，通过PhalApi框架搭建类似的平台接口架构。

4.2.2 优酷URL分析与路由Rewrite

以下是优酷平台的部分接口URL：

#单条视频基本信息(videos/show_basic)
https://openapi.youku.com/v2/videos/show_basic.json

#我的详细信息(users/myinfo)
https://openapi.youku.com/v2/users/myinfo.json

#评论创建(comments/create)
https://openapi.youku.com/v2/comments/create.json

#搜索节目通过关键词(searches/show/by_keyword)
https://openapi.youku.com/v2/searches/show/by_keyword.json

从上面的接口URL，我们可以明显发现一些规律。即：

URL =  接口域名 + 版本 + 相对路径.json

与PhalApi接口规则的冲突

但在PhalApi框架中，我们是通过&service=XXX参数来指定需要的服务的。为此，我们需要在服务端配置一些Rewrite规则以支持这些URL。 简单地，我们可以这样在nginx配置：

    if ( !-f $request_filename )
    {
        rewrite ^/v2/(.*)/(.*).json /v2/?service=$1.$2;
    }

并为了更兼容PhalApi的风格，我们在入口文件将接收到的service首字母强制为大写，即：

//$ vim ./Public/v2/index.php

if (isset($_REQUEST['service'])) {
    $_REQUEST['service'] = ucwords($_REQUEST['service']);
}

重启一下nginx后，我们可以有浏览器，试着访问：

https://openapi.youku.com/v2/videos/show_basic.json

我们将会看到：

{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：接口服务Videos.show_basic不存在"
}

即表明Rewrite规则已生效，并能正常工作了，哈哈！

以下是更完整的nginx配置：

server {
    root /path/to/openapi.youku.com/Public;
    index index.html index.htm index.php;

    error_log /var/log/nginx/.error_log;
    access_log /var/log/nginx/openapi.youku.com.access_log;

    server_name openapi.youku.com;

    location / {
        try_files $uri $uri/ /index.html;
    }

    if ( !-f $request_filename )
    {
        rewrite ^/v2/(.*)/(.*).json /v2/?service=$1.$2;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        fastcgi_pass 127.0.0.1:9000;
        include fastcgi_params;
    }

}

4.2.3 项目的主要目录结构

首先，我们建立了一个Youku的项目目录，并按不同的版本划分了不同的模块，如：

$ tree ./Youku/
./Youku/
└── V2
    ├── Api
    │   └── Default.php
    ├── Domain
    ├── Model
    └── Tests
        ├── Api
        ├── Domain
        ├── Model
        ├── phpunit_user_getbaseinfo.xml
        ├── phpunit.xml
        └── test_env.php

8 directories, 4 files

然后，为不同的版本，提供不同的入口，如：

$ tree ./Public/
./Public/
├── checkApiParams.php
├── index.php
├── init.php
└── v2
    ├── checkApiParams.php
    ├── index.php
    └── listAllApis.php

1 directory, 6 files

4.2.4 简单的模拟实现

现在，到了接口具体开发的环节，我们将模拟开发 单条视频基本信息(videos/show_basic)

首先，我们可以定义接口：

//$ vim ./Youku/V2/Api/Videos.php

<?php

class Api_Videos extends PhalApi_Api {

    public function getRules() {
        return array(
            'show_basic' => array(
                'clientId' => array('name' => 'client_id', 'require' => true, 'desc' => '应用Key'),
                'videoId' => array('name' => 'video_id', 'desc' => '视频ID'),
                'videoUrl' => array('name' => 'video_url', 'desc' => '视频播放页URL'),
            ),
        );
    }

    /**
     * 单条视频基本信息(videos/show_basic)
     *
     * @return string id 视频唯一ID
     * @return string title 视频标题
     * @return string link 视频播放链接
     * @return string thumbnail 视频截图
     * @return int duration 视频时长，单位：秒
     * @return ... ...
     */
    public function show_basic() {

    }
}

然后，使用浏览器在线访问接口文档，访问：

http://openapi.youku.com/v2/checkApiParams.php?service=Videos.show_basic

可以看到：
a pic
可以看出，这与优酷平台上的接口文档是非常相近的。

最后，我们可以简单模拟实现：

    public function show_basic() {
        $rs = '{
            "id" : "XMjg1MTcyNDQ0",
                "title" : "泡芙小姐的灯泡 11",
                "link" : "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
                "thumbnail" : "http://g4.ykimg.com/0100641F464E1FC...",
                "duration" : "910",
                "category" : "原创",
                "state" : "normal",
                "published" : "2011-07-15 09:00:42",
                "description" : "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
                "player" : "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
                "public_type" : "all",
                "copyright_type" : "original",
                "user" :
                {
                    "id" : 58921428,
                    "name" : "泡芙小姐",
                    "link" : "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
                },
                "operation_limit": ["COMMENT_DISABLED"],
                "streamtypes" : ["flv","flvhd","hd"]
        }';

        return json_decode($rs, true);
    }

4.2.5 接口调用效果

虽然是模拟返回（其实是直接强制返回优酷开放平台上的示例数据），但我们还是可以来看下在模拟实现后的接口调用效果。

首先，是缺少client_id时的非法请求：

#请求
http://openapi.youku.com/v2/videos/show_basic.json

#返回
{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：缺少必要参数client_id"
}

然后，尝试一个合法的请求：

#请求
http://openapi.youku.com/v2/videos/show_basic.json?client_id=test

#返回
{
    "ret": 200,
    "data": {
        "id": "XMjg1MTcyNDQ0",
        "title": "泡芙小姐的灯泡 11",
        "link": "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
        "thumbnail": "http://g4.ykimg.com/0100641F464E1FC...",
        "duration": "910",
        "category": "原创",
        "state": "normal",
        "published": "2011-07-15 09:00:42",
        "description": "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
        "player": "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
        "public_type": "all",
        "copyright_type": "original",
        "user": {
            "id": 58921428,
            "name": "泡芙小姐",
            "link": "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
        },
        "operation_limit": [
            "COMMENT_DISABLED"
        ],
        "streamtypes": [
            "flv",
            "flvhd",
            "hd"
        ]
    },
    "msg": ""
}

很好，目前运行效果相当流畅。
虽然如此，但我们明显看到了问题所在。

4.2.6 返回格式的自行调整

在上一节中，我们很明显看到了返回格式与优酷现有的不一样，因为PhalApi框架多了一层。
其实，这些调整对于不同的项目来说，都是非常简单。

当项目需要返回的格式有定制化需求时，可以先自实现response服务，再注册。
在此场景，即：

先自定义response服务

我们先创建一个公共的目录./Youku/Common，再创建项目需要的特定响应类：

//$ vim ./Youku/Common/Response.php

<?php

class Common_Response extends PhalApi_Response_Json {

    public function getResult() {
        return $this->data;
    }
}

注册response服务

接着，我们对response进行注册：

//$ vim ./Public/v2/index.php

//装载你的接口
DI()->loader->addDirs(array('Youku', 'Youku/V2'));

DI()->response = 'Common_Response';

这里需要稍微注意一下，我们要在装载Youku目录后，才能注册DI()->response。

再次返回

回到刚才那个请求链接，我们可以发现，当再次调用时，会返回：

{
    "id": "XMjg1MTcyNDQ0",
    "title": "泡芙小姐的灯泡 11",
    "link": "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
    "thumbnail": "http://g4.ykimg.com/0100641F464E1FC...",
    "duration": "910",
    "category": "原创",
    "state": "normal",
    "published": "2011-07-15 09:00:42",
    "description": "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
    "player": "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
    "public_type": "all",
    "copyright_type": "original",
    "user": {
        "id": 58921428,
        "name": "泡芙小姐",
        "link": "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
    },
    "operation_limit": [
        "COMMENT_DISABLED"
    ],
    "streamtypes": [
        "flv",
        "flvhd",
        "hd"
    ]
}

4.2.7 签名验证

以上我们调整了返回格式，这使得我们的项目开发，越来越达到优酷开放平台接口的标准了（当然，是假设）。

但有一点，我们是不能忽视的，在很多项目中同样是不能忽视的。那就是：接口的签名验证。

我们可以先来看下优酷开放平台是怎么处理接口签名的。

简单地，优酷会为每一个接入方提供一个client_id，然后在每次接口请求时，通过都需要传递此参数。

为此，我们针对这个client_id编写一个简单的客户端验证服务。如：

//$ vim ./Youku/Common/ClientCheck.php

<?php

class Common_ClientCheck implements PhalApi_Filter {

    public function check() {
        $clientId = DI()->request->get('client_id');

        $allCliendIds = array(
            'phalapi',
            'oschina'
        );

        if (!in_array($clientId, $allCliendIds)) {
            throw new PhalApi_Exception_BadRequest('illegal client id');
        }
    }
}

然后，在入口处注册一下：

//$ vim ./Public/v2/index.php

DI()->filter = 'Common_ClientCheck';

当我们，再次打开刚才那个链接：

http://openapi.youku.com/v2/videos/show_basic.json?client_id=test

我们会看到空的返回：

[]

这说明，对客户端的非法请求已拦截成功，但这样用户体验明显不好，因为没有任何的错误提示输出。

为此，我们需要回到刚才自定义的响应类，修改一下：

//$ vim ./Youku/Common/Response.php

<?php

class Common_Response extends PhalApi_Response_Json {

    public function getResult() {
        if ($this->ret != 200) {
            return array(
                'error' => array(
                    'code' => $this->ret,
                    'type' => 'SystemException',
                    'description' => $this->msg,
                ),
            );
        }

        return $this->data;
    }
}

再刷新一下，可以看到和优酷平台接口近似的返回了！

{
    "error": {
        "code": ,
        "type": "SystemException",
        "description": "非法请求：illegal client id"
    }
}

如需要能返回数据，我们只需要传递正确的client_id（目前是固定的两个）即可：

http://openapi.youku.com/v2/videos/show_basic.json?client_id=phalapi

4.2.8 尾声

当然，此次的优酷接口模拟开发，我们都只是很简单地表面说明。

这样的目的，不是为了让大家真的去了解优酷接口的内部实现，而是向大家展示，通过PhalApi框架，我们可以更灵活地实现各种业务需求和非功能性的需求。

希望对大家有帮助，夜已深，安。

源代码请访问：

https://git.oschina.net/dogstar/PhalApi-Demo-Youku.git

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[4.3]-开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动） | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

Give it some time, everything will be okay. -- 《那些年我们疯狂的青春/青春洋溢色彩/This Youth is Crazy》

4.3.1 模拟的业务场景

假设我们需要为“奔跑吧兄弟”综艺节目开发一套手机App的投票接口，以供用户在观看电视的同时，可以进行投票活动参与互动。

下面将以此模拟的业务场景，提供一个接口开发实战的过程。

4.3.2 源代码下载

PhalApi-Demo-Vote

4.3.3 接口总列表

以下是我们根据业务需求整理出来的接口：

  • 1、用户可以通过微信、QQ、新浪微博等渠道进行第三方登录
  • 2、用户可以创建团队进行参赛，但队名不能重复
  • 3、用户可以对已参赛的团队进行投票，且每个用户每天投票最多不能超过3次，支持可配置
  • 4、获取已参赛团队的得票排行榜

4.3.4 主要涉及技术功能点

  • 1、使用User扩展类库实现第三方登录操作
  • 2、使用缓存存放用户每天投票的次数（为方便起见，使用文件缓存，不落地）
  • 3、对接口进行签名验证（为方便起见，固定sign签名）
  • 4、数据库的基本操作
  • 5、自动化脚本的使用

4.3.4 快速开发流程

（题外音：整个示例的开发，我个人在单元测试驱动开发下，只用了两个多小时，其中还包括对模板场景的业务构思、建表、编写单元测试代码等）。

(1)创建项目和部署环境

把PhalApi最新的框架代码下载后，并将User扩展类库按文档说明配置后，将项目部署到了以下接口域名：

http://api.vote.phalapi.com

测试一下：

http://api.vote.phalapi.com/vote/?sign=phalapi

//返回
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.4",
        "time": 1431796924
    },
    "msg": ""
}

Good! 下面是简明的开发过程。

(2)单元测试驱动开发

在定好接口后：

<?php
class Api_Act extends PhalApi_Api {

    public function joinIn() {
    }

    public function showList() {
    }

    public function vote() {
    }
}

便可使用脚本，快速生成单元测试的骨架代码：

$ cd ./Vote/Tests/Api
$ phalapi_buildtests ../../Api/Act.php Api_Act ../test_env.php  > Api_Act_Test.php

(3)快速开发

开发过程此处略，但在单元测试驱动的引导下，很快就产出了以下高质量的代码：

.
├── Api
│   ├── Act.php
│   └── Default.php
├── Common
│   └── SignFilter.php
├── Domain
│   ├── Team.php
│   └── Vote.php
├── Model
│   ├── Team.php
│   ├── UserVoteRecord.php
│   └── Vote.php
└── Tests
    ├── Api
    │   ├── Api_Act_Test.php
    │   └── Api_Default_Test.php
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php

(4)单元测试全部通过了！

为了方便大家查看，已省略了部分的调试内容，但保留了测试过程中全部执行的SQL语句，如下：

$ phpunit ./Api_Act_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

[1 - 0.06911s]DELETE FROM phalapi_team WHERE (team_name = 'test team name');<br>
[2 - 0.06487s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[3 - 0.06553s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');<br>
[4 - 0.0653s]INSERT INTO phalapi_team (team_name) VALUES ('test team name');<br>
[5 - 0.0699s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[6 - 0.06555s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');<br>
[9 - 0.06778s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[10 - 0.06603s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[11 - 0.06825s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[12 - 0.07374s]UPDATE phalapi_vote SET team_id = 3, vote_num = 22 WHERE (team_id = 3);<br>
[13 - 0.07012s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[14 - 0.06682s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[15 - 0.07433s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[16 - 0.07283s]UPDATE phalapi_vote SET team_id = 3, vote_num = 23 WHERE (team_id = 3);<br>
[17 - 0.07307s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[18 - 0.07501s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[19 - 0.07135s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[20 - 0.07653s]UPDATE phalapi_vote SET team_id = 3, vote_num = 24 WHERE (team_id = 3);<br>
[21 - 0.07215s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[22 - 0.06722s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[23 - 0.06506s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[24 - 0.06732s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 404);<br>

Time: 1.97 seconds, Memory: 7.00Mb

OK (4 tests, 42 assertions)

(5)运行效果 - Part 1

在我们通过第三方登录后，我们就可以这样进行接口操作了。

首先，让我们添加两个参赛团队：

//奔跑吧兄弟（蓝队）
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E8%93%9D%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "team_id": "5"
    },
    "msg": ""
}

//奔跑吧兄弟（红队）
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "team_id": "6"
    },
    "msg": ""
}

然后，让我们进行疯狂地投票：

//第一次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":1},"msg":""}

//第二次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":2},"msg":""}

//第三次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":3},"msg":""}

//第四次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回 - 注意此返回！
{"ret":200,"data":{"code":2,"vote_num":0},"msg":""}

最后，看一下排行榜：

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.ShowList&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "teams": [
            {
                "id": 5,
                "team_name": "奔跑吧兄弟（蓝队）",
                "vote_num": 3
            },
            {
                "id": 6,
                "team_name": "奔跑吧兄弟（红队）",
                "vote_num": 0
            }
        ]
    },
    "msg": ""
}

至此，我们已经可以把接口交付给客户端同学使用啦！
当然，我们还需要稍微整理输出WIKI文档~~~
部分接口返回的结果可能与你实际看到的不一样，因为数据会在变化而且有单元测试的测试数据。

(6)运行效果 - Part 2

接口服务，不仅仅需要提供正常的业务功能，还需要考虑到各种客户端使用的情况，包括非法的请求，或者不合的调用，比如防刷票。

这一部分，主要展示接口在各种异常情况下的响应能力。

签名失败

http://api.vote.phalapi.com/vote/?sign=XXX&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：wrong sign"
}

无登录态

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=XXX&user_id=1
//返回
{
    "ret": 401,
    "data": [
    ],
    "msg": "非法请求：user need to login again"
}

重复参赛

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 1,
        "team_id": 0
    },
    "msg": ""
}

当天投票次数已达最大

//第四次投票后
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回 - 注意此返回！
{
    "ret": 200,
    "data": {
        "code": 2,
        "vote_num": 0
    },
    "msg": ""
}

投票的团队不存在

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=404&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 1,
        "vote_num": 0
    },
    "msg": ""
}

在进行上面的非法请求后，我们可以同时关注后台的日记：

$ tailf ./Runtime/log/201505/20150517.log
2015-05-17 01:35:32|DEBUG|user not login|{"userId":null,"token":null}
2015-05-17 01:36:01|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:37:52|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:45:25|DEBUG|user need to login again|{"expiresTime":0,"userId":"1","token":"XXX"}
2015-05-17 01:48:13|DEBUG|user can not vote today|{"userId":1,"teamId":5}

(7)需要的数据库表

以下为关键的表，其他表，可以通过脚本自动生成，然后导入。

-- ----------------------------
-- Table structure for `phalapi_team`
-- ----------------------------
DROP TABLE IF EXISTS `phalapi_team`;
CREATE TABLE `phalapi_team` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `team_name` varchar(100) DEFAULT '' COMMENT '队名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of phalapi_team
-- ----------------------------
INSERT INTO `phalapi_team` VALUES ('3', 'egg team');
INSERT INTO `phalapi_team` VALUES ('5', '奔跑吧兄弟（蓝队）');
INSERT INTO `phalapi_team` VALUES ('6', '奔跑吧兄弟（红队）');
INSERT INTO `phalapi_team` VALUES ('17', 'test team name');

INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');

INSERT INTO `phalapi_user_login_weixin` VALUES ('1', 'wx_122348561111', 'ASDF', '130000000', '1');

INSERT INTO `phalapi_user_session_1` VALUES ('1', '1', '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731', '', '1', '1431790647', '1934382647', null);

4.3.4 脚本的使用

(1)创建项目脚本 - phalapi_buildapp

此脚本可用于快速创建项目，其使用如下：

$ ./PhalApi/phalapi-buildapp
Usage: ./PhalApi/phalapi-buildapp <app>

此脚本会根据Demo示例，快速生成一个新的项目。

(2)自动生成单元测试骨架脚本 - phalapi_buildtest

这是一个用来自动生成单元测试骨架的脚本，其使用如下：

$ phalapi-buildtest

Usage:
        php /usr/bin/phalapi-buildtest <file_path> <class_name> [bootstrap] [author = dogstar]

Demo:
        php ./build_phpunit_test_tpl.php ./Demo.php Demo > Demo_Test.php

然后，我们就可以使用辅助的PhalApi_Helper_TestRunner::go()进行单元测试：

    public function testShowList()
    {
        //Step 1. 构建请求URL
        $url = 'service=Act.ShowList&sign=phalapi&user_id=1&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731';

        //Step 2. 执行请求
        $rs = PhalApiTestRunner::go($url);
        //var_dump($rs);

        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('teams', $rs);
        $this->assertEquals(0, $rs['code']);

        $this->assertNotEmpty($rs['teams']);
        foreach ($rs['teams'] as $team) {
            $this->assertArrayHasKey('id', $team);
            $this->assertArrayHasKey('team_name', $team);
            $this->assertArrayHasKey('vote_num', $team);

            $this->assertGreaterThanOrEqual(0, $team['vote_num']);
        }
    }

(3)自动生成分表建表SQL语句 - phalapi_buildsqls

此脚本用于根据dbs.php配置和./Data/*.sql文件生成完整的建表语句，其使用如下：

$ ./PhalApi/phalapi-buildsqls
Usage: ./PhalApi/phalapi-buildsqls <dbs.php> <table> [engine=InnoDB]

假设我们需要对vote进行分表，拆分成3个表，可以这样配置：

//$ vim ./Config/dbs.php

        'vote' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('start' => 0, 'end' => 2, 'db' => 'db_vote'),
            ),
        ),

然后准备基本的建表语句：

//$vim ./Data/vote.sql

  `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',

最后，执行生成脚本：

$ phalapi-buildsqls ./Config/dbs.php vote

/**
 * DB: phalapi_vote
 */

CREATE TABLE `phalapi_vote_0` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_vote_1` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_vote_2` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4.3.5 在线接口参数查看

可以通过以下链接，在线实时查看接口参数：

//参赛接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn

//投票接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.Vote

//排行榜接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.ShowList

其中，投票接口的参数如下：
a pic

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[5.1]-架构与思想：PhalApi核心设计和思想解读 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

设计软件有两种方法：一种是简单到明显没有缺陷，另外一种复杂到缺陷不那么明显。 --托尼.霍尔

5.1.1 前言

在软件工程这一学科和行业里，关于软件工程的解说有很多。有人说开发是一门艺术；有人说开发是一种技艺；也有人说开发是一门哲学。但个人认同从实用主义和理性的角度去理解。

例如一个框架，我们之所以认为它好是因为我们发现这个框架遵循了编程规范、适当地使用了设计模式、巧妙地结合了设计原则、有着稳定的依赖、代码复杂度低、并且有着很高代码覆盖率的单元测试等等。

也就是说，好的框架都是可以被解释的。既然可以被解释量化，也就可以学习、参考和借鉴。

5.1.2 共性和可变性分析

关于共性和可变性分析，在《设计模式解析》一书中有着非常到位的讲解。

CVA是一种很容易的理念，按我的理解即： 抽离共性、隔离变化 。有点类似易经里面的“变”与“不变”。

诚然，在过去的教育中（包括大学在内的），对于软件开发都着重谈论面向对象开发，即OOD，以致于很多人都对面向对象开发产生了很大的误解。而这种误解所带来的实际情况就是： 我们都在进行面向对象开发，但却是标准呆板的面向对象开发，缺少生气，缺少活力 。

很多人，都没有把我们开发人员作为专业人士看待，甚至连我们自己都否认我们是专业的。所以很多时候当产品提出需求时，我们提供的开发周期往往会被外界以讲价的方式削减。何以？为什么医生给出的手术时间病人没有讨价呢？因为很简单，在病人的眼里，医生是专业的。

若我们也想达到专业的层次时，何以为？学习、思考和实践，我认为至少这三者是必不可少的。

所以，当我们在对PhalApi进行设计时，我们进行了一次又一次地酝酿、尝试、思考。我们在思考：这些功能是否真的会在实际项目中被使用？开发人员是否可能很好地进行扩展？此种决策是否便于单元测试、从思路上减少代码异味？。。。

我们谨记敏捷开发，不过度设计。但我们也确实需要一种思想上的指导。正好，我们看到了 共性和可变性的分析（commonality and variability analysis, CVA） 。

5.1.3 CVA和三种视角、抽象类之间的关系

引用一下《设计模式解析》一书中的图表：

在这种理念的指导下，我们会更愿意将接口开发过程的共性抽离统一起来，而可变性部分的则可以由开发人员根据不同的项目情况进行快速定制实现。

5.1.4 不稳定性与抽象度分布

除了常谈及到的“低耦合、高内聚”外，在对代码进行静态分析和衡量其可维护度时，还有一个值得注意的值，即：不稳定的度量。

不稳定的度量可以根据以下公式计算获得：

I = 离心耦合 / (离心耦合 + 向心耦合)

因此从宏观上，我们的代码结构，从上层到下层，应该向着稳定的方向递增，也就是说越底层越应稳定。对应 稳定依赖原则的规则（SDP），包之间的依赖应该朝着稳定的方向：不稳定的包应该依赖于更稳定的包。

又结合不稳定性与抽象分布图，我们PhalApi框架的代码 应该大部分分布在上图中的抽象稳定区以实现框架高层的建设、少部分分布在具体不稳定区以提供一些基础的功能 。

5.1.5 创建和使用分离

在框架的特性中，包括：可重用、IoC Container以及SOLID五条原则的运用等。这里就部分SOLID原则的运用简单说明一下。

(1)S：单一职责原则

这是我们一直都坚持遵守的原则，因为，我们也坚持 短而美 的写法， 致力于编写优雅的代码、编写人容易理解的代码 。

(2)O：开放-封闭原则

我们首先希望的是在进行接口开发过程中，当需要新增一个接口时是开放的，对已有的响应调用流程是封闭的。即我们只需要实现新接口逻辑即可，不需要改动其他过程的调用。因此在OCP原则的指导下，我们通过结合工厂方法封装了对接口的初始化和调用。

(3)D：依赖倒置原则

PhalApi框架，最大的特色莫过于 它提供了一种如何快速进行接口开发的机制，但它不强制你使用不必要的功能，甚至它还鼓励你通过它来尝试研发自己的框架 。更进一步，PhalApi引入了新颖明确的概念，一如服务。我们把客户端调用的接口称之为接口服务，把服务端用到的资源称之为资源服务。对于后者，PhalApi提供了灵活的DI机制，以支持各项目定制化的开发。

5.1.6 PhalApi核心架构图

显然，到目前为止，从核心架构图所折射出PhalApi的结构和代码是如此的 简单明了、统一规范。至少我是这么认为的，也是一直这样努力的。

从上图的核心架构图可以看出，中间红色部分的DI处于汇点位置，提供各种资源服务的定位、创建、管理和提供。

而左上角的代码示例则表达本系统框架运行的主流程： 创建一个接口实例，运行响应。

右上解黄色部分则为多变的接口应用开发的代码，这里特意罗列了两组接口，意在表明可以在此框架下挂靠多套接口。

最下面是接口开发过程中所用到的各种基础设施和技术，如日记、配置读取、缓存、加密、请求和响应等。同样，除各应用项目中形式多变的接口开发外，这块的底层技术亦支撑不一而足的需求。因为，PhalApi只是作了共性的抽离，即提供一级抽象且稳定的接口或者抽象类，以约定规约视角中接口的函数签名，不作过多的具体实现。同时以DI作为辅助，支持快速扩展。

5.1.7 PhalApi核心执行流程

和其他框架不同，除了有文档对基本使用有说明外，我们还提供了我们框架核心的设计和思想，以便大家洞明其中的原理从而进一步优化扩展。

这里，扼要说明一下PhalApi框架中接口请求背后的核心执行流程。

PhalApi-接口处理主流程 - 0227

从上图的时序图中可以看出，在PhalApi中，一个接口的请求处理，只要分为两个环节： 接口服务初始化 和 接口服务调用 。

(1)接口服务初始化

在Web Service中，往往需要对服务进行注册发布后，才能开放请求。这里免去这一层，但遵循 创建和使用分离 的原则，我们将接口服务的初始化进行了封装，以便可以统一进行初始化、异常处理和一些权限ACL的控制

，甚至接口访问的统计等操作，更为重要的是接口开发人员可以进行无绪开发，而不需要过多知道如何合法创建接口服务。

在1.2. 步骤中，UML时序图中的::generateService()表示对静态函数的调用，即对应代码：

PhalApi_ApiFactory::generateService();

随后，可以看到（假设我们这次请求的服务为：?service=Demo.DoSth），我们创建了一个指定接口的实例（此接口类须继承于PhalApi_Api基类），并以变量a返回实例。

如果请求的服务非法，则会以 400非法请求 返回给客户端。而正确创建接口服务a后，则会进行接口的初始化，其中有接口参数规则的解析和注册了过滤器服务后的检测操作。

当这一系列的操作都成功执行后，我们将会得到一个接口服务实例a返回。
因此，在接口服务的创建过程中，我们没有过多地限制，而是预留了很大的空间给到接口项目定制开发。

至此，接口服务的创建完成。

(2)接口服务调用

在完成复杂的创建工作后，客户端（备注：这里指的是服务端开发的开发客户端）只需要简单调用需要进行的操作即可。

而这一块，则需要接口项目具体开发实现，也是我们项目级的核心部分。

在获取接口服务的背后，我们建议结合领域驱动设计的理念，对项目代码进行这样的层级划分：

  • Api接口层：用于接收参数并响应接口的请求；
  • Domain领域层：用于处理复杂的领域业务逻辑，保证规则只出现一次；
  • Model数据源层：更广义上的Model层，提供数据来源，不限于DB；

最后，是我们客户端关心的返回格式。 默认情况下，我们都是以JSON格式返回的，但仍然可以轻松支持其他格式的返回，如JSONP、XML等。只需要简单地开发实现，然后重新注册即可。

至此，接口服务的调用完毕。

5.1.8 DI支持下的轻松扩展

当使用一个开源框架时，我们既喜欢其强大的一面，但矛盾的同时我们又害怕其中的复杂性，原因莫过于：学习成本高、出现问题时怕驾驭不了。

而在这里，在PhalApi这里，这一切都是这么简单，简单地又如此明了。

当需要进行资源服务的扩展时，我们可以：

实现自己需要的服务

实现指定资源服务在规约视角约定的接口，假设我们需要用文件来当作新的缓存存储。则需：

class MyCache_File implements PhalApi_Cache {
    public function set($key, $value, $expire = 600) {
        //...
    }

    public function get($key) {
        //...
    }

    public function delete($key) {
        //...
    }
}

在入口重新注册

当实现自己的功能后，只需要简单地在入口文件重新注册即可。如：

DI()->cache = new MyCache_File();

最后，另人兴奋的是，原来全部的调用代码都不需要改动，即可享受后期调整升级后的新功能！完全避免了曾经那种“牵一发而动全身”的痛苦。并且，定制开发出来的实现类，还可以跨越业务在其他项目中共用。

这不正是我们常常所说的代码重用吗？而如今，我们很优雅地做到了！

然而，我们在实际开发中收获到的远远不是代码重用这么简单，而是一种更好的开发实践。因为通过DI使得创建和使用分离，所以我们可以让高级的开发同学实现服务功能的开发，然后再提供给普通的开发同学使用，新手亦然，因为对他们来说：会用就行。当然，对于高级的同学，还应该遵循开发的最佳实践，坚持单元测试，以保证我们提供了可靠的接口（广义上的接口，非HTTP请求的接口）给我们的“客户端”使用。

若如此，我们的开发合作岂不是很更合理、更明朗、更愉快？哈哈，我想是的。

作为一个框架，我们应当以发散的方式去设计；但为了能为应用提供可用的功能，我们又应当以收敛的方式去实现。
如果我们提供的功能不足以满足大部分主流的业务场景，那么我们至少需要提供可扩展的空间。

正是出于这样的考虑，我们虔诚地引入了DI。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[5.2]-杂谈：扯一些PhalApi的前世和今生 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

此篇章与框架技术无关，为课外读物，感兴趣的可以略读一下。

5.2.1 写在前面的话

记得曾经有一位大主教在自己的墓志铭上写道：

“小时候，我发誓要改变整个世界；
大些了，我努力改变我的祖国；
后来，我想我能改变我居住的这个城市；
直到临死之前才发现，我只能改变自己，从而影响他人，乃至影响整个世界”。

当然，我没有想过要去影响整个世界，我只想尽我自己的所学，为自己，为他人做一些有用的事情。

正如，最近在thephp.cc站点上面看到的这样一张PPT：

我不一定能记住这位国际出名的前辈的名字，但我记住了他的这句话：

Driven by his passion to help developers build better software.

5.2.2 然而，要布道，先要践道。

所以，当我希望把PhalApi框架开源出去时，我这个框架应该是初步完善并可以应用于实际项目开发的，至少我已经切身用过它来进行后台接口项目开发并不断优化重构之。

5.2.3 最初框架源于毕业论文的需要

当年，在进行毕业论文的项目开发时，因为需要用到后台接口。于是乎，便到图书馆和网上查阅整理了很多资料，知识理论包括RESTFull、phprpc、SOAP协议、Aamzon接口服务、WSDL、Web Service等等。

然而，在实际中我却发现很多人在用PHP开发接口是很简单的，或者说是很粗爆的，如直接使用fopen()函数进行获取接口结果。并且，当我需要去寻找一个可以进行后台接口开发的框架时，居然没找到很好框架支持。基于此，最初的接口框架就在这样的背景和研究下出来了。

附一下当前毕业论文的相关资料：

  • PhalApi前世： 毕业论文答辩汇报 - 精简版.ppt
  • PhalApiu前世：毕业论文 - 暗夜在火星 - 修改版.pdf
  • PhalApi前世文档：zenWS3完全开发手册 - 初稿 - 20130317.pdf

可以在 附件 下载上面的资料。

5.2.4 最初框架的首次商用和实站

在毕业后就职的第一家公司中，由于也有接口开发的需要，正好是管理后台 内部系统调用，所以在征得领导同意后，我将我毕业论文所研发的接口框架进行了部署和开发。首次投入了商业用途。^_^

并同时整理出了初步的接口框架，也就是PhalApi的前世。

5.2.5 PhalApi的前世

最初的框架命名是：zenphpWS3。
原因是zen表示开源、php表示用PHP开发、WS表示Web Service、3表示支持SOAP/HTTP/RPC三种协议以及JSON/XML/ARRAY等多种格式的返回。
更多可以访问我之前的博客： zenphpWS3：文档&示例&设计&源码

5.2.6 又一年的全职接口开发和开源框架学习

曾经有一年，我负责某个大型游戏项目的后台接口开发，同时提供接口供安卓版、iOS版和PC使用，对后台接口的开发有了更多的实战经验。

并且，在对很多国外优秀的开源框架的学习和研究后，有了很多的领会，如我个人最为推荐的Phalcon。Phalcon不仅仅在框架方面做得非常优秀，在文档编写方面也是不遗余力的，即使我们不用这个框架，也能从这个框架的文档上吸引很多有用的思想。

才发现最初提供的接口框架还有很多不成熟的地方。。。

5.2.7 PhalApi框架开源的前奏

到后来，在越来越多项目开发的需要下，出于仍然没有很好的接口框架支持（这里补充说明一下：一搜索“php接口开发框架”没有好的结果；二所接触到的框架虽然有支持接口开发，但只是其中一部分）。更为重要的是，项目往往又是如此重要，很担忧所选的框架进行接口开发后遇到问题不知如果在底层修正。

所以，还是用回了自己的接口开发框架。

当然，在做项目接口开发的同时，我也有意识地在对接口框架进行完善和重构，并融入框架所需要的特性、原则和模式。如可重用、IoC、5条设计原则、组件等。

至此，PhalApi接口框架也就慢慢地明朗了。接下来就是：

5.2.8 PhalApi正式走向开源

如今，很多大的企业都提供了开放平台，如腾讯开放平台、新浪微博开放平台、优酷开放平台等。与此相比，虽然小应用轻量级App项目的后台接口没有这么大的规模，但也是很有量级上的需求的，正如下面这张图所展示的。

在此大背景下，显然会有更多的应用需要后台接口的支撑，需要进行后台接口的开发，乃至对海量数据和访问的支持、快速扩容升级、一键发布、多端支持等。即使这样，PHP开发人员在接口开发时，仍然会显得有点“随心所欲”，缺少规范和统一，更为重要的是仍然没有发现好的PHP接口开发框架。

古云此日足可惜，吾辈更应惜秒阴。

所以，此框架通过我多个实际项目应用后，在不断整理重构、精益迭代下走向了开源。^_^

希望对你们有所帮助，谢谢。

我们通过这个框架，希望可以（正如文档里面所说的）：

一来，支撑轻量级项目后台接口的快速开发；
二来，阐明如何进行接口开发、设计和维护，以很好支持海量访问、大数据、向前向后兼容等；
三来，顺便分享一些好的思想、技巧和有用的工具、最佳实践。

其中，第一点是直接的也是最表面的，即你通过这个接口框架就可以快速开发了；第二点是更通用的接口开发中的一些好的或者必要的技术支撑和策略；第三点是面向框架级别的，即使你不用这个接口框架，也可以设计研发自己的开发框架，同时也是突破接口开发、超越PHP语言的。

书不尽言，欢迎多交流，再次谢谢！

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[5.3]-框架总结：术语表和PHP开发建议 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

5.3.1 术语表

我们并不想“制造”一些新的术语来增加大家的学习成本，但为了更高效地进行专业交流，我们将PhalApi框架中所用到的一些概念进行提炼并罗列如下。

(1)接口服务

通常，我们把远程第三方提供的接口称为API。但秉承Web Service的概念，我们更愿称接口为服务。为了同时保留这两者的意思，我们在这里统一将API称为 接口服务 。也就是我们对应的?service=XXX.XXX 。

(2)资源服务

在使用DI依赖注入进行注册的组件，我们也倾向称之为服务，同时也是一些服务端上可用的资源，如数据库、缓存、加解密等。因此统称为 资源服务 。

5.3.2 PHP开发建议

在实际项目开发过程中，通过观察不同开发人员编写的PHP代码，会很趣。因为你会发现每个人的编程风格都不尽相同，但我们更提倡约定编程、规范的代码和编写人容易理解的代码。所以，下面就发现的问题进行说明。

(1)滥用的静态方法

很多框架和很多项目都说自己在使用面向对象编程，但其实很多时候是在类中全部使用静态方法的伪面向对象。这里可能会引发争议，因为有些同学会认为静态类方法比成员函数更快速，而且也确实有相关的数据表明是快了一点点（严格上来讲，很微小）。但作为代价，我们失去的更多。如我们没能使用动态，也不方便在单元测试时使用桩、短件、模拟等技巧。更为重要的是，失去了高层的概念提炼和规约层的约定，不利于接口和实现地分离。

所以，请只有在需要的时候才使用静态static方法，如工具类或实用操作。

(2)非真正意义上的单元测试

很多时候，我看到很多框架和项目中没有单元测试的代码，就算有也不是真正意义上的单元测试。可测试的代码是美的，因为可测试的代码表明职责单一明了，有低耦合度，可以进行快速模拟和替换。关于单元测试，前面已有文档详细说明。

所以，请尽量尝试和坚持PHPUnit单元测试，体验测试驱动开发的乐趣，体验浮现式设计的激动。

(3)无处不在的单例模式

很多同学在学习了设计模式后，都很想试用一把，所以往往在很多时候是为了“用设计模式”而用设计模式，而不考虑是否合适，是否真的需要。尤其对于单例模式，这种情况更为普遍。

使用单例模式的时机包括有：提高系统性能、全局只能有且只有一个实例否则会导致问题发生、提供一个全局的公共访问点等。

然而其他很多情况则不需要。例如很多情况，实例为某容器所持有，则只需要在容器内做数量控制即可，不需要被持有的实例再作单例控制。

所以，请只有确切需要使用单例时才使用。

5.3.3 PhalApi框架的不足

在我们不断维护、演进PhalApi框架的同时，我们也在使用这个框架进行了很多项目的开发，与此同时也在阅读各方面的书籍以获得更深层次的理解。
在这样实践、思考、再设计的不断反馈迭代后，我们看到了PhalApi确实在某方面表现得出色。

但一个负责任的框架，应该也明确指出它的不足。
这里，我们将PhalApi开发中的不足罗列如下，希望为你进行框架设计或者对PhalApi的使用有更好的理解。

(1)接口结果中msg应该改名为error

我们推荐的接口返回格式为：

{
    "ret": 200,
    "data": {
        "code": 0,   //对操作码进行说明
        ....         //更多结果的说明
        "msg": ""
    },
    "msg": ""
}

显然，上面两个msg字段，会给开发团队带来困惑或混淆。
更好是应该把最外层的msg改成error更为贴切，因为只有错误时此字段才有效。

但基于前期的大量文档说明，此外层的ret、data、msg三个字段已约定。所以，只能从应用层的msg进行重命名，如tips。

(2)对NotORM中limit操作的错误优化

前期，由于没有深刻留意MySql中OFFSET关键字的作用，导致了做了一些不精确的优化。

可注意以下的微妙区别：

limit 5 OFFSET 10   #从第10个位置开始，查询前5个

limit 5, 10         #从第5个位置开始，查询前10个

但重点考虑到如果修复这个之前犯下的错误，会对项目升级后有很大的冲动。
可预料的故障有：”升级后，首页列表无任何数据显示“和“升级后，列表数据过多导致App加载崩溃”。

最后，出于对已在开发或已上线项目的保护和承诺向前兼容的原则，我们不得不保留了这个污点。
所以，当对底层进行改动时，须确保已透彻理解各操作的微妙区别。

(3)对数据库操作封装的欠缺

一直以来，数据库支持这块都是比较欠缺的。所以我们使用了NotORM。

但我们为了能更好把NotORM与PhalApi整合，将它调整成更适合我们的使用方式，我们又为NotORM提供了一个封装层，类似代理。

然而，这会为新手入门这个框架造成一定的迷惑。
因为，这有两套操作数据库的区别。但他们一开始不能很好地理解这样的区别，以及这样设计的初衷。

坦白来说，PhalApi对于数据库这块不是好的设计，但好在它可以使用并正常地工作。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[5.4]-许可 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

5.4.1 协议许可的权利 - GPL协议

PhalApi官方网站为 http://www.phalapi.net，依法独立拥有 PhalApi 及官网发布的PhalApi衍生产品著作权。PhalApi及其衍生产品著作权受到法律和国际公约保护。使用者需仔细阅读本协议，在理解、同意、并遵守本协议的全部条款后，方可开始使用 PhalApi及其衍生 软件。

本授权协议适用于 http://www.phalapi.net 发布的所有应用程序，PhalApi官方网站拥有对本授权协议的最终解释权。

5.4.2 协议规定的约束和限制

PhalApi软件及其PhalApi官网发布的PhalApi衍生产品在没有相关说明的情况下默认是免费的，可用于商业用途或者非商业用途。但是无论用途如何，是否经过修改，修改程度如何，未经过许可， 页面上的产品名称以及指向官网的连接还有源代码中关于PhalApi的版权信息及作者信息必须保留，不许修改或删除，除非您获得PhalApi官网的许可。

如果您未能遵守本协议的条款，您的授权将被终止，所被许可的权利将被收回，并承担相应法律责任。

5.4.3 有限担保和免责声明

本软件及所附带的文件是作为不提供任何明确的或隐含的赔偿或担保的形式提供的。 用户出于自愿而使用本软件，您必须了解使用本软件的风险，在尚未购买产品技术服务之前，我们不承诺提供任何形式的技术支持、使用担保，也不承担任何因使用本软件而产生问题的相关责任。 PhalApi官网不对使用本软件及官网发布的衍生产品构建的软件中的文章或信息承担责任。

有关 PhalApi 最终用户授权协议、商业授权与技术服务的详细内容，均由 PhalApi 官方网站独家提供。PhalApi官网拥有在不事先通知的情况下，修改授权协议和服务价目表的权力，修改后的协议或价目表对自改变之日起的新授权用户生效。 电子文本形式的授权协议如同双方书面签署的协议一样，具有完全的和等同的法律效力。您一旦开始安装 PhalApi以及官网发布的衍生产品，即被视为完全理解并接受本协议的各项条款，在享有上述条款授予的权力的同时，受到相关的约束和限制。 协议许可范围以外的行为，将直接违反本授权协议并构成侵权，我们有权随时终止授权，责令停止损害，并保留追究相关责任的权力。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[5.5]-联系和加入我们 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

5.5.1 官网

http://www.phalapi.net

5.5.2 交流与咨询

(1)国际渠道

邮箱：chanzonghuang@gmail.com

(2)国内通道

QQ：376741929
QQ交流群：421032344

(3)博客与文档

博客：http://my.oschina.net/dogstar/blog?catalog=3264711
WIKI文档：PhalApi Wiki

(4)问题反馈

Issues：http://git.oschina.net/dogstar/PhalApi/issues

5.5.2 感谢以下贡献者

dogstar
火柴
George
zz.guo
文振熙
黄苗笋
爱编程的小逗比
Aevit
Summer
冰霜

期待您的加入，您也可以通过其他的途径来支持我们，谢谢！

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[5.6]-更新日记 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

PhalApi从1.2版本进入多人协助开发和项目使用后，也开始遵守严格的版本控制。我们平均每周会修复已发现的BUG，每2-4周发布一个小版本。

分支说明

  • release 发布分支，稳定的版本，可直接用于团队的项目开发
  • master 主线分支，随时待发布的版本，测试验收后会合并到release进行发布
  • master-X.X.X 主线的历史分支，以保留或查看历史版本
  • dev 开发分支，当前用于协同开发的分支，以存放共享未完成的代码

更新日记

2017-06-30

1、文件上传fil类型，支持批量上传 感谢@Ederth 提交merge request
2、支持根据ret状态码转换为相应的Http状态码，使用方式：$rs->adjustHttpStatus()->output();

2017-06-24

1、启动开源产品项目PhalApi-Blog博客项目
2、在问答社区增设【招聘】栏目
3、收录基于Phalapi框架的gearman扩展(异步并发)

2017-06-17

1、为在线文档添加框架版本升级提示，方便提醒开发人员进行免费升级，并支持设置项目名称
2、phalapi-buildapp颜色在mac显示错误修正
3、phalapi-buildtest优化，并支持命名空间
4、修复MSSQL数据库插入时语法错误的问题，并且当前仅限制MySQL允许使用【`】符号

2017-06-16

1、重构listAllApis.php文件

2017-06-14

1、新收录的扩展类库：PhalApi-RongYun融云扩展类库、PhalApi-Workman、PhalApi-PHPWord

2017-06-12

1、补充接口示例到Examples，如文件上传、数据库CURD操作
2、官网文档代码支持高亮显示，方便阅读

2017-06-09

1、官网首页改版，感谢 @明 的贡献
2、在线接口列表文档优化，支持折叠和展开两种主题风格

2017-06-06

1、slogan更新为：助你创造价值！
2、官网美化

2017-06-03

1、修复swool的server端处理请求完成后无法关闭连接 感谢@Ederth 的提交
2、api文档增加测试功能 感谢@哲别 提供源码参考
3、数据库类库NotORM优化

2017-06-02

1、官网wiki支持短链访问，精简URL

2017-05-30

1、修正在使用notorm更新的时候，如果更新的字段名有desc等数据库关键字到时候，数据库报错的问题 感谢@wuxyyin 的提交
2、新增XML扩展 可以对XML 文件进行解析或生成 并且支持XML格式返回结果 感谢@喵了个咪 的提交
3、在线接口列表文档，添加显示接口服务总数
4、新增RabbitMQ扩展 可以对专业队列软件RabbitMQ进行队列出入操作

2017-05-16

1、添加SOAP扩展，可用于搭建Web Services
2、listAllApis.php支持生成离线文档，使用方式：php /path/to/listAllApis.php，感谢@秋寒 提供

2017-05-11 - v1.4.0版本同步

1、修复使用source参数规则配置时未能识别单元测试模拟测试数据的BUG，以及FastRoute的兼容性问题
2、公共的@exception 异常码和@return 返回值，可统一在类注释中标注
3、添加微信小程序扩展类库WechatMini

2017-04-21 - v1.4.0版本

1、当请求的接口服务不存在或无法调用时，把ret从原来的400调整为ret = 404。

2017-04-20

1、参数规则细化以便保持一致性，主要包括有对最值的转换处理，以及修复回调类型部分场景下不能正常工作的问题。

2017-04-14

1、追加调试功能，并把原来异常时直接500，在调试模式下更改为带有相应的异常堆栈信息的接口返回。更多说明请见文档：[2.13]-接口调试：在线SQL语句查看与性能优化
2、废弃__sql__数据库调试参数，统一只使用__debug__参数，并且不直接页面输出SQL语句，而是作为调试信息返回的一部分
3、添加全球追踪器DI()->tracer，可用于简单的执行追踪，支持自定义节点标识。注意事项：DI()->tracer当前被框架采用，如之前已注册，则会有冲突。
4、支持单个参数级别的接口参数来源，即可在接口参数规则中配置指定使用原始数据源，还是$_GET/$_POST/$_COOKIE/$_SERVER或其他。更多说明请见文档：[1.23]-请求和响应：GET和POST两者皆可得及超越JSON格式返回  感谢 @秋寒 提供
5、添加更多实用的工具函数到PhalApi_Tool，如：创建目录、删除目录等。
6、修复PhalApi_CUrl类中无法设置CURLOPT_HTTPHEADER 格式问题。
7、修复一些Issues。
8、根据Sonar报告，消除各类问题，减少技术债务。优化了44个主要代码问题。

2017-04-14

1、添加了APCU缓存支持

2017-04-12

1、优化重复的代码：DI()->request->get('service', 'Default.Index'); 并提取可重用的接口以获取接口服务名称、类名和方法名
2、支持自定义接口服务请求的方式，即更改：?service=XXX/XXX的传递方式
3、添加接口服务白名单支持，可以兼容升级，但若在生效，需要同步添加app.service_whitelist配置。以上感谢 @秋寒 提供建议
4、优化有语法上警告的代码 感谢 @Steven 反馈

2017-04-06

1、支持多个Memcache/Memcached实例配置

2017-03-16

1、官网www.phalapi.net切换到https

2017-03-12

1、添加文件上传接口到七牛扩展，并补充完善其他扩展接口服务注释

2017-03-09

1、安装模块及语言模块更新，感谢@高一平 提供
2、微信支付接口修复，感谢@秋寒 提供

2017-03-02 - v1.3.6 踏青版

1、listAllApis.php添加左侧菜单，便于多接口时文档分类显示，感谢 @初学者 提供
2、调试模式下，输出显示错误信息
3、添加英文文档：http://www.phalapi.net/wikis/en/，进度5%
4、把框架核心代码的注释翻译成英文，进度60%

2017-02-28

1、listAllApis.php支持显示扩展类库的接口

2017-02-20

1、添加Python版本的SDK包，感谢@高一平 提供

2017-02-18

1、把原来master-x.x.x分支，重命名为archive-x.x.x
2、添加繁体翻译文件，感谢@高一平 提供

2017-02-13

1、补充部分缺失的翻译
2、开启英文版本的文档编写与代码版本注释
3、修复FastRoute扩展缺少POST/GET参数的BUG

2017-02-08

1、微信支付接口增加服务商支持 感谢@442484396/秋寒 提供

2017-02-05

1、phalapi-buildapp脚本中sed命令的修正MacOS support 与GNU SED兼容，感谢@x2x4 反馈提供
2、添加@exception异常注释支持，并补充文档：[1.32]-在线接口文档：注释规范
3、添加CLI扩展类库，用于开发命令行项目

2017-01-01 - v1.3.5幺鸡贺岁小版本

1、C# SDK包代码优化，改用Json库，感谢@X反馈提供
2、phalapi-buildapp及phalapi-buildcode两个命令脚本的格式转换及注释修正

2016-10-15

1、将PhalApi_Model_NotORM::getTableName($id)抽象方法改为返回默认表名的实现，以防实现此方法而引起的报错。
2、提供PhalApi SDK for Ruby，项目地址：https://github.com/phalapi/phalapi-sdk-ruby

2016-10-15

1、错误日记追加，按现有的日记格式补充到已有的日记载体，感谢@442484396反馈

2016-10-13

1、调试模式下，数据库连接失败时显示详细的错误信息，方便定位排查问题

2016-9-3

1、C# SDK包编码从Default调整为UTF8，避免接口接收数据为乱码，感谢@shero反馈

2016-8-7

1、优化logs机制,对脚本程序进行优化,感谢@石头上的露珠 反馈

2016-07-27 - v1.3.4 暑假炫酷版

1、在线接口文档样式美化，感谢@转身.满目疮痍提供
2、#53 断开数据库链接优化，进一步对数据库断开进行深度精除
3、#56 postgresql无法获取新增数据的主键ID优化，感谢@clov4r-连友提供解决方案
4、NotORM_Result::query()由protected提升为public级别，放开限制 感谢@scott反馈

2016-07-25

1、优化NotROM进行Count时的效率

2016-06-18

1、修复生成文档时对返回参数匹配中含有空格显示问题BUG,感谢@clov4r-连友提供

2016-05-21

1、对请求类进行更新,新增了getHeader方法获取头信息
2、修复notrom-insert_multi键名是关键字会出错BUG

2016-05-14

1、添加生成代码的脚本命令：phalapi-buildcode

2016-04-30

1、Task扩展添加MC的MQ支持

2016-04-22 - v1.3.3

1、优化三个命令脚本的使用说明，使其更友好、更专业
2、# 48 接口文档注释中有空格问题优化
3、官方视频教程系列录制与发布
4、V1.3.3小版本再次更新及发布公告

2016-04-6

1、维护install优化目录权限访问验证

2016-04-2

1、修复sting类型长度验证是中文字符占用三个字符问题,使用format下标可用于定义字符编码的类型，如utf8、gbk,gb2312
2、install优化目录权限访问验证

2016-03-26

1、V1.3.3小版本发布

2016-03-18

1、对Redis缓存进行扩展完善，感谢 @風 提供

2016-03-12

1、添加PhalApi客户端SDK包（Go版），感谢@prettyyjnic 提供
2、添加PhalApi客户端SDK包（React-Native版），感谢@ダSimVlove辉 提供
3、修复NotORM在调试时输出日记的warning

2016-02-13

1、PhalApi_Api第54行!isset($this->name)判断bugfixed

2016-01-18

1、框架中文名起用：π框架，感谢@猫哥 @Lambda! 两位的建议！
2、将JSON返回格式中的header调整为：application/json

2016-01-13

1、添加兼容iOS下mcrypt加密的扩展类库，感谢@大蝉 提供
2、默认Demo示例下，数据库表字段从from改名为note，以防误导
3、紧急修复数据库isKeepPrimaryKeyIndex写反的问题，并紧急发布

2016-01-02 - v1.3.2

1、修改notorm使用sql语句使用:和?替换执行sql时,对于int,bool,null进行单独处理
2、优化了PhalApi_Request_Formatter_File,支持了对文件上传类型验证增加了ext后缀名验证
3、提供CHM格式的wiki文档
4、初始化文件init.php简化，并默认装载扩展类库Library目录
5、修正install可视化安装中对listAllApis.php 和 checkApiParams.php 文件中的项目名称
6、其他一些轻量bugfixed

2015-12-31（非100%完美兼容）

1、listAllApis.php在线接口列表，追加对“更多说明”的使用支持，即使用注释@desc
2、添加PhalApi基础教程到官方wiki，感谢 @喵了个咪 提供
3、添加Object-C的SDK包XSHttpTool，感谢 @garden！ 提供
4、默认采用以数组的形式返回结果集，1.3.1及以下版本需要保持原来主键为下标的返回时，需要在init.php文件手动切换：DI()->notorm->keepPrimaryKeyIndex();

2015-11-27 - v1.3.1

1、重构并设计格式化类：PhalApi_Request_Var，使之支持项目扩展定制化；并添加callable回调类型、为数组和日期补充最值检测
2、PhalApi_DB_NotORM上添加对数据库维度的事务操作支持 感谢@研究研究 反馈
3、重构PhalApi_DB_NotORM，以便支持定制MySQL外的数据库操作
4、bugfixed NotORM_Result::fetchPairs()的错误: Argument 1 passed to iterator_to_array() must implement interface Traversable, array given 感谢@scott 反馈
5、提供了Redis扩展类型，由 @喵了个咪 提供
6、添加支持Yaconf扩展读取配置
7、版本更新到v1.3.1 (感恩版)

2015-11-07

1、添加Ruby版SDK包
2、完善listAllApis.php，可按注释进行字典排列，支持多级目录扫描
3、添加了DB_Cluster扩展类库，由 @喵了个咪 提供
4、添加了Medoo扩展类库，由 @转身.满目疮痍 提供
5、DI()->notorm::queryAll()与DI()->notorm::queryRows()第二个参数$parmas改成缺省默认为array()

2015-10-25 - v1.3.0

1、用户扩展类库的完善，添加创建会话的入口，与文档完善
2、初始化文件init.php简化
3、提供可视化安装向导
4、首次线下活动尝试：官方非正式交流会第一期(广州站) --分享你在PHP接口开发中的心得与疑问
5、wiki文档二次系统整理
6、利用MDwiki部署全新的在线文档：http://wiki.phalapi.net/
7、官网首页版本迭代更新
8、添加示例目录结构图解 由@黄苗笋提供
9、listAllApis.php支持自动化生成 由@转身.满目疮痍 提供
10、落点应用框架：freeApi服务于互联网开发者的免费接口供应平台 由@转身.满目疮痍 提供，并正在致力开发完善中，请见：http://git.oschina.net/xiaoxunzhao/freeApi
11、提供基于接口查询语言的SDK包，并有JAVA版、PHP版、C#版、Objective-c版（由@Aevit 提供）、JS版（由@喵了个咪 提供）
12、PhalApi基础教程的编写 由@喵了个咪 提供，请见：http://my.oschina.net/wenzhenxi/blog/513004
13、添加了默认接口签名过滤器：PhalApi_Filter_SimpleMD5
14、加强对DI()->filter的类型检测，避免出现non-object的错误，增加容错能力
15、版本更新到v1.3.0 (1024狂欢版)

2015-09-07

1、添加扩展类库：基于FastRoute的快速路由
2、一些轻量bugfixed

2015-08-23

1、为接口参数规则扩展desc说明
2、补充了：[4.2] 开发实战2：模拟优酷开放平台接口项目开发
3、提供Auth 权限扩展 (由@黄苗笋提供，在这里表示再次感谢，^_^)
4、一些轻量bugfixed
5、版本更新到v1.2.1

2015-07-26

1、修正不能获取文件类型参数
2、修正在线接口参数查询工具中对默认值输出的展示
3、修正User扩展类库中登录态检测穿透，原因在于不该使用 isset(DI->cache) 进行判断

2015-07-11

1、计划任务Task扩展类库追加异常时的结果返回，以明确出错的原因
2、添加数据库配置注释

2015-06-27

1、自动生成SQL语句的脚本，追加对配置数据库字符集的读取，并无表时读取默认的路由配置以便可以生成SQL
2、User扩展类库的Bugfixed
3、docs文档更新

2015-06-06

1、接口返回参数，根据代码return注释，自动生成在线文档
2、接口参数规则中追加对文件参数的验证支持
3、将Task扩展类库转移并内置于PhalApi框架中
4、统一使用GPL协议
5、重构在线接口参数查看，使用OO的规范并提取成类放置在辅助类包中，以便接口更好地使用
6、提供统一的测试辅助类：PhalApi_Helper_TestRunner; 若需保留原来的写法，可追加： class PhalApiTestRunner extends PhalApi_Helper_TestRunner{}
7、一些bugfixed

2015-05-23

1、添加redis缓存
2、提供计划任务扩展类库及对应的文档
3、提供项目实战示例和对应的文档
4、补充更多的文档
5、自动生成SQL语句的脚本改从./Data/目录下获取对应的建表文件，原来为./Config/sql/
6、提供一个接口开发实战示例（投票活动）
7、补充数据库的port和charset配置，port默认为3306，charset默认为UTF8
8、完成计划任务续篇，提供统一的crontab调度

2015-05-07

1、PhalApi_Cache_Memcached更正类名为PhalApi_Cache_Memcached，注意此类名的修正所带来的影响，可以在init.php文件检测项目是否有使用MC
2、追加更低版本的Memcahce支持
3、开启1.2版本，进入多人协助开发时代

2015-05-01

1、五一小更新，提供一致性的DB操作接口
2、添加swoole扩展类库，以支持长链接和异步任务
3、版本更新到1.1.4

2015-04-24

1、修正数据库连接失败时显露帐号信息

2015-04-17

1、官网完善，添加备案信息、下载、关于、许可等页面
2、对框架添加COOKIE支持、SL快速函数、CURL重试次数、字符串正则匹配验证等
3、开源用户扩展类库
4、创建PhalApi框架交流群

2015-04-11

1、添加在线接口列表模板，及生成脚本同步更新
2、文档移至官网维护
3、官网页面开发

2015-04-04

1、WIKI文档排版，与更多开篇的编写
2、源代码注释的对齐与再整理
3、生成项目脚本与创建项目的一些工作准备
4、提供单元测试Runner及phpunit.xml配置文件

2015-03-27

1、代码注释完善与在线类参考手册生成：http://www.phalapi.net/docs/

2015-03-21

1、一些bugfixed：规则下标开头大小写问题、文件缓存个数问题预防等
2、扩展类库：七牛云存储接口调用
3、文档整理和编写新的文档

2015-03-15

1、增加对RSA加密的支持，以及超长字符串的解决方案
2、文件缓存目录拆分，以支持海量的文件缓存
3、官网再上线：www.phalapi.net

2015-02-26

1、对之前的模型代码、查询类、多级缓存等补充完善单元测试
2、WIKI文档补充UML图示
3、Release 1.1.2 发布

2015-02-24

1、文件缓存、空缓存及多级缓存的引入
2、表数据入口的Model基类，以供NotORM系列子类使用，关键点为主键映射和分表情况
3、结合多级缓存和广义Model，对高成本的数据获取的应对方案
4、WIKI文档补充

2015-02-15

1、年前的更新：三篇文档的编写，到此基础入门只差多级缓存未完成；

2015-02-13

1、代码小酌，重构代码、整理注释，让代码更明了，更统一，更达意；
2、添加工具类，其中有：IP地址获取、随机字符串生成；
3、添加扩展类库：微信开发，并编写相关使用文档；
4、添加扩展类库：phprpc协议支持及WIKI编写；

2015-02-11

1、官网发布上线：http://112.74.107.125/，域名已申请，待绑定：www.phalapi.net；

2015-02-09

1、将原来限制于JSON格式的返回调整成更灵活的组件形式，以便支持JSON、JSONP和测试环境下的格式返回，和扩展项目开发所需要的格式；
2、接口基类的初始化函数PhalApi_Api::initialize()精简名字为init()；
3、全部核心的代码注释中的author追加个人邮箱地址；
4、单元测试完善补充和文档整理；

2015-02-07

1、完善接口调试下SQL的输出、示例和单元测试，以及WIKI文档的编写；
2、日记接口文档的编写；
3、合并master到release，并将版本号更新到1.1.1；

2015-02-04

1、根据质量分析后Sonar提供的报告，整理代码，包括统一的注释、对齐、代码风格、命名规则等；
2、默认服务的注册，有：DI()->request、DI()->response；

2015-02-02 版本1.1.0 一个全新的开始！

此版本在原来的基础上进行了大量的重构和更多的规范统一，主要有：
1、分离框架代码和项目代码，便于以后框架升级；
2、统一全部的入口文件，以便不同的版本、终端、入口和测试环境使用，并隐藏PHP语言实现；
3、框架代码统一从原来的Core_改名为PhalApi_，并且把PhalApi_DI::one()统一为快速函数的写法：DI()；
4、重新界定应用项目的代码目录结构，以包的形式快速开发；
5、全部文档相应更新；
//注意！此版本不兼容旧的写法，如有问题，请与我联系。

2015-02-01

1、正常时，ret返回调整为：200，原来为0；
2、异常统一简化为两大类：客户端非法请求400、服务端运行错误500；
3、日记文件写入重构，并将权限更改为777，以便不同场合下日记写入时的permission denied；
4、单元测试整理；

2015-01-31

1、参数规则的解析，移除不必要的固定类型，以及addslashes转换，单元测试整理；
2、参数规则文档编写：http://my.oschina.net/u/256338/blog/372947

2015-01-29

1、examples代码重新整理，及入门文档同步更新；
2、入口文件的调整；

2015-01-28

1、补充入门开发示例的文档，及相关的测试代码和产品代码，主要是examples；
2、提供接口参数在线查询工具；

2015-01-24

1、PhalApi开源；

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[5.8]-致框架贡献者：加入PhalApi开源指南 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

人们对他们的工作以及他们完成工作的方法很感兴趣，并给它取了一个现在大家众所周知的称呼：集市。
而且当遇到那些开发、维护大量程序库和应用的传统组织所面临的挑战时，它将显得更加自由和狂热。
-- 《架构之美》

5.8.1 感谢加入！

首先，非常感谢您的加入！

(此处应该有掌声~~~)

其次，对于加入后，可以做的事情有很多，下面是其中一部分。你可以选择或者new一个idea出来贡献。

5.8.2 开发PhalApi核心框架 - PhalApi

此部分为PhalApi核心的框架代码的开发和维护，对于此部分，我们要求会高一点，因为这块将需要为每个即将使用PhalApi或者正在使用PhalApi开发的项目负责任。此外，也要考虑到框架的维护和学习成本，以及代码质量（请原谅程序员那一点点代码洁癖）、相关文档的编写和维护等。当然，还要面对各种用户的咨询、各样问题的应对等。

(1)基本的要求

所以，对于此块，建议是资源PHP或者有经验的同学来加入。下面是一些基本的要求：

  • 熟悉Linux系统的基本操作
  • 有丰富的PHP开发经验
  • 架构师或对框架有浓厚的兴趣
  • 推荐有使用PhalApi实际项目的开发经验

(2)简单的引导

如果你决定加入核心框架的开发，以下则是一些简单的引导。

  • 1、下载安装PhalApi
  • 2、翻看WIKI，并可根据文档的说明在本地试运行
  • 3、浏览框架源代码和在线类参考手册
  • 4、更多交流与启程

此过程中，有任何疑问，欢迎随时和我们交流。

5.8.3 开源扩展类库 - PhalApi Library

此部分开放的用户群体包括个人、项目团队、或者其他同学，即只需要扩展是未重复且有实际使用意义的，都可以来分享给大家。因此此块要求比较宽松，不需要像PhalApi核心框架那样和我们有紧密的联系。

如果你决定或者想对此块扩展类库开源，可以：

  • 1、下载安装PhalApi
  • 2、下载安装一个你喜欢的扩展类库
  • 3、查看扩展类型的WIKI
  • 4、开源你的扩展类库

此过程中，有任何疑问，也欢迎随时和我们交流。

5.8.4 基于PhalApi应用框架的开源

因为PhalApi是一个开发框架，而不是一个应用框架，所以你可以在它的基础上再作应用框架的研发。如果可以，我们也希望你能与大家分享你美丽动人的应用框架。

如果是此路径，你可以自己建立项目，自己维护。但请注明或保留PhalApi本来已有的版权信息，谢谢。

5.8.5 不用写代码的开源工作

上面都是针对开发同学的开源选择，当然，对于萌妹子或者文艺青年，我们也有相关的工作适合你。比如：运营、开源团队的建设等等。

5.8.6 捐赠

最后，但不是最重要的。

你也可以通过捐赠来支持我们，从而为开源工作提供强大而有力的保障。

5.8.7 最真实并具有活力的开源参与

据说，人类一切的智慧都蕴藏在这两个词之中：等待和希望。

我们之所以选择投身开源PhalApi和为之不断努力，同时也是充满活力的，是因为我们看到了国内外在移动端App开发下对后台接口的需求量。
虽然我们在接口开发框架上取得了一定的成果，但这仅仅限制在当前和过去。我们仍然需要不断进行创新、完善，以满足更为宽阔的业务开发场景。

明显地，这绝对不是一个人能做得到的。
这依赖于大家对开源框架的认可，更依赖于对开源社区的参考和反馈。

更为真实的开源参考，我觉得不是一时的心血来潮，而是你在使用PhalApi进行实际项目开发时对所遇到问题得到新的解决方案后的分享。
因为，这样后，你对已发现的问题有了更为深入的理解，得出的解决方案也是经过你深思熟虑、多次验证并通过测试、实战而来的。可以说，你已经是这个领域的专家，自然而然大家也更为信任你这套方案。
即使不是最好的，起码也是当前最优的。

简单来说：

  • 1、采用PhalApi或者其他框架进行接口开发
  • 2、对遇到的问题，设计出最优的解决方案并试验之
  • 3、反馈、分享和开源
  • 4、持续维护

希望，若干年后，我们仍能看到一个备具生气、充满活力的PhalApi框架，和更多的衍生品。
也希望，有朝一日，我们能在国际的范围内看到广为推崇的开源框架中，也有来自中国的。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[6.1]-基于接口查询语言的SDK包 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

6.1.1 用一句话来描述接口请求

为了给客户端统一接口请求调用的规范性、流畅性和简单易懂，我们特别为此使用了内部领域特定语言： 接口查询语言 （Api Structured Query Language） 。

从外部DSL的角度来看待接口查询的操作

create

withHost host
withFilter filter
withParser parser

reset   #特别注意：重复查询时须重置请求状态

withService service
withParams paramName1 paramValue1
withParams paramName2 paramValue2
withParams ... ...
withTimeout timeout

request

根据此设计理念，各客户端语言都可以实现此接口请求的操作。

6.1.2 接口查询语言设计理念与示例

(1)文法：create -> with -> request

所用到的查询文法如下（通常从上往下依次操作，顺序不强制）：

操作 | 参数 | 是否必须 | 是否可重复调用 | 作用说明
create | 无 | 必须 | 可以，重复调用时新建一个实例，非单例模式 | 需要先调用此操作创建一个接口实例
withHost | 接口域名 | 必须 | 可以，重复时会覆盖 | 设置接口域名，如：http://demo.phalapi.net/
withFilter | 过滤器 | 可选 | 可以，重复时会覆盖 | 设置过滤器，与服务器的DI()->filter对应，需要实现PhalApiClientFilter接口
withParser | 解析器 | 可选 | 可以，重复时会覆盖 | 设置结果解析器，仅当不是JSON返回格式时才需要设置，需要实现PhalApiClientParser接口
reset | 无 | 通常必须 | 可以 | 重复查询时须重置请求状态，包括接口服务名称、接口参数和超时时间
withService | 接口服务名称 | 通常必选 | 可以，重复时会覆盖 | 设置将在调用的接口服务名称，如：Default.Index
withParams | 接口参数名、值 | 可选 | 可以，累加参数 | 设置接口参数，此方法是唯一一个可以多次调用并累加参数的操作
withTimeout | 超时时间 | 可选 | 可以，重复时会覆盖 | 设置超时时间，单位毫秒，默认3秒
request | 无 | 必选 | 可以，重复发起接口请求 | 最后执行此操作，发起接口请求

(2)JAVA示例

以JAVA版本为例，演示如何调用：
最简单的调用，也就是默认接口的调用：

PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")  //接口域名
       .request();                            //发起请求

通常的调用，即有设置接口服务名称、接口参数和超时：

PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withService("Default.Index")          //接口服务
       .withParams("username", "dogstar")     //接口参数
       .withTimeout(3000)                     //接口超时
       .request();

高级复杂调用，即设置了过滤器、解析器的操作：

PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withService("Default.Index")
       .withParser(new PhalApiClientParserJson()) //设置JSON解析，默认已经是此解析，这里仅作演示
       .withParams("username", "dogstar")
       .withTimeout(3000)
       .request();

6.1.3 更好的建议

(1)不支持面向对象的实现方式

此接口查询的用法是属于基础的用法，其实现与宿主语言有强依赖关系，在不支持面向对象语言中，如javascript，可以使用函数序列的方式，如：

create();
withHost('http://demo.phalapi.net/');
withService('Default.Index');
withParams('username', 'dogstar');
withTimeout(3000);
var rs = request();

(2)封装自己的接口实例

通常，在一个项目里面我们只需要一个接口实例即可，但此语言没默认使用单例模式，是为了大家更好的自由度。
基于此，大家在项目开发时，可以再进行封装：提供一个全局的接口查询单例，并组装基本的接口公共查询属性。

即分两步：初始化接口实例，以及接口具体的查询操作。

如第一步先初始化：

PhalApiClient client = PhalApiClient.create()
     .withHost("http://demo.phalapi.net/")
     .withParser(new PhalApiClientParserJson());

第二步进行具体的接口请求：

PhalApiClientResponse response = client.reset()  #重复查询时须重置
     .withService("Default.Index")
     .withParams("username", "dogstar")
     .withTimeout(3000)
     .request();

这样，在其他业务场景下就不需要再重复设置这些共同的属性（如过滤器、解析器）或者共同的接口参数。

(3)超时重试

当接口请求超时时，统一返回 ret = 408，表示接口请求超时。此时可进行接口重试。

如：

PhalApiClient client = PhalApiClient.create()
     .withHost("http://demo.phalapi.net/")

PhalApiClientResponse response = client.request();

if (response.getRet() == 408) {
     response = client.request(); //请求重试
}

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[6.2]-SDK包（JAVA版） | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

6.2.1 使用说明

将框架目录下的 ./SDK/JAVA/net 目录中的全部代码拷贝到项目里面即可使用。如：

6.2.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

//FullscreenActivity.java
import net.phalapi.sdk.*;

然后，准备一个子线程调用，并在此线程中实现接口请求：

    /**
     * 网络操作相关的子线程
     */
    Runnable networkTask = new Runnable() {

        @Override
        public void run() {
            // TODO
            // 在这里进行 http request.网络请求相关操作

            PhalApiClient client = PhalApiClient.create()
                        .withHost("http://demo.phalapi.net/");

            PhalApiClientResponse response = client
                        .withService("Default.Index")
                        .withParams("username", "dogstar")
                        .withTimeout(3000)
                        .request();

            String content = "";
            content += "ret=" + response.getRet() + "\n";
            if (response.getRet() == 200) {
                try {
                    JSONObject data = new JSONObject(response.getData());
                    content += "data.title=" + data.getString("title") + "\n";
                    content += "data.content=" + data.getString("content") + "\n";
                    content += "data.version=" + data.getString("version") + "\n";
                } catch (JSONException ex) {

                }
            }
            content += "msg=" + response.getMsg() + "\n";

            Log.v("[PhalApiClientResponse]", content);

            Message msg = new Message();
            Bundle data = new Bundle();
            data.putString("value", content);
            msg.setData(data);
            handler.sendMessage(msg);
        }
    };

接着，实现线程回调的hander：

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Bundle data = msg.getData();
            String val = data.getString("value");
            Log.i("mylog", "请求结果为-->" + val);
            // TODO
            // UI界面的更新等相关操作
        }
    };

最后，在我们需要的地方启动：

    View.OnClickListener mDummyBtnClickListener = new View.OnClickListener() {

        @Override
        public void onClick(View arg0) {
            // 开启一个子线程，进行网络操作，等待有返回结果，使用handler通知UI
            new Thread(networkTask).start();

            // ....
        }
    };

再一次调用和异常请求

当我们需要再次使用同一个接口实例进行请求时，需要先进行重置，以便清空之前的接口参数，如：

//再一次请求
response = client.reset() //重置
        .withService("User.GetBaseInfo")
        .withParams("user_id", "1")
        .request();

content = "";
content += "ret=" + response.getRet() + "\n";
if (response.getRet() == 200) {
    try {
        JSONObject data = new JSONObject(response.getData());
        JSONObject info = new JSONObject(data.getString("info"));

        content += "data.info.id=" + info.getString("id") + "\n";
        content += "data.info.name=" + info.getString("name") + "\n";
        content += "data.info.from=" + info.getString("from") + "\n";
    } catch (JSONException ex) {

    }
}
content += "msg=" + response.getMsg() + "\n";

Log.v("[PhalApiClientResponse]", content);

异常情况下，即ret != 200时，将返回错误的信息，如：

//再来试一下异常的请求
response = client.reset()
        .withService("XXX.XXXX")
        .withParams("user_id", "1")
        .request();

content = "";
content += "ret=" + response.getRet() + "\n";
content += "msg=" + response.getMsg() + "\n";

Log.v("[PhalApiClientResponse]", content);

6.2.3 运行效果

运行后，查询log，可以看到：

可以注意到，在调试模式时，会有接口请求的链接和返回的结果日记，如：

10-17 07:40:55.268: D/[PhalApiClient requestUrl](1376): http://demo.phalapi.net/?service=User.GetBaseInfo&user_id=1
10-17 07:40:55.364: D/[PhalApiClient apiResult](1376): {"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","from":"oschina"}},"msg":""}

6.2.4 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

class MyFilter implements PhalApiClientFilter {

        public void filter(String service, Map<String, String> params) {
            //TODO ...
        }
}

然后设置过滤器：

PhalApiClientResponse response = PhalApiClient.create()
           .withHost("http://demo.phalapi.net/")
           .withFilter(new MyFilter())
           // ...
           .request();

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，可以重新实现此接口。

当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。

6.2.5 特别注意：Android之NetworkOnMainThreadException异常

由于此SDK包是使用HttpURLConnection发起请求时，所以在主线程调用时会触发NetworkOnMainThreadException异常，具体可参考： Android之NetworkOnMainThreadException异常

所以，需要使用子线程来发起请求，或者重新继承改用异步的请求。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[6.3]-SDK（PHP版） | PhalApi - PHP轻量级开源接口框架

  • 中文版
  • English
  • 官方交流群：421032344
  • 社区
  • 下载
  • 文档
  • 类参考手册
  • 在线体验
  • 关于我们

上一章 文档首页 下一章

6.2.1 使用说明

将框架目录下的 ./SDK/PHP/PhalApiClient 目录中的全部代码拷贝到项目里面即可使用。

6.2.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

<?php

require_once dirname(__FILE__) . '/PhalApiClient.php';

$rs = PhalApiClient::create()
    ->withHost('http://demo.phalapi.net/')
    ->withService('Default.Index')
    ->withParams('username', 'dogstar')
    ->withTimeout(3000)
    ->request();

var_dump($rs->getRet());
echo "\n";
var_dump($rs->getData());
echo "\n";
var_dump($rs->getMsg());

6.2.3 运行效果

运行后，可以看到：

int(200)

array(4) {
    ["title"]=>
    string(12) "Hello World!"
    ["content"]=>
    string(36) "dogstar您好，欢迎使用PhalApi！"
    ["version"]=>
    string(5) "1.2.1"
    ["time"]=>
    int(1444925238)
}

string(0) ""

6.2.4 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

<?php

class MyFilter implements PhalApiClientFilter {

        public function filter($service, array &$params) {
            //TODO ...
        }
}

然后设置过滤器：

<?php

               $rs = PhalApiClient.create()
                           .withHost("http://demo.phalapi.net/")
                           .withFilter(new MyFilter())
                           // ...
                           .request();

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，可以重新实现此接口。

当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。

上一章 文档首页 下一章

PhalApi是一个PHP轻量级后台接口开发框架。
我们致力于将PhalApi维护成像恒星一样：不断更新，保持生气；为接口负责，为开源负责！让后台接口开发更简单！

©2015 PhalApi All Rights Reserved. 粤ICP备15028808号

在这里，特别感谢

  • 开源中国
  • Phalcon
  • PHPUnit
  • ThoughtWorks
支付宝 开源捐赠：chanzonghuang@gmail.com

分享到： 新浪微博 更多


[6.3]-SDK包（PHP版） | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

6.3.1 使用说明

将框架目录下的 ./SDK/PHP/PhalApiClient 目录中的全部代码拷贝到项目里面即可使用。

6.3.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

<?php

require_once dirname(__FILE__) . '/PhalApiClient.php';

$client = PhalApiClient::create()
        ->withHost('http://demo.phalapi.net/');

$rs = $client->reset()
    ->withService('Default.Index')
    ->withParams('username', 'dogstar')
    ->withTimeout(3000)
    ->request();

var_dump($rs->getRet());
echo "\n";
var_dump($rs->getData());
echo "\n";
var_dump($rs->getMsg());

6.3.3 运行效果

运行后，可以看到：

int(200)

array(4) {
    ["title"]=>
    string(12) "Hello World!"
    ["content"]=>
    string(36) "dogstar您好，欢迎使用PhalApi！"
    ["version"]=>
    string(5) "1.2.1"
    ["time"]=>
    int(1444925238)
}

string(0) ""

6.3.4 更多调用

当需要重复调用时，需要先进行 重置操作 ，如：

//one more time
$rs = $client->reset()
    ->withService("User.GetBaseInfo")
    ->withParams("user_id", "1")
    ->request();

var_dump($rs->getRet());
echo "\n";
var_dump($rs->getData());
echo "\n";
var_dump($rs->getMsg());

当请求有异常时，返回的 ret!= 200，如：

//illegal request
$rs = $client->reset()
    ->withService("XXX.XXXXX")
    ->withParams("user_id", "1")
    ->request();

var_dump($rs->getRet());
echo "\n";
var_dump($rs->getData());
echo "\n";
var_dump($rs->getMsg());

以上的输出为：

--------------------
int(200)

array(3) {
  ["code"]=>
  int(0)
  ["msg"]=>
  string(0) ""
  ["info"]=>
  array(3) {
    ["id"]=>
    string(1) "1"
    ["name"]=>
    string(7) "dogstar"
    ["from"]=>
    string(7) "oschina"
  }
}

string(0) ""

--------------------
int(400)

array(0) {
}

string(45) "非法请求：接口服务XXX.XXXXX不存在"

6.3.5 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

<?php

class MyFilter implements PhalApiClientFilter {

        public function filter($service, array &$params) {
            //TODO ...
        }
}

然后设置过滤器：

<?php

$rs = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withFilter(new MyFilter())
       // ...
       .request();

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，可以重新实现此接口。

当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[6.4]-SDK包（object-c版） | PhalApi - PHP轻量级开源接口框架

  • 中文版
  • English
  • 官方交流群：421032344
  • 社区
  • 下载
  • 文档
  • 类参考手册
  • 在线体验
  • 关于我们

上一章 文档首页 下一章

该文档由 @Aevit 提供

6.4.1 使用说明

将框架目录下的 ./SDK/Objective-C/ 目录中的全部代码拷贝到项目里面即可使用。如：

请求方式，可以使用系统的NSURLSession，或第三方的网络请求库等
这里我们使用第三方的AFNetworking，进行二次封装（https://github.com/AFNetworking/AFNetworking）
如需使用其他网络请求方式（如NSURLSession），请继承自PhalApiClient，重写request方法即可，可参考AFNPhalApiClient

6.4.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

#import "AFNPhalApiClient.h"

然后，准备按以下方法实现接口请求即可：

// 待POST的参数
NSDictionary *params = @{@"demo_key_1": @"your_key", @"demo_key_2": @"1.0"};

// 使用AFNPhalApiClient
[[[[[AFNPhalApiClient sharedClient] withHost:@"http://api1.aevit.xyz/"] withService:@"Default.Index"] withParams:params] requestWithFormDataBlock:^(id formData) {
    // 如需上传文件（图片等），请参照如下格式
    [formData appendPartWithFileData:UIImageJPEGRepresentation([UIImage imageNamed:@"head.JPG"], 1) name:@"file" fileName:@"image.jpg" mimeType:@"image/jpeg"];
} completeBlock:^(id resultObject) {
    PALog(@"resultObject: %@", resultObject);
} failureBlock:^(NSError *error) {
    PALog(@"error: %@", error);
}];

// 打印url查看
PALog(@"total url: %@", [[AFNPhalApiClient sharedClient] printTotalUrlStr]);

6.4.3 运行效果

运行后，查询log，可以看到：

可以注意到，调用完成后，会有接口请求的链接和返回的结果，如：

2015-10-19 18:28:01.503 PhalApiClientDemo[23161:1199740] total url: http://api1.aevit.xyz/?service=Default.Index&amp;demo_key_2=1.0&amp;demo_key_1=your_key

2015-10-19 18:28:01.536 PhalApiClientDemo[23161:1199740] resultObject: {
    data =     {
        content = "PHPer\U60a8\U597d\Uff0c\U6b22\U8fce\U4f7f\U7528PhalApi\Uff01";
        time = 1445250481;
        title = "Hello World!";
        version = "1.2.1";
    };
    msg = "";
    ret = 200;
}

6.4.4 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

@interface MyFilter : PhalApiClientFilter
@end

@implementation PhalApiClientFilter
/**
 *  接口过滤器
 *  可用于接口签名生成
 *
 *  @param service 接口服务名称
 *  @param params  接口参数，注意是mutable变量，可以直接修改
 */
- (void)filter:(NSString*)service params:(NSMutableDictionary*)params {
    // 在此对接口进行过滤
}
@end

然后设置过滤器：

[[[[[[AFNPhalApiClient sharedClient] withHost:BASE_URL] withService:@"Default.Index"] withParams:params]
      withFilter:[MyFilter new]] // filter
     requestWithFormDataBlock:^(id formData) {
    } completeBlock:^(id resultObject) {
    } failureBlock:^(NSError *error) {
}];

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，如XML，请上google搜索“AFNetworking XML”相关资料即可）。

上一章 文档首页 下一章

PhalApi是一个PHP轻量级后台接口开发框架。
我们致力于将PhalApi维护成像恒星一样：不断更新，保持生气；为接口负责，为开源负责！让后台接口开发更简单！

©2015 PhalApi All Rights Reserved. 粤ICP备15028808号

在这里，特别感谢

  • 开源中国
  • Phalcon
  • PHPUnit
  • ThoughtWorks
支付宝 开源捐赠：chanzonghuang@gmail.com

分享到： 新浪微博 更多


[6.4]-SDK包（Objective-C版） | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

该文档由 @Aevit 提供

6.4.1 使用说明

将框架目录下的 ./SDK/Objective-C/ 目录中的全部代码拷贝到项目里面即可使用。如：

请求方式，可以使用系统的NSURLSession，或第三方的网络请求库等
这里我们使用第三方的AFNetworking，进行二次封装（https://github.com/AFNetworking/AFNetworking）
如需使用其他网络请求方式（如官方的NSURLSession），请继承自PhalApiClient，重写request方法即可，可参考AFNPhalApiClient

6.4.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

#import "AFNPhalApiClient.h"

然后，准备按以下方法实现接口请求即可：

// 待POST的参数
NSDictionary *params = @{@"demo_key_1": @"your_key", @"demo_key_2": @"1.0"};

// 使用AFNPhalApiClient
[[[[[AFNPhalApiClient sharedClient] withHost:@"http://api1.aevit.xyz/"] withService:@"Default.Index"] withParams:params] requestWithFormDataBlock:^(id formData) {
    // 如需上传文件（图片等），请参照如下格式
    [formData appendPartWithFileData:UIImageJPEGRepresentation([UIImage imageNamed:@"head.JPG"], 1) name:@"file" fileName:@"image.jpg" mimeType:@"image/jpeg"];
} completeBlock:^(id resultObject) {
    PALog(@"resultObject: %@", resultObject);
} failureBlock:^(NSError *error) {
    PALog(@"error: %@", error);
}];

// 打印url查看
PALog(@"total url: %@", [[AFNPhalApiClient sharedClient] printTotalUrlStr]);

6.4.3 运行效果

运行后，查询log，可以看到：

可以注意到，调用完成后，会有接口请求的链接和返回的结果，如：

2015-10-19 18:28:01.503 PhalApiClientDemo[23161:1199740] total url: http://api1.aevit.xyz/?service=Default.Index&amp;demo_key_2=1.0&amp;demo_key_1=your_key

2015-10-19 18:28:01.536 PhalApiClientDemo[23161:1199740] resultObject: {
    data =     {
        content = "PHPer\U60a8\U597d\Uff0c\U6b22\U8fce\U4f7f\U7528PhalApi\Uff01";
        time = 1445250481;
        title = "Hello World!";
        version = "1.2.1";
    };
    msg = "";
    ret = 200;
}

6.4.4 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

@interface MyFilter : PhalApiClientFilter
@end

@implementation PhalApiClientFilter
/**
 *  接口过滤器
 *  可用于接口签名生成
 *
 *  @param service 接口服务名称
 *  @param params  接口参数，注意是mutable变量，可以直接修改
 */
- (void)filter:(NSString*)service params:(NSMutableDictionary*)params {
    // 在此对接口进行过滤
}
@end

然后设置过滤器：

[[[[[[AFNPhalApiClient sharedClient] withHost:@"http://api1.aevit.xyz/"] withService:@"Default.Index"] withParams:params]
      withFilter:[MyFilter new]] // filter
     requestWithFormDataBlock:^(id formData) {
    } completeBlock:^(id resultObject) {
    } failureBlock:^(NSError *error) {
}];

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，如XML，请上google搜索“AFNetworking XML”相关资料即可。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[6.5]-SDK包（javascript版） | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

todo

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[6.6]-SDK包（Ruby版） | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

推荐使用Gem

推荐使用：PhalApi SDK for Ruby 。

6.6.1 使用说明

将框架目录下的 ./SDK/Ruby/PhalApiClient 目录中的全部代码拷贝到项目里面即可使用。

6.6.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

#demo.rb

require_relative './PhalApiClient/phalapi_client'

a_client = PhalApi::Client.create.withHost('http://demo.phalapi.net')
a_response = a_client.withService('Default.Index').withParams('username', 'dogstar').withTimeout(3000).request()

puts a_response.ret, a_response.data, a_response.msg

6.6.3 运行效果

运行后，可以看到：

200
{"title"=>"Hello World!", "content"=>"dogstar您好，欢迎使用PhalApi！", "version"=>"1.2.1", "time"=>1445741092}

6.5.4 更多调用

当需要重复调用时，需要先进行 重置操作 ，如：

#one more time
a_response = a_client.reset \
    .withService("User.GetBaseInfo") \
    .withParams("user_id", "1") \
    .request

puts a_response.ret, a_response.data, a_response.msg

当请求有异常时，返回的 ret!= 200，如：

#illegal request
a_response = a_client.reset.withService('XXXX.noThisMethod').request

puts a_response.ret, a_response.data, a_response.msg

以上的输出为：

--------------------
400
非法请求：接口服务XXXX.noThisMethod不存在

6.6.5 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

class MyFilter < PhalApi::ClientFilter
        def filter(service, *params)
            #TODO ...
        end
}

然后设置过滤器：

a_response = PhalApi::Client.create.withHost('http://demo.phalapi.net') \
       .withFilter(MyFilter.new) \
       # ... \
       .request

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，可以重新实现此接口。

当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。

6.6.6 一如既往的单元测试

最后，附一张单元测试运行的效果图：

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.1]-phalapi-入门篇1(简单介绍以及环境搭建) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-入门篇1(简单介绍以及环境搭建)

前言

先在这里感谢phalapi框架创始人@dogstar，为我们提供了这样一个优秀的开源框架。

在进行项目接口的开发过程中，笔者在寻找一个方便针对API开发的框架时，发现了PhalApi。经过了短时间的尝试，发现PhalApi确实很便捷，效率很高。 之后就把PhalApi运用到正式的项目上。而且在后面的项目仍一直使用。其间，也和很多希望了解PhalApi框架的朋友做过很多交流。为此，决定写一篇关于这个框架的教程。一方面是为了帮助一些初学者能够快速地了解PhalApi，另一方面是希望把自己在实际运用中总结出的一些小技巧分享出来。

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 简单的介绍

官网上面有一句话让我特别有感受--接口，从简单开始！

怎样的感受呢，在最近进行一个项目开发的时候，因为项目比较紧急需要快速地开发。从确定完需求以及接口之后，借助PhalApi，我仅用了8个小时就完成项目接口的开发。整个项目的接口差不多20来个，从这点可以看出，如果PhalApi框架用得熟练的话，效率将会提高更多。

其实，接口最关心的就是性能。通过测试下来，Phalapi消耗相对比较小，引入文件在10个左右，空框架返回2核2G并发点击数在1700左右。很是给力。在2核2G正常请求数据库接口相对于一套完整的业务。平均下来最快的1000并发，最慢的600并发。所以我觉得性能方面，PhalApi完全没有问题(压力测试使用的是Loadrunner，ab压力基本是Loadrunner的3倍不太清楚为什么)。

引用:

勿忘初心，方得始终

虽然现在这个社会的节奏很快，但如果有时间时，我们不妨静下来，稍微地思考一下，然后你会发现：慢慢来，比较快。

PhalApi 的初衷:

我们之所以开源这个框架，是因为我们想致力于提供可以快速进行开发后台接口的框架，并且通过这个框架分享更多开发的最佳实践、原则和模式！这就是我们的初心，也希望我们能一直坚持下去。

PhalApi命名的来源

得益于开源社区，现在有很多优秀的开源框架。确实有很多框架值得我们去学习和使用，其中高性能的Phalcon框架和可视化代码评审工具Phabricator都是相当令人敬佩和认可的。前者在思想、模式和原则上有很好的文档说明；后者在包管理、命名写法等都非常好地体现了代码重用。受此两个框架的影响，故取名为：PhalApi。

通过PhalApi，我们希望可以

支撑轻量级项目后台接口的快速开发；阐明如何进行框架设计、对应复杂领域业务、支撑海量服务等，以及分享好的思想、技巧工具和最佳实践。最后，争取成为国内主流的PHP接口开发框架。

2. 环境搭建

在这里推荐使用(我后面的配置等都会基于以下环境)lnmp:

Liunx+Nginx+php5.4以上+mysql

我使用的是centos6.5

有三种方式配置环境：

1.手动编译安装(网上资料也挺多的，但是容易出现一些依赖性的问题，新手不推荐。)。

2.yum或apt-get安装(yum安装无法指定目录，都会装到/etc下面去，不方便管理，但是要玩liunx必须要会。)。

3.lnmp一键安装工具(推荐：但是这个推荐是基于上面几种方式都尝试过的情况下，这个安装方式基于--编译安装。)。

我推荐一个lnmp一键配置软件:

官网:OneinStack – 一键PHP/JAVA安装工具: http://oneinstack.com/

因为官网介绍得比较详细，在这里就不再多做说明。其余几种方式教程都比较多也不做说明。

那不了解liunx怎么办？wampserver在windows下还是比较好用的。

3. 总结

最后，希望大家在通读PhalApi官方文档后，再接着看下面的文章，会更容易理解。在这一套教程中，我会以构建一个完整的项目为基础，拓展开来讲解一些框架的机制。大家可以跟着一起做，相信能够收获得更多。

注：笔者能力有限，有说的不对的地方，希望大家能够指出。也希望多多交流！

官网QQ交流群:421032344 欢迎大家的加入!

文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.10]-phalapi-进阶篇4(notorm进阶以及事务操作) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇4(notrom进阶以及事务操作)

前言

先在这里感谢phalapi框架创始人@dogstar为我们提供了这样一个优秀的开源框架。

写本篇教程的起因是因为在交流的时候有位童鞋提出了fetchPairs有BUG，之前一直没有仔细了解过notorm的细节，借此机会把notorm中的一些方便快捷的操作，以及notorm中有关事务的操作和使用，具体地作一下分析。 附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 进一步了解notorm

notorm中很多操作在PhalApi官方文档中并没有非常详细的介绍，这里把一些常用到的notorm函数作一下相关的介绍。

1.1 fetchPairs

fetchPairs的使用和fetchAll相似但不尽相同，fetchPairs需要传递一个参数,这个参数是字符串类型的key值(比如：name 、id),当参数传递之后会出现这样的对比效果：

DI()->notorm->user->fetchPairs('name');

DI()->notorm->user->fetchAll('name');

大家有看到区别吗？

嗯！是fetchPairs在返回的时候把在之前定义的key值放到了返回数组的key中。

提示:在老版本中，使用过程中 Result.php 会有一句 821行的报错，需要把

$values = array_values(iterator_to_array($row));
改成
$values = array_values($row);

1.2 lock

lock操作是一种锁的行为，对整表进行锁定，在当前用户释放锁之前此表不能再被操作。lock接受一个bool参数，默认是true，即锁表，使用如下：

$userdb = DI()->notorm->user->lock(); //锁定表
$rs     = $userdb->fetchAll();

生成的sql语句如下：

SELECT * FROM user FOR UPDATE;

1.3 加操作

在很多时候，我们会遇到数据库表里面的某个值需要+1操作，我们不能简单地在update的时候写入array('key' => 'key+1')，因为在解析sql的时候，key+1 会带上引号作为一个字符串被处理，所以，这样的操作并没有达到我们想要的效果，当然，这是有解决方法的。具体操作如下：

DI()->notorm->user->where('id', 1)->update(array('sum' => new NotORM_Literal("sum + 1")));

这样就可以生成不带引号的操作了：

UPDATE user SET phone = sum + 1 WHERE (id = 1);

1.4 group

group是去重操作，在notorm里面也有封装，我们可以使用group来统计比如有多少同名的用户等一些统计操作，或者是找出库里面不重复的城市名等。

DI()->notorm->user->select('name , count(*) as count')->group('name')->fetchAll();

会获得如下结果

1.5 快速函数sum，count，max，min

在我们使用notorm快速函数的过程中,虽然此类函数使用频率不高，但是使用起来却是很方便：

  return DI()->notorm->user->sum('id');  //做加法
  return DI()->notorm->user->max('id');  //获取这个key中最大的值
  return DI()->notorm->user->min('id');  //获取这个key中最小的值
  return DI()->notorm->user->count();    //统计一共几条数据

2. 事务操作

其实，很大一部分使用事务的原因是因为害怕在并发的情况下导致对数据库造成的重复操作，比如如下场景：

两个管理者对同一应用进行审核。他们同一时间查询到应用的状态都是未审核，然后都进行了审核操作，这时，返回的审核操作状态是成功的。但这时，会有一个管理者会发现审核状态和自己的审核结果不同，这就有问题了。大部分在解决此类问题时，都会考虑使用数据库的事务操作。其实，对于事务操作，我的建议是：能不用尽量不用。我简单聊一下我的几个观点：

  1. 是否真的那么重要？对于上面的问题，也许乍一看觉得：这怎么可能呢？但是仔细一想，这两个操作都是属于两个管理者的正常操作。对于业务来说，我认为是没有问题的。当然这是因为影响不大；如果是付款，一个订单从两个地方同时付款，然后都发现是未付款，然后都进行了付款，这种业务就必须有处理方式了，所以是否使用事务要看业务是否非常重要。

  2. 概率是怎么样的？对于一个程序来说，两个请求刚好通过了查询过程，并一同到了修改的操作，不管是什么场景，发生这种并发冲突的可能性是极其微小的，我觉得几乎可以忽略(当然前提是重要性没有那么的高，我们不能 以偏概全 )。

  3. 曲线救国，其实，我们可以使用其他方式避免这类问题。我们可以把我们需要验证条件加入到Update的条件中，这样两条语句终归会有一条语句执行失败。

notorm中使用到的事务操作方式：

notorm提供了常规的事务操作如下：

 //第一步：先指定待进行事务的数据库（通过获取一个notorm表实例来指定；否则会提示：PDO There is no active transaction）
 $user = DI()->notorm->user;
 //第二步：开启事务开关（此开关会将当前全部打开的数据库都进行此设置）
 DI()->notorm->transaction = 'BEGIN';

 //第三步：进行数据库操作
 $user->insert(array('name' => 'test3',));
 $user->insert(array('name' => 'test4',));

 //第四：提交/回滚
 DI()->notorm->transaction = 'COMMIT';
 //DI()->notorm->transaction = 'ROLLBACK';

还有一种处理方式就是在操作之前进行lock锁表

$user = DI()->notorm->user;
$user->lock();
$user->insert(array('name' => 'test3',));
$user->insert(array('name' => 'test4',));

这两种方式大家可以自行取舍。

注:在phalapiV1.31版本后有自带提供事务操作可参考文档。

3. 总结

在本小节中，对于notorm中的一些特别的封装函数进行了介绍，对于事务提出了自己的看法和观点以及使用notorm的解决方案。希望看了这篇教程的童鞋，在日常开发中对你有所帮助。

注：笔者能力有限，有说的不对的地方，希望大家能够指出，也希望多多交流！

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.10]-phalapi-进阶篇4(notrom进阶以及事务操作) | PhalApi - PHP轻量级开源接口框架

  • 中文版
  • English
  • 官方交流群：421032344
  • 社区
  • 下载
  • 文档
  • 类参考手册
  • 在线体验
  • 关于我们

phalapi-进阶篇4(notrom进阶以及事务操作)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

写本篇教程的起因是在于在交流的时候有位童鞋提出了fetchPairs有BUG,原来一直没有仔细的了解过notorm的细节,趁这次机会主要把notorm之中的一些方便快捷的操作简单的说明一下,以及对与事务操作做一些我自己的简介以及使用notorm如何实现.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 进一步了解notorm

在notorm有很多方便并没有在PhalApi官方文档中有并没有非常详细的介绍过,在这里把一些可能会用到的notorm函数坐下介绍以及功能

1.1 fetchPairs

fetchPairs的使用和fetchAll相似但是他有一个特别之处,fetchPairs需要传递一个参数,这个参数是字符串的key值(比如 name id)当传递之后会出现这样的对比效果

DI()->notorm->user->fetchPairs('name');

DI()->notorm->user->fetchAll('name');

大家有看到区别吗?

嗯!是在返回的时候把在之前定义的key值放到了返回数组的key中

提示:在老版本中在使用过程中 Result.php 会有一句 821行的报错需要把

$values = array_values(iterator_to_array($row));
改成
$values = array_values($row);

1.2 lock

lock操作是一种锁的行为,对于整表进行锁定,在当前用户释放锁之前此表不能在被操作,lock接受一个bool参数,默认是true锁表,使用如下

$userdb = DI()->notorm->user->lock(); //锁定表
$rs     = $userdb->fetchAll();

生成的sql语句如下:

SELECT * FROM user FOR UPDATE;

1.3 加操作

在很多时候我们会用到,数据库里面某个值+1或者是其他,我们不能在update的时候写入array('key' => 'key+1')因为在解析sql的时候 key+1 会带上引号作为一个字符串处理,当然是有解决方法的,如下

DI()->notorm->user->where('id', 1)->update(array('sum' => new NotORM_Literal("sum + 1")));

这样就可以生成不带引号的操作了

UPDATE user SET phone = sum + 1 WHERE (id = 1);

1.4 group

group是去重操作,在notorm里面也封装的有,我们可以使用group用于统计有多少同名的用户等一些统计操作,或者是找出库里面不重复的城市名

DI()->notorm->user->select('name , count(*) as count')->group('name')->fetchAll();

会获得如下结果

1.5 快速函数sum,count,max,min

在我们使用过程中虽然使用这类操作不是很多,但是使用notorm的快速函数依然感觉很方便,

  return DI()->notorm->user->sum('id');  //做加法
  return DI()->notorm->user->max('id');  //获取这个key中最大的值
  return DI()->notorm->user->min('id');  //获取这个key中最小的值
  return DI()->notorm->user->count();    //统计一共几条数据

2. 事务操作

其实很大一部分使用事务的目的是应为害怕并发的情况下导致,对数据库造成重复的操作,比如如下场景:

应用审核,有两个管理者对同一个应用审核,一个成功一个失败,然后是同时请求过来的,一起到了查询应用状态发现都是未审核,然后都去进行了审核操作,这个时候两个操作返回的都是审核成功,但是有一个人会看到审核状态和自己的审核结果不同,这个就是问题了,大部分解决此类问题时都会考虑到使用数据库事务操作,其实对于事务操作我的建议是能不用尽量不要使用,我简单聊一下我的几个观点

  1. 是否真的那么重要,对于上面的问题也许乍一看上去感觉,这样怎么可以呢,但是仔细想一想,这两个操作都是属于我们管理者的正常的操作,真正对于业务来说我认为是没有问题的,当然这是因为影响不大,如果是付款,一个订单从两个地方同时付款然后都发现是未付款,然后都进行了付款,这种业务就必须有处理方式了,所以是否使用事务要看业务是否非常需求

  2. 概率是怎么样的,对于一个程序来说,两条请求刚好通过了查询过程,一同到了修改的时候,不管是什么场景发生这种并发冲突的可能性是极其微小的,我觉得几乎可以忽略(当然前提是重要性没有那么的高,我们不能以偏概全)

  3. 曲线救国其实我们可以使用其他方式避免这种问题,我们可以把我们需要验证条件加入到Update的条件中这样两条语句总归会有一条语句执行失败

那么我们使用notorm中解决此类需要用到事务的问题呢

notorm提供了常规的事务操作如下

 //第一步：先指定待进行事务的数据库（通过获取一个notorm表实例来指定；否则会提示：PDO There is no active transaction）
 $user = DI()->notorm->user;
 //第二步：开启事务开关（此开关会将当前全部打开的数据库都进行此设置）
 DI()->notorm->transaction = 'BEGIN';

 //第三步：进行数据库操作
 $user->insert(array('name' => 'test3',));
 $user->insert(array('name' => 'test4',));

 //第四：提交/回滚
 DI()->notorm->transaction = 'COMMIT';
 //DI()->notorm->transaction = 'ROLLBACK';

还有一种处理方式就是在操作之前进行lock锁表

$user = DI()->notorm->user;
$user->lock();
$user->insert(array('name' => 'test3',));
$user->insert(array('name' => 'test4',));

这两种方式大家可以自行取舍

注:在phalapiV1.31版本后有自带提供事务操作可参考文档

3. 总结

在本小节中,对于notorm之中的一些特别的封装函数进行了一系列的介绍,以及对于事务提出了我的看法以及观点,和使用notorm的解决方案,希望看了这篇教程的童鞋对你的日常开发有所帮助

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi是一个PHP轻量级后台接口开发框架。
我们致力于将PhalApi维护成像恒星一样：不断更新，保持生气；为接口负责，为开源负责！让后台接口开发更简单！

©2015 PhalApi All Rights Reserved. 粤ICP备15028808号

在这里，特别感谢

  • 开源中国
  • Phalcon
  • PHPUnit
  • ThoughtWorks
支付宝 开源捐赠：chanzonghuang@gmail.com

分享到： 新浪微博 更多


[7.11]-phalapi-进阶篇5(数据库读写分离) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇5(数据库读写分离以及多库使用)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

读写分离是我们常用的一种解决方案,它可以解决大量读操作的时候数据库瓶颈的问题,我们在真正开发一个项目的过程中可能会遇一个接口或者一个操作中需要用到多个数据库操作的时候怎么办,今天的教程就从这个两个问题出来,来聊聊使用phalapi怎么解决这个问题.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 读写分离

在我们的生活中无时无刻不面临着并发的问题,比如下班高峰交通问题,人们用了红绿灯来解决的一部分问题(但是很痛苦这个就和查询的时候阻塞一样的,10秒做update操作,10秒做select操作一样的),那么人们有想到了一个方法高架,高架的路线是和正常的路线一致的(数据相同) ,高架分两条路而且一条路上的车不能到另外一条路上(读写分离) 这样就解决了我们这类常规并发的问题

其实看起来上面这种解决方案是比较好的其实并不是这样的,有两条路但是其实它的宽度是不一样的,我们这里把一个叫做写路,一个叫做读路,我们如果要做一个读路并且把他成倍的变宽是一件很简单的事情(也就是多从库),但是我们要建立一条写路可以,但是我们要把这条写路变宽是比较困难的,我们一般不这样做.

1.1 mysql主从配置

我是使用了阿里云的多读库服务,这里引用一下百度经验的mysql主从处理

----------------------------------------------------------------------------------------------------

MYSQL【一台主机多个实例的主从配置】

1、建立账号

进入mysql数据库，创建一个用于从库备份的账号

mysql>GRANT REPLICATION SLAVE ON *.* TO 'slave_test'@'10.19.194.57' IDENTIFIED BY 'Password@123456';

mysql>FLUSH PRIVILEGES;   ---刷新

该命令详情参考grant创建用户命令

2、如果已经有一个实例在运行（因为开始已经安装了mysql，所以有一个默认的），那么再启动一个实例，方法如下：

（1）shell>cd /usr/local/mysql/

（2）shell>mkdir -pv /data/mysql2

（3）shell>chown -R mysql.mysql /data/mysql2

（4）shell>./scripts/mysql_install_db  --user=mysql  --datadir=/data/mysql2  这个为另外一个目录，该步骤意味初始化目录，并创建一个实例

3、配置/etc/my.cnf，配置方法如下

[mysqld_multi]

mysqld = /usr/local/mysql/bin/mysqld_safe

mysqladmin = /usr/local/mysql/bin/mysqladmin

user = root

[mysqld] ## 本来mysql配置

server-id = 1

port = 3306

socket = /data/mysql/mysql.sock

datadir = /data/mysql

pid-file = /data/mysql/wzj.pid

log-error = /data/mysql/wzj.err

log-bin = /data/mysql/log/bin-log

log-bin-index = /data/mysql/log/mysql-bin.index

user = mysql

[mysqld2]  ##创建实例配置

server-id = 2

port = 3307

socket = /tmp/mysql2.sock

datadir = /data/mysql2/                         //mysql2存储数据库的地方，也就是实例

pid-file = /data/mysql2/mysql2.pid       //mysql2的pid文件记录

log-error = /data/mysql2/mysql2.err     //为错误日记文件的地方

user = mysql

#master-host =10.19.194.57

#master-user ='wzj_slave_test'

#master-pass ='Password@123456'

#master-port =3306

#master_connect_retry=60

#replicate-do-db =mysql_wzj

#replicate-ignore-db=mysql--------该段已注释，因为新版本中，需要用CHANGE MASTER TO 命令去设置

【注意】：以上文件的目录，皆为系统创建，并非手动去创建，设置配置文件后，重启mysql，文件就会自动创建

4、重启，然后可以使用msyql_multi命令启动实例，再查看一下master主库的状态，其中Binlog_Do_DB表示要备份的数据库，Binlog_Ignore_DB表示不备份的数据库

shell>/etc/rc.d/init.d/mysqld start/stop/restart

shell>/usr/local/mysql/bin/mysqld_multi start 1-2  启动实例 （1-2表示启动1、2两个实例）

shell>mysql master： mysql -P 3306 -u root -p -S /data/mysql/mysql.sock (密码：1)

mysql>show master status; or show master status \G;   显示主库的状态，其中File 和Position 数值要记住，下面的mysql语句要用到，用于设置从库

5、进入从库，并设置从库的一些参数

shell>slave:mysql -P 3307 -u root -p -S /tmp/mysql2.sock （密码：回车）   ---另外开个远程

mysql>CHANGE MASTER TO

mysql> MASTER_HOST='10.19.194.57',  ---主库的地址

mysql>MASTER_USER='wzj_slave_test',  ---主库用户

mysql>MASTER_PASSWORD='Password@123456',   ---主库的密码

mysql>MASTER_LOG_FILE='bin-log.000013' ,  ---此参数为master status中的File值

mysql>MASTER_LOG_POS=120;   ---此数值为Position数值

6、设置好后，运行start slave，如果下图红线地方是YES，则成功，否则，查看配置文件是否有错或有误。

slave_io_running  ：yes

slave_sql_running :yes

7、可以查看从库下mysql进程信息

mysql>show processlist;

8、测试

在主库上面建新的数据库，然后在从库中查看是否有同步~

----------------------------------------------------------------------------------------------------

1.2 开始实现

思想我们已经讲完了相信大家已经多多少少的理解了,但是我们要如何在phalapi去实现读写分离的操作

1.首先我们要在初始化一个数据库连接,我们需要建立一个配置文件read.php和dbs.php一样只是连接这读库的配置项,并且注册

//读库
DI()->read = function (){
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('read'), $debug);
};

2.对PhalApi_Model_NotORM文件中的getORM进行修改

/**
 * 快速获取ORM实例，注意每次获取都是新的实例
 */
protected function getORM($id = NULL){
    $table = $this->getTableName($id);
    if($id == 'read'){
        return DI()->read->$table;
    }
    return DI()->notorm->$table;
}

3.当我们在model层使用$this->getORM('read') 的时候就会使用读库,使用$this->getORM() 会默认使用写库

//select操作
$this->getORM('read')->select('*')->where('aId', $aId)->fetchAll();
//insert,update,delete操作
$this->getORM()->insert();

2. 多库处理

有些人会问,为什么需要进行多库处理,所有的表放到一个库不就好了吗?

在原来重构别人外包项目是遇到过一个库100来张表,没有备注没有文档,几乎没法看,在我自己做一个比较大的项目的时候我也是做了一些考虑把库根据业务分解成一个一个子库,可以很好的避免表太多结构混乱的问题,而且我们可以随时把任何一个子库移植出去,当我一个mysql承载了很多压力,我可以把这个mysql上面的子库分解到多个mysql上面来实现压力分流.

看了上面的这一段介绍相信大家不言而喻的了解了这样做的好处,但是如何实现呢,聪明的大家可能在想不久和上面一样在多注册一个数据库连接嘛,其实大家说对了一半,我们是需要注册一个连接,但是我们需要一种统一的规范来使用这类操作

1.初始化数据库连接,一样的复制一份dbs.php为developers.php配置连接开发者库

//开发者库
DI()->developers = function (){
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('developers'), $debug);
};

2.对PhalApi_Model_NotORM文件中的getORM进行修改

/**
 * 快速获取ORM实例，注意每次获取都是新的实例
 */
protected function getORM($id = NULL){
    $table = $this->getTableName($id);
    if($id == 'developers'){
        return DI()->developers->$table;
    }
    return DI()->notorm->$table;
}

3.在之前我们不是建立了我们的公用General文件我们在构建这样的结构

--General                   //公用目录
    --developers                //通用项目库目录
        --Domain                //项目库Domain层
        --Model             //项目库Model层

然后我们在这里的Model进行的操作都是使用$this->getORM('developers')进行操作

这样规范的好处就是在与共享和模块化 让正常的一个Model层不会使用到任何不同库的操作,我们多个项目用到其他的一个库可以进行代码复用,

3. 总结

其实实现今天介绍的两种解决问题的方案还有很多很多,不同的业务可能需求不同实现也就不一样希望今天的介绍对大家有帮助,后面两篇教程将基于我写的两个拓展分别介绍分表分库操作和redis实际的使用,希望大家进一步关注!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.12]-phalapi-进阶篇6(解决大量数据存储数据库分表分库拓展) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇6(解决大量数据存储数据库分表分库拓展)

前言

时隔半个月随着PHP7的推出为PHP打了一瓶兴奋剂,在性能提升了一倍的情况下我们会逐渐发现,瓶颈会集中在数据库操作,那我们的内容就接着数据库读写分离,来聊聊分表分库应该怎么玩,因为PhalApi的分表分库并不是非常方便,笔者在这里提供了一个分表分库数据库集群的拓展,详细文档请见博客基于PhalApi的DB集群拓展 V0.1bate 大家可以自行在开源中国扩展Git地址中找到Cluster进行下载使用.

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

开源中国扩展Git地址: http://git.oschina.net/dogstar/PhalApi-Library

1. 场景

在实际工作中,我信奉一句话一切抛开业务的架构设计都是耍流氓所以我们从场景进行开篇

1.1 单条数据多查多写多改

这里做的例子,大家都在玩游戏把,玩游戏里面是不是有角色,角色是不是有装备,经验,物品以及等等,而且他会有一个特别的要求就是实时(因为我角色打了一个怪物获得了100xp我们不可能告诉他你等6个小时缓存时间结束了再来看,必须是实时的),当然我们可以使用缓存来解决这个问题我们下节会说到这个问题

那么在这种场景下,一个用户对于角色的操作非常频繁而且唯一我们就很好采用分表分库的操作了,相对于单表操作他会把所有的操作分散到各个数据库去操作,这样对于单个数据库总执行sql语句量就会有个指数级的下降,以及数据量也会均衡分配到每个数据库,但是当我们进行这类单条数据操作的时候根本不会对性能有任何的影响,因为只是通过算法得出了这条记录存在于那个库那张表而已,

1.2 日记记录分析

就已上面的例子我们继续讲,如果有一天你的领导过来提了个需求,我需要一个数据分析系统来统计用户每天什么时间段最活跃.用户平均每人充值了多少钱啊,多少等级下用户充钱最多啊,如果遇到这种问题你们会怎么办?三分钟思考

我们先来看看我们会遇到什么样子的问题,数据量大积累当1000w+之后数据库执行sql基本没法看,大量的写入数据对数据库压力大

我们再来看看分表分库怎么解决这个问题,1000w+数据库的情况下 比如你是4表4库一共16张表,那每张表的数量就是1000w/16=62w也就是每张表只需要存储62w的数据就ok了,当写入数据的时候会根据ID的顺序均衡写入4库执行sql的压力也就分布到了4个数据库,唯一的问题就是在执行where条件的时候可能需要对前置表进行遍历,而前置表的数据量就是1000w,当然前置表里面只存放ID和where条件的字段

2. 实现思路

就笔者在工作中接触到了很多案例的分表分库，使用了根据城市,或者是其他的特性进行分表分库规则,这样一定会出现用户分布不均匀导致的某一个库表压力巨大,我这里使用了均等分分割

大家先看一组图就会明白了

  1. 当我们进行插入的时候的操作如下:

    插入前置表获取主键,通过id得出应该存入几库几表在相应的地方写入数据

  2. 当我们进行单条读取操作的时候操作如下:

    通过id获取应该在几库几表在相应的地方获取数据

  3. 当我们使用where查询的时候操作如下:

    如果where条件在前置表存在从前置表通过where获取结果集ID,通过ID分组到库和表,然后进行查询在拼接结果集统一返回

3. 优缺点

  1. 优点:

    很好的避开了数据库存放数据过多效率底下的瓶颈

    在单条记录操作性能指数及提升

    数据量大的情况下where条件查询性能提高基本

    能对亿级的数据进行处理而且效率较高

    不需要考虑分表分库规则数据均等分布

  2. 缺点

    where查询字段必须预先添加到,前置表不然就必须遍历数据库数量 * 表数量才能得到想要的结果

    where查询就算有前置表的情况下最坏的情况也需要遍历数据库数量 * 表数量才能得到想要的结果

    对一些特定查询天生不足比如排序

4. 总结

在本小节的最好简单提及一下,基于PhalApi的DB集群拓展 V0.1bate功能展示比较局限童鞋们可以根据自己的业务需求来觉得是否使用,笔者也会在后期继续更新维护完善为一个比较方便的集群拓展.

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.13]-phalapi-进阶篇7(使用缓存以及用redis拓展解决实际问题) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇7(使用缓存以及用redis拓展解决实际问题)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

当我们在开发一个项目时,我们可能会遇到很多问题,比如消息推送,发送邮件,发送短信,以及并发跟不上,这个时候就该轮到常用的缓存出手解救我们了,我们接下来来讲讲缓存Redis在实际中的使用,解决实际问题.在这里是基于redis的基本知识,和简单看一下PhalApi的redis拓展文档在前来阅读此小节.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

开源中国扩展Git地址: http://git.oschina.net/dogstar/PhalApi-Library

1. 能解决什么问题

当我们使用一门技术的时候,我们当然是为了解决问题才去使用它的,那么我们使用缓存技术Redis能解决什么具体的问题呢?

1.1 缓存结果集

这里给一个例子大家看一下就会明白缓存结果集是什么意思

//从缓存redis的clubcache库中查询club表where条件是city,city值是$city
$cache = DI()->redis->get_Time('club'.'city'.$city,'clubcache');
//如果查询到了就直接返回缓存的结果
if($cache){
    return $cache;
}
//如果不存在从数据库里面获取结果然后存入redis缓存key的条件和取值时一样,最后一个参数为过期时间
$rs = $this->getORM()->select('*')->where('city',$city)->fetchAll();
DI()->redis->set_Time('club'.'city'.$city,$rs,'clubcache',600);

上面做的事情就是把结果保存600秒,600秒内的再次查询会获取一样的结果

1.2 队列处理

Redis运用到时间中有一个比较关键的作用就是他的队列

我们先过一下几个特殊的redis函数

//写入队列左边
set_lPush
//写入队列左边 如果value已经存在，则不添加
set_lPushx
//写入队列右边
set_rPush
//写入队列右边 如果value已经存在，则不添加
set_rPushx

//读取队列左边
get_lPop
//读取队列右边
get_rPop
//读取队列左边 如果没有读取到阻塞一定时间
get_blPop
//读取队列右边 如果没有读取到阻塞一定时间
get_brPop

比如我们在做消息推送,发送邮件,发送短信这类业务的时候,我们需要请求第三方接口,请求的速度是第三方来决定的,比如微信一个推送接口就是200ms,如果放到我们的API业务里面就会出现一个巨大的问题,用户访问速度极度下降,解决这类问题的方案就是队列流程如下

当我们接收到用户的推送请求时
            ↓
把推送请求加入到队列API里面不做任何操作(比如加入到左边)
            ↓
在后台有一个PHP脚本运行一直在读取队列(读取右边就是后进后出,如果读取左边就是先进先出)
            ↓
然后执行响应的推送逻辑

一般我们的脚本是一个死循环,或者shell定时请求,我们会采用读取不到数据是阻塞来解决去不到值循环过快的问题

1.3 临时数据存储

临时数据就不需要太多的说明了,举个例子就够了

比如我们获取验证码,我们需要把验证码存到库中吗,我觉得是没有必要的,而且数据库并不好做过期的操作只能我们自己判断

那么我们使用redis把验证码存入redis 然后给一个过期时间就很好的解决这个问题了

1.4 数据库

把redis作为数据库用算是比较深入的使用了,这里聊下思想

大家之后service可以分布式,但是对于大部分数据库的分布式并不容易,所以导致了很多系统到后面拼接堆积在数据库,当然可以使用缓存存储结果集,但是这种解决方便治标不治本,在和童鞋们探讨的时候得出了一个解决方便,就是把redis作为第一数据库mysql作为元数据库

做了这种操作之后服务器会自动把热数据同步到redis,把冷数据存放到mysql,当使用到冷数据了在存放到redis,用户大部分的操作基本是基于redis进行的操作

当作这样实现的成本比较高要实现redis 数据同步 封装使用 where查询 等等需要很大的精力去做,在后期笔者有打算做一个通用的拓展

2. 规范化使用

其实以上的类容已经讲的差不多了,为什么还有单独拿出一段来讲一讲规范呢,因为缓存不像是数据库当你需要去查看缓存的时候,如果所有的数据都堆积在redis的一个库,你会非常痛苦

但是redis支持多库所以需要一套规范来划分,这里分享一下我这边是如何使用的

0~10库 作为正常业务库,也就是推送队列,临时数据,每一个库都只存储一种业务的数据,比如微信推送就存在5库,而邮件推送的数据就存在6库,发送验证码的临时数据存储在3库,一次类推,如果觉得10个库还不够用可以根据业务增加

10库以上作为cache库用来存储每张表的结果集数据,或者是其余的数据

所有的key的命名规范必须带有类型+表名+条件

3. 总结

看了本小节之后相信大家都对缓存在时间开发中起到了什么样的作用有了个了解,这一小节的完成,我们的进阶篇也步入尾声了,下一篇是对于进阶篇的总结了,也多谢大家一路的陪伴!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.14]-phalapi-进阶篇8(PhalApi能带来什么和进阶篇总结) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇8(PhalApi能带来什么和进阶篇总结)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

到今天位置PhalApi已经开源一周年了,他从一个不起眼的小框架,到现在一个在不断迎合业务需求不断成长,也能为大家带来便捷的框架,从当初的群里只有几个人到现在群里已经有300+位童鞋,从没有项目使用到实际项目28+,这一切都要感谢开源精神以及那么多 小伙伴的支持,在本次进阶篇的尾声我们来一同聊一聊PhalApi能带来什么以及对进阶篇进行简单的总结!

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. PhalApi能带来什么

当谈论到这个话题的时候,有的童鞋会说了,框架不就是拿来用的吗,其实不然,笔者觉得有一下几点

  1. 敏捷开发

    PhalApi给我最直观的感受就是,方便快捷,开发一套接口的成本非常低,我记得是在从头到尾开发一套权限接口仅仅用了8个小时就完成了,可见使用PhalApi效率是非常可观的,所以它很适合敏捷开发

  2. 代码规范

    第二点就是代码规范了,PhalApi在代码规范上面有一套比较独特的方式,怎么说呢应为PhalApi是专注对于Api开发的框架,所有的操作都必须按照已经定义好的开发模式去执行,但是又不缺乏灵活,如果想使用自动文档也就要按照要求进行备注,我觉得这一点是其他MVC等框架所不及的

  3. 性能优秀

    在从性能的角度上来看,它有两个优点,没有view层和内部机制非常简单,导致了如果运用来对性能有要求的项目中非常适合,相对于使用MVC框架自己改写Api和使用原生PHP,它的优点结合了两者(当如除了没有view层之外)

  4. 值得深入了解

    关于这一点我考虑了很久,我相信很多人都是和我一样thinkphp是我们的入门框架,但是我们真的有深入了解过thinkphp吗,或者说我们有阅读过它的内部机制吗,我觉得有两方面原因,它具有一定的复杂度,或者说它设计出来就是复杂的(高度封装),源码不易阅读,相对于我觉得PhalApi是一个很适合深入了解源码,而且能从里面学习到很多在做业务工作的时候学习不到的内容

2. 进阶篇总结

经过了2个多月的编写,进阶篇也就到此结束了,我们来一同回顾一下

我们从对于Api,Domain和Model三层结构思想开始,到后面的单例模式,依赖注入以及自动加载和拦截器,对框架的基本思想进行了一些简单的介绍,接着对notorm,事务,读写分离,多库,分表分库进行了介绍,在最后对于缓存技术redis使用场景以及如何使用进行了说明

我相信了解过以上内容的童鞋都能过运用以上的知识解决在实际中遇到的问题,当然这也是我想要达到的效果(让跟多的人了解PhalApi框架)!

3. 最后

在最后祝愿PhalApi框架能在开源的路上越走越远!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.2]-phalapi-入门篇2(把它玩起来) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-入门篇1(把它玩起来)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

这一小节主要是在搭建好的环境下,先把这个框架运行起来,和数据库交互起来(这里用的是mysql),对整个框架的结构进行最简单的一些介绍,以及点出一些新手长遇到的一些问题和一些小技巧.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 运行起来吧PhalApi

不用说首先当然是下载项目文件咯.

推荐使用git工具(如果不会使用Git 可以直接去开源中国下载源码):

使用git把https://git.oschina.net/dogstar/PhalApi.git克隆下来

然后切换到1.2.1分支上面(注:这里使用的是当前最新版本)

现在我们能看到真正的源码了 我们先来分析一下目录结构其实在项目里面有一个文件README.md里面已经有介绍了(我在补充一点)

│
├── PhalApi         //PhalApi框架，后期可以整包升级(尽量小的改动,大改动会影响框架的迭代)
│
│
├── Public          //对外访问目录，建议隐藏PHP实现(增加安全性)
│   └── demo        //Demo服务访问入口(我认为是所谓的项目名)
│
│
├── Config          //项目接口公共配置，主要有：app.php, sys.php, dbs.php(配置文件不多说)
├── Data            //项目接口公共数据(也就是从一开始的数据库sql文件以及后面结构有任何变化的sql文件都存放这里,方便自己查找,好习惯之一)
├── Language        //项目接口公共翻译(需要国际化的话很有用,让返回更优雅高可用)
├── Runtime         //项目接口运行文件目录，用于存放日记，可软链到别的区(主要是日记,一定要有写的权限不然会报错)
│
│
└── Demo            //应用接口服务，名称自取，可多组(项目文件所谓真正实现业务的地方)
    ├── Api             //接口响应层(主要用于接收请求,返回结果,把一块大的业务分发成一块块小的业务)
    ├── Domain          //接口领域层(我觉得可以叫做逻辑层容易理解一点,分成的各个小的业务在这里进行处理)
    ├── Model           //接口持久层(实现最简单的数据库操作,不要有任何逻辑因为是高可用)
    └── Tests           //接口单元测试(不用多说)

然后我们把项目文件放到网站的根目录然后我们运行http://localhost/Public/(localhost请自行替换)我们可以获得以下结果:

强烈推荐使用json解析工具(这里使用的chorme插件json-handle附上下载地址json-handle)

这样第一步运行PhalApi就已经完成了

注:有些童鞋可能运行起来是一片白(这点请大家特别注意,有很多人问我怎么自己写了一个模块调用一直是一片白,其实是php报错了只是你没有打开php报错)解决方法是修改php.ini文件中的两个参数

error_reporting=E_ALL
display_errors=ON

2. 他是如何运行起来的

先说说为什么会有上面这一串返回,有php基础的人都知道访问了Public目录默认是访问到了Index.php这个文件我们来看看它的代码

<?php
/**
 * $APP_NAME 统一入口
 */
require_once dirname(__FILE__) . '/init.php';                     //引入初始化文件我们以后再讲
//装载你的接口
DI()->loader->addDirs('Demo');                                    //这个就是定义了你接口所在的目录
/** ---------------- 响应接口请求 ---------------- **/
$api = new PhalApi();
$rs = $api->response();                                           //执行你的接口
$rs->output();                                                    //返回结果

大家要是进入到$api->response();里面就会发现有这样一条语句

$service = DI()->request->get('service', 'Default.Index');

也就是当你没有定义访问某个接口的时候,它会指向到Default模块下的Index接口

所以http://localhost/Public/等同于http://localhost/Public/?service=Default.Index试一试会得到一样的结果

机智的小伙伴一定发现了在项目中的Demo/Api里面有Default.php这样一个文件而恰巧里面有一个index方法,没错就是通过service=Default.Index找到了Default模块中的index方法

实现这一机制的语句是:

$rs->setData(call_user_func(array($api, $action)));

有兴趣的童鞋可以研究研究

3. 数据库操作

Demo下面的Api文件除了上面所调用的Default.php还有一个User.php没错就是User模块里面分别有两个接口可供调用 getBaseInfo,getMultiBaseInfo那我们直接调用getBaseInfo看看结果http://localhost/Public/?service=User.getBaseInfo

相信大家一眼就能看出来是缺少了传参我们在试试带上参数会怎么样http://localhost/Public/?service=User.getBaseInfo&user_id=1

当然这个是因为没有配置数据库引起的如何配置?当然是在配置文件里面配置,大家打开Config目录下的 dbs文件key看到以下类容

'db_demo' => array(                             //服务器标记
        'host'      => 'localhost',             //数据库域名
        'name'      => 'phalapi',               //数据库名字
        'user'      => 'root',                  //数据库用户名
        'password'  => '',                      //数据库密码
        'port'      => '3306',                  //数据库端口
        'charset'   => 'UTF8',                  //数据库字符集
    ),

我们把它配置好然后再运行,可能有些人还是看到的和上面的情况是一样的,要不就是数据库不通要不就是没有创建phalapi数据库(自行创建)

当我们保证数据库能够联通,phalapi数据库也存在的情况下再去链接可能会出现类似于Warning: mkdir(): Permission denied in PhalApi/PhalApi/Logger/File.php on line 45这类错误,这是应为没有创建文件权限造成的,windows下面基本不会应为读写权限出问题,liunx下面要注意这点

当我们给目录赋予权限之后在运行可能会出现一个很长的报错PDOException大概的意思就是找不到tbl_user表

其实sql文件已经在Data文件夹里面了phalapi_test.sql

我们把sql文件运行一下创建好的效果是这样的(phalapi_test库名改成phalapi)

让我们再次运行那么结果就出来了

这样我们就已经把框架自带的例子跑了起来

4. 总结

这一小节主要是把phalapi的一些基本的机制做了一些简单的介绍,以及把框架自带的实例运行了起来,下一小节会着重介绍一下框架自带的请求验证,返回结果和自动生成的接口文档!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.3]-phalapi-入门篇3(请求和返回) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-入门篇3(请求和返回)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

这一小节主要讲一下请求(参数验证),返回(怎么返回自己想要的结果),官方文档也有比较详细的说明所以在有些比较基础的地方就一笔带过,讲一点比较实用在项目开发中怎么使用phalapi框架定制出自己想要的请求和返回结果.

请先通读:

[1.12] 参数规则：接口参数规则配置
[1.13] 统一的接口请求方式：?sevice=XXX.XXX
[1.14] 统一的返回格式和结构：ret data msg

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 请求

稍微看过Demo代码的童鞋和读了官方文档的童鞋应该都已经知道,请求参数验证是通过模块文件中的getRules方法进行过滤校验的

当然还有default默认值,默认值一般不和requie同时出现,基本上有了这些框架自带的参数过滤校验基本上可以满足大家的需求, 在代码层面使用$this->参数名就能获取到请求的值(这里建议请求的参数名与获取的参数名,名称保持一致避免因为名称混淆导致的出错)

那么具体的业务需求来了,大家都知道get请求不安全一般要是传递参数都需要使用post请求,在phalapi中自带接受请求机制是接受_REQUEST 也就是get请求和post请求通通接受,怎么做到只接受post参数而不接受get参数呢

官方文档有说:

但是这样有个弊端,包括接口名称在内的service参数许必须用post请求传递才能生效,可以在init.php的最前面加上想从get获取的参数负值给post($_POST['service'] = $_GET['service'];)但是不是很推荐这样使用,如果要从根本上解决问题就需要使用自定义的拦截器了(如何搭建使用拦截器会在后面的进阶篇的项目实践讲到)

2. 返回

2.1 定制成功返回结果

返回我么用个最简单的例子说明,就是官方自带的默认方法

大家可以看下代码层

public function index() {
    return array(
        'title' => 'Hello World!',
        'content' => T('Hi {name}, welcome to use PhalApi!', array('name' => $this->username)),
        'version' => PHALAPI_VERSION,
        'time' => $_SERVER['REQUEST_TIME'],
    );
}

在接口荣return的内容是在返回值的data参数里面(这样已经是一个约定俗成的规范了)

那么有的人就说了msg里面怎么没有值啊,如果是登录成功了是不是应该返回登录成功的提示?其实这些都是已经封装好的方法大家在Default.index接口的最前面加上DI()->response->setMsg('获取参数成功');就能看到如下返回:

这样就有了,那么又有的人说了ret和msg的名字可以修改吗(这种情况经常出现因为要兼容老业务,或者公司内部已经约定俗成)当然是能修改的,大家打开/PhalApi/Response.php这个文件里面可以看到一个getResult方法

 public function getResult() {
    $rs = array(
        'ret' => $this->ret,
        'data' => $this->data,
        'msg' => $this->msg,
    );
    return $rs;
}

这里就可以定义返回的key是什么我们把ret改成code就能得到如下返回

那么如果成功的表示是1怎么办?在Response.php开头就有一个变量设置了成功的返回值位200大家可以根据自己的需求改成任何想要的返回值.

/**
 * @var int $ret 返回状态码，其中：200成功，400非法请求，500服务器错误
 */
protected $ret = 200;

2.2 失败了返回

讲了这么多都是围绕着成功所说的,当然我们经常会遇到失败的情况我们要如何应对呢?大家试着在Default.index接口前面加上如下一段代码:

throw new PhalApi_Exception_BadRequest('失败了');

会得到如下结果:

这就是所谓的报错机制当然这个报错机制也是可以定制的,大家可以进入到PhalApi_Exception_BadRequest看一下

class PhalApi_Exception_BadRequest extends PhalApi_Exception{

    public function __construct($message, $code = 0) {
        parent::__construct(
            T('Bad Request: {message}', array('message' => $message)), 400 + $code
        );
    }
}

失败默认$code是0+400,如果你在'失败了'后面加上一个参数

throw new PhalApi_Exception_BadRequest('失败了',1);

那么code就会返回401,当然你也可以去掉默认的+400那么就会返回你输入的值了,具体也是看业务需求

3. 总结

从本小节可以看出phalapi框架确实很灵活完全可以按照自己定义想要的业务需求,而且报错机制也非常优雅,在本节后也希望大家能动手去尝试一下各种各样不同的请求以及返回,下一小节会讲返回结果国际化高可用以及自动生成文档.

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.4]-phalapi-入门篇4(国际化高可用和自动生成文档) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-入门篇4(国际化高可用和自动生成文档)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

在本小节主要讲解如何使用phalapi框架自带的国际化和文档自动生成,以及这样做可以的优点和好处和能解决哪方面的问题

附上:

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 国际化

说道国际化大家应该不陌生,对于显示端(web,andorid,ios)来说就是对显示的内容可以任意切换语言,

但对于服务端来说就是对于返回值可以任意切换语言,来达到给国外友人去使用这种业务,

虽然说这种需求是少数,但是在框架内部有提供良好处理国际化这种业务的工具,让我们先看看具体是什么样子的

大家在init.php下面可以看到这样一句话

//翻译语言包设定
SL('zh_cn');

这句话是指向了/Language/zh_cn/common.php文件,打开这个文件可以发现这样一组对照关系

return array(
    'Hi {name}, welcome to use PhalApi!' => '{name}您好，欢迎使用PhalApi！',
    'user not exists' => '用户不存在',
);

我们看看自带的Demo例子里面有使用到第一个,使用方法是这样的

 'content' => T('Hi {name}, welcome to use PhalApi!', array('name' => $this->username)),

大家是不是感觉突然之间发现了什么,对的就是使用T方法我们看一下T方法的注释

/**
 * 快速翻译
 * @param string $msg 待翻译的内容
 * @param array $params 动态参数
 */

这里的$msg 也就是在common.php中的key比如user not exists

$params参数是用来替换key里面用{name}括弧括起来的参数的传递一个数组key就和括弧里面的参数相同value就是你需要显示的类容

是不是感觉简单易用啊,那我们动手实验一下我们把zh_cn/common.php加入一条对照

'Login success'                      => '登录成功',

在en/common.php也加入一条数据

'Login success'                      => 'Login success1',

我们修改Default.index接口如下

public function index() {
    return T('Login success');
}

我们看能得到什么结果

那我们把init.php中的SL('zh_cn');改成SL('en');会有什么结果呢

就能得到我们在en/common.php中写下的Login success1这样就可以轻易的切换返回语言从而实现国际化

但是它带来的好处不仅如此,还有易于修改和高可用这两点好处

先说第一点易于修改:我在原来刚刚开始做项目的时候每个接口里面返回值全是写死的,比如用户不存在这种等等的错误返回,当要修改的时候我们面临了一个问题全局替换,这样是很不可取的所以后面就统一成了一个统一的文件来存放返回结果,当我们需要把用户不存在修改成请确认用户的真实性这样的业务的时候,我们只需要修改一个统一的文件

好处之二就是高可用,高可用的表现又有两种一种是上面所说的项目内的复用,第二种就是新项目的复用,当你开始一个新项目的时候其实已经有很多返回是通用的了,那么你就可以直接使用或者是拷贝过来,很多重复的工作量就可以省去

2. 文档自动生成

大家可以发现和index.php以及init.php并行的还有一个checkApiParams.php一直没有用到,当我们试着去访问一下

看到了这样的页面,访问这个文件的时候和访问接口一样的默认访问了Default.index的文档页面也有说明

它的机制是通过接口文件上的注释生成的返回结果

/**
 * 默认接口服务
 * @return string title 标题
 * @return string content 内容
 * @return string version 版本，格式：X.X.X
 * @return int time 当前时间戳
 */

剖析一下@return string title 标题 第一个是必要参数@return后面接着一个空格(千万不能多空格否则会失效)返回类型在接着一个空格返回名称

请求参数会按照接口文档中的getRules生成一些限定条件会自动生成说明需要加上'desc' => 说明就能生成大家可以试着访问一下user.getBaseInfo接口文档就能看到请求参数是有说明的

使用自动生成文档的好处在于会时时更新不必在开发过程中要取维护接口文档而烦恼,还有一点就是容错高因为是事实的所以不会出现请求参数接口和文档不同步的情况所以很推荐使用,但是对于返回结果来说需要手工维护不过也比维护文档来的方便,而且不用下载网上直接看,

其实大家看到/Public/demo/listAllApis.php里面这个相当于一个导航访问 http://localhost/Public/demo/listAllApis.php可以看到以下结果

对于这个文件可以自定义成自己的文档入口文件,可以自己划分模块然后把接口文档的地址写上去名称和说明这样也不需要手动在url里面去输入接口名称了

注:在这里有一个坑,如果要使用自动生成文档的话需要关掉一些PHP缓存拓展,因为这些拓展在缓存的时候会去除掉注释之后在缓存会导致返回结果无法生成,比如opcache.xcache这类的拓展都会导致此类问题

3. 总结

本小节主要讲解了一下如何使用国际化使用的好处,以及运用官方工具生成及时文档,在下一章节我们会讲解激动人心的数据库操作希望大家能持续关注!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.5]-phalapi-入门篇5(数据库操作和Model层) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-入门篇5(数据库操作和Model层)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

本小节主要讲解基于notorm的数据库操作以及使用Model层进行快速的数据层的开发,请确保装有PDO拓展.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 基于PDO的notorm进行的数据库操作

phalapi的数据库操作是使用的开源的notorm进行的,notorm是基于PDO链接数据库,在框架内部默认链接的是mysql数据库,如需修改链接其他数据库请修改 /PhalApi/PhalApi/DB/NotORM.php中的getPdo方法:

 $dsn = sprintf('mysql:dbname=%s;host=%s;port=%d',
                $dbCfg['name'],
                isset($dbCfg['host']) ? $dbCfg['host'] : 'localhost',
                isset($dbCfg['port']) ? $dbCfg['port'] : 3306
            );

再讲之前其实这里是有一个坑的,机智的童鞋应该发现了框架自带的user数据库里面有一个以from命名字段,应为在notorm生成sql语句的时候不会给自动自动加上引号 ,所以在修改添加删除有涉及这个字段的时候会报错,所以我们在这里把它改成phone(所以大家要注意字段名不能为关键字)

下面我们正式来讲解如何使用,我们先在Demo/Api下面创建一个DB.php文件作为我们的DB模块,

<?php
/**
 * 数据库接口服务类
 */
class Api_DB extends PhalApi_Api{
    public function getRules(){
        return array(
            'insert' => array(
                'id'    => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
                'name'  => array('name' => 'name', 'require' => true, 'desc' => '用户名称'),
                'phone' => array('name' => 'phone', 'require' => true, 'desc' => '用户手机号码'),
            ),
            'select' => array(
                'id' => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
            ),
            'update' => array(
                'id'    => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
                'name'  => array('name' => 'name', 'require' => true, 'desc' => '用户名称'),
                'phone' => array('name' => 'phone', 'require' => true, 'desc' => '用户手机号码'),
            ),
            'delete' => array(
                'id' => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
            ),
        );
    }

一共是增删改查四个接口代表四种操作(这里一定要配置好数据库,以及运行框架自带的sql文件phalapi_test.sql)

1.1 insert接口

我们先写增加接口如下:

/**
 * 新增表服务
 * @return int id 新增列的Id
 */
public function insert(){
    $data = array(                                               //用数组构成需要插入键值一一对应
        'id'    => $this->id,
        'name'  => $this->name,
        'phone' => $this->phone,
    );
    $rs   = DI()->notorm->user->insert($data);                  //执行数据库操作user代表的是表,返回结果是插入成功的值
    return $rs['id'];                                           //返回插入的id
}

重要的是 $rs = DI()->notorm->user->insert($data); 这段代码执行了sql语句,user是表名(这里的表名会加下在dbs中配置的表前缀组成一个完整的表名)我们试着运行一下 http://localhost/Public/?service=DB.insert&name=miaomi&phone=13010001000&id=2会得到以下结果

1.2 select接口

查询接口如下:

/**
 * 查询
 * @return array data 结果集
 */
public function select(){
    $data   = array();
    $data[] = DI()->notorm->user->select('name,phone')->where('id', $this->id)->fetch();
    $data[] = DI()->notorm->user->select('name,phone')->where('id = ?', $this->id)->fetchAll();
    $data[] = DI()->notorm->user->select('name,phone')->where('id != ?', $this->id)->fetchRows();
    return $data;
}

执行 http://localhost/Public/?service=DB.select&id=2会得到以下结果

为什么会有这样的区别,通过下面的一些小提示大家就能看到区别在哪里:

1.2.1 select方法

select方法主要是用来指定返回值,接受的是一个string他的作用于真正查询语句select和from之间填充,大家如果把select('name,phone') 改为 select('*') 就会得到包括id的所有字段的返回

1.2.2 where方法和排序

where方法是查询中的重要的一个环节

where('id', $this->id)等同于where('id = ?', $this->id)

where('id != ?', $this->id)这种方式只要是为了指定条件大于,等于,小于,不等于等

当然如果是需要有多个条件就使用连续的where就可以->where('id != ?',1)->where('phone','1301000100')这种形式

关于排序的使用其实和where差不多使用->order('字段名')如果要反排序在字段名后面加上DESC

1.2.3 fetch,fetchAll和fetchRows

大家有看到上面执行的三条查询语句后面的结束放到都不同这里讲解一下他们的区别和怎么用他们使用单独去执行sql语句

fetch方法是获取单独的一条数据返回结果是不带下标的数组 ,fetchAll和fetchRows不同在于他们返回的是包含多条数据一个带下标的数组,可以看到在条件一样的情况下第一条和第二条查询出来的结果区别是第二条多了一个0的下标,从此可得到如果是确定返回结果只有一条优先使用fetch,如果是多条结果优先使用fetchAll和fetchRows.

fetchAll和fetchRows还提供了一个功能就是单独执行sql语句

$sql = 'select * from tbl_user where id = :id';
$params = array(':id' => $this->id);                 //替换:id为请求参数的id
DI()->notorm->user->queryAll($sql, $params);         //或fetchRows($sql, $params)

这样就可以执行sql语句,包括一些复杂的查询sql可以使用此内方法执行(关联查询应当优先使用这种形式)

1.3 update接口

修改接口如下:

/**
 * 修改
 */
public function update(){
    $data = array(
        'name'  => $this->name,
        'phone' => $this->phone,
    );
    $rs   = DI()->notorm->user->where('id', $this->id)->update($data);
    if($rs === false){
        throw new PhalApi_Exception_BadRequest('修改数据失败');
    }
}

大家可以试一试执行之后是否有修改数据库 http://localhost/Public/?service=DB.update&id=2&phone=13011112222&name=wenwenwen

使用其实和添加接口差不多只是一个是吧id作为值,一个是作为条件

比较值得讲一下的是为什么使用if($rs === false)

原因是这样的,这里执行update方法之后获取得是影响行数,如果原本值就是一样的那就回返回0,只有在真正语句失败的时候会返回false所以这里使用全等于false作为判断是否执行成功的条件

1.4 delete接口

删除接口如下:

/**
 * 删除
 */
public function delete(){
    $rs   = DI()->notorm->user->where('id', $this->id)->delete();
    if($rs === false){
        throw new PhalApi_Exception_BadRequest('删除数据失败');
    }
}

http://localhost/Public/?service=DB.delete&id=2

删除的操作也很简单,不过if($rs === false)就算没有删除到数据也会返回成功,只有当语句失败会反悔false,如果需要未删除到数据提示出错的同学可以把等号减少一个

1.5 打印sql语句

有的时候光靠自己去看代码很难确定是不是哪里写的有问题,但是如果查看生成出来的sql语句就能很快的确定问题出现在哪里

大家可以试试在请求参数中加上 [image] 就可以打印出来生成的sql语句方便调试

包括执行时间和先后顺序也一同打印出来了,也可以帮助大家找到慢查询在哪里

2. 使用Model进行数据库操作

使用Model操作是为了提高开发效率,让同样数据库操作可以进行高度的复用,也便于修改起来改一处则全改这种效果

2.1 传统的Model操作

所谓传统的Model操作也就是把数据操作封装起来,方便调用比如/Model/User.php下面的getByUserId方法

public function getByUserId($userId) {
    return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
}

在内部直接封装数据库操作使用如下代码调用

    $model = new Model_User();
    $rs = $model->getByUserId($userId);

2.2 框架自带的Model操作

当然这里介绍model的目的当然是解读一下phalapi内部提供的model操作

使用自带model操作只需要继承PhalApi_Model_NotORM 在实现如下方法

protected function getTableName($id) {
    return 'user';
}

这个方法主要作用是为了添加这个model 的表名,其实这两个操作在/Model/User.php中已经实现了,我们来重构一下getByUserId方法如下

public function getByUserId($userId) {
    return $this->getORM()->select('*')->where('id = ?', $userId)->fetch();
}

$this->getORM()相当于DI()->notorm->(getTableName中设置的表名)

然后我们重写select接口如下:

public function select(){
    $model = new Model_User();
    return $model->getByUserId($this->id);
}

可以获得以下结果

在这里phalapi自带的model和传统的model对比起来区别在于,phalapi统一制定表名不会应为方法果断导致的表名写错的失误

另一方面phalapi自带的model提供了很多字基础操作,利用自动提示功能可以看到

我们来再次改造一下selete接口使用model自带的方法

public function select(){
    $model = new Model_User();
    return $model->get($this->id);
}

执行结果和上面是一样的,这里注意一点这里Id的名字是dbs中配置的'key' => 'id',要和数据库中的ID字段名对应,但是这样会有一些问题会在后面进阶篇提及到

3. 总结

在本小节着重讲了CURD操作,以及其中的一些操作的使用和怎么使用phalapi的model层,希望大家看完本小节之后进行一些练习来熟练的掌握使用phalapi对数据库的操作,关于数据库操作的一些小技巧会单独在进阶篇中抽出一小节来讲讲在实际项目开发中遇到的问题以及如何解决,希望大家进一步关注!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.6]-phalapi-入门篇6(小技巧和浅谈API适用范围以及入门篇总结) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-入门篇6(小技巧和浅谈API适用范围以及入门篇总结)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

本篇文章的目的在于把框架自带的一些好用,封装好的一些方便开发的类库简单的说一下,灵活使用可以提高开发效率,再就是因为在交流过程中有很多童鞋提出了一些关于什么样的项目怎么用API和web端怎么使用API这类的问题,我就我的理解把我的一些想法和大家交流一下,最后是对于入门篇做一下总结,以及对后面的一些教程做一下说明.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 小技巧

在这里介绍的是一些比较基础是比如curl,log,和tools着一些基础的,关于cache,Cookie等一些会在后面进阶篇慢慢道来

1.1 curl请求类

其实在框架内部已经提供了Curl的请求类位置是在/Phalpi/Phalapi/CUrl.php

// 构造方法接受参数为失败重试次数默认是1可以自己指定
$curl = new PhalApi_CUrl(2);

// 进行GET 返回请求结果
$rs = $curl->get('http://phalapi.oschina.mopaas.com/Public/demo/?service=Default.Index');

// 进行POST 返回请求结果
$data = array('username' => 'dogstar');
$rs = $curl->post('http://phalapi.oschina.mopaas.com/Public/demo/?service=Default.Index', $data);

1.2 log日记类

在大部分时候都没办法实时知道报错信息,但是如果发生了错误我们需要记录起来,来提供给自己日后分析,或者是在运营环境中有个问题但是自己不是很确定可以通过debug日记的形式写入文件进行分析,框架有提供这方面的功能,在接口init.php中已经有下面这段话了

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

这样就已经注册了日记类API_ROOT . '/Runtime'这个是表示日记存放的路径(一定要保证有写入权限才能正常使用)使用如下

//error 系统异常类
DI()->logger->error('fail to insert DB', $data);
//对应的LOG
2015-02-07 20:37:55|ERROR|fail to insert DB|{"name":"dogstar","password":"123456"}

//info 业务纪录类
DI()->logger->info('add user exp', array('name' => 'dogstar', 'before' => 10, 'addExp' => 2, 'after' => 12, 'reason' => 'help one more phper'));
//对应的LOG
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}

//debug 开发调试类
DI()->logger->debug('just for test', array('name' => 'dogstar', 'password' => '******'));
//对应的LOG
2015-02-07 20:37:55|DUBUG|just for test|{"name":"dogstar","password":"******"}

文件存放的文件路径一般为Runtime/log/201502(年月)/20150207(年月日).log

1.3 tool工具类

工具类的路径在/Phalpai/Phalapi/Tool.php中目前里面提供了两个工具(IP地址获取,随机字符串生成)使用方法入下,自己会经常使用的工具都可以放进去

//实例化工具类
$PhalApi_Tool = new PhalApi_Tool();

//生成一个"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"组成的一个随机字符串根据输入的值决定长度
$PhalApi_Tool->createRandStr(5);

//获取请求的IP这里的IP是外网IP
$PhalApi_Tool->getClientIp();

2. 浅谈API适用范围

其实想谈谈这个的原因是应为有人有疑问,Api适用范围是不是只局限于APP,还有就是如果是用 phalapi 提供一个后台管理的web项目的话要怎么去做,在这里根据一些笔者本人的一些肤浅的了解谈一谈我的认为,分一下几个场景进行说明.

2.1 移动端

我认为排在第一位的应该是移动应用比如Android和IOS上面的应用通讯肯定是需要使用到API通讯的(基本上是首选,很少有看到其他非接口形式的).

2.1 提供服务(主要是平台类)

提供服务(主要是平台类)比如银行来说,提供的支付接口,付款接口等,还有就是一些外卖平台,团购平台都有自己的一套接入API接受数据(这一些可能会偏向高端一点)

2.1 长周期web项目

长周期web项目,为什么要叫做长周期web项目,因为短期的项目比如5天的一个抽奖活动我是觉得实在没有必要去做成接口通讯的形式,这种短周期的项目直接拿着静态页面加上MVC框架去实现来的是划算一点(因为API和web通讯成本还是比较大的),对于长周期的项目来说使用API是有好处的如下:

1.前后端分离,后端提供API服务,前端进行请求展示,让前端专注做前端的事情,让后端专注做后端的事情.

2.升级迭代可以有效的减小维护成本,当迭代升级的时候,如果是后端没有变动,只是前端进行变动了可以很好的分开工作量.(前端一样)

3.分离之后规范更加清晰,API有API的一套完整的开发规范,前端也一样,就不会和MVC框架一样代码堆积在一个地方

4.高并发web项目优化目标准确,优化空间大,对于API来说就是可以单点优化.

当前也有他的缺点,前期工作量会有所增加,沟通成本增加,但是我认为他带来的好处是持续的

还有一个重点就是怎么实现通讯,我现在在项目中前端是使用js进行请求通讯的,这是比较常用的,还有一种设想PHP提供接口,然后node.js或者其他语言进行请求然后渲染出html代码给到前端进行展示,这样可以解决安全问题,而且node.js不需要进行数据操作性能也是非常好的.

3. 入门篇总结

经过了一个月交流入门篇也就在这里结束了,在入门篇里面吧一些关于Phalapi的基本使用方式都过了一遍,我也希望大家能动手都操作一遍,当如入门篇讲的是比较基础的东西,经验老到的童鞋过一过就行了,在后面的进阶篇里面跟过的会讲到概念和思想性的东西,比如DI思想单例模式,三层的好处等,而在实战篇中会着重讲解如何运用到实际开发中,怎么多项目共用框架等一些比较实际的一些教程,也希望大家能够进一步关注,再一次感谢喜欢Phalapi框架的你们,你们的关注就是我的动力!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.7]-phalapi-进阶篇1(三层结构Api,Domain,和Model) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇1(Api,Domain,和Model)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

本小节已经步入了进阶篇,在进阶篇中会着重谈论一些框架中运用的一些好的思想并且进行解读,本小节主要是讲解在Phalapi框架中推荐使用的三层结构Api+Domain+Model将从各个角度和整体角度进行讲解.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. Api+Domain+Model

其实这样的三层结构和java中的web+service+dao比较像只是说web和api一个进行页面显示一个不进行页面显示这个区别,在着重讲一下这三层在Phalapi中分工是怎么样的,他们分别担当者什么样的角色,需要做什么样的事情.

1.1 Api层

为什么说Api层像java中的web层呢,因为他们有一个共同的特性就是接受请求和返回结果.只不过java中没有表现得那么强烈,它会通过控制器把请求转发到service层作处理,并将处理结果在页面展示,所以Api更像担当控制器的作用.

Api层中需要做的事情如下:

  1. 注册接口/定义接口和控制请求参数

    这是首先要做的事情,和在web中的url一样,就是添加需要接受的参数以及对参数进行验证,如下:

    public function getRules(){
    return array(
        'index' => array(   //定义接口名
            //添加参数验证机制
            'username' => array('name' => 'username', 'default' => 'PHPer',),
        ),
);

    }

  2. 进行业务/逻辑的拼接

    在这里进行业务/逻辑拼接,比如说是查询列表接口需要做两件事情,第一验证用户(是否存在或是否有权限),第二查询出应该返回的列表,应该是如下样式:

    //验证用户
$Domain_Developers = new Domain_Developers();
$Domain_Developers->checkDev($this->dId);
//获取用户的APP列表
$Domain_App = new Domain_App();
$Domain_App->getMyAppList($this->dId);

  3. 返回结果

    返回结果就比较简单了,就是把你经过上面拼接处理后获得的数据返回,如下:

     return $data;

1.2 Domain层

Domain层主要负责的是具体的业务实现,如用户验证,一个Domain方法就是一个小的业务具体实现(注意尽量把业务划分得细一点方便通用)

/**
* 用户验证
*/
public function checkDev($dId){
    //通过ID 获取用户
    $Model_Developers = new Model_Developers();
    $dev              = $Model_Developers->checkDevdId($dId);
    //用户不存在处理
    if(!$dev){
        throw new PhalApi_Exception_BadRequest(T('No Dev'), -1);
    }
}

1.3 Model层

Model层其实无需多讲,也就是把数据库操作单独提炼出来统一处理,如下:

/**
 * 验证用户存不存在
 */
public function checkDevdId($dId){
    return $this->getORM()->select('dId')->where('dId', $dId)->fetch();
}

2. 三层结合使用的好处

  1. 结构清晰,互不干扰

    就我个人感觉来说,在实际开发中使用这样的三层结构带来的最大的好处在于结构清晰,为什么这么说呢,因为每一层需要做的事情都是非常独立的,你永远不会在API层中看到数据操作的代码,所以在排查问题的时候,如果是数据出了问题,肯定不会去API层里面去找,这样就非常方便错误的定位,再者就是代码可读性非常高,相对于mvc框架来说这样的好处是非常明显的.

  2. 高度解耦,灵活高可用

    带来的第二个很重要的好处就是解耦和高可用,高可用体现在Api可以重复利用Domain,Domain可以重复利用Model,这样可以减少很多不必要的代码量.如果相互的关系仅仅只是拼接(除非是结果会互相影响)的情况下就实现了解耦.

  3. 分工合作,提高效率

    在有这样的一套规范之后在分工合用时,对方不需要去看你的代码具体实现了什么,只需要看你这个方法干了什么,直接拿起来用就可以了,当然是在业务划分成小块的情况下,而且可以很明确的划分出来模块,当你需要用到对方的模块的时候只需要让对方提供即可,这样可以增加模块的专注性,从而提高合作开发的效率.

3. 总结

其实在刚刚接触这个框架的时候,我也是特别不能理解这样划分的作用,在后面的开发和别人的交流中进行了一些尝试后,发现这样用起来确实有很多的好处,希望今天的教程能让大家理解这样的一种规范可以带来很多的好处,并且自己去尝试和使用.

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.8]-phalapi-进阶篇2(DI依赖注入和单例模式) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇2(DI依赖注入和单例模式)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

离上一次更新过去了快两周,在其中编写了一个关于DB分表分库解决大数据量的拓展,有兴趣的童鞋可以了解了解.废话不多说,本小节在于解释一下在PhalApi框架中两个比较好的思想,单例模式和依赖注入.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 单例模式

单例模式对于长期进行过面向对象编程的童鞋应该不算陌生,对学习php的童鞋也应该有听过,这里简单的聊一聊单例模式它到底是一个怎么样的东东,解决了怎么样的问题,并且在PhalApi中它是如何实现的.

单例单例,所谓单例也就是保证一个类仅有一个实例，并提供一个访问它的全局访问点,这就是单例,不难看出他的好处：资源利用少，因为只有一个,大家都是知道要使用一个类必须要实例他，也就是new，在每次new一个对象的时候都会在内存中生成一块区域来存放这个实例,如果在程序一次运行中使用了很多的new实例化了同一个对象,那就比较消耗资源了,但是如果是通用一个使用全局变量global程序又会显得不那么优雅而且会很乱,在这种情况下，单例模式就产生了.

单例模式是一个很好的解决方案，既可以全局通用,又不必担心占用过多的资源,且非常优雅。我们来一起看看在PhalApi中是如何实现单例模式的:

//大家看到我们常用的DI方法内部实现的是PhalApi_DI中的静态方法one方法
function DI() {
    return PhalApi_DI::one();
}

然后我们看向one方法内部

每当请求过来时先验证静态变量instance是否已被类初始化赋值，若无，则会在内部去实例化PhalApi_DI类，然后赋值给$instance并返回,若有，则当下次请求过来时，直接返回已实例化的对象。简言之，PhalApi框架所有的地方使用的DI方法，其实都是返回同一个对象实例,在内存中只存在一块区域,代码如下:

public static function one(){
    if(self::$instance == NULL){
        self::$instance = new PhalApi_DI();
        self::$instance->onConstruct();
    }

    return self::$instance;
}

2. 依赖注入

依赖注入又称之为"控制反转",如果是熟悉javaweb开发的spring框架应该有比较深的感触,在这里也不往深的讲,就简单讲解一下PhalApi中DI依赖注入的实现，以便让大家了解这种设计模式实现原理以及自此基础上实现的惰性加载机制.

2.1 DI依赖注入实现

大家在PhalApi中常用的DI()方法,也就是采用我们上面所谓的单例模式,我们每次使用DI()就是在使用PhalApi_DI类,其实我们依赖注入的关键也就是在PhalApi_DI之中

先来讲一下他的一个实现方式再来讲具体实现,这里举个例子:

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

其实在内部有一个数组,它把config作为了key,把new PhalApi_Config_File(API_ROOT . '/Config')作为了value，然后保存了起来。当我们下一次使用DI->config->get()的时候，它就会根据key值config拿出开始new好的类,所以可以说config操作是依赖于DI(),而且在使用DI()->config的时候永远都是在使用同一个实例,减少资源的消耗.

有的童鞋就好奇了为什么DI()->config会存到数组里而在需要的时候会拿出来？感兴趣的童鞋可以百度一下魔法方法set和get

/**大家可以看到这是PhalApi_DI中的魔法方法__set
 * 也就是当使用DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');的时候
 * 获得的name值就是config,获得的value也就是new PhalApi_Config_File(API_ROOT . '/Config');
 */get同理,在内部实现都是调用了内部get和set方法
public function __set($name, $value){
    $this->set($name, $value);
}

public function __get($name){
    return $this->get($name, NULL);
}

看完之后大家是不是觉得很简单啊,大家以后也可以在设计自己的类的时候采用这种灵活的魔法方法实现.

2.2 惰性加载

在PhalApi中的DI()方法也提供惰性加载,惰性加载如字意也就是当类没有被使用到的时候不会被加载,这样的操作也是为了避免浪费不必要的资源,当我们不使用的时候永远不会去实例化，只有当你使用到的时候才会去实例化。接下来，我们来看看是如何实现的.

//当我们执行以下语句的时候,在依赖注入的时候存的是value值是字符串的test
DI()->test = 'test';

//使用DI()->test->test();的时候会使用到PhalApi中的get方法,在get方法中有一段代码
$this->data[$key] = $this->initService($this->data[$key]);

//在initService方法内部验证了value是字符串就实例化了再返回

if($config instanceOf Closure){
    $rs = $config();
}elseif(is_string($config) && class_exists($config)){
    $rs = new $config();
    if(is_callable(array($rs, 'onInitialize'))){
        call_user_func(array($rs, 'onInitialize'));
    }
}else{
    $rs = $config;
}

3. 总结

在本节中简单的讲解了关于单例模式,依赖注入以及惰性加载,这几种设计模式都是常用的，能够减少资源利用率,希望大家看了这篇介绍能够亲自地去体验一下PhalApi中的这几种模式。在下一小节就讲解如何构建自己的拦截器,希望大家能够继续关注!

注:笔者能力有限，有说的不对的地方，希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.9]-phalapi-进阶篇3(自动加载和拦截器) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

phalapi-进阶篇3(自动加载和拦截器)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

最近工作上的事情比较多，所以文档更新得比较慢，还望大家见谅!接下来为大家带来的是对此框架自动加载机制的一个小的引导,简单地来聊聊它是如何实现此类机制的,并在了解之后要怎样去使用。同时再探讨一下如何使用自动加载来构建自己的公用函数以及拦截器,包括如何使用拦截器等,在这里强调一下,在进阶篇中谈论思想会比较多，大家可以慢慢去消化。涉及的很多地方都是此框架优秀的地方所在,因此，拿出来分享是为了让大家能够更好的了解phalapi框架，并且可以运用到自己的代码中去!

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 自动加载机制

关于自动加载，使用phalapi框架的大家应该已经非常熟悉了。三层结构自动加载请求调用在大家看来基本上感觉不到自动加载机制的存在,其实，它早就被注册到接口文件里了,只是有一套完整的机制来封装这类事务让我们摸不着,我们的接口文件就是在index.php

//装载你的接口
DI()->loader->addDirs(array('Demo', 'Library'));

大家不妨进入到addDirs里面瞧瞧

public function addDirs($dirs) {
    if(!is_array($dirs)) {
        $dirs = array($dirs);
    }
    $this->dirs = array_merge($this->dirs, $dirs);
}

其实，当我们使用到addDirs的时候，框架内部只是把我们定义好的路径存了起来；其实关键的地方已经在初始化PhalApi_Loader的时候被触发了,我们来一起看下PhalApi_Loader的构造函数

public function __construct($basePath, $dirs = array()) {
    $this->setBasePath($basePath);
    if (!empty($dirs)) {
        $this->addDirs($dirs);
    }
    spl_autoload_register(array($this, 'load'));
}

第一句方法的意思是设置根目录,后面一个判断条件和我们使用addDirs是一样的作用,那么问题来了spl_autoload_register做了什么呢？官方给出的解释是这样的：

将函数注册到SPL__autoload函数队列中。如果该队列中的函数尚未激活，则激活它们

参数

欲注册的自动装载函数。如果没有提供任何参数，则自动注册autoload的默认实现函数spl_autoload()

其实说白了就是当实例化类的时候找不到可以用的类的时候，就会请求在spl_autoload_register中定义好的处理方法,在此框架中配置的处理函数也就是PhalApi_Loader/load函数。

关于具体是如何实现匹配，如何找到路径，再如何引入，这里就不详细说明了。当我们每次去new一个类的时候，都会先去寻找有没有匹配的类,当没有匹配到的情况下,我们new的类名就会传递到PhalApi_Loader/load函数中去处理,根据框架规则引入我们需要的文件.

2. 公用函数和拦截器

当我们会使用自动加载机制了之后，我们就可以来构建我们项目的公用函数和拦截器了,为什么要使用公用函数呢？在大部分请求下我们一套框架可能支持多个项目,当然项目与项目之间会有不同的函数等等,但是也有公用的比如debug，发送短信等等一些所有项目都可能去用到的一些功能性函数,或者是一些针对项目的拦截器。

2.1 公共函数

首先在我们的根目录建立一个文件夹叫做General通用的意思,里面分别有两个文件夹Common(受TP的影响)和Intercept两个文件,我们在里面放入我们自己的文件,当然需要按照正确的规则比如Common_Base等

然后我们要使用的时候，先加载这个general文件作为自带加载文件,然后注册自己需要用的类,如下:

//加载项目通用文件
DI()->loader->addDirs('General');
//通用函数基础类
DI()->base = new Common_Base();

2.2 拦截器

对于API来说，拦截器的意义无疑是巨大的,为什么这么说呢，应为API是提供服务器的,我们可以用拦截器做很多的限制,比如限制开放接口,参数转移、json解包、请求解密,token校验等等。

//拦截器
$Intercept           = new Intercept_app_default();
//转换参数
$_REQUEST            = $Intercept->turnparameter($_REQUEST);

作为参考,拦截器接收所有的post和get参数后在内部统一做处理。参数转换、请求解密、token校验等这些都是日常用到的，不详提。这里拿一个有代表性的来说明一下就是json解包,为什么要json解包呢？因为在很多公司使用请求传参的时候，都是封装成一个json请求过来的,框架接收是能接收到,但是无法使用框架自带的参数做完整性校验了。令我非常难受(也有很多朋友也遇到此类问题),所以自己写了一个解包方法：

//递归解包
function test($params, $server){
    if(is_array($params)){
        foreach($params as $k => $v){
            $server[$k] = $v;
            $server = test($v, $server);
        }
    }
    return $server;
}
//模拟一个json数组
$params = array(
    'a' => array(
        'c' => 3,
        'd' => 4
    ),
    'b' => 2,
);
$server = array();
var_dump(test($params, $server));

我们获取到的返回结果是array(4) { ["a"]=> array(2) { ["c"]=> int(3) ["d"]=> int(4) } ["c"]=> int(3) ["d"]=> int(4) ["b"]=> int(2) }也就是把数组中的底层元素全部移到了和a,b同级的目录，并且也保证了a的数组完整性,必须要注意同名的问题。

3. 总结

本小节,主要讲述了一下关于自动加载的机制,以及为什么要使用拦截器的原因和怎么让json通过拦截器也能使用框架的校验机制。今天就先到这里了,希望此教程能够给你们带来帮助。在下一小节中将会对model层和事务进行已经深入的讲述。祝大家天天开心~

注:笔者能力有限，有说的不对的地方，希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


附录1：接口文档参考模板 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

虽然提供了在线接口参数的查看，但在和客户端对接过程中，我们作为后台开发，还是需要人工提供接口文档给客户端的，这里提供一个接口文档编写的模板，以供参考，并且以我们熟悉的?service=User.GetBaseInfo为例说明如何编写高效的文档。

温馨提示：斜体字表示是注释说明。

功能说明

对接口功能的简单说明。
获取用户的基本信息。

接口URL

请求的相对链接和当前接口级参数，通常为?service=XXX.XXX + 公共接口参数。
/demo/?service=User.GetBaseInfo

参数说明

对当前接口级参数的说明，建议使用在线接口参数查询工具，但以下的参数说明也是需要的。

参数 | 名字 | 是否必须 | 说明 | 示例
userId | 用户ID | 是 | 表示用户的ID | &user_iduser_id=1

返回参数

对当前接口级返回参数的说明，即对{"ret":返回状态码,"data":"应该业务数据","msg":"错误提示"}中的data部分进行说明。

{
    "ret": 200,
    "data": {
        "code": 0,  //code=0表示正确获取用户信息，code=1时表示用户不存在
        "msg": "",   //业务提示文案
        "info": {  //仅当code=0的情况下非空且有用户信息
            "id": "1",  //用户ID
            "name": "dogstar",  //用户名
            "note": "oschina"  //用户来源
        }
    },
    "msg": ""
}

示例

至少应包括成功示例，失败示例可选

成功示例

请求：

http://phalapi.oschina.mopaas.com/Public/demo/?service=User.GetBaseInfo&user_iduser_id=1

返回：

{
    "ret": 200,
    "data": {
        "code": 0,
        "msg": "",
        "info": {
            "id": "1",
            "name": "dogstar",
            "note": "oschina"
        }
    },
    "msg": ""
}

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


PhalApi - PHP轻量级后台接口开发框架

  • 中文版
  • English
  • 感谢Simba赞助了服务器！
  • 下载
  • 文档
  • 类参考手册
  • 博客
  • 在线体验
  • Git@OSC
  • 关于我们

官网 - 待部署

http://www.phalapi.net

交流与咨询

国际渠道

邮箱：chanzonghuang@gmail.com

国内通道

QQ：376741929

博客与文档

博客：http://my.oschina.net/dogstar/blog?catalog=3264711
WIKI文档：PhalApi Wiki

问题反馈

Issues：http://git.oschina.net/dogstar/PhalApi/issues

感谢以下贡献者

dogstar<chanzonghuang@gmail.com>

PhalApi是一个PHP轻量级后台接口开发框架。
我们致力于将PhalApi维护成像恒星一样：不断更新，保持生气；为接口负责，为开源负责！让后台接口开发更简单！

© PhalApi 开发团队 All Rights Reserved.

在这里，特别感谢

  • 开源中国
  • Phalcon
  • PHPUnit
  • ThoughtWorks
支付宝 捐赠：chanzonghuang@gmail.com


[1 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.32]-在线接口文档：注释规范 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.32.1 在线接口文档

在线接口文档，主要有两种：

  • 接口列表：用于罗列全部的接口；
  • 接口文档：针对单个接口的文档，用于显示该接口服务的基本信息、接口说明、接口参数、返回结果以及异常情况等。

下面分别进行解释。

1.32.2 接口列表

如何访问接口列表？

在创建一个自己的项目后，可以通过访问对应项目下的listAllApis.php文件来访问接口列表。
具体的URL路径，根据项目的部署情况而定，通常是：

#域名 + 路径 + 项目名 + listAllApis.php
http://localhost/Public/demo/listAllApis.php

如访问： http://demo.phalapi.net/listAllApis.php，效果如下：

接口列表的注释规范

接口列表的注释比较简单，其规范如下：

  • 序号：自动生成，按接口服务名称字典排序后从小到大依次编号
  • 接口服务：自动生成，即service英文接口服务名称
  • 接口名称：接口中文名称，不带任何注解的注释，通常为第一行注释
  • 更多说明：对应@desc 注释，注释内容中不能带有空格

与上面接口列表对应的注释，以User.GetBaseInfo为例，其注释为：

class Api_User extends PhalApi_Api {

    /**
     * 获取用户基本信息
     * @desc 用于获取单个用户基本信息
     */
    public function getBaseInfo() {
    }
}

结合以上示例，可以看出：

  • 接口服务：由类名Api_User和方法名getBaseInfo组合而成，即：User.GetBaseInfo
  • 接口名称：即：获取用户基本信息
  • 更多说明：对应@desc 注释，即：用于获取单个用户基本信息

1.32.3 接口文档

如何访问接口文档？

在进入接口列表后，点击对应的接口服务即可跳转到对应的接口文档。或者，也可以手动拼接访问。其URL路径与listAllApis.php类似，但需要访问的是checkApiParams.php，通常是：

#域名 + 路径 + 项目名 + checkApiParams.php?service=接口服务
http://localhost/Public/demo/checkApiParams.php?service=User.GetBaseInfo

如访问： http://demo.phalapi.net/checkApiParams.php?service=User.GetBaseInfo，效果如下：

接口文档的注释规范

接口文档的注释较多，开发人员可在进行项目开发时按需注释。但各部分也很简单明了，如下：

  • 接口服务与接口名称：分别对应上面接口列表中接口服务英文名称和的接口中文名称
  • 接口说明：对应@desc 注释，注释内容中不能带有空格
  • 接口参数：由PhalApi_Api::getRules()中具体接口类定义的接口参数自动生成
  • 返回结果：对应@return 注释，可以有多组，格式为：@return 返回类型 返回字段 说明（不带空格）
  • 异常情况：对应@exception 注释，可以有多组，格式为：@exception 错误码 错误描述信息

温馨提示：@exception 注释需要PhalApi 1.3.6及以上版本

上面示例中，User.GetBaseInfo接口文档对应的注释为：

class Api_User extends PhalApi_Api {

    public function getRules() {
        return array(
            'getMultiBaseInfo' => array(
                'userIds' => array('name' => 'user_ids', 'type' => 'array', 'format' => 'explode', 'require' => true, 'desc' => '用户ID，多个以逗号分割'),
            ),
        );
    }

    /**
     * 批量获取用户基本信息
     * @desc 用于获取多个用户基本信息
     * @return int code 操作码，0表示成功
     * @return array list 用户列表
     * @return int list[].id 用户ID
     * @return string list[].name 用户名字
     * @return string list[].note 用户来源
     * @return string msg 提示信息
     * @exception 400 参数传递错误
     * @exception 500 服务器内部错误
     */
    public function getMultiBaseInfo() {
    }
}

其中，

  • 接口服务与接口名称：由Api_User类名和getMultiBaseInfo方法名生成英文名称，中文名称为不带任何注解的注释：批量获取用户基本信息
  • 接口说明：对应@desc 注释，即：用于获取多个用户基本信息
  • 接口参数：由Api_User::getRules()中定义的接口参数自动生成
  • 返回结果：对应@return 注释，共有6组
  • 异常情况：对应@exception 注释，共有2组

公共的@exception和@return注释

对于当前类函数成员公共的@exception异常情况注释和@return返回结果注释，可在类注释中统一放置。而对于多个类公共的@exception和@return```注释，则可以在父类的类注释中统一放置。

例如，对于上面的Api_User类，可以添加全部函数成员都适用的注释。例如，Api_User类的全部接口都返回code字段，且都返回400和500异常，则可以：

<?php
/**
 * @return int code 操作码，0表示成功
 * @exception 400 参数传递错误
 * @exception 500 服务器内部错误
 */
class Api_User extends PhalApi_Api {

这样就不需要在每个函数成员的注释中重复注释。此外，也会依次解析父类中的相关注释。对于相同异常码的@exception注释，子类的注释会覆盖父类的注释，方法的注释会覆盖类的注释；而对于相同的返回结果@return注释，也一样。

温馨提示：公共的@exception和@return注释需要PhalApi 1.4.0及以上版本

1.32.4 生成离线文档

除了使用访问在线文档，也可以生成熟线文档。在命令行，通过CLI方式访问上面的listAllApis.php文件即可生成离线文档到相应的项目目录下。

以Demo项目为例，先执行listAllApis.php脚本生成离线文档。

$ php /path/to/Public/demo/listAllApis.php

生成完毕后，可以看到类似这样的输出：

脚本执行完毕！离线文档保存路径为：/path/to/PhalApi/Public/demo/doc

随后，在浏览器打开并访问：

http://localhost/PhalApi/Public/demo/doc/index.html

便可看到生成好的离线文档了。

自 PhalApi 1.4.1 版本开始，在线接口列表文档有两种主题风格，分别是折叠版和展开版。折叠版是指带有左侧分类菜单的方式，展开版则是一次性罗列全部接口服务，便于搜索。在生成离线文档时，可以通过第二个参数，指定主题风格。如：

  • 生成展开版

    $ php /path/to/Public/demo/listAllApis.php expand
  • 生成折叠版
    $ php /path/to/Public/demo/listAllApis.php fold

温馨提示：生成离线功能需要 PhalApi 1.4.1 及以上版本支持。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[1.33]-开发者工具：实用函数 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

1.33 PhalApi_Tool开发工具

PhalApi提供了一系列工具,方便开发人员直接使用封装好的工具进行开发作业降低工具性代码编写成本,具体工具清单使用方式如下:

注意:希望PhalApi能够提供其他的开发工具可以联系我们

1.33.1 请求IP获取

有时需要获取用户IP做一系列操作,但是获取用户的IP又很有讲究,PhalApi_Tool提供获取用户请求IP地址getClientIp方法,失败的情况下，返回空:

$ip = PhalApi_Tool::getClientIp();

1.33.2 随机字符生成

在验证码或token经常会用到随机字符串生成,PhalApi_Tool提供createRandStr方法生成随机字符串:

createRandStr接受两个参数

  • (必填)len生成随机字符串的长度
  • (选填)生成字符串的范围默认[0~9][a~z][A~Z]
// 生成5位随机字符串
$str = PhalApi_Tool::createRandStr(5);

// 生成6位纯数字字符串
$str = PhalApi_Tool::createRandStr(6, "0123456789");

1.33.3 数组取值

进行数组操作的时候如果获取一个不存在的数组key的时候经常会出现PHP Notice: Undefined index 警告,PhalApi_Tool提供arrIndex方法来解决此类异常并且可以指定默认值

$arr = array(
    "test" => "test"
);
// 获取不存在的key默认返回空字符串
$val = PhalApi_Tool::arrIndex($arr, "test2");
// 指定默认值获取不到时返回默认值
$val = PhalApi_Tool::arrIndex($arr, "test2", "test2");

1.33.4 目录创建

在创建目录操作过程中很容易遇到上级目录未创建导致的创建目录失败,PhalApi_Tool提供createDir方法创建目录结构并且会对上级目录不存在的进行创建

// 使用相对路径会在入口index.php目录进行创建
PhalApi_Tool::createDir("./file/images");
// 使用绝对路径
PhalApi_Tool::createDir("/app/file/images");

1.33.4 目录删除

删除目录也很容易遇到创建目录同样的问题,需要删除的目录下有很多文件以及子目录系统是不允许删除的,PhalApi_Tool提供deleteDir方法来删除目录下的所有内容

注意:推荐使用在临时目录上,请注意不要删除重要目录！

// 使用相对路径会在入口index.php目录进行删除
PhalApi_Tool::deleteDir("./file");
// 使用绝对路径
PhalApi_Tool::deleteDir("/app/file");

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.18]-定制化：资源服务的再实现 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

美到极致不是无可增加，而是无可减少。 -- Antonie de Saint-Exupery

2.18.1 资源服务

资源可以理解成接口开发过程中所使用到的第三方系统、功能，如数据库，COOKIE、缓存；而服务则是指领域驱动设计中无状态的服务操作，如加密、过滤器、加载器等。
而在PhalApi中，资源和服务更多是通过DI来统一进行管理。并且我们使用资源服务来统称这两者。

在使用PhalApi进行开发时，应该分别从以下三个层面来使用资源服务。

  • 1、基本使用
  • 2、高级应用
  • 3、定制扩展

其中，基本使用是指使用框架默认提供的特性、功能，只需要稍微修改调整一下配置（甚至连配置都不需要修改）即可使用； 高级应用是指切换使用具有同类特性、功能的资源服务，对客户端而言，其实现的功能是等价的，只在于内部实现不同，通常此层面适用于有不同需求的应用场景； 最后是，定制扩展则是充满活力、生气的做法，对于项目需要用到而框架未提供的，都可以通过此方式进行定制、扩展。

2.18.2 仍然以缓存为例

下面以大家熟悉的缓存为例，分别说明这三个层面的演进使用。

(1)基本使用

在./Public/init.php初始化文件中，我们可以去掉对Memcache的注释，从而在项目启用cache这一资源服务。

//缓存 - Memcache/Memcached
DI()->cache = function () {
    return new PhalApi_Cache_Memcache(DI()->config->get('sys.mc'));
};

或者，我们还需要根据配置的MC不同，稍微更改一下./Config/sys.php中对应的配置：

    /**
     * MC缓存服务器参考配置
     */
     'mc' => array(
           'host' => '127.0.0.1',
           'port' => 11211,
     ),

然后，就可以在项目中如前面说的那样使用cache进行对缓存进行操作。

//设置缓存
DI()->cache->set($key, $value, $expireTime);

//读取缓存
$rs = DI()->cache->get($key);

(2)高级应用

但是我们都知道，缓存的实现方案以及存储的方式多种多样，包括但不限于：数据库缓存、文件缓存、COOKIE/session缓存、Redis缓存、APC缓存。

假设我们需要切换到Redis缓存，则可以在init.php初始化中，把DI()->cache注册成Redis缓存即可。

DI()->cache = function () {
    return new PhalApi_Cache_Redis(DI()->config->get('sys.redis'));
};

同时，在./Config/sys.php中添加对应的配置：

     'redis' => array(
           'host' => '127.0.0.1',
           'port' => 9527,
     ),

(3)定制扩展

毫无疑义，实际项目需要面对的开发场景更为复杂，可能需要使用到加密、压缩、分片或者其他处理手段，又可能使用其他存储媒介。

当框架基本的特性无法满足时，这时就需要考虑进行定制扩展了。值得庆幸的是，实现这一点并不难。一如往常那样，我们需要 先实现、再注册 。

实现资源服务接口

很多通常的资源服务，我们都统一了接口规约。在[1.19]-DI服务速查：各资源服务一览表 中可以查到每个资源服务所需要实现的接口。

如这次的缓存接口类是：

<?php

interface PhalApi_Cache {

    /**
     * 设置缓存
     *
     * @param string $key 缓存key
     * @param mixed $value 缓存的内容
     * @param int $expire 缓存有效时间，单位秒，非时间戳
     */
    public function set($key, $value, $expire = 600);

    /**
     * 读取缓存
     *
     * @param string $key 缓存key
     * @return mixed 失败情况下返回NULL
     */
    public function get($key);

    /**
     * 删除缓存
     *
     * @param string $key
     */
    public function delete($key);
}

我们可以先在项目中，实现此接口，如：

class Common_MyCache implements PhalApi_Cache {
     // ... ...
}

具体的实现，这里不再举例。

再注册

当实现好子类后，我们便可以在init.php初始化文件中，切换到我们自己的定制类。

DI()->cache = 'Common_MyCache';

2.18.3 实现、定制化你的资源服务

正如《项目百态》书中隐藏的美模式一节中写道的“恰恰相反，只有减少特性才能提升设计的美感。最好的设计都是功能明确又留有答题，也是易于测试的，即使要作修改，也不会带来新的麻烦。”

是以，作为一个框架，我们应当以发散的方式去设计；但为了能为应用提供可用的功能，我们又应当以收敛的方式去实现。如果我们提供的功能不足以满足大部分主流的业务场景，那么我们至少需要提供可扩展的空间。

类似的场景，还包括有接口签名、对接口参数的获取与接口结果的返回。当大家在进行项目开发过程中，有不同的场景需求时，可以按上面介绍的三个层面分别进行。

先尝试使用简单、默认的方式进行配置解决，再切换到框架或已有的解决方案，最后再尝试扩展自己的定制类。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[2.21]-服务器统一部署方案简明版 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

2.21.1 前言

目前服务器架构通常采用：CentOs + Nginx + php-fpm + MySql [+ Memcached]，现将部署方案安装方法、遇到的问题和解决方法、启动方式总结如下，以便后期服务器快速部署。

以下文档的是经过几个深夜整理并在阿里云服务器真实部署通过，运行良好，整个过程熟悉后约共半小时，可以进一步考虑做成自动化部署脚本。
如是，请把脚本也邮件发我一份。^_^

2.21.2 统一约定

  • 全部使用编译安装
  • 安装路径路径：/usr/local/

  • 安装包存放位置：/home/apps/web/

    特别注意：请尽量不要使用root账号操作服务器，特别是对外服务器，谢谢。

2.21.3 依赖库和开发工具

yum -y install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers

# nginx
yum -y install pcre-devel zlib-devel

# php
yum -y install gd-devel libjpeg-devel libpng-devel freetype-devel libxml2-devel curl-devel freetype-devel

# mysql
yum -y install bison gcc gcc-c++ autoconf automake zlib* libxml* ncurses-devel libtool-ltdl-devel* mysql-devel

# sz rz
yum install lrzsz

2.21.4 安装Nginx 1.6.0

参考官方网站

http://wiki.nginx.org/Install

(1)安装

./configure --prefix=/usr/local/nginx
make
make install

(2)启动

/usr/local/nginx/sbin/nginx

(3)重启

/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload

(4)更多配置

server {
    listen 80;
    server_name test.wetime.com;
    root /home/apps/projects/test.wetime.com;
    charset utf-8;
    access_log logs/test.wetime.com.access.log;
    error_log logs/test.wetime.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

2.21.5 安装PHP-FPM 5.3.5

(1)参考

http://blog.csdn.net/dc_726/article/details/9519619

(2)安装

./configure --prefix=/usr/local/php --with-mcrypt --with-zlib --with-curl --with-gd --disable-debug --enable-fpm --enable-mbstring --enable-sockets --enable-zip --enable-xml --enable-pdo

温馨提示

如果提示以下错误：

If configure fails try --with-jpeg-dir=<DIR>
configure: error: libpng.(a|so) not found.

则需要执行以下命令：

cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so

yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

(3)启动

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

(4)重启

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini -t
killall php
/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

(5)错误处理与依赖

cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so
yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

我们可以在命令下输入：

yum localinstall –nogpgcheck http://mirrors.ustc.edu.cn/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/free/el/updates/6/x86_64/rpmfusion-free-release-6-1.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/nonfree/el/updates/6/x86_64/rpmfusion-nonfree-release-6-1.noarch.rpm

安装这3个东东后，我们就可以直接使用yum install libmcrypt来安装啦～

(6)php扩展安装

#安装mysql扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysql=/usr/local/mysql/
make
make install

#安装mysqli扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysqli=/usr/local/mysql/bin/mysql_config
make
make install

扩展包位置在./php-5.3.5/ext/mysql，需要使用到 /usr/local/php/bin/phpize，扩展so应该放到：/usr/lib/php/modules/

此外，

#PHP之pdo_mysql扩展安装步骤

# http://www.smallbigcake.com/2012/08/05/1572/
# http://pecl.php.net/get/PDO_MYSQL-1.0.2.tgz

./configure --with-php-config=/usr/local/php/bin/php-config --with-pdo-mysql=/usr/local/mysql

如果提示“libmysqlclient.so.18: cannot open shared object file: No such file or directory in ”错误则参考 ：http://blog.itpub.net/26230597/viewspace-1161322/，
或进行软链：
ln -s /usr/local/mysql/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.18

2.21.6 安装MYSQL 5.6.10

(1)参考

http://www.cnblogs.com/xiongpq/p/3384681.html

(2)mysql-5.6.10.tar.gz下载

http://pan.baidu.com/share/link?shareid=3729862999&uk=1327790029

(3)前期清理和依赖

rpm -e mysql   //普通删除模式
rpm -e --nodeps mysql    // 强力删除模式，如果使用上面命令删除时，提示有依赖的其它文件，则用该命令可以对其进行强力删除
yum -y install make gcc-c++ cmake bison-devel  ncurses-devel

(4)安装

cmake \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc \
-DWITH_MYISAM_STORAGE_ENGINE=1 \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_MEMORY_STORAGE_ENGINE=1 \
-DWITH_READLINE=1 \
-DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock \
-DMYSQL_TCP_PORT=3306 \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DEXTRA_CHARSETS=all \
-DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci

make && make install

(5)ERROR! The server quit without updating PID file

#去掉干扰的配置
mv /etc/my.cnf /etc/my.cnf.bak

#添加日记纪录
vim /usr/local/mysql/my.cnf
log-error=/var/log/mysqld.log
innodb_force_recovery=1

2.21.7 [可选]MEMCACHE 1.4.20

(1)参考官网

http://www.memcached.org/downloads

(2)更多参考

http://crtlib.blog.163.com/blog/static/133158013201392661912837/

(3)先安装依赖包

./configure --prefix=/usr/local/libevent/
make
make install

(4)安装

wget http://memcached.org/latest tar -zxvf memcached-1.x.x.tar.gz cd memcached-1.x.x ./configure && make && make test && sudo make install

(5)最后成功安装后启动

/usr/local/memcached/bin/memcached -d -m 64 -u root -l 127.0.0.1 -p 11211 -c 128 -P /tmp/memcached.pid

(6)memcached 扩展

#http://blog.sina.com.cn/s/blog_66ac09390101drxc.html
#http://www.linuxidc.com/Linux/2013-07/87743.htm

#准备依赖包
./configure --prefix=/usr/local/libmemcached --with-memcached
make
make install

#安装 扩展
/usr/local/php/bin/pecl install memcached
接着输入
libmemcached directory [no] : /usr/local/libmemcached

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


PhalApi - PHP轻量级后台接口开发框架

  • 中文版
  • English
  • 感谢Simba赞助了服务器！
  • 下载
  • 文档
  • 类参考手册
  • 博客
  • 在线体验
  • Git@OSC
  • 关于我们

前言

目前服务器架构通常采用：CentOs + Nginx + php-fpm + MySql [+ Memcached]，现将部署方案安装方法、遇到的问题和解决方法、启动方式总结如下，以便后期服务器快速部署。

以下文档的是经过几个深夜整理并在阿里云服务器真实部署通过，运行良好，整个过程熟悉后约共半小时，可以进一步考虑做成自动化部署脚本。
如是，请把脚本也邮件发我一份。^_^

统一约定

  • 全部使用编译安装
  • 安装路径路径：/usr/local/

  • 安装包存放位置：/home/apps/web/

    特别注意：请尽量不要使用root账号操作服务器，特别是对外服务器，谢谢。

依赖库和开发工具

yum -y install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers

# nginx
yum -y install pcre-devel zlib-devel

# php
yum -y install gd-devel libjpeg-devel libpng-devel freetype-devel libxml2-devel curl-devel freetype-devel

# mysql
yum -y install bison gcc gcc-c++ autoconf automake zlib* libxml* ncurses-devel libtool-ltdl-devel* mysql-devel

# sz rz
yum install lrzsz

安装Nginx 1.6.0

参考官方网站

http://wiki.nginx.org/Install

安装

./configure --prefix=/usr/local/nginx
make
make install

启动

/usr/local/nginx/sbin/nginx

重启

/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload

更多配置

server {
    listen 80;
    server_name test.wetime.com;
    root /home/apps/projects/test.wetime.com;
    charset utf-8;
    access_log logs/test.wetime.com.access.log;
    error_log logs/test.wetime.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

安装PHP-FPM 5.3.5

参考

http://blog.csdn.net/dc_726/article/details/9519619

安装

./configure --prefix=/usr/local/php --with-mcrypt --with-zlib --with-curl --with-gd --disable-debug --enable-fpm --enable-mbstring --enable-sockets --enable-zip --enable-xml --enable-pdo

启动

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

重启

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini -t
killall php
/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

错误处理与依赖

cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so
yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

我们可以在命令下输入：

yum localinstall –nogpgcheck http://mirrors.ustc.edu.cn/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/free/el/updates/6/x86_64/rpmfusion-free-release-6-1.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/nonfree/el/updates/6/x86_64/rpmfusion-nonfree-release-6-1.noarch.rpm

安装这3个东东后，我们就可以直接使用yum install libmcrypt来安装啦～

php扩展安装

#安装mysql扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysql=/usr/local/mysql/
make
make install

#安装mysqli扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysqli=/usr/local/mysql/bin/mysql_config
make
make install

扩展包位置在./php-5.3.5/ext/mysql，需要使用到 /usr/local/php/bin/phpize，扩展so应该放到：/usr/lib/php/modules/

此外，

#PHP之pdo_mysql扩展安装步骤

# http://www.smallbigcake.com/2012/08/05/1572/
# http://pecl.php.net/get/PDO_MYSQL-1.0.2.tgz

./configure --with-php-config=/usr/local/php/bin/php-config --with-pdo-mysql=/usr/local/mysql

如果提示“libmysqlclient.so.18: cannot open shared object file: No such file or directory in ”错误则参考 ：http://blog.itpub.net/26230597/viewspace-1161322/，
或进行软链：
ln -s /usr/local/mysql/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.18

安装MYSQL 5.6.10

参考

http://www.cnblogs.com/xiongpq/p/3384681.html

mysql-5.6.10.tar.gz下载

http://pan.baidu.com/share/link?shareid=3729862999&uk=1327790029

前期清理和依赖

rpm -e mysql   //普通删除模式
rpm -e --nodeps mysql    // 强力删除模式，如果使用上面命令删除时，提示有依赖的其它文件，则用该命令可以对其进行强力删除
yum -y install make gcc-c++ cmake bison-devel  ncurses-devel

安装

cmake \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc \
-DWITH_MYISAM_STORAGE_ENGINE=1 \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_MEMORY_STORAGE_ENGINE=1 \
-DWITH_READLINE=1 \
-DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock \
-DMYSQL_TCP_PORT=3306 \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DEXTRA_CHARSETS=all \
-DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci

make && make install

ERROR! The server quit without updating PID file

#去掉干扰的配置
mv /etc/my.cnf /etc/my.cnf.bak

#添加日记纪录
vim /usr/local/mysql/my.cnf
log-error=/var/log/mysqld.log
innodb_force_recovery=1

[可选]MEMCACHE 1.4.20

参考官网

http://www.memcached.org/downloads

更多参考

http://crtlib.blog.163.com/blog/static/133158013201392661912837/

先安装依赖包

./configure --prefix=/usr/local/libevent/
make
make install

安装

wget http://memcached.org/latest tar -zxvf memcached-1.x.x.tar.gz cd memcached-1.x.x ./configure && make && make test && sudo make install

最后成功安装后启动

/usr/local/memcached/bin/memcached -d -m 64 -u root -l 127.0.0.1 -p 11211 -c 128 -P /tmp/memcached.pid

memcached 扩展

#http://blog.sina.com.cn/s/blog_66ac09390101drxc.html
#http://www.linuxidc.com/Linux/2013-07/87743.htm

#准备依赖包
./configure --prefix=/usr/local/libmemcached --with-memcached
make
make install

#安装 扩展
/usr/local/php/bin/pecl install memcached
接着输入
libmemcached directory [no] : /usr/local/libmemcached

PhalApi是一个PHP轻量级后台接口开发框架。
我们致力于将PhalApi维护成像恒星一样：不断更新，保持生气；为接口负责，为开源负责！让后台接口开发更简单！

© PhalApi 开发团队 All Rights Reserved.

在这里，特别感谢

  • 开源中国
  • Phalcon
  • PHPUnit
  • ThoughtWorks
支付宝 捐赠：chanzonghuang@gmail.com


[3.7]-扩展类库：微信小程序开发 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

上一章 文档首页 下一章

3.7 扩展类库：基于think-wxminihelper的微信小程序开发扩展

此扩展基于think-wxminihelper二次开发实现，并已征得原作者wulongtao同意。目前，此扩展提供了：

  • 直接可访问的登录接口?service=WechatMini_WXLoginHelper.CheckLogin
  • 直接可访问会话检测接口?service=WechatMini_WXLoginHelper.CheckSession
  • 在内部获取用户登录态的接口

安装与配置

(1) 扩展的安装

从PhalApi-Library扩展库中下载获取WechatMini扩展包，如使用：

$ git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把WechatMini目录复制到项目./PhalApi/Library/目录下，即：

$ cp ./PhalApi-Library/WechatMini/ ./PhalApi/Library/ -R

到此安装完毕！接下是扩展的配置。

(2) 扩展的配置

我们需要在./Config/app.php配置文件中追加以下配置：

    /**
     * 扩展类库 - 微信小程序
     */
    'WechatMini'  => array(
        'appid'      => '', // AppID(小程序ID)
        'secret'     => '', // AppSecret(小程序密钥)
        'url'        => 'https://api.weixin.qq.com/sns/jscode2session',
        'grant_type' => 'authorization_code'
    ),

其中，appid和secret根据自己的小程序配置，相应更新。url和grant_type则不需要更改。

(3) 扩展的注册

可以在初始化文件./Public/init.php或者项目入口文件如./Public/demo/index.php注册此微信小程序扩展。

DI()->wechatMini = new WechatMini_Lite();

这里需要使用显式初始化，WechatMini扩展完成一些内部初始化工作。

注册好后，便可以开始使用了。

接口服务介绍

此扩展提供了客户端直接可访问使用的接口服务，主要有：登录接口、会话检测接口。下面分别讲解。

(1) 登录接口?service=WechatMini_WXLoginHelper.CheckLogin

  • 功能说明

此接口服务用于小程序初次登录，背后的实现原理参考自微信开放接口文档。

  • 接口URI

在请求此接口服务时，需要在前面加上接口域名和项目访问路径。而接口服务service名称为：

/?service=WechatMini_WXLoginHelper.CheckLogin
  • 接口参数
参数 | 必须 | 默认值 | 说明
code | 是 | 无 | 微信登录凭证
  • 返回结果
返回字段 | 类型 | 说明
code | int | 操作状态码，0表示成功，否则表示失败
openid | string | 微信openid，成功时才返回此字段
session3rd | string | 3rd session标识，成功时才返回此字段
message | string | 失败时的提示信息，调试模式下透传微信接口返回的错误信息

例如，成功时会返回：

{
    "ret": 200,
    "data": {
        "code": 0,
        "openid": "oh1Mb0W63bd6u5rpiJ6eTqrYnYOc",
        "session3rd": "sTyOmVSFKL4h1hKs"
    },
    "msg": ""
}

失败时返回：

{
    "ret": 200,
    "data": {
        "code": -41005,
        "message": "请求Token失败"
    },
    "msg": ""
}

当开启调试模式时，例如secret设置不对时，会返回微信接口返回的错误信息，如：

{
    "ret": 200,
    "data": {
        "code": 40125,
        "message": "invalid appsecret, view more at http://t.cn/RAEkdVq, hints: [ req_id: TVDXha0288s161 ]"
    },
    "msg": "",
    "debug": {
        ... ...
    }
}
  • 小程序使用示例

在小程序中，可以这样调用此接口。

wx.login({
      success: function(res) {
        if (res.code) {
          //发起网络请求
          wx.request({
            url: 'http://demo.phalapi.net/?service=WechatMini_WXLoginHelper.checkLogin',
            data: {
              code: res.code
            },
            success: function (res) {
              var data = res.data
              if (data.ret == 200 && data.data.code == 0) {
                console.log("登录成功")
                console.log("openid = " + data.data.openid)
                console.log("session3rd = " + data.data.session3rd)
              } else {
                console.log("登录失败")
                console.log("code = " + data.data.code)
                console.log("message = " + data.data.message)
              }
            }
          })
        } else {
          console.log('获取用户登录态失败！' + res.errMsg)
        }
      }
    });

(2) 会话检测接口?service=WechatMini_WXLoginHelper.CheckSession

  • 功能说明

在调用登录接口后，接口服务会缓存成功返回的会话信息。这时可使用此会话检测接口判断用户是否已登录且在有效会话期间内。

  • 接口URI

在请求此接口服务时，需要在前面加上接口域名和项目访问路径。而接口服务service名称为：

/?service=WechatMini_WXLoginHelper.CheckSession
  • 接口参数
参数 | 必须 | 默认值 | 说明
session3rd | 是 | 无 | 3rd session标识
  • 返回结果
返回字段 | 类型 | 说明
code | int | 操作状态码，0表示成功，否则表示失败
openid | string | 微信openid，成功时才返回此字段
session3rd | string | 3rd session标识，成功时才返回此字段
message | string | 失败时的提示信息，调试模式下透传微信接口返回的错误信息

如成功时返回：

{
  "ret": 200,
  "data": {
    "code": 0,
    "openid": "oh1Mb0W63bd6u5rpiJ6eTqrYnYOc",
    "session3rd": "bV0FBXyjt3YsQxfc"
  },
  "msg": ""
}

会话过期时返回：

{
  "ret": 200,
  "data": {
    "code": -2,
    "message": "登录态已过期"
  },
  "msg": ""
}

内部获取用户登录态的接口

在内部，根据session3rd，可以这样获取会话信息。

$session = DI()->wechatMini->getSession($session3rd);

未设置缓存DI()->cache时返回FALSE，无会话时返回NULL，正常时返回数组：array('openid' => , 'session_key' => )。

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.1]-简单介绍以及环境搭建 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-入门篇1(简单介绍以及环境搭建)

前言

先在这里感谢phalapi框架创始人@dogstar，为我们提供了这样一个优秀的开源框架。

在进行项目接口的开发过程中，笔者在寻找一个方便针对API开发的框架时，发现了PhalApi。经过了短时间的尝试，发现PhalApi确实很便捷，效率很高。 之后就把PhalApi运用到正式的项目上。而且在后面的项目仍一直使用。其间，也和很多希望了解PhalApi框架的朋友做过很多交流。为此，决定写一篇关于这个框架的教程。一方面是为了帮助一些初学者能够快速地了解PhalApi，另一方面是希望把自己在实际运用中总结出的一些小技巧分享出来。

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 简单的介绍

官网上面有一句话让我特别有感受--接口，从简单开始！

怎样的感受呢，在最近进行一个项目开发的时候，因为项目比较紧急需要快速地开发。从确定完需求以及接口之后，借助PhalApi，我仅用了8个小时就完成项目接口的开发。整个项目的接口差不多20来个，从这点可以看出，如果PhalApi框架用得熟练的话，效率将会提高更多。

其实，接口最关心的就是性能。通过测试下来，Phalapi消耗相对比较小，引入文件在10个左右，空框架返回2核2G并发点击数在1700左右。很是给力。在2核2G正常请求数据库接口相对于一套完整的业务。平均下来最快的1000并发，最慢的600并发。所以我觉得性能方面，PhalApi完全没有问题(压力测试使用的是Loadrunner，ab压力基本是Loadrunner的3倍不太清楚为什么)。

引用:

勿忘初心，方得始终

虽然现在这个社会的节奏很快，但如果有时间时，我们不妨静下来，稍微地思考一下，然后你会发现：慢慢来，比较快。

PhalApi 的初衷:

我们之所以开源这个框架，是因为我们想致力于提供可以快速进行开发后台接口的框架，并且通过这个框架分享更多开发的最佳实践、原则和模式！这就是我们的初心，也希望我们能一直坚持下去。

PhalApi命名的来源

得益于开源社区，现在有很多优秀的开源框架。确实有很多框架值得我们去学习和使用，其中高性能的Phalcon框架和可视化代码评审工具Phabricator都是相当令人敬佩和认可的。前者在思想、模式和原则上有很好的文档说明；后者在包管理、命名写法等都非常好地体现了代码重用。受此两个框架的影响，故取名为：PhalApi。

通过PhalApi，我们希望可以

支撑轻量级项目后台接口的快速开发；阐明如何进行框架设计、对应复杂领域业务、支撑海量服务等，以及分享好的思想、技巧工具和最佳实践。最后，争取成为国内主流的PHP接口开发框架。

2. 环境搭建

在这里推荐使用(我后面的配置等都会基于以下环境)lnmp:

Liunx+Nginx+php5.4以上+mysql

我使用的是centos6.5

有三种方式配置环境：

1.手动编译安装(网上资料也挺多的，但是容易出现一些依赖性的问题，新手不推荐。)。

2.yum或apt-get安装(yum安装无法指定目录，都会装到/etc下面去，不方便管理，但是要玩liunx必须要会。)。

3.lnmp一键安装工具(推荐：但是这个推荐是基于上面几种方式都尝试过的情况下，这个安装方式基于--编译安装。)。

我推荐一个lnmp一键配置软件:

官网:OneinStack – 一键PHP/JAVA安装工具: http://oneinstack.com/

因为官网介绍得比较详细，在这里就不再多做说明。其余几种方式教程都比较多也不做说明。

那不了解liunx怎么办？wampserver在windows下还是比较好用的。

3. 总结

最后，希望大家在通读PhalApi官方文档后，再接着看下面的文章，会更容易理解。在这一套教程中，我会以构建一个完整的项目为基础，拓展开来讲解一些框架的机制。大家可以跟着一起做，相信能够收获得更多。

注：笔者能力有限，有说的不对的地方，希望大家能够指出。也希望多多交流！

官网QQ交流群:421032344 欢迎大家的加入!

文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.2]-把它玩起来 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-入门篇1(把它玩起来)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

这一小节主要是在搭建好的环境下,先把这个框架运行起来,和数据库交互起来(这里用的是mysql),对整个框架的结构进行最简单的一些介绍,以及点出一些新手长遇到的一些问题和一些小技巧.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 运行起来吧PhalApi

不用说首先当然是下载项目文件咯.

推荐使用git工具(如果不会使用Git 可以直接去开源中国下载源码):

使用git把https://git.oschina.net/dogstar/PhalApi.git克隆下来

然后切换到1.2.1分支上面(注:这里使用的是当前最新版本)

现在我们能看到真正的源码了 我们先来分析一下目录结构其实在项目里面有一个文件README.md里面已经有介绍了(我在补充一点)

│
├── PhalApi         //PhalApi框架，后期可以整包升级(尽量小的改动,大改动会影响框架的迭代)
│
│
├── Public          //对外访问目录，建议隐藏PHP实现(增加安全性)
│   └── demo        //Demo服务访问入口(我认为是所谓的项目名)
│
│
├── Config          //项目接口公共配置，主要有：app.php, sys.php, dbs.php(配置文件不多说)
├── Data            //项目接口公共数据(也就是从一开始的数据库sql文件以及后面结构有任何变化的sql文件都存放这里,方便自己查找,好习惯之一)
├── Language        //项目接口公共翻译(需要国际化的话很有用,让返回更优雅高可用)
├── Runtime         //项目接口运行文件目录，用于存放日记，可软链到别的区(主要是日记,一定要有写的权限不然会报错)
│
│
└── Demo            //应用接口服务，名称自取，可多组(项目文件所谓真正实现业务的地方)
    ├── Api             //接口响应层(主要用于接收请求,返回结果,把一块大的业务分发成一块块小的业务)
    ├── Domain          //接口领域层(我觉得可以叫做逻辑层容易理解一点,分成的各个小的业务在这里进行处理)
    ├── Model           //接口持久层(实现最简单的数据库操作,不要有任何逻辑因为是高可用)
    └── Tests           //接口单元测试(不用多说)

然后我们把项目文件放到网站的根目录然后我们运行http://localhost/Public/(localhost请自行替换)我们可以获得以下结果:

强烈推荐使用json解析工具(这里使用的chorme插件json-handle附上下载地址json-handle)

这样第一步运行PhalApi就已经完成了

注:有些童鞋可能运行起来是一片白(这点请大家特别注意,有很多人问我怎么自己写了一个模块调用一直是一片白,其实是php报错了只是你没有打开php报错)解决方法是修改php.ini文件中的两个参数

error_reporting=E_ALL
display_errors=ON

2. 他是如何运行起来的

先说说为什么会有上面这一串返回,有php基础的人都知道访问了Public目录默认是访问到了Index.php这个文件我们来看看它的代码

<?php
/**
 * $APP_NAME 统一入口
 */
require_once dirname(__FILE__) . '/init.php';                     //引入初始化文件我们以后再讲
//装载你的接口
DI()->loader->addDirs('Demo');                                    //这个就是定义了你接口所在的目录
/** ---------------- 响应接口请求 ---------------- **/
$api = new PhalApi();
$rs = $api->response();                                           //执行你的接口
$rs->output();                                                    //返回结果

大家要是进入到$api->response();里面就会发现有这样一条语句

$service = DI()->request->get('service', 'Default.Index');

也就是当你没有定义访问某个接口的时候,它会指向到Default模块下的Index接口

所以http://localhost/Public/等同于http://localhost/Public/?service=Default.Index试一试会得到一样的结果

机智的小伙伴一定发现了在项目中的Demo/Api里面有Default.php这样一个文件而恰巧里面有一个index方法,没错就是通过service=Default.Index找到了Default模块中的index方法

实现这一机制的语句是:

$rs->setData(call_user_func(array($api, $action)));

有兴趣的童鞋可以研究研究

3. 数据库操作

Demo下面的Api文件除了上面所调用的Default.php还有一个User.php没错就是User模块里面分别有两个接口可供调用 getBaseInfo,getMultiBaseInfo那我们直接调用getBaseInfo看看结果http://localhost/Public/?service=User.getBaseInfo

相信大家一眼就能看出来是缺少了传参我们在试试带上参数会怎么样http://localhost/Public/?service=User.getBaseInfo&user_id=1

当然这个是因为没有配置数据库引起的如何配置?当然是在配置文件里面配置,大家打开Config目录下的 dbs文件key看到以下类容

'db_demo' => array(                             //服务器标记
        'host'      => 'localhost',             //数据库域名
        'name'      => 'phalapi',               //数据库名字
        'user'      => 'root',                  //数据库用户名
        'password'  => '',                      //数据库密码
        'port'      => '3306',                  //数据库端口
        'charset'   => 'UTF8',                  //数据库字符集
    ),

我们把它配置好然后再运行,可能有些人还是看到的和上面的情况是一样的,要不就是数据库不通要不就是没有创建phalapi数据库(自行创建)

当我们保证数据库能够联通,phalapi数据库也存在的情况下再去链接可能会出现类似于Warning: mkdir(): Permission denied in PhalApi/PhalApi/Logger/File.php on line 45这类错误,这是应为没有创建文件权限造成的,windows下面基本不会应为读写权限出问题,liunx下面要注意这点

当我们给目录赋予权限之后在运行可能会出现一个很长的报错PDOException大概的意思就是找不到tbl_user表

其实sql文件已经在Data文件夹里面了phalapi_test.sql

我们把sql文件运行一下创建好的效果是这样的(phalapi_test库名改成phalapi)

让我们再次运行那么结果就出来了

这样我们就已经把框架自带的例子跑了起来

4. 总结

这一小节主要是把phalapi的一些基本的机制做了一些简单的介绍,以及把框架自带的实例运行了起来,下一小节会着重介绍一下框架自带的请求验证,返回结果和自动生成的接口文档!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.3]-请求和返回 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-入门篇3(请求和返回)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

这一小节主要讲一下请求(参数验证),返回(怎么返回自己想要的结果),官方文档也有比较详细的说明所以在有些比较基础的地方就一笔带过,讲一点比较实用在项目开发中怎么使用phalapi框架定制出自己想要的请求和返回结果.

请先通读:

[1.12] 参数规则：接口参数规则配置
[1.13] 统一的接口请求方式：?sevice=XXX.XXX
[1.14] 统一的返回格式和结构：ret data msg

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 请求

稍微看过Demo代码的童鞋和读了官方文档的童鞋应该都已经知道,请求参数验证是通过模块文件中的getRules方法进行过滤校验的

当然还有default默认值,默认值一般不和requie同时出现,基本上有了这些框架自带的参数过滤校验基本上可以满足大家的需求, 在代码层面使用$this->参数名就能获取到请求的值(这里建议请求的参数名与获取的参数名,名称保持一致避免因为名称混淆导致的出错)

那么具体的业务需求来了,大家都知道get请求不安全一般要是传递参数都需要使用post请求,在phalapi中自带接受请求机制是接受_REQUEST 也就是get请求和post请求通通接受,怎么做到只接受post参数而不接受get参数呢

官方文档有说:

但是这样有个弊端,包括接口名称在内的service参数许必须用post请求传递才能生效,可以在init.php的最前面加上想从get获取的参数负值给post($_POST['service'] = $_GET['service'];)但是不是很推荐这样使用,如果要从根本上解决问题就需要使用自定义的拦截器了(如何搭建使用拦截器会在后面的进阶篇的项目实践讲到)

2. 返回

2.1 定制成功返回结果

返回我么用个最简单的例子说明,就是官方自带的默认方法

大家可以看下代码层

public function index() {
    return array(
        'title' => 'Hello World!',
        'content' => T('Hi {name}, welcome to use PhalApi!', array('name' => $this->username)),
        'version' => PHALAPI_VERSION,
        'time' => $_SERVER['REQUEST_TIME'],
    );
}

在接口荣return的内容是在返回值的data参数里面(这样已经是一个约定俗成的规范了)

那么有的人就说了msg里面怎么没有值啊,如果是登录成功了是不是应该返回登录成功的提示?其实这些都是已经封装好的方法大家在Default.index接口的最前面加上DI()->response->setMsg('获取参数成功');就能看到如下返回:

这样就有了,那么又有的人说了ret和msg的名字可以修改吗(这种情况经常出现因为要兼容老业务,或者公司内部已经约定俗成)当然是能修改的,大家打开/PhalApi/Response.php这个文件里面可以看到一个getResult方法

 public function getResult() {
    $rs = array(
        'ret' => $this->ret,
        'data' => $this->data,
        'msg' => $this->msg,
    );
    return $rs;
}

这里就可以定义返回的key是什么我们把ret改成code就能得到如下返回

那么如果成功的表示是1怎么办?在Response.php开头就有一个变量设置了成功的返回值位200大家可以根据自己的需求改成任何想要的返回值.

/**
 * @var int $ret 返回状态码，其中：200成功，400非法请求，500服务器错误
 */
protected $ret = 200;

2.2 失败了返回

讲了这么多都是围绕着成功所说的,当然我们经常会遇到失败的情况我们要如何应对呢?大家试着在Default.index接口前面加上如下一段代码:

throw new PhalApi_Exception_BadRequest('失败了');

会得到如下结果:

这就是所谓的报错机制当然这个报错机制也是可以定制的,大家可以进入到PhalApi_Exception_BadRequest看一下

class PhalApi_Exception_BadRequest extends PhalApi_Exception{

    public function __construct($message, $code = 0) {
        parent::__construct(
            T('Bad Request: {message}', array('message' => $message)), 400 + $code
        );
    }
}

失败默认$code是0+400,如果你在'失败了'后面加上一个参数

throw new PhalApi_Exception_BadRequest('失败了',1);

那么code就会返回401,当然你也可以去掉默认的+400那么就会返回你输入的值了,具体也是看业务需求

3. 总结

从本小节可以看出phalapi框架确实很灵活完全可以按照自己定义想要的业务需求,而且报错机制也非常优雅,在本节后也希望大家能动手去尝试一下各种各样不同的请求以及返回,下一小节会讲返回结果国际化高可用以及自动生成文档.

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.4]-国际化高可用和自动生成文档 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-入门篇4(国际化高可用和自动生成文档)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

在本小节主要讲解如何使用phalapi框架自带的国际化和文档自动生成,以及这样做可以的优点和好处和能解决哪方面的问题

附上:

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 国际化

说道国际化大家应该不陌生,对于显示端(web,andorid,ios)来说就是对显示的内容可以任意切换语言,

但对于服务端来说就是对于返回值可以任意切换语言,来达到给国外友人去使用这种业务,

虽然说这种需求是少数,但是在框架内部有提供良好处理国际化这种业务的工具,让我们先看看具体是什么样子的

大家在init.php下面可以看到这样一句话

//翻译语言包设定
SL('zh_cn');

这句话是指向了/Language/zh_cn/common.php文件,打开这个文件可以发现这样一组对照关系

return array(
    'Hi {name}, welcome to use PhalApi!' => '{name}您好，欢迎使用PhalApi！',
    'user not exists' => '用户不存在',
);

我们看看自带的Demo例子里面有使用到第一个,使用方法是这样的

 'content' => T('Hi {name}, welcome to use PhalApi!', array('name' => $this->username)),

大家是不是感觉突然之间发现了什么,对的就是使用T方法我们看一下T方法的注释

/**
 * 快速翻译
 * @param string $msg 待翻译的内容
 * @param array $params 动态参数
 */

这里的$msg 也就是在common.php中的key比如user not exists

$params参数是用来替换key里面用{name}括弧括起来的参数的传递一个数组key就和括弧里面的参数相同value就是你需要显示的类容

是不是感觉简单易用啊,那我们动手实验一下我们把zh_cn/common.php加入一条对照

'Login success'                      => '登录成功',

在en/common.php也加入一条数据

'Login success'                      => 'Login success1',

我们修改Default.index接口如下

public function index() {
    return T('Login success');
}

我们看能得到什么结果

那我们把init.php中的SL('zh_cn');改成SL('en');会有什么结果呢

就能得到我们在en/common.php中写下的Login success1这样就可以轻易的切换返回语言从而实现国际化

但是它带来的好处不仅如此,还有易于修改和高可用这两点好处

先说第一点易于修改:我在原来刚刚开始做项目的时候每个接口里面返回值全是写死的,比如用户不存在这种等等的错误返回,当要修改的时候我们面临了一个问题全局替换,这样是很不可取的所以后面就统一成了一个统一的文件来存放返回结果,当我们需要把用户不存在修改成请确认用户的真实性这样的业务的时候,我们只需要修改一个统一的文件

好处之二就是高可用,高可用的表现又有两种一种是上面所说的项目内的复用,第二种就是新项目的复用,当你开始一个新项目的时候其实已经有很多返回是通用的了,那么你就可以直接使用或者是拷贝过来,很多重复的工作量就可以省去

2. 文档自动生成

大家可以发现和index.php以及init.php并行的还有一个checkApiParams.php一直没有用到,当我们试着去访问一下

看到了这样的页面,访问这个文件的时候和访问接口一样的默认访问了Default.index的文档页面也有说明

它的机制是通过接口文件上的注释生成的返回结果

/**
 * 默认接口服务
 * @return string title 标题
 * @return string content 内容
 * @return string version 版本，格式：X.X.X
 * @return int time 当前时间戳
 */

剖析一下@return string title 标题 第一个是必要参数@return后面接着一个空格(千万不能多空格否则会失效)返回类型在接着一个空格返回名称

请求参数会按照接口文档中的getRules生成一些限定条件会自动生成说明需要加上'desc' => 说明就能生成大家可以试着访问一下user.getBaseInfo接口文档就能看到请求参数是有说明的

使用自动生成文档的好处在于会时时更新不必在开发过程中要取维护接口文档而烦恼,还有一点就是容错高因为是事实的所以不会出现请求参数接口和文档不同步的情况所以很推荐使用,但是对于返回结果来说需要手工维护不过也比维护文档来的方便,而且不用下载网上直接看,

其实大家看到/Public/demo/listAllApis.php里面这个相当于一个导航访问 http://localhost/Public/demo/listAllApis.php可以看到以下结果

对于这个文件可以自定义成自己的文档入口文件,可以自己划分模块然后把接口文档的地址写上去名称和说明这样也不需要手动在url里面去输入接口名称了

注:在这里有一个坑,如果要使用自动生成文档的话需要关掉一些PHP缓存拓展,因为这些拓展在缓存的时候会去除掉注释之后在缓存会导致返回结果无法生成,比如opcache.xcache这类的拓展都会导致此类问题

3. 总结

本小节主要讲解了一下如何使用国际化使用的好处,以及运用官方工具生成及时文档,在下一章节我们会讲解激动人心的数据库操作希望大家能持续关注!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.5]-数据库操作和Model层 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-入门篇5(数据库操作和Model层)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

本小节主要讲解基于notorm的数据库操作以及使用Model层进行快速的数据层的开发,请确保装有PDO拓展.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 基于PDO的notorm进行的数据库操作

phalapi的数据库操作是使用的开源的notorm进行的,notorm是基于PDO链接数据库,在框架内部默认链接的是mysql数据库,如需修改链接其他数据库请修改 /PhalApi/PhalApi/DB/NotORM.php中的getPdo方法:

 $dsn = sprintf('mysql:dbname=%s;host=%s;port=%d',
                $dbCfg['name'],
                isset($dbCfg['host']) ? $dbCfg['host'] : 'localhost',
                isset($dbCfg['port']) ? $dbCfg['port'] : 3306
            );

再讲之前其实这里是有一个坑的,机智的童鞋应该发现了框架自带的user数据库里面有一个以from命名字段,应为在notorm生成sql语句的时候不会给自动自动加上引号 ,所以在修改添加删除有涉及这个字段的时候会报错,所以我们在这里把它改成phone(所以大家要注意字段名不能为关键字)

下面我们正式来讲解如何使用,我们先在Demo/Api下面创建一个DB.php文件作为我们的DB模块,

<?php
/**
 * 数据库接口服务类
 */
class Api_DB extends PhalApi_Api{
    public function getRules(){
        return array(
            'insert' => array(
                'id'    => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
                'name'  => array('name' => 'name', 'require' => true, 'desc' => '用户名称'),
                'phone' => array('name' => 'phone', 'require' => true, 'desc' => '用户手机号码'),
            ),
            'select' => array(
                'id' => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
            ),
            'update' => array(
                'id'    => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
                'name'  => array('name' => 'name', 'require' => true, 'desc' => '用户名称'),
                'phone' => array('name' => 'phone', 'require' => true, 'desc' => '用户手机号码'),
            ),
            'delete' => array(
                'id' => array('name' => 'id', 'require' => true, 'desc' => '用户Id'),
            ),
        );
    }

一共是增删改查四个接口代表四种操作(这里一定要配置好数据库,以及运行框架自带的sql文件phalapi_test.sql)

1.1 insert接口

我们先写增加接口如下:

/**
 * 新增表服务
 * @return int id 新增列的Id
 */
public function insert(){
    $data = array(                                               //用数组构成需要插入键值一一对应
        'id'    => $this->id,
        'name'  => $this->name,
        'phone' => $this->phone,
    );
    $rs   = DI()->notorm->user->insert($data);                  //执行数据库操作user代表的是表,返回结果是插入成功的值
    return $rs['id'];                                           //返回插入的id
}

重要的是 $rs = DI()->notorm->user->insert($data); 这段代码执行了sql语句,user是表名(这里的表名会加下在dbs中配置的表前缀组成一个完整的表名)我们试着运行一下 http://localhost/Public/?service=DB.insert&name=miaomi&phone=13010001000&id=2会得到以下结果

1.2 select接口

查询接口如下:

/**
 * 查询
 * @return array data 结果集
 */
public function select(){
    $data   = array();
    $data[] = DI()->notorm->user->select('name,phone')->where('id', $this->id)->fetch();
    $data[] = DI()->notorm->user->select('name,phone')->where('id = ?', $this->id)->fetchAll();
    $data[] = DI()->notorm->user->select('name,phone')->where('id != ?', $this->id)->fetchRows();
    return $data;
}

执行 http://localhost/Public/?service=DB.select&id=2会得到以下结果

为什么会有这样的区别,通过下面的一些小提示大家就能看到区别在哪里:

1.2.1 select方法

select方法主要是用来指定返回值,接受的是一个string他的作用于真正查询语句select和from之间填充,大家如果把select('name,phone') 改为 select('*') 就会得到包括id的所有字段的返回

1.2.2 where方法和排序

where方法是查询中的重要的一个环节

where('id', $this->id)等同于where('id = ?', $this->id)

where('id != ?', $this->id)这种方式只要是为了指定条件大于,等于,小于,不等于等

当然如果是需要有多个条件就使用连续的where就可以->where('id != ?',1)->where('phone','1301000100')这种形式

关于排序的使用其实和where差不多使用->order('字段名')如果要反排序在字段名后面加上DESC

1.2.3 fetch,fetchAll和fetchRows

大家有看到上面执行的三条查询语句后面的结束放到都不同这里讲解一下他们的区别和怎么用他们使用单独去执行sql语句

fetch方法是获取单独的一条数据返回结果是不带下标的数组 ,fetchAll和fetchRows不同在于他们返回的是包含多条数据一个带下标的数组,可以看到在条件一样的情况下第一条和第二条查询出来的结果区别是第二条多了一个0的下标,从此可得到如果是确定返回结果只有一条优先使用fetch,如果是多条结果优先使用fetchAll和fetchRows.

fetchAll和fetchRows还提供了一个功能就是单独执行sql语句

$sql = 'select * from tbl_user where id = :id';
$params = array(':id' => $this->id);                 //替换:id为请求参数的id
DI()->notorm->user->queryAll($sql, $params);         //或fetchRows($sql, $params)

这样就可以执行sql语句,包括一些复杂的查询sql可以使用此内方法执行(关联查询应当优先使用这种形式)

1.3 update接口

修改接口如下:

/**
 * 修改
 */
public function update(){
    $data = array(
        'name'  => $this->name,
        'phone' => $this->phone,
    );
    $rs   = DI()->notorm->user->where('id', $this->id)->update($data);
    if($rs === false){
        throw new PhalApi_Exception_BadRequest('修改数据失败');
    }
}

大家可以试一试执行之后是否有修改数据库 http://localhost/Public/?service=DB.update&id=2&phone=13011112222&name=wenwenwen

使用其实和添加接口差不多只是一个是吧id作为值,一个是作为条件

比较值得讲一下的是为什么使用if($rs === false)

原因是这样的,这里执行update方法之后获取得是影响行数,如果原本值就是一样的那就回返回0,只有在真正语句失败的时候会返回false所以这里使用全等于false作为判断是否执行成功的条件

1.4 delete接口

删除接口如下:

/**
 * 删除
 */
public function delete(){
    $rs   = DI()->notorm->user->where('id', $this->id)->delete();
    if($rs === false){
        throw new PhalApi_Exception_BadRequest('删除数据失败');
    }
}

http://localhost/Public/?service=DB.delete&id=2

删除的操作也很简单,不过if($rs === false)就算没有删除到数据也会返回成功,只有当语句失败会反悔false,如果需要未删除到数据提示出错的同学可以把等号减少一个

1.5 打印sql语句

有的时候光靠自己去看代码很难确定是不是哪里写的有问题,但是如果查看生成出来的sql语句就能很快的确定问题出现在哪里

大家可以试试在请求参数中加上 [image] 就可以打印出来生成的sql语句方便调试

包括执行时间和先后顺序也一同打印出来了,也可以帮助大家找到慢查询在哪里

2. 使用Model进行数据库操作

使用Model操作是为了提高开发效率,让同样数据库操作可以进行高度的复用,也便于修改起来改一处则全改这种效果

2.1 传统的Model操作

所谓传统的Model操作也就是把数据操作封装起来,方便调用比如/Model/User.php下面的getByUserId方法

public function getByUserId($userId) {
    return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
}

在内部直接封装数据库操作使用如下代码调用

    $model = new Model_User();
    $rs = $model->getByUserId($userId);

2.2 框架自带的Model操作

当然这里介绍model的目的当然是解读一下phalapi内部提供的model操作

使用自带model操作只需要继承PhalApi_Model_NotORM 在实现如下方法

protected function getTableName($id) {
    return 'user';
}

这个方法主要作用是为了添加这个model 的表名,其实这两个操作在/Model/User.php中已经实现了,我们来重构一下getByUserId方法如下

public function getByUserId($userId) {
    return $this->getORM()->select('*')->where('id = ?', $userId)->fetch();
}

$this->getORM()相当于DI()->notorm->(getTableName中设置的表名)

然后我们重写select接口如下:

public function select(){
    $model = new Model_User();
    return $model->getByUserId($this->id);
}

可以获得以下结果

在这里phalapi自带的model和传统的model对比起来区别在于,phalapi统一制定表名不会应为方法果断导致的表名写错的失误

另一方面phalapi自带的model提供了很多字基础操作,利用自动提示功能可以看到

我们来再次改造一下selete接口使用model自带的方法

public function select(){
    $model = new Model_User();
    return $model->get($this->id);
}

执行结果和上面是一样的,这里注意一点这里Id的名字是dbs中配置的'key' => 'id',要和数据库中的ID字段名对应,但是这样会有一些问题会在后面进阶篇提及到

3. 总结

在本小节着重讲了CURD操作,以及其中的一些操作的使用和怎么使用phalapi的model层,希望大家看完本小节之后进行一些练习来熟练的掌握使用phalapi对数据库的操作,关于数据库操作的一些小技巧会单独在进阶篇中抽出一小节来讲讲在实际项目开发中遇到的问题以及如何解决,希望大家进一步关注!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.6]-小技巧和浅谈API适用范围以及入门篇总结 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-入门篇6(小技巧和浅谈API适用范围以及入门篇总结)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

本篇文章的目的在于把框架自带的一些好用,封装好的一些方便开发的类库简单的说一下,灵活使用可以提高开发效率,再就是因为在交流过程中有很多童鞋提出了一些关于什么样的项目怎么用API和web端怎么使用API这类的问题,我就我的理解把我的一些想法和大家交流一下,最后是对于入门篇做一下总结,以及对后面的一些教程做一下说明.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 小技巧

在这里介绍的是一些比较基础是比如curl,log,和tools着一些基础的,关于cache,Cookie等一些会在后面进阶篇慢慢道来

1.1 curl请求类

其实在框架内部已经提供了Curl的请求类位置是在/Phalpi/Phalapi/CUrl.php

// 构造方法接受参数为失败重试次数默认是1可以自己指定
$curl = new PhalApi_CUrl(2);

// 进行GET 返回请求结果
$rs = $curl->get('http://phalapi.oschina.mopaas.com/Public/demo/?service=Default.Index');

// 进行POST 返回请求结果
$data = array('username' => 'dogstar');
$rs = $curl->post('http://phalapi.oschina.mopaas.com/Public/demo/?service=Default.Index', $data);

1.2 log日记类

在大部分时候都没办法实时知道报错信息,但是如果发生了错误我们需要记录起来,来提供给自己日后分析,或者是在运营环境中有个问题但是自己不是很确定可以通过debug日记的形式写入文件进行分析,框架有提供这方面的功能,在接口init.php中已经有下面这段话了

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

这样就已经注册了日记类API_ROOT . '/Runtime'这个是表示日记存放的路径(一定要保证有写入权限才能正常使用)使用如下

//error 系统异常类
DI()->logger->error('fail to insert DB', $data);
//对应的LOG
2015-02-07 20:37:55|ERROR|fail to insert DB|{"name":"dogstar","password":"123456"}

//info 业务纪录类
DI()->logger->info('add user exp', array('name' => 'dogstar', 'before' => 10, 'addExp' => 2, 'after' => 12, 'reason' => 'help one more phper'));
//对应的LOG
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}

//debug 开发调试类
DI()->logger->debug('just for test', array('name' => 'dogstar', 'password' => '******'));
//对应的LOG
2015-02-07 20:37:55|DUBUG|just for test|{"name":"dogstar","password":"******"}

文件存放的文件路径一般为Runtime/log/201502(年月)/20150207(年月日).log

1.3 tool工具类

工具类的路径在/Phalpai/Phalapi/Tool.php中目前里面提供了两个工具(IP地址获取,随机字符串生成)使用方法入下,自己会经常使用的工具都可以放进去

//实例化工具类
$PhalApi_Tool = new PhalApi_Tool();

//生成一个"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"组成的一个随机字符串根据输入的值决定长度
$PhalApi_Tool->createRandStr(5);

//获取请求的IP这里的IP是外网IP
$PhalApi_Tool->getClientIp();

2. 浅谈API适用范围

其实想谈谈这个的原因是应为有人有疑问,Api适用范围是不是只局限于APP,还有就是如果是用 phalapi 提供一个后台管理的web项目的话要怎么去做,在这里根据一些笔者本人的一些肤浅的了解谈一谈我的认为,分一下几个场景进行说明.

2.1 移动端

我认为排在第一位的应该是移动应用比如Android和IOS上面的应用通讯肯定是需要使用到API通讯的(基本上是首选,很少有看到其他非接口形式的).

2.1 提供服务(主要是平台类)

提供服务(主要是平台类)比如银行来说,提供的支付接口,付款接口等,还有就是一些外卖平台,团购平台都有自己的一套接入API接受数据(这一些可能会偏向高端一点)

2.1 长周期web项目

长周期web项目,为什么要叫做长周期web项目,因为短期的项目比如5天的一个抽奖活动我是觉得实在没有必要去做成接口通讯的形式,这种短周期的项目直接拿着静态页面加上MVC框架去实现来的是划算一点(因为API和web通讯成本还是比较大的),对于长周期的项目来说使用API是有好处的如下:

1.前后端分离,后端提供API服务,前端进行请求展示,让前端专注做前端的事情,让后端专注做后端的事情.

2.升级迭代可以有效的减小维护成本,当迭代升级的时候,如果是后端没有变动,只是前端进行变动了可以很好的分开工作量.(前端一样)

3.分离之后规范更加清晰,API有API的一套完整的开发规范,前端也一样,就不会和MVC框架一样代码堆积在一个地方

4.高并发web项目优化目标准确,优化空间大,对于API来说就是可以单点优化.

当前也有他的缺点,前期工作量会有所增加,沟通成本增加,但是我认为他带来的好处是持续的

还有一个重点就是怎么实现通讯,我现在在项目中前端是使用js进行请求通讯的,这是比较常用的,还有一种设想PHP提供接口,然后node.js或者其他语言进行请求然后渲染出html代码给到前端进行展示,这样可以解决安全问题,而且node.js不需要进行数据操作性能也是非常好的.

3. 入门篇总结

经过了一个月交流入门篇也就在这里结束了,在入门篇里面吧一些关于Phalapi的基本使用方式都过了一遍,我也希望大家能动手都操作一遍,当如入门篇讲的是比较基础的东西,经验老到的童鞋过一过就行了,在后面的进阶篇里面跟过的会讲到概念和思想性的东西,比如DI思想单例模式,三层的好处等,而在实战篇中会着重讲解如何运用到实际开发中,怎么多项目共用框架等一些比较实际的一些教程,也希望大家能够进一步关注,再一次感谢喜欢Phalapi框架的你们,你们的关注就是我的动力!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.7]-三层结构Api,Domain,和Model | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇1(Api,Domain,和Model)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

本小节已经步入了进阶篇,在进阶篇中会着重谈论一些框架中运用的一些好的思想并且进行解读,本小节主要是讲解在Phalapi框架中推荐使用的三层结构Api+Domain+Model将从各个角度和整体角度进行讲解.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. Api+Domain+Model

其实这样的三层结构和java中的web+service+dao比较像只是说web和api一个进行页面显示一个不进行页面显示这个区别,在着重讲一下这三层在Phalapi中分工是怎么样的,他们分别担当者什么样的角色,需要做什么样的事情.

1.1 Api层

为什么说Api层像java中的web层呢,因为他们有一个共同的特性就是接受请求和返回结果.只不过java中没有表现得那么强烈,它会通过控制器把请求转发到service层作处理,并将处理结果在页面展示,所以Api更像担当控制器的作用.

Api层中需要做的事情如下:

  1. 注册接口/定义接口和控制请求参数

    这是首先要做的事情,和在web中的url一样,就是添加需要接受的参数以及对参数进行验证,如下:

    public function getRules(){
    return array(
        'index' => array(   //定义接口名
            //添加参数验证机制
            'username' => array('name' => 'username', 'default' => 'PHPer',),
        ),
);

    }

  2. 进行业务/逻辑的拼接

    在这里进行业务/逻辑拼接,比如说是查询列表接口需要做两件事情,第一验证用户(是否存在或是否有权限),第二查询出应该返回的列表,应该是如下样式:

    //验证用户
$Domain_Developers = new Domain_Developers();
$Domain_Developers->checkDev($this->dId);
//获取用户的APP列表
$Domain_App = new Domain_App();
$Domain_App->getMyAppList($this->dId);

  3. 返回结果

    返回结果就比较简单了,就是把你经过上面拼接处理后获得的数据返回,如下:

     return $data;

1.2 Domain层

Domain层主要负责的是具体的业务实现,如用户验证,一个Domain方法就是一个小的业务具体实现(注意尽量把业务划分得细一点方便通用)

/**
* 用户验证
*/
public function checkDev($dId){
    //通过ID 获取用户
    $Model_Developers = new Model_Developers();
    $dev              = $Model_Developers->checkDevdId($dId);
    //用户不存在处理
    if(!$dev){
        throw new PhalApi_Exception_BadRequest(T('No Dev'), -1);
    }
}

1.3 Model层

Model层其实无需多讲,也就是把数据库操作单独提炼出来统一处理,如下:

/**
 * 验证用户存不存在
 */
public function checkDevdId($dId){
    return $this->getORM()->select('dId')->where('dId', $dId)->fetch();
}

2. 三层结合使用的好处

  1. 结构清晰,互不干扰

    就我个人感觉来说,在实际开发中使用这样的三层结构带来的最大的好处在于结构清晰,为什么这么说呢,因为每一层需要做的事情都是非常独立的,你永远不会在API层中看到数据操作的代码,所以在排查问题的时候,如果是数据出了问题,肯定不会去API层里面去找,这样就非常方便错误的定位,再者就是代码可读性非常高,相对于mvc框架来说这样的好处是非常明显的.

  2. 高度解耦,灵活高可用

    带来的第二个很重要的好处就是解耦和高可用,高可用体现在Api可以重复利用Domain,Domain可以重复利用Model,这样可以减少很多不必要的代码量.如果相互的关系仅仅只是拼接(除非是结果会互相影响)的情况下就实现了解耦.

  3. 分工合作,提高效率

    在有这样的一套规范之后在分工合用时,对方不需要去看你的代码具体实现了什么,只需要看你这个方法干了什么,直接拿起来用就可以了,当然是在业务划分成小块的情况下,而且可以很明确的划分出来模块,当你需要用到对方的模块的时候只需要让对方提供即可,这样可以增加模块的专注性,从而提高合作开发的效率.

3. 总结

其实在刚刚接触这个框架的时候,我也是特别不能理解这样划分的作用,在后面的开发和别人的交流中进行了一些尝试后,发现这样用起来确实有很多的好处,希望今天的教程能让大家理解这样的一种规范可以带来很多的好处,并且自己去尝试和使用.

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.8]-DI依赖注入和单例模式 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇2(DI依赖注入和单例模式)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

离上一次更新过去了快两周,在其中编写了一个关于DB分表分库解决大数据量的拓展,有兴趣的童鞋可以了解了解.废话不多说,本小节在于解释一下在PhalApi框架中两个比较好的思想,单例模式和依赖注入.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 单例模式

单例模式对于长期进行过面向对象编程的童鞋应该不算陌生,对学习php的童鞋也应该有听过,这里简单的聊一聊单例模式它到底是一个怎么样的东东,解决了怎么样的问题,并且在PhalApi中它是如何实现的.

单例单例,所谓单例也就是保证一个类仅有一个实例，并提供一个访问它的全局访问点,这就是单例,不难看出他的好处：资源利用少，因为只有一个,大家都是知道要使用一个类必须要实例他，也就是new，在每次new一个对象的时候都会在内存中生成一块区域来存放这个实例,如果在程序一次运行中使用了很多的new实例化了同一个对象,那就比较消耗资源了,但是如果是通用一个使用全局变量global程序又会显得不那么优雅而且会很乱,在这种情况下，单例模式就产生了.

单例模式是一个很好的解决方案，既可以全局通用,又不必担心占用过多的资源,且非常优雅。我们来一起看看在PhalApi中是如何实现单例模式的:

//大家看到我们常用的DI方法内部实现的是PhalApi_DI中的静态方法one方法
function DI() {
    return PhalApi_DI::one();
}

然后我们看向one方法内部

每当请求过来时先验证静态变量instance是否已被类初始化赋值，若无，则会在内部去实例化PhalApi_DI类，然后赋值给$instance并返回,若有，则当下次请求过来时，直接返回已实例化的对象。简言之，PhalApi框架所有的地方使用的DI方法，其实都是返回同一个对象实例,在内存中只存在一块区域,代码如下:

public static function one(){
    if(self::$instance == NULL){
        self::$instance = new PhalApi_DI();
        self::$instance->onConstruct();
    }

    return self::$instance;
}

2. 依赖注入

依赖注入又称之为"控制反转",如果是熟悉javaweb开发的spring框架应该有比较深的感触,在这里也不往深的讲,就简单讲解一下PhalApi中DI依赖注入的实现，以便让大家了解这种设计模式实现原理以及自此基础上实现的惰性加载机制.

2.1 DI依赖注入实现

大家在PhalApi中常用的DI()方法,也就是采用我们上面所谓的单例模式,我们每次使用DI()就是在使用PhalApi_DI类,其实我们依赖注入的关键也就是在PhalApi_DI之中

先来讲一下他的一个实现方式再来讲具体实现,这里举个例子:

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

其实在内部有一个数组,它把config作为了key,把new PhalApi_Config_File(API_ROOT . '/Config')作为了value，然后保存了起来。当我们下一次使用DI->config->get()的时候，它就会根据key值config拿出开始new好的类,所以可以说config操作是依赖于DI(),而且在使用DI()->config的时候永远都是在使用同一个实例,减少资源的消耗.

有的童鞋就好奇了为什么DI()->config会存到数组里而在需要的时候会拿出来？感兴趣的童鞋可以百度一下魔法方法set和get

/**大家可以看到这是PhalApi_DI中的魔法方法__set
 * 也就是当使用DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');的时候
 * 获得的name值就是config,获得的value也就是new PhalApi_Config_File(API_ROOT . '/Config');
 */get同理,在内部实现都是调用了内部get和set方法
public function __set($name, $value){
    $this->set($name, $value);
}

public function __get($name){
    return $this->get($name, NULL);
}

看完之后大家是不是觉得很简单啊,大家以后也可以在设计自己的类的时候采用这种灵活的魔法方法实现.

2.2 惰性加载

在PhalApi中的DI()方法也提供惰性加载,惰性加载如字意也就是当类没有被使用到的时候不会被加载,这样的操作也是为了避免浪费不必要的资源,当我们不使用的时候永远不会去实例化，只有当你使用到的时候才会去实例化。接下来，我们来看看是如何实现的.

//当我们执行以下语句的时候,在依赖注入的时候存的是value值是字符串的test
DI()->test = 'test';

//使用DI()->test->test();的时候会使用到PhalApi中的get方法,在get方法中有一段代码
$this->data[$key] = $this->initService($this->data[$key]);

//在initService方法内部验证了value是字符串就实例化了再返回

if($config instanceOf Closure){
    $rs = $config();
}elseif(is_string($config) && class_exists($config)){
    $rs = new $config();
    if(is_callable(array($rs, 'onInitialize'))){
        call_user_func(array($rs, 'onInitialize'));
    }
}else{
    $rs = $config;
}

3. 总结

在本节中简单的讲解了关于单例模式,依赖注入以及惰性加载,这几种设计模式都是常用的，能够减少资源利用率,希望大家看了这篇介绍能够亲自地去体验一下PhalApi中的这几种模式。在下一小节就讲解如何构建自己的拦截器,希望大家能够继续关注!

注:笔者能力有限，有说的不对的地方，希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.9]-自动加载和拦截器 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇3(自动加载和拦截器)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

最近工作上的事情比较多，所以文档更新得比较慢，还望大家见谅!接下来为大家带来的是对此框架自动加载机制的一个小的引导,简单地来聊聊它是如何实现此类机制的,并在了解之后要怎样去使用。同时再探讨一下如何使用自动加载来构建自己的公用函数以及拦截器,包括如何使用拦截器等,在这里强调一下,在进阶篇中谈论思想会比较多，大家可以慢慢去消化。涉及的很多地方都是此框架优秀的地方所在,因此，拿出来分享是为了让大家能够更好的了解phalapi框架，并且可以运用到自己的代码中去!

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 自动加载机制

关于自动加载，使用phalapi框架的大家应该已经非常熟悉了。三层结构自动加载请求调用在大家看来基本上感觉不到自动加载机制的存在,其实，它早就被注册到接口文件里了,只是有一套完整的机制来封装这类事务让我们摸不着,我们的接口文件就是在index.php

//装载你的接口
DI()->loader->addDirs(array('Demo', 'Library'));

大家不妨进入到addDirs里面瞧瞧

public function addDirs($dirs) {
    if(!is_array($dirs)) {
        $dirs = array($dirs);
    }
    $this->dirs = array_merge($this->dirs, $dirs);
}

其实，当我们使用到addDirs的时候，框架内部只是把我们定义好的路径存了起来；其实关键的地方已经在初始化PhalApi_Loader的时候被触发了,我们来一起看下PhalApi_Loader的构造函数

public function __construct($basePath, $dirs = array()) {
    $this->setBasePath($basePath);
    if (!empty($dirs)) {
        $this->addDirs($dirs);
    }
    spl_autoload_register(array($this, 'load'));
}

第一句方法的意思是设置根目录,后面一个判断条件和我们使用addDirs是一样的作用,那么问题来了spl_autoload_register做了什么呢？官方给出的解释是这样的：

将函数注册到SPL__autoload函数队列中。如果该队列中的函数尚未激活，则激活它们

参数

欲注册的自动装载函数。如果没有提供任何参数，则自动注册autoload的默认实现函数spl_autoload()

其实说白了就是当实例化类的时候找不到可以用的类的时候，就会请求在spl_autoload_register中定义好的处理方法,在此框架中配置的处理函数也就是PhalApi_Loader/load函数。

关于具体是如何实现匹配，如何找到路径，再如何引入，这里就不详细说明了。当我们每次去new一个类的时候，都会先去寻找有没有匹配的类,当没有匹配到的情况下,我们new的类名就会传递到PhalApi_Loader/load函数中去处理,根据框架规则引入我们需要的文件.

2. 公用函数和拦截器

当我们会使用自动加载机制了之后，我们就可以来构建我们项目的公用函数和拦截器了,为什么要使用公用函数呢？在大部分请求下我们一套框架可能支持多个项目,当然项目与项目之间会有不同的函数等等,但是也有公用的比如debug，发送短信等等一些所有项目都可能去用到的一些功能性函数,或者是一些针对项目的拦截器。

2.1 公共函数

首先在我们的根目录建立一个文件夹叫做General通用的意思,里面分别有两个文件夹Common(受TP的影响)和Intercept两个文件,我们在里面放入我们自己的文件,当然需要按照正确的规则比如Common_Base等

然后我们要使用的时候，先加载这个general文件作为自带加载文件,然后注册自己需要用的类,如下:

//加载项目通用文件
DI()->loader->addDirs('General');
//通用函数基础类
DI()->base = new Common_Base();

2.2 拦截器

对于API来说，拦截器的意义无疑是巨大的,为什么这么说呢，应为API是提供服务器的,我们可以用拦截器做很多的限制,比如限制开放接口,参数转移、json解包、请求解密,token校验等等。

//拦截器
$Intercept           = new Intercept_app_default();
//转换参数
$_REQUEST            = $Intercept->turnparameter($_REQUEST);

作为参考,拦截器接收所有的post和get参数后在内部统一做处理。参数转换、请求解密、token校验等这些都是日常用到的，不详提。这里拿一个有代表性的来说明一下就是json解包,为什么要json解包呢？因为在很多公司使用请求传参的时候，都是封装成一个json请求过来的,框架接收是能接收到,但是无法使用框架自带的参数做完整性校验了。令我非常难受(也有很多朋友也遇到此类问题),所以自己写了一个解包方法：

//递归解包
function test($params, $server){
    if(is_array($params)){
        foreach($params as $k => $v){
            $server[$k] = $v;
            $server = test($v, $server);
        }
    }
    return $server;
}
//模拟一个json数组
$params = array(
    'a' => array(
        'c' => 3,
        'd' => 4
    ),
    'b' => 2,
);
$server = array();
var_dump(test($params, $server));

我们获取到的返回结果是array(4) { ["a"]=> array(2) { ["c"]=> int(3) ["d"]=> int(4) } ["c"]=> int(3) ["d"]=> int(4) ["b"]=> int(2) }也就是把数组中的底层元素全部移到了和a,b同级的目录，并且也保证了a的数组完整性,必须要注意同名的问题。

3. 总结

本小节,主要讲述了一下关于自动加载的机制,以及为什么要使用拦截器的原因和怎么让json通过拦截器也能使用框架的校验机制。今天就先到这里了,希望此教程能够给你们带来帮助。在下一小节中将会对model层和事务进行已经深入的讲述。祝大家天天开心~

注:笔者能力有限，有说的不对的地方，希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.10]-notorm进阶以及事务操作 | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档
  • 类参考手册
  • 在线体验
  • 捐赠
  • 关于

phalapi-进阶篇4(notrom进阶以及事务操作)

前言

先在这里感谢phalapi框架创始人@dogstar为我们提供了这样一个优秀的开源框架。

写本篇教程的起因是因为在交流的时候有位童鞋提出了fetchPairs有BUG，之前一直没有仔细了解过notorm的细节，借此机会把notorm中的一些方便快捷的操作，以及notorm中有关事务的操作和使用，具体地作一下分析。 附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 进一步了解notorm

notorm中很多操作在PhalApi官方文档中并没有非常详细的介绍，这里把一些常用到的notorm函数作一下相关的介绍。

1.1 fetchPairs

fetchPairs的使用和fetchAll相似但不尽相同，fetchPairs需要传递一个参数,这个参数是字符串类型的key值(比如：name 、id),当参数传递之后会出现这样的对比效果：

DI()->notorm->user->fetchPairs('name');

DI()->notorm->user->fetchAll('name');

大家有看到区别吗？

嗯！是fetchPairs在返回的时候把在之前定义的key值放到了返回数组的key中。

提示:在老版本中，使用过程中 Result.php 会有一句 821行的报错，需要把

$values = array_values(iterator_to_array($row));
改成
$values = array_values($row);

1.2 lock

lock操作是一种锁的行为，对整表进行锁定，在当前用户释放锁之前此表不能再被操作。lock接受一个bool参数，默认是true，即锁表，使用如下：

$userdb = DI()->notorm->user->lock(); //锁定表
$rs     = $userdb->fetchAll();

生成的sql语句如下：

SELECT * FROM user FOR UPDATE;

1.3 加操作

在很多时候，我们会遇到数据库表里面的某个值需要+1操作，我们不能简单地在update的时候写入array('key' => 'key+1')，因为在解析sql的时候，key+1 会带上引号作为一个字符串被处理，所以，这样的操作并没有达到我们想要的效果，当然，这是有解决方法的。具体操作如下：

DI()->notorm->user->where('id', 1)->update(array('sum' => new NotORM_Literal("sum + 1")));

这样就可以生成不带引号的操作了：

UPDATE user SET phone = sum + 1 WHERE (id = 1);

1.4 group

group是去重操作，在notorm里面也有封装，我们可以使用group来统计比如有多少同名的用户等一些统计操作，或者是找出库里面不重复的城市名等。

DI()->notorm->user->select('name , count(*) as count')->group('name')->fetchAll();

会获得如下结果

1.5 快速函数sum，count，max，min

在我们使用notorm快速函数的过程中,虽然此类函数使用频率不高，但是使用起来却是很方便：

  return DI()->notorm->user->sum('id');  //做加法
  return DI()->notorm->user->max('id');  //获取这个key中最大的值
  return DI()->notorm->user->min('id');  //获取这个key中最小的值
  return DI()->notorm->user->count();    //统计一共几条数据

2. 事务操作

其实，很大一部分使用事务的原因是因为害怕在并发的情况下导致对数据库造成的重复操作，比如如下场景：

两个管理者对同一应用进行审核。他们同一时间查询到应用的状态都是未审核，然后都进行了审核操作，这时，返回的审核操作状态是成功的。但这时，会有一个管理者会发现审核状态和自己的审核结果不同，这就有问题了。大部分在解决此类问题时，都会考虑使用数据库的事务操作。其实，对于事务操作，我的建议是：能不用尽量不用。我简单聊一下我的几个观点：

  1. 是否真的那么重要？对于上面的问题，也许乍一看觉得：这怎么可能呢？但是仔细一想，这两个操作都是属于两个管理者的正常操作。对于业务来说，我认为是没有问题的。当然这是因为影响不大；如果是付款，一个订单从两个地方同时付款，然后都发现是未付款，然后都进行了付款，这种业务就必须有处理方式了，所以是否使用事务要看业务是否非常重要。

  2. 概率是怎么样的？对于一个程序来说，两个请求刚好通过了查询过程，并一同到了修改的操作，不管是什么场景，发生这种并发冲突的可能性是极其微小的，我觉得几乎可以忽略(当然前提是重要性没有那么的高，我们不能 以偏概全 )。

  3. 曲线救国，其实，我们可以使用其他方式避免这类问题。我们可以把我们需要验证条件加入到Update的条件中，这样两条语句终归会有一条语句执行失败。

notorm中使用到的事务操作方式：

notorm提供了常规的事务操作如下：

 //第一步：先指定待进行事务的数据库（通过获取一个notorm表实例来指定；否则会提示：PDO There is no active transaction）
 $user = DI()->notorm->user;
 //第二步：开启事务开关（此开关会将当前全部打开的数据库都进行此设置）
 DI()->notorm->transaction = 'BEGIN';

 //第三步：进行数据库操作
 $user->insert(array('name' => 'test3',));
 $user->insert(array('name' => 'test4',));

 //第四：提交/回滚
 DI()->notorm->transaction = 'COMMIT';
 //DI()->notorm->transaction = 'ROLLBACK';

还有一种处理方式就是在操作之前进行lock锁表

$user = DI()->notorm->user;
$user->lock();
$user->insert(array('name' => 'test3',));
$user->insert(array('name' => 'test4',));

这两种方式大家可以自行取舍。

注:在phalapiV1.31版本后有自带提供事务操作可参考文档。

3. 总结

在本小节中，对于notorm中的一些特别的封装函数进行了介绍，对于事务提出了自己的看法和观点以及使用notorm的解决方案。希望看了这篇教程的童鞋，在日常开发中对你有所帮助。

注：笔者能力有限，有说的不对的地方，希望大家能够指出，也希望多多交流！

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.10]-notrom进阶以及事务操作 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇4(notrom进阶以及事务操作)

前言

先在这里感谢phalapi框架创始人@dogstar为我们提供了这样一个优秀的开源框架。

写本篇教程的起因是因为在交流的时候有位童鞋提出了fetchPairs有BUG，之前一直没有仔细了解过notorm的细节，借此机会把notorm中的一些方便快捷的操作，以及notorm中有关事务的操作和使用，具体地作一下分析。 附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 进一步了解notorm

notorm中很多操作在PhalApi官方文档中并没有非常详细的介绍，这里把一些常用到的notorm函数作一下相关的介绍。

1.1 fetchPairs

fetchPairs的使用和fetchAll相似但不尽相同，fetchPairs需要传递一个参数,这个参数是字符串类型的key值(比如：name 、id),当参数传递之后会出现这样的对比效果：

DI()->notorm->user->fetchPairs('name');

DI()->notorm->user->fetchAll('name');

大家有看到区别吗？

嗯！是fetchPairs在返回的时候把在之前定义的key值放到了返回数组的key中。

提示:在老版本中，使用过程中 Result.php 会有一句 821行的报错，需要把

$values = array_values(iterator_to_array($row));
改成
$values = array_values($row);

1.2 lock

lock操作是一种锁的行为，对整表进行锁定，在当前用户释放锁之前此表不能再被操作。lock接受一个bool参数，默认是true，即锁表，使用如下：

$userdb = DI()->notorm->user->lock(); //锁定表
$rs     = $userdb->fetchAll();

生成的sql语句如下：

SELECT * FROM user FOR UPDATE;

1.3 加操作

在很多时候，我们会遇到数据库表里面的某个值需要+1操作，我们不能简单地在update的时候写入array('key' => 'key+1')，因为在解析sql的时候，key+1 会带上引号作为一个字符串被处理，所以，这样的操作并没有达到我们想要的效果，当然，这是有解决方法的。具体操作如下：

DI()->notorm->user->where('id', 1)->update(array('sum' => new NotORM_Literal("sum + 1")));

这样就可以生成不带引号的操作了：

UPDATE user SET phone = sum + 1 WHERE (id = 1);

1.4 group

group是去重操作，在notorm里面也有封装，我们可以使用group来统计比如有多少同名的用户等一些统计操作，或者是找出库里面不重复的城市名等。

DI()->notorm->user->select('name , count(*) as count')->group('name')->fetchAll();

会获得如下结果

1.5 快速函数sum，count，max，min

在我们使用notorm快速函数的过程中,虽然此类函数使用频率不高，但是使用起来却是很方便：

  return DI()->notorm->user->sum('id');  //做加法
  return DI()->notorm->user->max('id');  //获取这个key中最大的值
  return DI()->notorm->user->min('id');  //获取这个key中最小的值
  return DI()->notorm->user->count();    //统计一共几条数据

2. 事务操作

其实，很大一部分使用事务的原因是因为害怕在并发的情况下导致对数据库造成的重复操作，比如如下场景：

两个管理者对同一应用进行审核。他们同一时间查询到应用的状态都是未审核，然后都进行了审核操作，这时，返回的审核操作状态是成功的。但这时，会有一个管理者会发现审核状态和自己的审核结果不同，这就有问题了。大部分在解决此类问题时，都会考虑使用数据库的事务操作。其实，对于事务操作，我的建议是：能不用尽量不用。我简单聊一下我的几个观点：

  1. 是否真的那么重要？对于上面的问题，也许乍一看觉得：这怎么可能呢？但是仔细一想，这两个操作都是属于两个管理者的正常操作。对于业务来说，我认为是没有问题的。当然这是因为影响不大；如果是付款，一个订单从两个地方同时付款，然后都发现是未付款，然后都进行了付款，这种业务就必须有处理方式了，所以是否使用事务要看业务是否非常重要。

  2. 概率是怎么样的？对于一个程序来说，两个请求刚好通过了查询过程，并一同到了修改的操作，不管是什么场景，发生这种并发冲突的可能性是极其微小的，我觉得几乎可以忽略(当然前提是重要性没有那么的高，我们不能 以偏概全 )。

  3. 曲线救国，其实，我们可以使用其他方式避免这类问题。我们可以把我们需要验证条件加入到Update的条件中，这样两条语句终归会有一条语句执行失败。

notorm中使用到的事务操作方式：

notorm提供了常规的事务操作如下：

 //第一步：先指定待进行事务的数据库（通过获取一个notorm表实例来指定；否则会提示：PDO There is no active transaction）
 $user = DI()->notorm->user;
 //第二步：开启事务开关（此开关会将当前全部打开的数据库都进行此设置）
 DI()->notorm->transaction = 'BEGIN';

 //第三步：进行数据库操作
 $user->insert(array('name' => 'test3',));
 $user->insert(array('name' => 'test4',));

 //第四：提交/回滚
 DI()->notorm->transaction = 'COMMIT';
 //DI()->notorm->transaction = 'ROLLBACK';

还有一种处理方式就是在操作之前进行lock锁表

$user = DI()->notorm->user;
$user->lock();
$user->insert(array('name' => 'test3',));
$user->insert(array('name' => 'test4',));

这两种方式大家可以自行取舍。

注:在phalapiV1.31版本后有自带提供事务操作可参考文档。

3. 总结

在本小节中，对于notorm中的一些特别的封装函数进行了介绍，对于事务提出了自己的看法和观点以及使用notorm的解决方案。希望看了这篇教程的童鞋，在日常开发中对你有所帮助。

注：笔者能力有限，有说的不对的地方，希望大家能够指出，也希望多多交流！

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.11]-数据库读写分离 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇5(数据库读写分离以及多库使用)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

读写分离是我们常用的一种解决方案,它可以解决大量读操作的时候数据库瓶颈的问题,我们在真正开发一个项目的过程中可能会遇一个接口或者一个操作中需要用到多个数据库操作的时候怎么办,今天的教程就从这个两个问题出来,来聊聊使用phalapi怎么解决这个问题.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. 读写分离

在我们的生活中无时无刻不面临着并发的问题,比如下班高峰交通问题,人们用了红绿灯来解决的一部分问题(但是很痛苦这个就和查询的时候阻塞一样的,10秒做update操作,10秒做select操作一样的),那么人们有想到了一个方法高架,高架的路线是和正常的路线一致的(数据相同) ,高架分两条路而且一条路上的车不能到另外一条路上(读写分离) 这样就解决了我们这类常规并发的问题

其实看起来上面这种解决方案是比较好的其实并不是这样的,有两条路但是其实它的宽度是不一样的,我们这里把一个叫做写路,一个叫做读路,我们如果要做一个读路并且把他成倍的变宽是一件很简单的事情(也就是多从库),但是我们要建立一条写路可以,但是我们要把这条写路变宽是比较困难的,我们一般不这样做.

1.1 mysql主从配置

我是使用了阿里云的多读库服务,这里引用一下百度经验的mysql主从处理

----------------------------------------------------------------------------------------------------

MYSQL【一台主机多个实例的主从配置】

1、建立账号

进入mysql数据库，创建一个用于从库备份的账号

mysql>GRANT REPLICATION SLAVE ON *.* TO 'slave_test'@'10.19.194.57' IDENTIFIED BY 'Password@123456';

mysql>FLUSH PRIVILEGES;   ---刷新

该命令详情参考grant创建用户命令

2、如果已经有一个实例在运行（因为开始已经安装了mysql，所以有一个默认的），那么再启动一个实例，方法如下：

（1）shell>cd /usr/local/mysql/

（2）shell>mkdir -pv /data/mysql2

（3）shell>chown -R mysql.mysql /data/mysql2

（4）shell>./scripts/mysql_install_db  --user=mysql  --datadir=/data/mysql2  这个为另外一个目录，该步骤意味初始化目录，并创建一个实例

3、配置/etc/my.cnf，配置方法如下

[mysqld_multi]

mysqld = /usr/local/mysql/bin/mysqld_safe

mysqladmin = /usr/local/mysql/bin/mysqladmin

user = root

[mysqld] ## 本来mysql配置

server-id = 1

port = 3306

socket = /data/mysql/mysql.sock

datadir = /data/mysql

pid-file = /data/mysql/wzj.pid

log-error = /data/mysql/wzj.err

log-bin = /data/mysql/log/bin-log

log-bin-index = /data/mysql/log/mysql-bin.index

user = mysql

[mysqld2]  ##创建实例配置

server-id = 2

port = 3307

socket = /tmp/mysql2.sock

datadir = /data/mysql2/                         //mysql2存储数据库的地方，也就是实例

pid-file = /data/mysql2/mysql2.pid       //mysql2的pid文件记录

log-error = /data/mysql2/mysql2.err     //为错误日记文件的地方

user = mysql

#master-host =10.19.194.57

#master-user ='wzj_slave_test'

#master-pass ='Password@123456'

#master-port =3306

#master_connect_retry=60

#replicate-do-db =mysql_wzj

#replicate-ignore-db=mysql--------该段已注释，因为新版本中，需要用CHANGE MASTER TO 命令去设置

【注意】：以上文件的目录，皆为系统创建，并非手动去创建，设置配置文件后，重启mysql，文件就会自动创建

4、重启，然后可以使用msyql_multi命令启动实例，再查看一下master主库的状态，其中Binlog_Do_DB表示要备份的数据库，Binlog_Ignore_DB表示不备份的数据库

shell>/etc/rc.d/init.d/mysqld start/stop/restart

shell>/usr/local/mysql/bin/mysqld_multi start 1-2  启动实例 （1-2表示启动1、2两个实例）

shell>mysql master： mysql -P 3306 -u root -p -S /data/mysql/mysql.sock (密码：1)

mysql>show master status; or show master status \G;   显示主库的状态，其中File 和Position 数值要记住，下面的mysql语句要用到，用于设置从库

5、进入从库，并设置从库的一些参数

shell>slave:mysql -P 3307 -u root -p -S /tmp/mysql2.sock （密码：回车）   ---另外开个远程

mysql>CHANGE MASTER TO

mysql> MASTER_HOST='10.19.194.57',  ---主库的地址

mysql>MASTER_USER='wzj_slave_test',  ---主库用户

mysql>MASTER_PASSWORD='Password@123456',   ---主库的密码

mysql>MASTER_LOG_FILE='bin-log.000013' ,  ---此参数为master status中的File值

mysql>MASTER_LOG_POS=120;   ---此数值为Position数值

6、设置好后，运行start slave，如果下图红线地方是YES，则成功，否则，查看配置文件是否有错或有误。

slave_io_running  ：yes

slave_sql_running :yes

7、可以查看从库下mysql进程信息

mysql>show processlist;

8、测试

在主库上面建新的数据库，然后在从库中查看是否有同步~

----------------------------------------------------------------------------------------------------

1.2 开始实现

思想我们已经讲完了相信大家已经多多少少的理解了,但是我们要如何在phalapi去实现读写分离的操作

1.首先我们要在初始化一个数据库连接,我们需要建立一个配置文件read.php和dbs.php一样只是连接这读库的配置项,并且注册

//读库
DI()->read = function (){
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('read'), $debug);
};

2.对PhalApi_Model_NotORM文件中的getORM进行修改

/**
 * 快速获取ORM实例，注意每次获取都是新的实例
 */
protected function getORM($id = NULL){
    $table = $this->getTableName($id);
    if($id == 'read'){
        return DI()->read->$table;
    }
    return DI()->notorm->$table;
}

3.当我们在model层使用$this->getORM('read') 的时候就会使用读库,使用$this->getORM() 会默认使用写库

//select操作
$this->getORM('read')->select('*')->where('aId', $aId)->fetchAll();
//insert,update,delete操作
$this->getORM()->insert();

2. 多库处理

有些人会问,为什么需要进行多库处理,所有的表放到一个库不就好了吗?

在原来重构别人外包项目是遇到过一个库100来张表,没有备注没有文档,几乎没法看,在我自己做一个比较大的项目的时候我也是做了一些考虑把库根据业务分解成一个一个子库,可以很好的避免表太多结构混乱的问题,而且我们可以随时把任何一个子库移植出去,当我一个mysql承载了很多压力,我可以把这个mysql上面的子库分解到多个mysql上面来实现压力分流.

看了上面的这一段介绍相信大家不言而喻的了解了这样做的好处,但是如何实现呢,聪明的大家可能在想不久和上面一样在多注册一个数据库连接嘛,其实大家说对了一半,我们是需要注册一个连接,但是我们需要一种统一的规范来使用这类操作

1.初始化数据库连接,一样的复制一份dbs.php为developers.php配置连接开发者库

//开发者库
DI()->developers = function (){
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('developers'), $debug);
};

2.对PhalApi_Model_NotORM文件中的getORM进行修改

/**
 * 快速获取ORM实例，注意每次获取都是新的实例
 */
protected function getORM($id = NULL){
    $table = $this->getTableName($id);
    if($id == 'developers'){
        return DI()->developers->$table;
    }
    return DI()->notorm->$table;
}

3.在之前我们不是建立了我们的公用General文件我们在构建这样的结构

--General                   //公用目录
    --developers                //通用项目库目录
        --Domain                //项目库Domain层
        --Model             //项目库Model层

然后我们在这里的Model进行的操作都是使用$this->getORM('developers')进行操作

这样规范的好处就是在与共享和模块化 让正常的一个Model层不会使用到任何不同库的操作,我们多个项目用到其他的一个库可以进行代码复用,

3. 总结

其实实现今天介绍的两种解决问题的方案还有很多很多,不同的业务可能需求不同实现也就不一样希望今天的介绍对大家有帮助,后面两篇教程将基于我写的两个拓展分别介绍分表分库操作和redis实际的使用,希望大家进一步关注!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.12]-解决大量数据存储数据库分表分库拓展 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇6(解决大量数据存储数据库分表分库拓展)

前言

时隔半个月随着PHP7的推出为PHP打了一瓶兴奋剂,在性能提升了一倍的情况下我们会逐渐发现,瓶颈会集中在数据库操作,那我们的内容就接着数据库读写分离,来聊聊分表分库应该怎么玩,因为PhalApi的分表分库并不是非常方便,笔者在这里提供了一个分表分库数据库集群的拓展,详细文档请见博客基于PhalApi的DB集群拓展 V0.1bate 大家可以自行在开源中国扩展Git地址中找到Cluster进行下载使用.

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

开源中国扩展Git地址: http://git.oschina.net/dogstar/PhalApi-Library

1. 场景

在实际工作中,我信奉一句话一切抛开业务的架构设计都是耍流氓所以我们从场景进行开篇

1.1 单条数据多查多写多改

这里做的例子,大家都在玩游戏把,玩游戏里面是不是有角色,角色是不是有装备,经验,物品以及等等,而且他会有一个特别的要求就是实时(因为我角色打了一个怪物获得了100xp我们不可能告诉他你等6个小时缓存时间结束了再来看,必须是实时的),当然我们可以使用缓存来解决这个问题我们下节会说到这个问题

那么在这种场景下,一个用户对于角色的操作非常频繁而且唯一我们就很好采用分表分库的操作了,相对于单表操作他会把所有的操作分散到各个数据库去操作,这样对于单个数据库总执行sql语句量就会有个指数级的下降,以及数据量也会均衡分配到每个数据库,但是当我们进行这类单条数据操作的时候根本不会对性能有任何的影响,因为只是通过算法得出了这条记录存在于那个库那张表而已,

1.2 日记记录分析

就已上面的例子我们继续讲,如果有一天你的领导过来提了个需求,我需要一个数据分析系统来统计用户每天什么时间段最活跃.用户平均每人充值了多少钱啊,多少等级下用户充钱最多啊,如果遇到这种问题你们会怎么办?三分钟思考

我们先来看看我们会遇到什么样子的问题,数据量大积累当1000w+之后数据库执行sql基本没法看,大量的写入数据对数据库压力大

我们再来看看分表分库怎么解决这个问题,1000w+数据库的情况下 比如你是4表4库一共16张表,那每张表的数量就是1000w/16=62w也就是每张表只需要存储62w的数据就ok了,当写入数据的时候会根据ID的顺序均衡写入4库执行sql的压力也就分布到了4个数据库,唯一的问题就是在执行where条件的时候可能需要对前置表进行遍历,而前置表的数据量就是1000w,当然前置表里面只存放ID和where条件的字段

2. 实现思路

就笔者在工作中接触到了很多案例的分表分库，使用了根据城市,或者是其他的特性进行分表分库规则,这样一定会出现用户分布不均匀导致的某一个库表压力巨大,我这里使用了均等分分割

大家先看一组图就会明白了

  1. 当我们进行插入的时候的操作如下:

    插入前置表获取主键,通过id得出应该存入几库几表在相应的地方写入数据

  2. 当我们进行单条读取操作的时候操作如下:

    通过id获取应该在几库几表在相应的地方获取数据

  3. 当我们使用where查询的时候操作如下:

    如果where条件在前置表存在从前置表通过where获取结果集ID,通过ID分组到库和表,然后进行查询在拼接结果集统一返回

3. 优缺点

  1. 优点:

    很好的避开了数据库存放数据过多效率底下的瓶颈

    在单条记录操作性能指数及提升

    数据量大的情况下where条件查询性能提高基本

    能对亿级的数据进行处理而且效率较高

    不需要考虑分表分库规则数据均等分布

  2. 缺点

    where查询字段必须预先添加到,前置表不然就必须遍历数据库数量 * 表数量才能得到想要的结果

    where查询就算有前置表的情况下最坏的情况也需要遍历数据库数量 * 表数量才能得到想要的结果

    对一些特定查询天生不足比如排序

4. 总结

在本小节的最好简单提及一下,基于PhalApi的DB集群拓展 V0.1bate功能展示比较局限童鞋们可以根据自己的业务需求来觉得是否使用,笔者也会在后期继续更新维护完善为一个比较方便的集群拓展.

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.13]-使用缓存以及用redis拓展解决实际问题 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇7(使用缓存以及用redis拓展解决实际问题)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

当我们在开发一个项目时,我们可能会遇到很多问题,比如消息推送,发送邮件,发送短信,以及并发跟不上,这个时候就该轮到常用的缓存出手解救我们了,我们接下来来讲讲缓存Redis在实际中的使用,解决实际问题.在这里是基于redis的基本知识,和简单看一下PhalApi的redis拓展文档在前来阅读此小节.

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

开源中国扩展Git地址: http://git.oschina.net/dogstar/PhalApi-Library

1. 能解决什么问题

当我们使用一门技术的时候,我们当然是为了解决问题才去使用它的,那么我们使用缓存技术Redis能解决什么具体的问题呢?

1.1 缓存结果集

这里给一个例子大家看一下就会明白缓存结果集是什么意思

//从缓存redis的clubcache库中查询club表where条件是city,city值是$city
$cache = DI()->redis->get_Time('club'.'city'.$city,'clubcache');
//如果查询到了就直接返回缓存的结果
if($cache){
    return $cache;
}
//如果不存在从数据库里面获取结果然后存入redis缓存key的条件和取值时一样,最后一个参数为过期时间
$rs = $this->getORM()->select('*')->where('city',$city)->fetchAll();
DI()->redis->set_Time('club'.'city'.$city,$rs,'clubcache',600);

上面做的事情就是把结果保存600秒,600秒内的再次查询会获取一样的结果

1.2 队列处理

Redis运用到时间中有一个比较关键的作用就是他的队列

我们先过一下几个特殊的redis函数

//写入队列左边
set_lPush
//写入队列左边 如果value已经存在，则不添加
set_lPushx
//写入队列右边
set_rPush
//写入队列右边 如果value已经存在，则不添加
set_rPushx

//读取队列左边
get_lPop
//读取队列右边
get_rPop
//读取队列左边 如果没有读取到阻塞一定时间
get_blPop
//读取队列右边 如果没有读取到阻塞一定时间
get_brPop

比如我们在做消息推送,发送邮件,发送短信这类业务的时候,我们需要请求第三方接口,请求的速度是第三方来决定的,比如微信一个推送接口就是200ms,如果放到我们的API业务里面就会出现一个巨大的问题,用户访问速度极度下降,解决这类问题的方案就是队列流程如下

当我们接收到用户的推送请求时
            ↓
把推送请求加入到队列API里面不做任何操作(比如加入到左边)
            ↓
在后台有一个PHP脚本运行一直在读取队列(读取右边就是后进后出,如果读取左边就是先进先出)
            ↓
然后执行响应的推送逻辑

一般我们的脚本是一个死循环,或者shell定时请求,我们会采用读取不到数据是阻塞来解决去不到值循环过快的问题

1.3 临时数据存储

临时数据就不需要太多的说明了,举个例子就够了

比如我们获取验证码,我们需要把验证码存到库中吗,我觉得是没有必要的,而且数据库并不好做过期的操作只能我们自己判断

那么我们使用redis把验证码存入redis 然后给一个过期时间就很好的解决这个问题了

1.4 数据库

把redis作为数据库用算是比较深入的使用了,这里聊下思想

大家之后service可以分布式,但是对于大部分数据库的分布式并不容易,所以导致了很多系统到后面拼接堆积在数据库,当然可以使用缓存存储结果集,但是这种解决方便治标不治本,在和童鞋们探讨的时候得出了一个解决方便,就是把redis作为第一数据库mysql作为元数据库

做了这种操作之后服务器会自动把热数据同步到redis,把冷数据存放到mysql,当使用到冷数据了在存放到redis,用户大部分的操作基本是基于redis进行的操作

当作这样实现的成本比较高要实现redis 数据同步 封装使用 where查询 等等需要很大的精力去做,在后期笔者有打算做一个通用的拓展

2. 规范化使用

其实以上的类容已经讲的差不多了,为什么还有单独拿出一段来讲一讲规范呢,因为缓存不像是数据库当你需要去查看缓存的时候,如果所有的数据都堆积在redis的一个库,你会非常痛苦

但是redis支持多库所以需要一套规范来划分,这里分享一下我这边是如何使用的

0~10库 作为正常业务库,也就是推送队列,临时数据,每一个库都只存储一种业务的数据,比如微信推送就存在5库,而邮件推送的数据就存在6库,发送验证码的临时数据存储在3库,一次类推,如果觉得10个库还不够用可以根据业务增加

10库以上作为cache库用来存储每张表的结果集数据,或者是其余的数据

所有的key的命名规范必须带有类型+表名+条件

3. 总结

看了本小节之后相信大家都对缓存在时间开发中起到了什么样的作用有了个了解,这一小节的完成,我们的进阶篇也步入尾声了,下一篇是对于进阶篇的总结了,也多谢大家一路的陪伴!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页 下一章

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.14]-PhalApi能带来什么和进阶篇总结 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

phalapi-进阶篇8(PhalApi能带来什么和进阶篇总结)

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

到今天位置PhalApi已经开源一周年了,他从一个不起眼的小框架,到现在一个在不断迎合业务需求不断成长,也能为大家带来便捷的框架,从当初的群里只有几个人到现在群里已经有300+位童鞋,从没有项目使用到实际项目28+,这一切都要感谢开源精神以及那么多 小伙伴的支持,在本次进阶篇的尾声我们来一同聊一聊PhalApi能带来什么以及对进阶篇进行简单的总结!

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1. PhalApi能带来什么

当谈论到这个话题的时候,有的童鞋会说了,框架不就是拿来用的吗,其实不然,笔者觉得有一下几点

  1. 敏捷开发

    PhalApi给我最直观的感受就是,方便快捷,开发一套接口的成本非常低,我记得是在从头到尾开发一套权限接口仅仅用了8个小时就完成了,可见使用PhalApi效率是非常可观的,所以它很适合敏捷开发

  2. 代码规范

    第二点就是代码规范了,PhalApi在代码规范上面有一套比较独特的方式,怎么说呢应为PhalApi是专注对于Api开发的框架,所有的操作都必须按照已经定义好的开发模式去执行,但是又不缺乏灵活,如果想使用自动文档也就要按照要求进行备注,我觉得这一点是其他MVC等框架所不及的

  3. 性能优秀

    在从性能的角度上来看,它有两个优点,没有view层和内部机制非常简单,导致了如果运用来对性能有要求的项目中非常适合,相对于使用MVC框架自己改写Api和使用原生PHP,它的优点结合了两者(当如除了没有view层之外)

  4. 值得深入了解

    关于这一点我考虑了很久,我相信很多人都是和我一样thinkphp是我们的入门框架,但是我们真的有深入了解过thinkphp吗,或者说我们有阅读过它的内部机制吗,我觉得有两方面原因,它具有一定的复杂度,或者说它设计出来就是复杂的(高度封装),源码不易阅读,相对于我觉得PhalApi是一个很适合深入了解源码,而且能从里面学习到很多在做业务工作的时候学习不到的内容

2. 进阶篇总结

经过了2个多月的编写,进阶篇也就到此结束了,我们来一同回顾一下

我们从对于Api,Domain和Model三层结构思想开始,到后面的单例模式,依赖注入以及自动加载和拦截器,对框架的基本思想进行了一些简单的介绍,接着对notorm,事务,读写分离,多库,分表分库进行了介绍,在最后对于缓存技术redis使用场景以及如何使用进行了说明

我相信了解过以上内容的童鞋都能过运用以上的知识解决在实际中遇到的问题,当然这也是我想要达到的效果(让跟多的人了解PhalApi框架)!

3. 最后

在最后祝愿PhalApi框架能在开源的路上越走越远!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:421032344 欢迎大家的加入!

上一章 文档首页

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[7.15]-phalapi-实战篇1(Redis队列处理异步任务) | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流主群：421032344，副群：459352221
  • 社区
  • 下载
  • 文档(新)
  • 文档(旧)
  • 类参考手册
  • 在线体验
  • 增值服务
  • 关于

[PhalApi实战篇(1)]Redis队列处理异步任务

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

哈喽大家好呀!之前编写的PhalApi入门篇和进阶篇已经过去了好久了,在此之间也回答了很多小伙伴各种各样的问题,这里也希望吧里面一些问的比较多的和比较有趣的以及笔者在使用PhalApi一些新的体会,都提取出来为大家带来一些能够在实际开发中可以使用的技术或思想,那么我们就开始我们实战篇中的第一节 Redis队列处理异步任务

大家希望喵咪在PhalApi实战推出一些什么样的内容?可以私信我邮箱wenzhenxi@vip.qq.com

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1.为什么需要队列

为什么需要队列?其实已经是一个老生常谈的一个问题了,队列有诸多好处比如:

在项目中，将一些无需即时返回且耗时的操作提取出来，进行了异步队列处理，而这种异步队列处理的方式大大的节省了服务器的请求响应时间，从而提高了系统的吞吐量。 比较通俗易懂的解释就是 一个请求处理一些事情 A 业务耗时 30ms B业务 耗时 20ms 然后发邮件 耗时 50ms ,吧其中的发送邮件 写入队列 有一个专门负责发送邮件的程序接受这个队列的消息在吧邮件发送出去,这样这个请求原来要用100ms现在只需要50ms , 借助队列可以吧很多原本很消耗时间的操作单独有序处理

队列软件也很多:RabbitMQ,KafKa这两款都是非常主流的队列软件PhalApi也有提供对应的扩展程序来去使用,但是需要使用它们的成本对相对高一些需要搭建很多复杂的组件,但是相对redis,redis虽然没有那么多丰富的功能工具但是它也是队列软件中的不二之选

2.理解Redis处理队列特点

对于PHP来说对Redis支持是特别好的,redis的单线程保障了队列不会应为并发的问题导致一条消息多人获取所有也是很适合做为队列传输,PhalApi不仅仅自带简单的Redis库笔者也在它的基础上封装了一个更为完善的redis库,如下:

Redis · 暗夜在火星/PhalApi Library - 码云 - 开源中国

队列主要由两部分组成一部分是客户端一部分是消费端,客户端负责提供消息消费端负责处理队列这样的一个形式

在使用redis队列主要会使用到redis队列中的List类型,List类型可以从左右两边读取和写入数据,这样的形式就可以做到先入先出或者是后入先出这种队列模式

3.具体实践(基于PhalApi-Redis扩展)

客户端的使用比较简单只需要初始化Redis链接后向左边写入数据即可:

客户端:

//redis链接
DI()->redis = new Redis_Lite(DI()->config->get('app.redis.servers'));
//写入队列左边
DI()->redis->set_lPush(队列键名,值, 库名);

关键是消费端的用法,怎么让消费端一直不停的处理队列呢?很多童鞋应该已经想到了利用死循环不停的读取队列处理来解决及时处理的问题,但是这样又会带来一个新的问题,如果说队列空了死循环会不会一直高额的消耗CPU资源啊?由于这点就衍生出来两种具体的用法:

用法一

有的小伙伴就说了,这个还不简单吗?当队列里面没有内容了我就关掉消费端,然后使用crontab过一段时间再启动进行处理,这就没有所谓的死循环CPU消耗的问题了,这种解法如下:

while (true) {
    // 读取队列右边
    $msg = DI()->redis->get_rPop(队列键名, 库名);
  if( !$msg ){
      break;
  }
    // 处理逻辑
  .....
}

然后通过crontab进行定时任务即可

用法二

第二种用法是通过redis队列的另外一种机制来解决这类问题,相对于get_rPop还有提供一个方法get_brPop,我们可以看一下这个方法的描述读取队列右边 如果没有读取到阻塞一定时间这个阻塞时间是通过配置文件blocking字段来配置的,使用方式如下

while (true) {
    // 读取队列右边
    $msg = DI()->redis->get_brPop(队列键名, 库名);
    // 处理逻辑
  .....
}

这种方法相对上面那种的好处是实时性最好,如果阻塞时间设置的是5秒等待了2秒有消息进来了就里面会进入处理模式

上述方式可以使用Supervisor进行常驻内存执行

总结

本次实战篇为大家讲述了怎么使用Redis来处理队列来处理异步任务,以及队列有什么特点为什么使用redis队列,那么后续的实战篇也会为大家带来比较使用的PhalApi各项技术,如果大家有什么希望喵咪能够加入到实战篇的内容可以@我哦!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:①群:421032344 ②群:459352221 欢迎大家的加入!

文档首页

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


[7.15]-Redis队列处理异步任务 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

[PhalApi实战篇(1)]Redis队列处理异步任务

前言

先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架.

哈喽大家好呀!之前编写的PhalApi入门篇和进阶篇已经过去了好久了,在此之间也回答了很多小伙伴各种各样的问题,这里也希望吧里面一些问的比较多的和比较有趣的以及笔者在使用PhalApi一些新的体会,都提取出来为大家带来一些能够在实际开发中可以使用的技术或思想,那么我们就开始我们实战篇中的第一节 Redis队列处理异步任务

大家希望喵咪在PhalApi实战推出一些什么样的内容?可以私信我邮箱wenzhenxi@vip.qq.com

附上:

喵了个咪的博客:w-blog.cn

官网地址: http://www.phalapi.net/

开源中国Git地址: http://git.oschina.net/dogstar/PhalApi/tree/release

1.为什么需要队列

为什么需要队列?其实已经是一个老生常谈的一个问题了,队列有诸多好处比如:

在项目中，将一些无需即时返回且耗时的操作提取出来，进行了异步队列处理，而这种异步队列处理的方式大大的节省了服务器的请求响应时间，从而提高了系统的吞吐量。 比较通俗易懂的解释就是 一个请求处理一些事情 A 业务耗时 30ms B业务 耗时 20ms 然后发邮件 耗时 50ms ,吧其中的发送邮件 写入队列 有一个专门负责发送邮件的程序接受这个队列的消息在吧邮件发送出去,这样这个请求原来要用100ms现在只需要50ms , 借助队列可以吧很多原本很消耗时间的操作单独有序处理

队列软件也很多:RabbitMQ,KafKa这两款都是非常主流的队列软件PhalApi也有提供对应的扩展程序来去使用,但是需要使用它们的成本对相对高一些需要搭建很多复杂的组件,但是相对redis,redis虽然没有那么多丰富的功能工具但是它也是队列软件中的不二之选

2.理解Redis处理队列特点

对于PHP来说对Redis支持是特别好的,redis的单线程保障了队列不会应为并发的问题导致一条消息多人获取所有也是很适合做为队列传输,PhalApi不仅仅自带简单的Redis库笔者也在它的基础上封装了一个更为完善的redis库,如下:

Redis · 暗夜在火星/PhalApi Library - 码云 - 开源中国

队列主要由两部分组成一部分是客户端一部分是消费端,客户端负责提供消息消费端负责处理队列这样的一个形式

在使用redis队列主要会使用到redis队列中的List类型,List类型可以从左右两边读取和写入数据,这样的形式就可以做到先入先出或者是后入先出这种队列模式

3.具体实践(基于PhalApi-Redis扩展)

客户端的使用比较简单只需要初始化Redis链接后向左边写入数据即可:

客户端:

//redis链接
DI()->redis = new Redis_Lite(DI()->config->get('app.redis.servers'));
//写入队列左边
DI()->redis->set_lPush(队列键名,值, 库名);

关键是消费端的用法,怎么让消费端一直不停的处理队列呢?很多童鞋应该已经想到了利用死循环不停的读取队列处理来解决及时处理的问题,但是这样又会带来一个新的问题,如果说队列空了死循环会不会一直高额的消耗CPU资源啊?由于这点就衍生出来两种具体的用法:

用法一

有的小伙伴就说了,这个还不简单吗?当队列里面没有内容了我就关掉消费端,然后使用crontab过一段时间再启动进行处理,这就没有所谓的死循环CPU消耗的问题了,这种解法如下:

while (true) {
    // 读取队列右边
    $msg = DI()->redis->get_rPop(队列键名, 库名);
  if( !$msg ){
      break;
  }
    // 处理逻辑
  .....
}

然后通过crontab进行定时任务即可

用法二

第二种用法是通过redis队列的另外一种机制来解决这类问题,相对于get_rPop还有提供一个方法get_brPop,我们可以看一下这个方法的描述读取队列右边 如果没有读取到阻塞一定时间这个阻塞时间是通过配置文件blocking字段来配置的,使用方式如下

while (true) {
    // 读取队列右边
    $msg = DI()->redis->get_brPop(队列键名, 库名);
    // 处理逻辑
  .....
}

这种方法相对上面那种的好处是实时性最好,如果阻塞时间设置的是5秒等待了2秒有消息进来了就里面会进入处理模式

上述方式可以使用Supervisor进行常驻内存执行

总结

本次实战篇为大家讲述了怎么使用Redis来处理队列来处理异步任务,以及队列有什么特点为什么使用redis队列,那么后续的实战篇也会为大家带来比较使用的PhalApi各项技术,如果大家有什么希望喵咪能够加入到实战篇的内容可以@我哦!

注:笔者能力有限有说的不对的地方希望大家能够指出,也希望多多交流!

官网QQ交流群:①群:421032344 ②群:459352221 欢迎大家的加入!

文档首页

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


[8.1]-PhalApi视频教程 | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

[8.1] PhalApi视频教程

先在这里感谢以下录制小组的同学，是你们让世界看到PhalApi的视频教程！

A西瓜妹子、听风不语、彩色的雨、Catch、喵了个咪、dogstar

PhalApi教程视频大纲，分为：基础教程、进阶教程、实战教程三类。

附上:

  • 喵了个咪的博客
  • PhalApi官网
  • PhalApi Git项目仓库
  • 本次视频教程源代码

1 基础教程

1.1 环境搭建,安装和helloword

百度网盘 优酷视频

1) phalapi介绍以及使用场景
2）建议使用liunx和各项环境配置最低
3）推荐IDE
4）框架编写的install展示
5）运行helloword!

1.2 初识PhalApi

百度网盘 优酷视频

1）目录功能讲解
2）数据库建库建表
3）连接数据库官方DEMO

1.3 init介绍以及配置文件讲解log记录

百度网盘 优酷视频

1）init文件作用内容讲解
2）配置文件使用讲解
3）以及log记录的活灵活用

1.4 请求返回

百度网盘 优酷视频

1）请求的参数验证getRules方法使用讲解
2）返回json数据讲解
3）返回报错讲解

1.5 国际化和自动生成文档

百度网盘 优酷视频

1）返回国际化T方法讲解
2）自动生成文档规范讲解
3）如何查看
4）注意不要使用缓存会出不来

1.6 Model操作讲解

百度网盘 优酷视频

1）Model表配置
2）NotORM的使用
3）简单的 curd 执行原生sql

1.7 简单的小项目

百度网盘 优酷视频

1）对用户的一个登录注册等操作的接口实际项目编写实战
2）并且总结

2 进阶教程

2.1 DI思想讲解

百度网盘 优酷视频

一、DI简介
1)DI的通用说明
 + 容器
 + Martin Fowler
 + 示例

2)DI在π框架的角色
 + 汇点
 + 管理全部服务资源
 + 形成 **通用语言** ，实现快速开发
 + 减少重复写一些单例模式的代码，以及泛滥的全局变量

3)π框架开发的使用

二、DI资源服务分类
1)框架本身的资源服务
 + 公共服务和内部服务
 + 公共服务的使用

2)如何自定义资源服务
 + 三步走：实现 -> 注册 -> 使用
 + 示例

三、技巧与需要注意的事项
 + 一行优雅的代码： DI()->get('filter', 'PhalApi_Filter_None')->check();
 + 注意别踩坑！

2.2 ADM模式深入浅出

百度网盘 优酷视频

一、MVC模式与ADM模式
+ 为什么PhalApi自创ADM新模式？

二、ADM模式详解
 + Api接口层
 + Domain领域层
 + Model数据层

三、三层结构的优势与使用
1) 优势
 + 分层结构
 + 关注点分离
 + 更明确的层级调用

2）投票Demo的三层实现解说

3）TDD下简单接口开发实践
 + 阅后即焚功能场景
 + 先写单元测试
 + TDD下的三层实现

2.3 自动加载和拦截器

百度网盘 优酷视频

1）自动加载机制的活灵活用
2）定义自己的项目功能文件
3）使用拦截器过滤参数并且使用token验证请求

2.4 自定义参数验证规则

百度网盘 优酷视频

1）对getRules自定义参数验证规则
2）一个邮箱验证的小案例

2.5 NotORM进阶细节使用

百度网盘 优酷视频

1）方法补全
2）事务操作

2.6 数据库读写分离以及多库使用

百度网盘 优酷视频

1）数据库实现读写分离
2）实现跨库使用

2.7 使用缓存

百度网盘 优酷视频

1）使用缓存来实现缓存API请求结果
2）使用缓存来实现对数据库查询结果进行缓存

3 实战教程

3.1 多项目多版本处理

百度网盘 优酷视频

//TODO

3.2 核心拓展介绍

百度网盘 优酷视频

一、π框架中的扩展类库
1)扩展类库的简单介绍
 + 即插即用
 + 可重用的、业务无关的基础设施类库
 + 致力于与开源项目一起提供企业级的解决方案！

2)目前有哪些扩展类库
 + 23+个

二、如何使用扩展类库
1)使用步骤
 + 1、下载安装
 + 2、配置
 + 3、注册
 + 4、使用

2)扩展类库使用示例
 + Log4php日记
 + View视图渲染

三、如何开发、贡献扩展类库
 + 如何开发尚未提供的扩展类库
 + 如何分享

官网QQ交流群:421032344 欢迎大家的加入!

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


config | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

{}

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


Home | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

温馨提示：最新文档请移步官网

目录

温馨提示：客官莫急，文档正在努力编写整理中，或者您可以告诉我们希望先看到哪篇TODO。

前言

  • 首页

  • 接口，从简单开始！

基础入门

开篇

  • [1.1] 下载与安装

  • [1.2] 创建一个自己的项目

  • [1.3] 在线体验

  • [1.4] 文档、帮助和官网

  • [1.5] 对PhalApi框架的抉择

开发手册

  • [1.11] 快速入门: 接口开发示例(源码+图文)

  • [1.12] 参数规则：接口参数规则配置

  • [1.13] 统一的接口请求方式：?sevice=XXX.XXX

  • [1.14] 统一的返回格式和结构：ret data msg

  • [1.15] 数据库操作：基于NotORM的使用及优化

  • [1.16] 配置读取：内外网环境配置的完美切换

  • [1.17] 日记纪录：简化版的日记接口

  • [1.18] 快速函数：人性化的关怀

  • [1.19] DI服务速查：各资源服务一览表

更多技艺

  • [1.21] 类的自动加载：遵循PEAR包的命名规范

  • [1.22] 签名验证：自定义签名规则

  • [1.23] 请求和响应：GET和POST两者皆可得及超越JSON格式返回

  • [1.24] 缓存策略：更灵活地可配置化的多级缓存

  • [1.25] 国际化翻译：为走向国际化提前做好翻译准备

  • [1.26] 数据安全：数据对称加密方案

  • [1.27] 精益开发：更富表现力的Model层和重量级数据获取的应对方案

高级专题

思想与应用

  • [2.11] 核心思想：Di依赖注入 让资源更可控

  • [2.12] 海量数据：可配置的分库分表

  • [2.13] 接口调试：在线sql语句查看与性能优化

  • [2.14] 测试驱动开发：意图导向编程下的接口开发

  • [2.15] 计划任务：异步的后台定时计划任务 //TODO

  • [2.16] 领域驱动设计：应对复杂领域业务的Domain层

  • [2.17] 远程外观模式：Web Service和接口批量请求 //TODO

  • [2.18] 定制化：资源服务的再实现 //TODO

  • [2.19] 扩展库：可重用的扩展类库

  • [2.20] 约定编程：架构明显的编程风格

工具与实践

  • [2.21] 服务器统一部署方案简明版：CentOs + Nginx + php-fpm + MySql [+ Memcached]

  • [2.22] 更多工具：精英项目和团队建设

扩展类库

  • [3.1] 扩展类库：微信开发

  • [3.2] 扩展类库：代理模式下phprpc协议的轻松支持

  • [3.3] 扩展类库：基于PHPMailer的邮件发送

  • [3.4] 扩展类库：优酷开放平台接口调用

  • [3.5] 扩展类库：七牛云存储接口调用

  • [3.6] 扩展类库：后台计划任务 //TODO

  • [3.7] 扩展类库：基于Service_Apns的苹果推送 //TODO

  • [3.8] 用户、会话和第三方登录集成 //TODO

项目实践

  • [4.1] 开发实战1：利用扩展库进行微信服务号开发 //TODO
  • [4.2] 开发实战2：模拟优酷开放平台接口项目开发 //TODO
  • [4.3] 开发实战3：模拟腾讯开放平台接口项目开发 //TODO
  • [4.4] 开发实战4：一个可能的大型项目开发（多终端多版本） //TODO

其他

  • [5.1] 架构与思想：Phal Api核心设计和思想解读

  • [5.2] 杂谈：扯一些phal Api的前世和今生

  • [5.3] 框架总结：术语表和PHP开发建议

  • [5.4] 许可

  • [5.5] 联系和加入我们

  • [5.6] 更新日记

  • [5.8] 致框架贡献者：加入PhalApi开源指南

  • 附录1：接口文档参考模板

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


navigation | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

PhalApi 在线文档

社区

下载

文档(新)

文档(旧)

类参考手册

在线体验

增值服务

关于

gimmick:ThemeChooser

gimmick:forkmeongithub

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


PhalApi-Wikis | PhalApi(π框架) - PHP轻量级开源接口框架

 Fork me on GitHub
  • 中文版
  • English
  • 官方交流群：459352221
  • 社区
  • 下载
  • 文档
  • 视频教程
  • 类参考手册
  • 在线体验
  • 增值服务
  • 捐赠
  • 关于

index

前言

  • 首页
  • 接口，从简单开始！

一、基础入门

开篇

  • [1.1] 下载与安装
  • [1.2] 创建一个自己的项目(含四个脚本命令的使用说明)
  • [1.3] 在线体验
  • [1.4] 文档、帮助和官网
  • [1.5] 对PhalApi框架的抉择

开发手册

  • [1.11] 快速入门: 接口开发示例(源码+图文)
  • [1.12] 参数规则：接口参数规则配置
  • [1.13] 统一的接口请求方式：?sevice=XXX.XXX
  • [1.14] 统一的返回格式和结构：ret data msg
  • [1.15] 数据库操作：基于NotORM的使用及优化
  • [1.16] 配置读取：内外网环境配置的完美切换
  • [1.17] 日记纪录：简化版的日记接口
  • [1.18] 快速函数：人性化的关怀
  • [1.19] DI服务速查：各资源服务一览表
  • [1.20] DB操作：数据库基本操作速查

更多技艺

  • [1.21] 类的自动加载：遵循PEAR包的命名规范
  • [1.22] 签名验证：自定义签名规则
  • [1.23] 请求和响应：GET和POST两者皆可得及超越JSON格式返回
  • [1.24] 缓存策略：更灵活地可配置化的多级缓存
  • [1.25] 国际化翻译：为走向国际化提前做好翻译准备
  • [1.26] 数据安全：数据对称加密方案
  • [1.27] 精益开发：更富表现力的Model层和重量级数据获取的应对方案
  • [1.28] COOKIE：对COOKIE原生态的支持及记忆加密升级版
  • [1.29] 开放与封闭：多入口和统一初始化
  • [1.30] 保持的力量：接口开发最佳实践
  • [1.31] 新型计划任务：以接口形式实现的计划任务
  • [1.32] 在线接口文档：注释规范

二、高级专题

思想与应用

  • [2.11] 核心思想：Di依赖注入 让资源更可控
  • [2.12] 海量数据：可配置的分库分表
  • [2.13] 接口调试：在线sql语句查看与性能优化
  • [2.14] 测试驱动开发：意图导向编程下的接口开发
  • [2.15] 演进：新型计划任务续篇
  • [2.16] 领域驱动设计：应对复杂领域业务的Domain层
  • [2.17] 微服务：Api接口服务层
  • [2.18] 定制化：资源服务的再实现
  • [2.19] 扩展库：可重用的扩展类库
  • [2.20] 约定编程：架构明显的编程风格

工具与实践

  • [2.21] 服务器统一部署方案简明版：CentOs + Nginx + php-fpm + MySql [+ Memcached]
  • [2.22] 更多工具：精英项目和团队建设

三、扩展类库

  • [3.1] 扩展类库：微信开发
  • [3.2] 扩展类库：代理模式下phprpc协议的轻松支持
  • [3.3] 扩展类库：基于PHPMailer的邮件发送
  • [3.4] 扩展类库：优酷开放平台接口调用
  • [3.5] 扩展类库：七牛云存储接口调用
  • [3.6] 扩展类库：新型计划任务
  • [3.7] 扩展类库：基于Service_Apns的苹果推送 //TODO
  • [3.8] 扩展类库：用户、会话和第三方登录集成
  • [3.9] 扩展类库：swoole支持下的长链接和异步任务实现
  • [3.10] 扩展类库：Auth 权限扩展 (由@黄苗笋提供)
  • [3.11] 扩展类库：基于FastRoute的快速路由（支持RESTful）
  • [3.12] 扩展类库：基于PhalApi的DB集群拓展Cluster (由@喵了个咪提供)
  • [3.13] 扩展类库：基于PhalApi的Redis拓展 (由@喵了个咪提供)
  • [3.14] 基于PhalApi的图片上传拓展 (由@Summer)
  • [3.15] 基于PhalApi的第三方支付拓展 (由@Summer)
  • [3.16] PhalApi-Image -- 图像处理 (由@喵了个咪提供)

四、项目实践

  • [4.1] 开发实战1：利用扩展库进行微信服务号开发 //TODO
  • [4.2] 开发实战2：模拟优酷开放平台接口项目开发
  • [4.3] 开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动）
  • [4.4] 开发实战4：一个可能的大型项目开发（多终端多版本） //TODO
  • [4.5] 落地应用框架：freeApi服务于互联网开发者的免费接口供应平台（由@转身.满目疮痍 提供，并正在致力开发完善中）

五、其他

  • [5.1] 架构与思想：Phal Api核心设计和思想解读
  • [5.2] 杂谈：扯一些phal Api的前世和今生
  • [5.3] 框架总结：术语表和PHP开发建议
  • [5.4] 许可
  • [5.5] 联系和加入我们
  • [5.6] 更新日记
  • [5.8] 致框架贡献者：加入PhalApi开源指南
  • 附录1：接口文档参考模板

六、SDK包

  • [6.1] 基于接口查询语言的SDK包
  • [6.2] SDK包（JAVA版）
  • [6.3] SDK包（PHP版）
  • [6.4] SDK包（object c版） 由@Aevit 提供
  • [6.5] SDK包（javascript版） 由@喵了个咪 提供
  • [6.6] SDK包（Ruby版）

七、教程

教程-入门篇

  • [7.1]-phalapi-入门篇1(简单介绍以及环境搭建)
  • [7.2]-phalapi-入门篇2(把它玩起来)
  • [7.3]-phalapi-入门篇3(请求和返回)
  • [7.4]-phalapi-入门篇4(国际化高可用和自动生成文档)
  • [7.5]-phalapi-入门篇5(数据库操作和Model层)
  • [7.6]-phalapi-入门篇6(小技巧和浅谈API适用范围以及入门篇总结)

教程-进阶篇

  • [7.7]-phalapi-进阶篇1(三层结构Api,Domain,和Model)
  • [7.8]-phalapi-进阶篇2(DI依赖注入和单例模式)
  • [7.9]-phalapi-进阶篇3(自动加载和拦截器)
  • [7.10]-phalapi-进阶篇4(notrom进阶以及事务操作)
  • [7.11]-phalapi-进阶篇5(数据库读写分离)
  • [7.12]-phalapi-进阶篇6(解决大量数据存储数据库分表分库拓展)
  • [7.13]-phalapi-进阶篇7(使用缓存以及用redis拓展解决实际问题)
  • [7.14]-phalapi-进阶篇8(PhalApi能带来什么和进阶篇总结)

八、视频教程

  • [8.1]-PhalApi视频教程 #接口，从简单开始！

    文档首页 下一章

编程于我们，犹如著书而存世。表达，从简单开始；代码，亦然。 -- 暗夜在火星

接口，从简单开始！

一个隐喻

假设我们有这样的表达式：

1 + 1 = 2

显然，这是简单明了而易于理解的。但倘若我们这样：

1 + (96 - 867 + 700 - 6 + 7 - 30/10 + 100  - 27) +  1 = 2

同样可以获得相同的结果，但表达却羞涩难懂。

你可能会偷笑：怎么可能会有人这样编写复杂又没用的代码呢？然而，如果你回顾一下曾经做过的项目或留意一下周围的代码，你会发现，这种情况是真实存在的。

当然，在不同领域的项目开发中，因各自需求不同各代码的编写也不尽相同。纵使这样，即使我们不能简化到最理想的状态，但至少我们可以通过努力以达到“编写人容易理解的代码”。一如这样：

1 + (0) +  1 = 2

PhalApi框架所做的

使用PhalApi框架进行项目开发，我们不能保证项目编写的代码一定会“短而美”，因为更多的代码编写来自于你双手的输入、来自你自己切身的思考和设计。但我们希望PhalApi可以在支持接口快速开发的基础上，为你和更多开发团队提供关于项目开发的一些技艺、参考和帮助。

所以，与其他很多关注服务器性能的框架不同， PhalApi更加关注的是人的心情、开发效率和团队合作 ，而这些正是通过测试驱动开发、自动化工具、持续集成和敏捷开发等途径来达成的 。

然而，这一切一切，都要从代码的编写开始。毕竟我们作为专业人士，代码是我们连接世界的媒介。而接口代码的编写，又应从简单开始。

泡一杯咖啡，让我们开始吧。

本框架和文档适用于

  1. 工匠：基于PHP语言的后台接口项目开发，特别是创业项目的快速迭代
  2. 探险者：进行定制和扩展，打造成公司内部产品簇的接口应用框架
  3. 架构师：专注框架的设计、思想和开源

文档首页 下一章

[1.1]-下载与安装

上一章 文档首页 下一章

我们的时代是一个问题时代，一个问题很大的时代。在科技的神奇影响下，一切都在经历着变革。如果我们愿意在生活中睁大双眼，每天都有问题等着我们去研究，去解决。
-- 教皇保罗六世，1969年5月18日 摘自《全球通史-从史前史到21世纪》

1.1.1 温馨提示

  • 请从release分支拉取稳定的代码
  • 推荐在Linux服务器上进行开发
  • 建议PHP >= 5.3.3

1.1.2 下载

你可以选择下面你喜欢的方式之一来下载 。

(1) 网页下载

进入 Git@OSC - PhalApi ，然后点击下载。
0403

(2)使用git

命令下，使用git命令快速拉取：

$ git clone https://git.oschina.net/dogstar/PhalApi.git

1.1.3 安装

安装如同其他的框架一样，将代码上传到到服务器后解压即可。下面是不同环境的参考：

(1)Windowns系统下的XAMPP

将PhalApi框架下载后，解压到xampp的htdocs目录下，如：

访问：

http://localhost/phalapi/public/demo/

即可看到： QQ截图20150403225621

(2)Linux系统下的Nginx

将PhalApi框架下载并解压到指定目录，假设为： /home/apps/projects/ ，则先配置nginx：

server {
    listen 80;
    server_name dev.phalapi.com;
    root /home/apps/projects/PhalApi/Public;
    charset utf-8;
    access_log logs/dev.phalapi.com.access.log;
    error_log logs/dev.phalapi.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

然后，检测并重启nginx：

$ nginx -t
$ nginx -s reload

在本地绑定HOST：

#PhalApi小牛试刀
127.0.0.1 dev.phalapi.com

最后，访问默认接口服务： http://dev.phalapi.com/demo/ ，正常时会返回类如：

{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1422779027
    },
    "msg": ""
}

T_FUNCTION错误的处理方案

如果你当前的PHP环境不支持匿名函数，会报T_FUNCTION这个错误。这时，可这样处理：

先把匿名函数的写法换成直接new的方式，如：

 //数据操作 - 基于NotORM
DI()->notorm = function() {
    $debug = isset($_GET['debug']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

换成：

 //数据操作 - 基于NotORM
$debug = isset($_GET['debug']) ? true : false;
DI()->notorm = new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);

然后，在init.php文件最后添加以下代码：

 if (!class_exists('Closure')) {
    class Closure {
    }
}

即可解决。

但由于匿名函数是DI中所需要的重要语言特性，所以建议还是将PHP版本更新到5.3以上。

上一章 文档首页 下一章

[1.2]-创建一个自己的项目

上一章 文档首页 下一章

有梦想的人学习能力特别强，而且不拒绝任何新东西。 -- 《理想丰满》

1.2.1 可视化安装

将代码上传到服务器并解压后，访问： ./Public/install/ 下的安装向导，如在浏览器打开：

http://dev.phalapi.com/install/

然后按照提示，一步步操作即可：

(0)第零步：同意安装

(1)第一步：环境检查

(2)第二步：系统配置

(3)第三步：接口请求

(4)运行效果

1.2.2 手动创建

在Windows系统下，可以手动创建一个新的项目。

假设目录为： F:\xampp\htdocs\PhalApi
以下是简单的创建操作：

(1)新建项目目录

在./PhalApi目录下新建一个自己的项目目录，通常以项目名称命名，如项目MyApp对应：

然后，再分别在里面添加接口目录Api、领域目录Domain、数据源目录Model和通用目录Common，即：

(2)准备一个接口

全部的接口类文件需要放置到./Api 目录下，你可以尝试编写一个简单的Default.Index接口，也可以直接将./Demo/Api/Default.php文件复制过来。

(3)创建访问入口

将./Public/demo目录复制一份为对应项目名称小写的目录，如这里的myapp：

注意！这里需要修改./Public/myapp/index.php和./Public/myapp/checkApiParams.php中的目录：

(4)浏览器访问

在浏览器输入以下链接：

http://localhost/phalapi/public/myapp/

应该可以看到：

1.2.3 自动生成

在Linux系统下，我们可以使用脚本快速生成一个项目结构，此过程只需要1分钟左右。

简明操作如下：

(1)使用phalapi-buildapp脚本

phalapi-buildapp脚本用于创建项目，进入 ./PhalApi/PhalApi/ 后，可以看到这个脚本。

不过，首先，需要为这个脚本添加执行权限：

$ chmod +x ./phalapi-buildapp

然后，调用脚本创建一个MyApp的项目：

$ ./phalapi-buildapp MyApp

OK!

(2)查看生成的项目及访问

回到 /path/to/PhalApi 目录，你会发现生成了一个MyApp目录，并且里面的文件如下：

$ cd MyApp/
$ tree
.
├── Api
│   └── Default.php
├── Common
├── Domain
├── Model
└── Tests
    ├── Api
    │   └── Api_Default_Test.php
    ├── Common
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php

9 directories, 4 files

在Public目录下，则会有myapp的访问入口：

└── myapp
    ├── checkApiParams.php
    └── index.php

在浏览器访问：

#根目录设置为./Public
http://dev.phalapi.com/myapp/

#或没设置根目录为./Public时访问
http://dev.phalapi.com/Public/myapp/

可以看到接口结果输出。

(3)执行单元测试

如果安装了PHPUnit，执行一下单元测试，也是通过的！

$ phpunit ./Tests/
PHPUnit 4.3.4 by Sebastian Bergmann.

...

Time: 7 ms, Memory: 5.75Mb

OK (3 tests, 13 assertions)

1.2.4 更多技巧

(1)日记目录需要0777权限

请确保日记目录Runtime拥有写入的权限，可以给0777 。

(2)脚本加执行权限和软链

对于脚本命令，需要添加执行权限和软链，便于日后使用：

$ cd /pah/to/PhalApi/PhalApi

$ chmod +x ./phalapi-buildapp
$ sudo ln -s /path/to/phalapi-buildapp /usr/bin/phalapi-buildapp

$ chmod +x ./phalapi-buildsqls
$ sudo ln -s /path/to/phalapi-buildsqls /usr/bin/phalapi-buildsqls

$ chmod +x ./phalapi-buildtest
$ sudo ln -s /path/to/phalapi-buildtest /usr/bin/phalapi-buildtest

(3)No such file or directory的解决方案

解决方案一：增加 /usr/bin/php软链

可以将php的执行路径追加到/usr/bin下，即类似：

ln -s /usr/local/php/bin/php /usr/bin/php

解决方案二：使用alias起别名

或者通过使用别名 alias 的方式来注册脚本命令：

# vim /etc/bashrc

#PhalApi - 自动生成SQL语句
alias phalapi-buildsqls='/usr/local/php/bin/php /path/to/PhalApi/phalapi-buildsqls'

需要注意的是，保存后，需要source一下，即：

# source /etc/bashrc

$ source /etc/bashrc

1.2.5 四个脚本命令使用说明

(1) 创建应用项目

如前文所述，可以通过phalapi-buildapp脚本来创建一个新的项目，其使用说明如下：

(2)生成单元测试骨架代码

当需要对某个类进行单元测试时，可使用phalapi-buildtest脚本生成对应的单元测试代码，其使用如下：

其中，

  • 第一个参数file_path：是待测试的源文件相对/绝对路径
  • 第二个参数class_name：是待测试的类名
  • 第三个参数bootstrap：是测试启动文件，通常是./test_env.php文件
  • 第四个参数author：你的名字，默认是dogstar

通常，可以先写好类名以及相应的接口，然后再使用此脚本生成单元测试骨架代码。
以Default.Index默认接口服务为例，需要为此生成测试骨架代码，可以依次这样操作：

$ cd /path/to/PhalApi
$ cd ./Demo/Tests
$ ../../PhalApi/phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php > ./Api/Api_Default_Test.php

最后，需要将生成好的代码，重定向保存到你要保存的位置。通常与产品代码对齐，并以“{类名} + _Test.php”方式命名。

(3)生成数据库建表SQL

当需要创建数据库表时，可以使用phalapi-buildsqls脚本结合配置文件dbs.php生成建表SQL，这个工具在创建分表时尤其有用，其使用如下：

其中，

  • 第一个参数dbs_config：是指向数据库配置文件./Config/dbs.php的路径，可以使用相对路径
  • 第二个参数table：是需要创建sql的表名，每次生成只支持一个
  • 第三个参数engine：（可选）是指数据库表的引擎，可以是：Innodb或者MyISAM

温馨提示：需要提前先将建表的SQL语句（除主键id和ext_data字段外）放置到./Data/目录下，文件名为：{表名}.sql。

如，我们需要生成10用户user_session表的的建表语句，那么需要添加数据文件./Data/user_session.sql（除主键id和ext_data字段外）：

      `user_id` bigint(20) DEFAULT '0' COMMENT '用户id',
      `token` varchar(64) DEFAULT '' COMMENT '登录token',
      `client` varchar(32) DEFAULT '' COMMENT '客户端来源',
      `times` int(6) DEFAULT '0' COMMENT '登录次数',
      `login_time` int(11) DEFAULT '0' COMMENT '登录时间',
      `expires_time` int(11) DEFAULT '0' COMMENT '过期时间',

然后，进入到项目根目录，执行命令：

$ php ./PhalApi/phalapi-buildsqls ./Config/dbs.php user_session

就会看到生成好的SQL语句了，类似：

CREATE TABLE `phalapi_user_session_0` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_1` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_2` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_3` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_4` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_5` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_6` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_7` (
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_8` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_9` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

最后，便可把生成好的SQL语句，导入到数据库，进行建表操作。

(4)生成接口代码 - V1.3.4及以上版本支持

当需要编写开发一个接口时，可以使用phalapi-buildcode脚本生成基本的Api、Domain和Model代码。此脚本不是很强悍的，项目可以根据自己的喜欢使用，或者修改定制自己的模板。其使用如下：

其中，

  • 第一个参数app_path：是指项目根目录到你的项目的相对路径
  • 第二个参数api_path：是需要创建接口的相对项目的相对路径，支持多级目录，可不带.php后缀
  • 第三个参数author：（可选）你的名字，默认为空
  • 第四个参数overwrite：（可选）是否覆盖已有的代码文件，默认为否

例如，我们要为Demo项目生成一个新的接口文件./AA/BB/CC.php，则可以：

$ cd /path/to/PhalApi
$ ./PhalApi/phalapi-buildcode Demo AA/BB/CC dogstar
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Api/AA/BB ...
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Domain/AA/BB ...
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Model/AA/BB ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Api/AA/BB/CC.php ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Domain/AA/BB/CC.php ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Model/AA/BB/CC.php ...

OK! AA/BB/CC has been created successfully!

可以看到生成的代码有：

访问接口：

最后，在线接口列表，可以看到：

上一章 文档首页 下一章#[1.3]-在线体验

上一章 文档首页 下一章

片刻之欢愉，不如须臾之宁静。

1.3.1 在线体验

//默认的接口服务
http://demo.phalapi.net/
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.2.0",
        "time": 1443278491
    },
    "msg": ""
}

//带参数的示例接口
http://demo.phalapi.net/?service=Default.Index&username=oschina
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "oschina您好，欢迎使用PhalApi！",
        "version": "1.2.0",
        "time": 1443278475
    },
    "msg": ""
}

//故意请求一个非法的服务
http://demo.phalapi.net/?service=Demo.None
{
    "ret": 400,
    "data": [],
    "msg": "非法请求：服务Demo.None不存在"
}

1.3.2 在线接口调试

这里说的接口调试，更多是针对接口调用过程中所执行的SQL语句查看，如：

//带&__sql__=1可打印SQL语句
http://demo.phalapi.net/?service=User.getMultiBaseInfo&user_ids=1,2,3&__sql__=1

返回：
[1 - 0.00032s]SELECT * FROM tbl_user WHERE (id = ?); -- 1
[2 - 0.00019s]SELECT * FROM tbl_user WHERE (id = ?); -- 2
[3 - 0.00016s]SELECT * FROM tbl_user WHERE (id = ?); -- 3
{"ret":200,"data":{"code":0,"msg":"","list":[{"id":"1","name":"dogstar","note":"oschina"},{"id":"2","name":"aevit","note":"fami"},{"id":"3","name":"test","note":"phalapi"}]},"msg":""}

1.3.3 [酷！]接口参数在线查询

为了方便客户端查看最新的接口参数，特别提供此在线工具，根据接口代码实时生成接口参数报表，完全不需要后台开发编写维护额外的文档。我觉得，这很符合敏捷开发之道。

//接口参数在线查询工具链接
http://demo.phalapi.net/checkApiParams.php

如：http://demo.phalapi.net/checkApiParams.php ，访问效果如下：

因此，接口所需要的参数，对于接口开发人员，也只是简单配置一下参数规则，便可以轻松获取。
关于具体的参数规则配置和接口结果返回格式定义，后期会详细讲到。

上一章 文档首页 下一章#[1.4]-文档、帮助和官网

上一章 文档首页 下一章

创造性源于专注并且持久的思考，直至灵感来临。 -- 《快速软件开发(珍藏版)》

1.4.1 开发文档

开发文档，是由PhalApi团队精心编写的，意在给广大开发同学提供开发指导和使用帮助。最全和最新的开发文档，请查看这里：

http://www.phalapi.net/wikis/

1.4.2 在线类参考手册

此部分在线类参考手册，是使用phpdoc自动生成，支持移动端访问：

1.4.3 问题与反馈

我们建议使用issue来纪录来共享问题，你也可以通过其他方式来与我们进行沟通，以获得更多帮助。

1.4.4 官网

官网链接为：

http://www.phalapi.net/

支持移动端显示：

1.4.4 框架升级

最后，补充说明一下，大家较为关心的框架升级问题。

由于PhalApi框架的核心代码，已经在前期有意识地单独隔离了出来，即在 ./PhalApi/PhalApi 目录下。

所以，当后期项目需要升级框架时，直接替换此目录下全部的文件即可。为了保险起见，在升级前，你可以做好备份工作。
我们会争取100%做到框架的向前兼容。如果升级后有什么 问题，可随时与我们联系。

对此，项目不应该直接针对核心部分进行改动，而是在其他地方放置，以免影响框架升级。

上一章 文档首页 下一章#[1.10]-对PhalApi框架的抉择

上一章 文档首页 下一章

人类一思考，上帝就偷笑了。 -- 《下一个倒下的会不会是华为》

1.10.1 对PhalApi框架的抉择

能使用框架来进行项目开发，和知道为何使用此框架进行项目开发明显不同。

对框架的选择，名义上是架构师的职责，但对于充满好奇心和有着更强求知欲的开发人员来说，同样应该给予关注。

之所以选择一个框架进行项目开发，明显可以减少很多不必要的重复编码，但更深层次，则是可以减少开发周期、统一开发规范、降低项目风险，不致于项目失控。

所以，在决定使用PhalApi框架前，对于此框架是否适用于即将启动的项目进行一番思索，是大有裨益的。通过 推定框架 ，有助于避免因采用不当的框架而对项目造成不必要的阻力。

1、适用的场景和项目

海量数据和移动App

此框架特别适用于现在各种 移动App项目的后台接口开发，以及服务器间的后台接口开发，同时具有应对 海量数据 的能力。并且还可以挂靠多个项目，也可以很好地支持多个终端、开放不同的入口。

同时提供了可以很好应对海量数据的解决方案：如没有提供图片上传的代码或工具而希望开发人员将图片上传到CDN，支持大数据的存储，以及后台计划任务。

HTTP协议和JSON格式

基于此，我们采用了主流设计，即将框架设计成使用HTTP协议并JSON格式返回结果的接口请求，因为这能为大众熟悉并潜意识接受。 通过规范的接口调用和返回，有利于客户端和后台开发人员的关系融洽。

2、敏捷开发和快速交付

虽然框架和工程实践间没有必然的联系，但这其中有一些微妙的关系。

一种代码编写的方式会形成一种开发的风格；一种开发风格会奠定一个团队的合作氛围；一个团队的合作氛围会决定项目交付的质量。

而PhalApi所提倡和希望做到的正是通过让后台接口开发更简单以让后台开发人员心情更愉悦，从而为客户端提供高质量稳定的接口以支撑更多优质App的开发。并且，结合重构、测试驱动开发和持续集成等，可以让你的项目如虎添翼，在快速交付的同时，体验编码开发的乐趣。

3、约束和关注

架构约束程序。

约束有时会让开发者处处受阻。但好的约束能够统一规范而不致于项目代码凌乱不堪，也不会轻易地允许低级开发新手犯下一些本可避免的BUG。正如，我们都讨厌等待红绿灯，但我们必须肯定它对交通和生命安全保障的作用。

4、复杂领域业务的应对和解决方案

正如前面说到的，我们关注在海量数据下为移动App提供稳定的接口，我们提倡敏捷开发下的快速交付。所以我们去掉与接口开发无关的功能，没有提供视图渲染和模板解析的操作。

但只是这样而已吗？

不！我们还关注对复杂领域业务的应对和解决方案。

如果我们PhalApi框架所关注的，也是你们项目所关注的，那么我们有理由相信PhalApi能为你的项目带来很多友好的约束和贴心的帮助。

5、框架的性能

很多框架都要强调其能提供的性能，然后PHP本身就是动态的脚本语言，要想提高项目的运行速度，就是要进行减法，即减少不必要的PHP代码。但是前面强调性能的框架则做了与其承诺矛盾的做法：为框架增添了很多项目可能不需要用到的功能。明显地就是一系列既定的执行流程和侦听事件、回调、调度等等。
而这些，势必会对性能有所影响，特别当应用项目不需要这些特性却又不能定制简化时，框架所谓的强悍功能会适得其反。

对于这一块，我们则提供了极大的空间。因为，入口的文件，由你指定。除了一些必要的加载外，很多都可以支持自定义和定制化。

而且我们也使用autobench进行了压力测试和通过xhprof进行了性能剖析，证明PhalApi框架在性能上确实如我们预料的那样 -- 快！

6、PhalApi的成熟度、部署与学习成本

成就度

不可否认，PhalApi还是太年轻了。

PhalApi正式开源于2015年1月，但是我们在努力完善中并致力于有助于快速开发的后台接口。我们“减少不必要的创新”，我们坚持恒星一般的接口并很好地支持框架升级， 我们尽量提供优秀的文档和及时的沟通帮助。

更为重要的是，这是一个不只为框架而框架的PhalApi框架。

如果在开发过程中对所遇到的问题而框架不能很好解决和支持时，可以考虑改写框架，将某一目标或属性提升至架构，其他团队成员则无须再编写重复代码去重复实现。因为，你会发现，在PhalApi下，你可以轻松做到这一点： 小步快跑下的浮现式设计 。

部署

在Nginx下，你可以快速部署此框架。

以下是为初学者参考的Nginx配置：

server {
    listen       80;
    server_name  demo.phalapi.com;

    root   /home/apps/projects/demo.phalapi.com/Public;

    charset utf-8;

    access_log  logs/demo.phalapi.com.access.log;
    error_log  logs/demo.phalapi.com.error.log;

    location / {
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include        fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

除此之前，我们已经对框架的结构目录进行了很好地划分。包括配置文件、日记文件、框架代码、多个项目挂靠、不同的入口开放以及多App的支持等等。

在结合Phing进行一键发布时，你会发现，项目的部署发布同样方便快捷。

学习成本

这块学习成本是不可避免的，毕竟PhalApi是一个新的框架，很多开发同学之前都没有接触过。

但我们尽量站在后台开发同学的角度，用心地设计每一个接口，编写每一行代码。在源代码里面倾注了必要的注释和使用示例，并同步提供了详细的文档说明和实战代码，以便快速入门上手。

更为重要的是，我们都是尽可能按照主流惯例来设计接口规范，以便各位同学可以快速上手，就仿佛这个框架是出自你的设计一样。

1.10.2 何时可以采用PhalApi？

我们致力于提供可以快速开发后台接口的框架，并希望能对你的项目有实际的帮助。所以，我们不会鼓吹PhalApi框架，因为没有可以适用于任何领域、任何实际项目的框架。也就是没有所谓的银弹，也没有一种放之四海而皆准的方法，起码你可能需要作一些调整。

而且，我们也不会赞同你不加考虑就采用PhalApi框架，因为如果此框架不利于你项目的开发，甚至有反作用，这显然有悖于我们的初心：为你的项目提供实质的帮助。因此，在采用PhalApi框架前，你应该稍微查看一下我们的文档，和相关的源代码。以下是一些初步的建议。

 1、架构无关：如果你的只是小项目，无需要过多考虑涉及大规模决策、各模块及元素、属性间的微妙关系，即架构无关时。
PhalApi和其他框架一样，也能满足小项目的开发需求。

 2、专注架构：如果你需要专注框架，则更需要理解你项目将面临的约束、风险和需要满足的质量属性。
然后评估PhalApi是否能统一这此约束、降低风险并满足这些质量属性。若能，则用之；若不能，则另取之。

 3、提升架构：最后，如果你发现PhalApi拥有不断演进的能力并有助于浮现式设计（我们也正是如此设计和研发的），并且愿意
将PhalApi框架提升到你们产品线的后台接口框架，那么你将能为你的团队、项目乃至公司提供一个更优质的框架，并会为之感到兴奋和骄傲。
当然，“能力越大，责任也就越大”。但我们作为程序员，不正是希望能以已之所学，服务于人吗？

1.10.3 何时不应该使用PhalApi？

正如其他负责任的开源框架一样，我们在说明PhlaApi框架适用场景的同时，也应该指明不应该使用PhalApi的时机。其中包括但不限于：

  • 需要开发CLI项目时（但已提供支持命令行项目开发的CLI扩展类库）；
  • 需要开发网站项目，即有界面展示和视图渲染（但已提供支持视图渲染的View扩展类库）；
  • 对数据严谨性要求高，如金融行业的相关项目，毕竟PHP是弱类型语言；

上一章 文档首页 下一章#[1.11]-快速入门(backup)

上一章 文档首页 下一章

一个真正的强者，不是摆平了多少人，而是他能帮助到多少人。 --开源中国源创会分享广州站 @海洋之心-悟空

1.11.1 模拟开发：获取开源中国用户信息接口

首次使用此接口开发框架时，可以先查看此开发示例。

假设，我们需要为开源中国打造一个平放平台，其中有一个接口是可以根据用户ID来获取用户的基本信息。

本文，就以模拟获取开源中国用户信息接口开发（即：从数据库获取用户的基本信息并以JSON格式返回给客户端）为例，简明的接口开发中的流程，以及用到的基本功能和操作，主要包括有：统一入口文件、参数规则配置、接口层/领域层/模型持久层、日记纪录、数据库操作、配置读取、国际化翻译等。

以给大家一个感观的认识。

最终接口的调用与返回结果如下：

//接口请求格式
http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=帐号ID

//返回结果格式
{
    "ret": 200,
    "data": {
        "code": 0,    //状态码，0表示正常获取，1表示用户不存在
        "msg": "",
        "info": {      //用户信息
            "id": "1",    //用户ID
            "name": "dogstar",   //帐号
            "note": "oschina"   //来源
        }
    },
    "msg": ""
}

1.11.2 开发流程

(1) 统一入口文件

为了更好保护我们的项目代码，建议将接口的访问路径设置在：./PhalApi/Pubic目录，并且各套接口（如按版本分：v1/v2/v3等等；按不同终端分：ios/android/pc等）各自独立入口。所以本示例中将./PhalApi/Pubic/demo/index.php下。

参数入口文件的写法，我们可以快速得到基本的入口文件如下：

// $ vim ./Public/demo/index.php

<?php
/**
 * Demo 统一入口
 */

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()->loader->addDirs('Demo');

/** ---------------- 响应接口请求 ---------------- **/

$server = new PhalApi();
$rs = $server->response();
$rs->output();

此外，我们还需要一个公共的初始化文件：

//$ vim ./Public/init.php

<?php
/**
 * 统一初始化
 */

/** ---------------- 根目录定义，自动加载 ---------------- **/

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

/** ---------------- 注册&初始化服务组件 ---------------- **/

//自动加载
DI()->loader = $loader;

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

//调试模式，$_GET['__debug__']可自行改名
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

//翻译语言包设定
SL('zh_cn');

(2)TDD测试驱动开发

遵循最佳实践，我们在编写代码前先编写单元测试。但同时为了减少编写测试代码的痛苦，我们可以先定义接口函数签名，再通过脚本自动生成测试代码骨架来提高我们的开发效率。

//$vim ./Demo/Api/User.php

<?php

class Api_User extends PhalApi_Api
{
    public function getBaseInfo()
    {
    }
}

通过脚本生成测试骨架：

$ mkdir -p ./Demo/Tests/Api/
$ cd ./Demo/Tests/Api
$ php ../../../PhalApi/build_phpunit_test_tpl.php ../../Api/User.php Api_User ./../test_env.php

然后，根据/Public/demo/index.php入口文件，也搭建一个测试环境的入口文件：

vim ./Demo/Tests/test_env.php

修正一下Api_Examples_User_Test.php里，测试环境test_env.php的包含路径：

//手动调用test_env.php的路径
require_once dirname(__FILE__) . '/../test_env.php';

并且，修改测试，以符合我们通过userId=1获取基本信息（名字为dogstar，来源为oschina）：

//$vim ./Demo/Tests/Api/Api_User_Test.php

    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        $str = 'service=User.GetBaseInfo&user_id=1';
        parse_str($str, $params);

        DI()->request = new PhalApi_Request($params);

        $api = new Api_User();
        //自己进行初始化
        $api->init();
        $rs = $api->getBaseInfo();

        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('msg', $rs);
        $this->assertArrayHasKey('info', $rs);

        $this->assertEquals(0, $rs['code']);

        $this->assertEquals('dogstar', $rs['info']['name']);
        $this->assertEquals('oschina', $rs['info']['note']);
    }

此时，单元测试是预期失败的：

$ phpunit ./Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

F

Time: 3 ms, Memory: 5.25Mb

There was 1 failure:

1) PhpUnderControl_ApiUser_Test::testGetBaseInfo
Failed asserting that a NULL is not empty.

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

(3)Api接口层

此接口层，主要是负责响应客户端的请求，调用需要的领域层进行必要的服务功能提供。

配置参数规则

为了获取到用户ID，我们可以在getRules()函数里面定义参数规则，以便框架自动帮我们过滤获取需要的参数。

温馨提示：
接口层的全部类成员函数都应以小写开头。
但对外，函数首字母不区分大小写，因为框架会将请求的函数强制转换成小写再执行。

原因在于：
1、我们坚持驼峰法的代码风格；
2、对外界我们可以统一使用大写来提供服务名称，如：User.Login，这样更显专业。

//$vim ./Demo/Api/User.php

<?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'getBaseInfo' => array(
                'userId' => array('name' => 'user_id', 'type' => 'int', 'min' => 1, 'require' => true),
            ),
        );
    }

    //...

如上，我们就定义了getBaseInfo接口中的user_id参数，对应的变量名字为userId，整型，最小值为1，必须。

接口实现

//$vim ./Demo/Api/User.php

    public function getBaseInfo()
    {
        $rs = array('code' => 0, 'msg' => '', 'info' => array());

        $domain = new Domain_User();
        $info = $domain->getBaseInfo($this->userId);

        if (empty($info)) {
            DI()->logger->debug('user not found', $this->userId);

            $rs['code'] = 1;
            $rs['msg'] = T('user not exists');
            return $rs;
        }

        $rs['info'] = $info;

        return $rs;
    }

(4)Domain领域层

领域层主要是关注复杂业务的处理，以及缓存的处理、耗时操作后台异步处理等，并调用Model持久层获取需要的数据。因此，是Api与Model层之间的桥梁。

在此示例中，我们只需要简单地调用Model层获取用户的信息即可，再加强一下用户ID的合法性判断。

//$ vim ./Demo/Domain/User.php

<?php

class Domain_User
{
    public function getBaseInfo($userId)
    {
        $rs = array();

        $userId = intval($userId);
        if ($userId <= 0) {
            return $rs;
        }

        $model = new Model_User();
        $rs = $model->getByUserId($userId);

        return $rs;
    }
}

(5)Model持久层

此一层主要关注数据从持久存储的获取，特别是针对数据库的操作，但不排除其他媒介，如文件、缓存等。

首先，先准备一下我们需要的表和数据：

CREATE TABLE `phalapi_test`.`tbl_user` (
  `id` INT NOT NULL,
  `name` VARCHAR(45) NULL,
  `note` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

INSERT INTO `phalapi_test`.`tbl_user` (`id`, `name`, `note`) VALUES ('1', 'dogstar', 'oschina');

然后，编写需要的Model代码，即利用NotORm实现对数据的操作：

//$ vim ./Demo/Model/User.php

<?php

class Model_User
{
    public function getByUserId($userId)
    {
        return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
    }
}

(6)单元测试通过啦！

在完成上面简单的开发后，我们即可以实现接口的开发，运行一下刚才的单元测试，完美通过！

$ phpunit ./Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

SELECT * FROM tbl_user WHERE (id = ?); -- 1

.SELECT * FROM tbl_user WHERE (id = ?); -- 1<br />

Time: 34 ms, Memory: 6.50Mb

OK (2 tests, 7 assertions)

在单元测试的保证下，我们便可以放心大胆地将我们的代码发布到外网，提供给更多的开发者，和终端用户使用。

(7)数据库配置

因为是单元测试，所以我们配置搭建了新的一个测试环境，特别对于数据库的配置，如下：

//$ vim ./Config/dbs.php

<?php
/**
 * examples配置
 */

return array(
    /**
     * DB数据库服务器集群
     */
    'servers' => array(
        'db_demo' => array(
            'host'      => '192.168.0.104',         //数据库域名
            'name'      => 'phalapi_test',          //数据库名字
            'user'      => 'root',                  //数据库用户名
            'password'  => '123456',                //数据库密码
            'port'      => '3306',                  //数据库端口
        ),
    ),

    /**
     * 自定义路由表
     */
    'tables' => array(
        '__default__' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
            ),
        ),
    ),
);

温馨提示：
为了方便在单元测试时进行调试，和查看日记，对于全部查询、执行的SQL语句都会显示出来，全部的日记改用控制台输出。

(8)最终接口调用

接口调用的链接，这时已经相当明了了，即：域名 + 路径（/demo） + 参数（可从单元测试那直接获取）

如本示例的是：

http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=1

返回的结果是：

{
    "ret": 200,
    "data": {
        "code": 0,
        "msg": "",
        "info": {
            "id": "1",
            "name": "dogstar",
            "note": "oschina"
        }
    },
    "msg": ""
}

截图效果：
show

温馨提示：
如果提示日记写入失败，请确保./Runtime目录具有写入权限，即0777。

1.11.3 更多简明的使用

(1)日记纪录

当我们访问一个不存在的用户时，将会触发日记纪录：

DI()->logger->debug('user not found', $this->userId);

如访问：

http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=2

然后，可以在Runtime下看到按天分目录的日记：

$ tailf ./Runtime/log/201501/20150128.log
2015-01-28 00:37:34|DEBUG|user not found|2

温馨提示：
外网环境上，请把Runtime目录软链到磁盘空间很大的路径。

(2)国际化翻译

当需要翻译时，可以使用人性化的函数T()，如：

$rs['msg'] = T('user not exists');

对应地需要补充翻译的内容：

//$ vim ./Language/zh_cn/common.php

    'user not exists' => '用户不存在',

还是以上面的用户不存在为例，看下运行的截图效果： show

(3)配置读取

配置的读取，使用方便，直接通过以下方式便可以获取，以点号分割：

DI()->config->get('dbs')

第一段，必须为文件名，后面的为用点号相连的数组下标，不限级。

(4)[酷！]接口参数在线查询

为了方便客户端实时查看最新的接口参数，这里提供了一个快速的在线工具：

http://demo.phalapi.net/checkApiParams.php?service=User.GetBaseInfo

用浏览器打开后，即可以看到最新的接口参数说明， 不需要后台接口开发人员编写文档维护，直接从代码中生成参数报表：

1.11.4 Demo相关代码文件

从上面可以得到，此示例相关的代码如下：

Demo$ tree
.
├── Api
│   ├── Default.php
│   └── User.php
├── Domain
│   └── User.php
├── Model
│   └── User.php
└── Tests
    ├── Api
    │   ├── Api_Default_Test.php
    │   └── Api_User_Test.php
    └── test_env.php

上一章 文档首页 下一章

[1.12]-参数规则：接口参数规则配置

上一章 文档首页 下一章

“播下一种思想，收获一种行为；播下一种行为，收获一种习惯；播下一种习惯，收获一种性格；播下一种性格，收获一种命运。” --《成君忆：水煮三国》

1.12.1 参数解析

参数，对于接口来说，是非常重要的输入。对于外部调用来说，同等重要。

因此，对于参数这块，我们是希望能够既减轻后台开发对接口参数获取、判断、验证、文档编写的痛苦；又便于客户端方便的、自由的调用；既利已又利他。

由此，我们引入了 参数解析 这一概念，即：通过配置参数的规则，即可自动实现参数的获取和验证。

1.12.2 参数解析的配置规则

熟悉Yii的同学，对于以下的规则配置应该倍感亲切，但是不熟悉的同学也可以同样快速上手。因为，你会慢慢发现，这样的规则很符合我们PHP开发的规范，如果没有，我们继续努力改进。

格式如下：

array(
    '参数名' => array('name' => '接口参数名称', 'type' => '类型', 'default' => '默认值', ...),
    ... ...
)

1.12.3 示例

(1)简单的示例

假设这样的业务场景，我们需要提供一个用户登录的接口，其中需要用户名和密码，因此：

<?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'login' => array(
                'username' => array('name' => 'username'),
                'password' => array('name' => 'password'),
            ),
        );
    }

    public function login()
    {
        return array('username' => $this->username, 'password' => $this->password);
    }
}

当我们这样调用接口时：

/?service=User.Login&username=test&password=123456

就可以获取到需要的参数：

{"ret":0,"data":{"username":"test","password":"123456"},"msg":""}

从中，可以很容易理解：参数规则需要统一配置在接口实现类里面的 getRules() 函数，随后即可以通过类成员属性方式获取，如： $this->username 。

(2)更完善的示例

很多时候我们都会对用户名和密码作一些验证，如是否必须、长度、最值，以及默认值等。

继续上面的业务场景，我们登录下用户名和密码必须，且密码长度至少为6个字符，则可以调整参数规则：

'login' => array(
   'username' => array('name' => 'username', 'require' => true),
   'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),

尝试一下非法的参数请求，如无任何参数的情况下，访问/?service=User.Login，返回：

{"ret":400,"data":[],"msg":"Illegal Param: wrong param: username"}

再尝试一下密码长不对的情况，访问/?service=User.Login&username=test&password=123，返回：

{"ret":400,"data":[],"msg":"Illegal Param: password.len should >= 6, but now password.len = 3"}

1.12.4 三级参数

(1)系统参数

已被系统固定占有的参数，目前只有一个，即：service，为需要调用的服务，类型为字符串，格式为：XXX.XXX，首字母不区分大小写，建议统一以大写开头。

以下是一些示例：

#推荐写法
/?service=User.GetBaseInfo

#正确写法（开头小写）
/?service=user.getBaseInfo
#正确写法（方法名小写，但类名只能开头小写，否则会导致linux系统下文件加载失败）
/?service=user.getbaseinfo

#错误写法（缺少方法名）
/?service=User
#错误写法（缺少点号分割）
/?service=UserGetBaseInfo
#错误写法（默认只支持点号分割）
/?service=User|GetBaseInfo

(2)应用参数

应用参数是指在一个项目中，全部接口都需要的参数，或者通用的参数规则。假如我们的项目中全部需要签名sign参数，且必须；以及非必须的版本号，则可以在./Config/app.php中的apiCommonRules配置：

//$vim ./Config/app.php
<?php

return array(
    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' => array(
        //签名
        'sign' => array(
            'name' => 'sign', 'require' => true,
        ),
        //客户端App版本号，如：1.0.1
        'version' => array(
            'name' => 'version', 'default' => '',
        ),
    ),

    ... ...

(3)接口参数

接口参数即为上面在各个接口子类中配置的规则，为特定接口所持有。同时，为了方便同一套接口的规则重用，可以使用下标为 '*' 表示是本接口通用规则，如我们为了加强安全性，为全部的用户接口操作都加上4位的验证码：

    public function getRules()
    {
        return array(
            '*' => array(
                'code' => array('name' => 'code', 'require' => true, 'min' => 4, 'max' => 4),
                ),
            'login' => array(
                'username' => array('name' => 'username', 'require' => true),
                'password' => array('name' => 'password', 'require' => true, 'min' => 6),
            ),
        );
    }

在完成对上面的应用参数规则、接口通用规则和指定规则的参数进行配置后，对用户登录的接口进行请求时就需要这样访问：

/?service=User.login&sign=77f81c17d512302383e5f26b99dae4d9&username=test&password=123456&code=abcd

温馨提示： 在Api类里面配置规则时，下标不区分大小写。因为框架会自动将请求的函数名和全部的规则下标转换成小写进行匹配。

这里，再小结一下，接口参数可以分为两种： 通用接口参数 和 指定接口参数 。前者用 * 号下标表示，后者则用函数名作为下标表示。

(4)多个参数规则的优先级

当同一个参数规则分别在应用参数、接口通用参数及特定接口参数出现时，后面的规则会覆盖前面的，即具体化的规则会替换通用的规则，以保证接口在特定场合的定制性。

简而言之，多个参数规则的优先级从高到下，分别是（正如你想到的那样）：

  • 1、指定接口参数
  • 2、通用接口参数
  • 3、应用参数
  • 4、系统参数（通常忽略，因为只有service）

1.12.5 在线接口参数查询工具

为了便于理解上面全部的参数规则，对于具体接口调用的要求，这里可以使用在线接口参数查询工具在浏览器访问查看：

/demo/checkApiParams.php?service=User.Login

可以看到：
show

此工具同时也可以方便客户端实时查看接口文档时，进行辅助的接口规则说明。

自描述数据

这里值得一提的是，我们这里所定义的参数规则实际上也是自描述数据。即配置的代码真实同步反映了参数的相关属性。

1.12.6 参数传递的方式

系统下GET和POST皆可，但是推荐：

  • 1、service参数以GET方式传递，接口统一以/?service=XXX.XXX链接请求，便于交流，更重要的是当接口发生问题时，可以快速在服务器上通过nginx日记定位问题；

  • 2、其他参数以POST方式传递，特别对于敏感数据，如密码，以相对保护数据安全；

  • 3、在编写文档，或者进行调试时，可以全部临时使用GET方式，如本文档的写法，同时在浏览器时也可以使用GET；

  • 4、如果需要对数据包进行加密或者压缩、自定义参数格式，可以重载PhalApi_Request::genData()，然后再继续使用参数规则解析；

1.12.7 参数规则

类型type | 参数名称 name | 是否必须require | 默认值default | 最小值&最大值min&max | 更多
字符串 | string | true/false，默认false | 应为字符串 | 可选 | regex下标为正则匹配的规则；format下标可用于定义字符编码的类型，如utf8、gbk,gb2312
整数 | int | true/false，默认false | 应为整数 | 可选 | ---
浮点数 | float | true/false，默认false | 应为浮点数 | 可选 | ---
布尔值 | boolean | true/false，默认false | true/false | --- | 以下值会转换为true： ok, true, success, on, yes, 1
时间戳/日期 | date | true/false，默认false | 会按格式转换 | 可选，仅当为timestamp时才判断 | 格式：format 为timestamp时会将字符串的日期转换
数组 | array | true/false，默认false | 为非数组会自动转换/解析成数组 | 可选，判断数组元素个数 | 格式：format 为explode时，会根据separator将字符串分割成数组, 为json时，会json解析
枚举 | enum | true/false，默认false | 应为range中的某个元素 | --- | 必须，range,以数组指定枚举的范围
文件 | file | true/false，默认false | 数组类型 | min和max表示文件大小范围 | range下标表示允许上传的文件类型，ext表示需要过滤的文件扩展名
回调 | callable | true/false，默认false | --- | --- | callback设置回调函数，params为回调函数的第三个参数，第一个为参数值，第二个为所配置的规则

温馨提示：
全部的参数规则，都可以配置desc下标，对应在线接口文档的”说明“部分。
如： array('name' => 'username', 'desc' => '用户名')

下面是对各类型的示例说明。

(1)字符串 string

当一个参数规则 未指定类型时，默认为string。一个完整的写法可以为：

array('name' => 'username', 'type' => 'string', 'require' => true, 'default' => 'nobody', 'min' => 1, 'max' => 10)

若传递的参数长度过长，如&username=alonglonglonglongname，则会异常失败返回：

{"ret":400,"data":[],"msg":"Illegal Param: username.len should <= 10, but now username.len = 21"}

但是当需要验证的是类型是中文的话会出现一点问题一个中文字符会占用3个字节所以在min和max验证的时候会出现一些问题,PhalApi提供了format方式对你需要验证长度的string进行指定格式可以排除此问题

array('name' => 'username', 'type' => 'string','format' => 'utf8', 'min' => 1, 'max' => 10)

对于正则表达式的验证，一个邮箱的例子是：

'email' => array(
    'name' => 'email',
    'require' => true,
    'min' => '1',
    'regex' => "/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i",
    'desc' => '邮箱',
),

(2)整型 int

如通常数据库中的id，即可配置成：

array('name' => 'id', 'type' => 'int', 'require' => true, 'min' => 1 )

当传递的参数，不在其配置的范围内时，如&id=0，则会异常失败返回：

{"ret":400,"data":[],"msg":"Illegal Param: id should >= 1, but now id = 0"}

(3)浮点 float

浮点型，类似整型的配置，此处略。

(4)布尔值 boolean

布尔值，主要是可以对一些字符串转换成布尔值，如ok, true, success, on, yes, 以及会被PHP解析成true的字符串，都会转换成true，方便调用。如通常的是否记住我：

array('name' => 'isRememberMe', 'type' => 'boolean', 'default' => true)

(5)日期 date

日期可以按自己约定的格式传递，可以这样配置：

array('name' => 'registerData', 'type' => 'date')

对应地，risterData=2015-01-31 10:00:00则会被获取到为："2015-01-31 10:00:00"。

当需要将字符串的日期转换成timestamp时，则配置成：

array('name' => 'registerData', 'type' => 'date', 'format' => 'timestamp')

则上面的参数再请求时，则会被转换成：1422669600。

(6)数组 array

很多时候在接口进行批量获取时，都需要提供一组参数，所以这时可以使用数组来进行配置。如：

array('name' => 'uids', 'type' => 'array', 'format' => 'explode', 'separator' => ',')

对应&uids=1,2,3则会被转换成：

array ( 0 => '1', 1 => '2', 2 => '3', )

又如接口需要使用JSON来传递整块参数时，可以这样配置：

array('name' => 'params', 'type' => 'array', 'format' => 'json')

对应&params={"username":"test","password":"123456"}则会被转换成：

array ( 'username' => 'test', 'password' => '123456', )

特别地，当配置成了数组，却未指定格式format时，会转换成一个元素的数组，如：&name=test，会转换成：array('test')。

(7)枚举 enum

在需要对接口参数进行范围限制时，可以使用此枚举型。如对于性别的参数，可以这样配置：

array('name' => 'sex', 'type' => 'enum', 'range' => array('female', 'male'))

当传递的参数不合法时，如&sex=unknow，则会被拦截，返回失败：

{"ret":400,"data":[],"msg":"Illegal Param: sex should be in female\/male, but now sex = unknow"}

关于枚举类型的配置，这里需要特别注意配置时，应尽量使用字符串的值。
因为通常而言，接口通过GET/POST方式获取到的参数都是字符串的，而如果配置规则时指定范围用了整型，会导致底层规则验证时误判。如：

//接口参数为： &type=N

//接口参数规则为：
array('name' => 'type', 'type' => 'enum', 'range' => array(0, 1, 2))

//误判，因为：
var_dump(in_array('N', array(0, 1, 2))); //结果为true，因为 'N' == 0

为了避免这类情况发生，应该这样配置：

//接口参数规则为（使用字符串）：
array('name' => '&type', 'type' => 'enum', 'range' => array(`0`, `1`, `2`))

(8)文件 file

在需要对上传的文件进行过滤、接收和处理时，可以使用文件类型，如：

array(
    'name' => 'upfile',
    'type' => 'file',
    'min' => 0,
    'max' => 1024 * 1024,
    'range' => array('image/jpeg', 'image/png') ,
    'ext' => array('jpeg', 'png')
)

其中，min和max分别对应文件大小的范围，单位为字节；range为允许的文件类型，使用数组配置，且不区分大小写。

如果成功，返回的值对应的是$_FILES["upfile"]，即会返回：

array(
     'name' => '',
     'type' => '',
     'size' => '',
     'tmp_name' => '',
)

对应的是：

  • $_FILES["upfile"]["name"] - 被上传文件的名称
  • $_FILES["upfile"]["type"] - 被上传文件的类型
  • $_FILES["upfile"]["size"] - 被上传文件的大小，以字节计
  • $_FILES["upfile"]["tmp_name"] - 存储在服务器的文件的临时副本的名称
  • $_FILES["upfile"]["error"] - 由文件上传导致的错误代码

若需要配置默认值default选项，则也应为一数组，且其格式应类似如上。

其中,ext是对文件后缀名进行验证,当如果上传文件后缀名不匹配时将抛出异常。文件扩展名的过滤可以类似这样进行配置：

//单个后缀名 - 数组形式
'ext' => array('jpg')

//单个后缀名 - 字符串形式
'ext' => 'jpg'

//多个后缀名 - 数组形式
'ext' => array('jpg', 'jpeg', 'png', 'bmp')

//多个后缀名 - 字符串形式（以英文逗号分割）
'ext' => 'jpg,jpeg,png,bmp'

(9)回调 callable

当需要利用已有函数进行自定义验证时，可采用回调参数规则，如：

//配置规则
array('name' => 'version', 'type' => 'callable', 'callback' => array('Common_MyVersion', 'formatVersion'))

然后，回调时将调用下面这个函数：

//新增一个自定义的版本检测函数
class Common_MyVersion {

    public static function formatVersion($value, $rule) {
        if (count(explode('.', $value)) < 3) {
            throw new PhalApi_Exception_BadRequest('版本号格式错误');
        }
    }
}

温馨提示： 第一个为参数值，第二个为所配置的规则，第三个参数为配置规则中的params（可忽略）

1.12.8 关于参数设计的原则

(1)通配的$_REQUEST

使用$_REQUEST获取参数，便于在不同场合下GET/POST之间的切换，同时在初始化DI()->request服务时，可以指定传递的参数，以便于灵活的单元测试；

(2)更自由的名称映射

之所以没把规则配置的下标默认成与客户端传递的name一致，是为了更自由的名称映射；
如可能我们PHP后台开发喜欢用驼峰法来表示，但客户端想用下划线来分割，则通过这样配置：

array(
    'isRememberMe' => array('name' => 'is_remember_me', 'type' => 'boolean', 'default' => true),
)

更重要的是，有时我们希望能缩短客户端请求的参数名称以节省流量时，可以这样配置：

array(
    'isRememberMe' => array('name' => 're', 'type' => 'boolean', 'default' => true),
)

(3)异常返回

对于客户端参数不合法时，以异常失败返回，而不是隐性地转换，是因为后台接口往往需要手动对传递的参数进行人工的验证，而不是希望得到隐性转换的值。即当客户端参数传递不对时，我们需要明确提示说：参数非法。

1.12.9 扩展你的参数

当PhalApi提供的参数规则不能满足接口参数的规则验证时，除了使用callable类型进行扩展外，还可以扩展PhalApi_Request_Formatter接口来定制项目需要的类型。

一如既往，分两步：

  • 1、扩展实现PhalApi_Request_Formatter接口
  • 2、在DI注册你的类型

下面以大家所熟悉的邮件类型为例，说明扩展的步骤。

首先，我们需要一个实现了邮件类型验证的功能类：

<?php

class Common_MyFormatter_Email implements PhalApi_Request_Formatter {

    public function parse($value, $rule) {
        if (!preg_match('/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/', $value)) {
            throw new PhalApi_Exception_BadRequest('邮箱地址格式错误');
        }

        return $value;
    }
}

然后，注册一下：

DI()->_formatterEmail = 'Common_MyFormatter_Email';

温馨提示： 在DI中手动注册服务时，名称的格式为： 下划线("_") + 统一前缀（"formatter") + 参数类型（全部小写后，首字母大写）；
若需要实现自动注册，扩展的类名格式须为：

class PhalApi_Request_Formatter_{类型名称} implements PhalApi_Request_Formatter { ...

系统已自动注册的格式化服务有：

  • _formatterArray 数组格式化服务
  • _formatterBoolean 布尔值格式化服务
  • _formatterCallable 回调格式化服务
  • _formatterDate 日期格式化服务
  • _formatterEnum 枚举格式化服务
  • _formatterFile 上传文件格式化服务
  • _formatterFloat 浮点数格式化服务
  • _formatterInt 整数格式化服务
  • _formatterString 字符串格式化服务

至此，便可使用自己定制的类型规则了，

array('name' => 'user_email', 'type' => 'email')

上一章 文档首页 下一章#[1.13]-统一的接口请求方式：_sevice=XXX.XXX

上一章 文档首页 下一章

有句话说得好，最可怕的事情不是别人比你优秀，而是优秀的人竟然还比你更努力。 --《考拉小巫的留学成长日记》

此篇章主要是讲述接口统一请求的方式，以及提供一个PHP实现的简单客户端。

1.13.1 指定接口服务：?service=XXX.XXX

我们统一固定用service参数来表示需要请求获得的服务，并通过GET方式传递，即请求的URI格式为：

接口域名 + 入口路径 + ?service=XXX.XXX

如：
http://dev.phalapi.com   +   /demo/  +  ?service=User.GetBaseInfo

当我们在浏览器以GET方式请求时，可以在nignx看到这样的日记：

127.0.0.1 - - [07/Feb/2015:22:46:46 -0800] "GET /demo/?service=User.GetBaseInfo&sign=&user_id=1 HTTP/1.1" 200 107 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:11.0) Gecko/20100101 Firefox/11.0"

如果通过接口用POST方式请求，则会看到：

127.0.0.1 - - [07/Feb/2015:19:32:05 -0800] "POST /demo/?service=User.GetBaseInfo&sign= HTTP/1.1" 200 135 "-" "-"

这里service的名称，开头不区分大小写，建议统一以大写开头，以显得专业。对应的接口是：

class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }
}

1.13.2 统一参数用GET

在一个项目中，会有很多公共的接口参数，如客户端、版本号、密钥等。这些同样可以纳入GET参数里面，或者也可以放到POST里面。

温馨提示：
这样要求是有目的的，因为这样的话可以在nginx的access日记里面查看来自客户端的快照信息，以便统计或者定位问题。

1.13.3 接口参数用POST

特别地，接口参数我们建议统一使用POST方式传递，理由很简单：

  • 1、相对保护上传的数据，一如密码；
  • 2、避免特殊字符或者过大数据包在GET下的限制；

1.13.4 获取Header头信息

每个请求都有自己的Header头,获取Header头信息可以判断请求来源或者是把一些敏感的内容存放到Header头信息中(如:加密后的参数),框架也提供了方便快捷获取Header的方法:

DI()->request->getHeader("Host");

1.13.5 测试下的模拟参数

默认地，PhalApi框架会将$_REQUEST作为接口参数的来源：

DI()->request = 'PhalApi_Request';

当我们需要统一强制用$_GET，可以在init.php文件中这样简单定制：

DI()->request = new PhalApi_Request($_GET);

同样，也可以在init.php文件中强制用$_POST：

DI()->request = new PhalApi_Request($_POST);

在测试环境下，为了模拟接口请求，我们需要人工提供接口参数，因此可以这样轻松模拟：

$str = 'service=User.GetBaseInfo&user_id=1';

parse_str($str, $params);
DI()->request = new PhalApi_Request($params);

1.13.6 PHP接口客户端示例

先看下调用和使用的代码示例：

<?php
require_once './PhalApiClient.php';

$config = array(
    'host' => 'http://dev.phalapi.com/demo',
    'secrect' => '******'
);

$client = new PhalApiClient($config);
$rs = $client->request('User.GetBaseInfo', array('userId' => 1));

if ($client->getRet() == PhalApiClient::RET_OK) {
    var_dump($rs);
} else {
    var_dump($client->getMsg());
    var_dump($client->getUrl());
}

附调用接口的客户端源代码：

//$ vim ./PhalApiClient.php
<?php

class PhalApiClient
{
    protected $host;
    protected $secrect = '';

    protected $params = array();

    protected $moreParams = array();

    protected $url;
    protected $ret;
    protected $msg;
    protected $data = array();

    const RET_OK = 'OK';
    const RET_WRONG = 'WRONG';
    const RET_ERROR = 'ERROR';

    public function __construct($config)
    {
        $this->host = rtrim($config['host'], '/') . '/';
        $this->secrect = $config['secrect'];
    }

    public function request($service, $params = array(), $timeoutMs = 3000)
    {
        if (!empty($service)) {
            $this->params['service'] = $service;
        }
        $this->params['sign'] = $this->encryptAppKey($params, $this->secrect);
        $this->url = $this->host . '?' . http_build_query($this->params);
        $this->moreParams = $params;

        $rs = $this->doRequest($this->url, $params, $timeoutMs);

        if ($rs === false) {
            $this->ret = self::RET_ERROR;
            $this->msg = '后台接口请求超时';
            return $this->getData();
        }

        $rs = json_decode($rs, true);

        if (isset($rs['data']['code']) && $rs['data']['code'] != 0) {
            $this->ret = self::RET_WRONG;
            $this->msg = '接口调用失败[code =' . $rs['data']['code'] . ']' . ', 错误>信息：' . isset($rs['data']['msg']) ? $rs['data']['msg'] : '无';
            return $this->getData();
        }

        $this->ret = intval($rs['ret']) == 200 ? self::RET_OK : self::RET_WRONG;
        $this->data = $rs['data'];
        $this->msg = $rs['msg'];

        return $this->getData();
    }

    public function getRet()
    {
        return $this->ret;
    }

    public function getData()
    {
        return $this->data;
    }

    public function getMsg()
    {
        return $this->msg;
    }

    public function getUrl()
    {
        return $this->url . '&' . http_build_query($this->moreParams);
    }

    protected function encryptAppKey($params, $secrect)
    {
        return '';
    }

    protected function doRequest($url, $data, $timeoutMs = 3000)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $timeoutMs);

        if (!empty($data)) {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }

        $rs = curl_exec($ch);

        curl_close($ch);

        return $rs;
    }
}

上一章 文档首页 下一章#[1.14]-统一的返回格式和结构：ret-data-msg

上一章 文档首页 下一章

表达，从简单开始。--《Robin Williams：写给大家看的设计书》

1.14.1 统一返回的格式

很明显地，默认情况下，我们选择了 JSON 作为统一的格式返回接口结果。这里简单说明一下选取JSON统一返回的原因：

  • JSON当前很流行，且普通接口都采用此格式返回
  • JSON在绝大部分开发语言中都支持，跨语言

  • JSON在浏览器浏览时，有可视化插件支持，如FF下：

1.14.2 统一返回结构

通常，我们正常情况下请求接口会返回类似：

{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1423142802
    },
    "msg": ""
}

其中，ret表示为返回状态码，200表示成功；data为领域业务数据，由接口自定义；最后msg为错误的提示信息。下面分别解释之。

(1)返回状态码 ret

参照HTTP的状态码，特约定：

  • 200：接口正常请求并返回
  • 4XX：客户端非法请求
  • 5XX：服务器运行错误

200 正常返回

当返回200时，需要同时返回data部分数据，以便客户端实现所需要的业务功能。

4XX 客户端非法请求

此类请求是由客户端不正确调用引起的，如请求的接口服务不存在，或者接口参数不对，验证失败等等。当这种情况发生时，客户端同学只需要调整修正调用即可。

对于此系统的状态码，在进行接口开发时，可由项目自已定义约定。 通常地，我们需要告知客户端签名失败时，可以这样：

throw new PhalApi_Exception_BadRequest('wrong sign', 1);

即抛出PhalApi_Exception_BadRequest异常即可，错误信息会返回客户端，对应msg字段；状态为1，系统对此类的异常会在400基础上相加的，即： 401 = 400 + 1 。

5XX 服务器运行错误

此类错误是应该避免的，但当客户端发现有这种情况时，应该知会后台接口开发人员进行修正。
如当配置的参数规则不符合要求时，或者获取了不存在的参数等即会触发此类异常错误，通常由框架抛出。

(2)业务数据 data

data为接口和客户端主要沟通对接的数据部分，可以为任何类型，由接口自定义。但为了更好地扩展、向后兼容，建议都使用array。

返回格式的定义与在线查看

当我们在开发接口时，可以通过为接口添加注释的方式来定义接口的返回格式，然后就可以为外部提供在线文档的实时查看了。

如：

<?php

class Api_User extends PhalApi_Api {

    /**
     * 获取用户基本信息
     * @desc 用于获取单个用户基本信息
     * @return int code 操作码，0表示成功，1表示用户不存在
     * @return object info 用户信息对象
     * @return int info.id 用户ID
     * @return string info.name 用户名字
     * @return string info.note 用户来源
     * @return string msg 提示信息
     */
    public function getBaseInfo() {
        // ... ...
    }

然后在浏览器访问：
http://demo.phalapi.net/checkApiParams.php?service=User.getBaseInfo

可以看到：
apic

注释格式

格式是以docs的 return 注释来标明的，其格式为：

@return  返回的类型  字段名字路径(以点号连接)  字段名字及解析

其中，返回的类型可以为：

关键字 | 说明
string | 字符串
int | 整型
float | 浮点型
boolean | 布尔型
date | 日期
array | 数组
fixed | 固定值
enum | 枚举类型
object | 对象

温馨提示：array与object的区别
array是指没有下标的一个数组集合，或者有下标但下标是连续的自然数，且各元素的结构相同；object则是指一个结构体，类似字典。

此外，为了明确数组与对象间的返回格式，我们也推荐如果是元素来自数组，则在返回字段的后面添加方括号来表明，以提醒客户端在接收到此类返回时需要循环处理。如：

     * @return array list 用户列表
     * @return int list[].id 用户ID
     * @return string list[].name 用户名字
     * @return string list[].note 用户来源

当需要对接口进行更多说明时，可使用@desc注释，即：

     * @desc 用于获取单个用户基本信息

(3)错误信息 msg

当返回状态码不为200时，此字段不为空。即当有异常（如上面所说的客户端非法请求和服务端运行错误两大类）触发时，会自动将异常的错误信息作为错误信息msg返回。

但对于服务端的异常，出于对接口隐私的保护，框架在错误信息时没有过于具体地描述；相反，对于客户端的异常，由会进行必要的说明，以提醒客户端该如何进行调用调整。

此外，我们根据需要可以考虑是否需要进行国际化的翻译。如果项目在可预见的范围内需要部署到国外时，提前做好翻译的准备是很有帮助的。如下，开发时可以这样返回异常错误信息：

throw new PhalApi_Exception_BadRequest(T('wrong sign'), 1);

(4)头部信息 header

当使用的是HTTP/HTTPS协议时，并且需要设置头部header时，可以使用PhalApi_Response::addHeaders()进行设置。对于JSON格式的返回默认设置的头部有：

Content-Type:"application/json;charset=utf-8"

若需要其他的头部设置，可类似这样，在init.php初始化文件进行设置跨域访问（但通常不建议允许全部跨域，这里仅作演示）：

DI()->response->addHeaders('Access-Control-Allow-Origin', '*');

后面相同的头部信息会覆盖前面的。

1.14.3 关于Exception类异常没捕捉的原因

我们没有对Exception类的异常进行捕捉，封装返回非200的形式，是因为我们出于以下的考虑：

  • 一来为了方便开发过程中快速发现及定位具体出错的位置；
  • 二来为了便于线上环境中nginx服务器对错误的捕捉和纪录；

1.14.4 JsonP格式和其他的返回

在部分H5页面异步请求的情况下，客户端需要我们返回JSONP格式的结果，则可以这样在入口文件重新注册response：

if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

但是在测试环境中，我们是不希望有内容输出的，所以我们可以测试时这样注册response：

DI()->response = 'PhalApi_Response_Explorer';

1.14.5 扩展你的返回格式

当你的项目需要返回其他格式时，如返回XML，则可以先这样实现你的格式类：

class MyResponse_XML extends PhalApi_Response {

    protected function formatResult($result) {
        //TODO：把数组$result格式化成XML ...
    }
}

随后，也是简单重新注册一下即可：

DI()->response = new MyResponse_XML();

1.14.6 各状态码产生的时机

1.14.7 更好地建议

很多时候，很多业务场景，客户端在完成一个接口请求并获取到所需要的数据后，需要进行不同的处理的。

  • 就登录来说，当登录失败时，可能需要知道：
  • 是否用户名不存在？
  • 是否密码错误？
  • 是否已被系统屏蔽？
  • 是否密码错误次数超过了最大的重试次数？
  • ...

显然，这里也有一个返回状态码，更准备来说，是业务操作状态码。并且，此类的状态依接口不同而不同，很难做到统一。

SO？

我们建议的是，项目接口在业务数据data里面统一再定义一个状态码，通常为code字段，完整路径即： data.code ，同时为0时表示操作成功，非0时为不同的失败场景。如上面的登录：

  • code = 0 登录成功
  • code = 1 用户名不存在
  • code = 2 密码错误
  • code = 3 系统已屏蔽此账号
  • code = 4 密码错误次数超过了最大的重试次数
  • ...

最后，客户端在获取到接口返回的数据后，先统一判断ret是否正常请求并正常返回，即ret = 200；若是，则再各自判断操作状态码code是否为0，如果不为0，则提示相应的文案并进行相应的引导，如果为0，则走正常流程！

1.14.8 领域特定设计与Fiat标准

在《RESTful Web APIs》一书中提及到，标准可以划归到4个分类，分别是：fiat标准、个人标准、公司标准以及开放标准。

显然，我们这里推荐的是 JSON + ret-data-msg 返回格式既不是个人标准，也不是公司标准（就笔者观察的范筹而言，未发现某个公司定义了此格式）。而且，也不属于开放标准，因为也还没达到此程度。更多的，它是fiat标准。
我们很容易发现，身边的应用、系统以及周围项目都在使用诸如此类的返回结构格式，如一些AJAX的接口。

当然，我们可希望可以消除语义上的鸿沟，以便在后台接口开发上有一个很好地共识。

同时，JSON + ret-data-msg 返回格式也是一种领域特定的格式，它更多是为app多端获取业务数据而制作的规范。虽然它不是很完美，不具备自描述消息，也没有资源链接的能力，但我们认为它是一种恰到好处的格式。
在基于JSON通用格式的基础上，加以 ret-data-msg 的约束，它很好地具备了统一性，可能门槛低，容易理解。

上一章 文档首页 下一章#[1.15]-数据库操作：基于NotORM的使用及优化

上一章 文档首页 下一章

有些裁缝只会塞塞衬里，修修补补，而有些裁缝却能做出新东西--他们之间隔着一条深渊。 -- 《外套》

1.15.1 NotORM官网

这里使用了NotORM进行DB操作，具体的数据库操作使用文档请见NotORM官网： http://www.notorm.com

(1) 基本CURD

//查询
$row = DI()->notorm->user->where('id', 1)->fetch();

//更新
$data = array('name' => 'test', 'update_time' => time());
DI()->notorm->user->where('id', 1)->update($data);

//插入（须是同一个对象才能正确获取插入的ID）
$data = array('name' => 'phalapi');
$userORM = DI()->notorm->user;
$userORM->insert($data);
$id = $userORM->insert_id();

//删除
DI()->notorm->user->where('id', 1)->delete();

(2)update相同的数据的判断

在使用update操作时，如果更新的数据和原来的一样，则会返回0（影响0行）。这时，会和更新失败（同样影响0行）混淆。

但NotORM是一个优秀的类库，所以提供了优秀的解决文案。我们在使用update时，只须了解这两者返回的结果的微妙区别即可。
因为失败异常时，返回false；而相同数据更新会返回0。即：

  • 1、update相同的数据时，返回0，严格来说是：int(0)
  • 2、update失败时，如更新一个不存在的字段，返回false，即：bool(false)

用代码表示，就是：

$rs = DI()->notorm->user->where('id', $userId)->update($data);

if ($rs >= 1) {
    //成功
} else if ($rs === 0) {
    //相同数据，无更新
} else if ($rs === false) {
    //更新失败
}

以下单元测试代码，可以验证上面的判断：

    public function testUpdateOk()
    {
        $userId = 87;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('reg_time' => time()));

        $this->assertSame(1, $rs);
    }

    public function testUpdateZero()
    {
        $userId = 1;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('username' => 'aevit'));

        $this->assertSame(0, $rs);
    }

    public function testUpdateFail()
    {
        $userId = 1;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('wrong_username' => 'aevit'));

        $this->assertSame(FALSE, $rs);
    }

(3)简单的关联查询

如果是简单的关联查询，可以使用NotORM支持的写法，这样的好处在于我们使用了一致的开发，并且能让PhalApi框架保持分布式的操作方式（注意，关联的表仍然需要在同一个数据库）。

以下是一个简单的示例。

假设我们有这样的数据：

INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');
INSERT INTO `phalapi_user_session_0` VALUES ('1', '1', 'ABC', '', '0', '0', '0', null);

那么对应关联查询的代码如下面：

    public function testLeftJoin()
    {
        $rs = DI()->notorm->user_session_0
            ->select('expires_time, user.username, user.nickname')
            ->where('token', 'ABC')
            ->fetchRow();

        var_dump($rs);
    }

运行一下，我们可以看到这样的输出：

SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;

.[1 - 0.06318s]SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;<br>
array(3) {
  ["expires_time"]=>
  string(1) "0"
  ["username"]=>
  string(35) "wx_edebc877070133c65161d00799e00544"
  ["nickname"]=>
  string(10) "weixinName"
}

这样，我们就可以实现关联查询的操作。按照NotORM官网的说法，则是：

If the dot notation is used for a column anywhere in the query ("$table.$column") then NotORM automatically creates left join to the referenced table. Even references across several tables are possible ("$table1.$table2.$column"). Referencing tables can be accessed by colon: $applications->select("COUNT(application_tag:tag_id)").

->select('expires_time, user.username, user.nickname')这一行调用将会【自动产生关联操作】，而ON 的字段，则是这个字段关联你配置的【表结构】，外键默认为： 表名_id 。

(4)加1操作

NotORM已提供了NotORM_Literal，其用法如下：

DI()->notorm->user->where('id', 1)->update(array('age' => new NotORM_Literal("age + 1")));

当需要更新为当前时间，可以：

$array = array(
    "title" => "NotORM",
    "author_id" => null,
    "created" => new NotORM_Literal("NOW()"),
);

1.15.2 NotORM的优化

但为了更符合项目的开发，这里对NotORM的底层作了升级修改，以下为主要修改点和新的使用：

(1)将原来返回的结果全部从对象改成数组

对原来的大部分使用无特别影响，可按原来的方式开发。主要目的是为了更方面处理返回的数据，以及简化对结果的再解析，简单明了。
如：

DI()->notorm->user->where('username = ?', 'dogstar')->fetch();

返回的将是一个数组：

array(7) {
  ["id"]=>
  string(3) "180"
  ["username"]=>
  string(17) "dogstar"
  ["regtime"]=>
  string(10) "1414811954"
  //...
}

(2)提供获取全部结果的接口 - fetchAll() / fetchRows()

如：

$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchAll();

或：

$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchRows();

即可获取全部的数据，不再受限于分页。
这里提供了fetchAll()和fetchRows()两种等效的操作，是为了减少记忆的痛苦，下同。

(3)提供更灵活的查询方式 - queryAll() / queryRows()

当需要进行复杂的SQL查询时，可以使用此接口，如：
(注意:limit替换值:start和:num必须使用int类型)

$sql = 'select * from example AS ep LEFT JOIN user AS u ON ep.ui
d = u.id  where ep.touid = :userId ORDER BY dateline desc LIMIT :start,:num';
$params = array(':userId' => $userId, ':start' => $start, ':num' => $num);
$rs= DI()->notorm->example->queryAll($sql, $params);

或：

$rs= DI()->notorm->example->queryRows($sql, $params);

(4)limit 操作的调整

取消了NotORM中对OFFSET关键字的使用，改用逗号的写法，修改后正确的使用方法应该是：

$table->limit(10);  // limit 10   # 查询前10个

$table->limit(5, 10); // limit 5,10   # 从第5个位置开始，查询前10个

(5)禁止全表删除，防止误删

出于对数据的保护，当执行删除操作却又没有任何where条件时，将会禁止进行全表操作。如：

    public function testDeleteAll()
    {
        DI()->notorm->user->delete();
    }

可以看到：

$ phpunit --filter testDeleteAll ./Api/Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

E

Time: 315 ms, Memory: 6.25Mb

There was 1 error:

1) PhpUnderControl_ApiUser_Test::testDeleteAll
Exception: sorry, you can not delete the whole table --dogstar

(6)添加& __sql__ =1请求参数，可开启HTTP调试模式

当处于debug模式时，可以输入执行的全部SQL语句，以便调试。

如：

SELECT times FROM tpl_user_session_10 WHERE (user_id = ?); -- '74110'
{"ret":0,"data":{"code":0},"msg":""}

(7)关于NotORM中fetch()操作没有limit 1的处理方案 - fetchOne() / fetchRow()

之前，有开发同学提及到，为什么notorm的基类fetch为啥没用limit(1)呢。后来，我去发了下NotORM的写法，确实做得很微妙。
其实NotORM之所以没有在fetch()里面自动limit 1是因为，你可以循环地获取数据，如：

$user = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->limit(3);
while(($row = $user->fetch())) {
    var_dump($row);
 }

但是，更多情况下，我们只需要获取某一行的数据，上面的做法会造成不必要的SQL查询。为了保留原来的写法，我特意添加扩展了一个新的操作：fetchRow()，用法同fetch()，但只会取第一条。
以下是使用示例：

$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow());

var_dump($rs);

//结果输出类如：
array(3) {
  ["id"]=>
  string(1) "1"
  ["username"]=>
  string(5) "aevit"
  ["nickname"]=>
  string(4) "test"
}

//对应执行的SQL语句：
[2 - 0.06544s]SELECT id, username, nickname FROM fami_user WHERE (id > ?) LIMIT 1; -- 0<br>

如果，我们只需要获取这一行的某个字段，也可以像fetch()那样使用，即：

$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow('nickname'));

var_dump($rs);

//结果输出类如：
string(4) "test"

//纪录不存在时，返回 bool(false)

(8)显式的SQL语法异常提示

很多时候，在开发时，我们对数据库操作一开始会存在一些SQL语法的问题，PDO会返回false,且原来NotORM也是使用 静默方式 来处理这类错误，从而使得开发人员有时难以发现这些问题，除非将调试的SQL手动放到数据库执行才能发现问题所在。

为了能给开发同学更早、更直观的方式查看问题的所在，这里对NotORM底层进行了调整，使用了 显式方式 的策略来处理，即：直接抛出PDO异常。

如：

$userId = 1;

//OK
$rs = DI()->notorm->user->select('username')->where('id', $userId)->fetchOne();

//WRONG
$rs = DI()->notorm->user->select('wrong_username')->where('id', $userId)->fetchOne();

将会看到：

[1 - 0.06437s]SELECT username FROM fami_user WHERE (id = 1) LIMIT 1;<br>
[2 - 0.06496s]SELECT wrong_username FROM fami_user WHERE (id = 1) LIMIT 1;<br>

PDOException: Unknown column 'wrong_username' in 'field list'

(9)复杂的关联查询

如果是复杂的关联查询，则是建议使用原生态的SQL语句，但我们仍然可以保持很好的写法，如这样一个示例：

        $sql = 'SELECT t.id, t.team_name, v.vote_num '
            . 'FROM phalapi_team AS t LEFT JOIN phalapi_vote AS v '
            . 'ON t.id = v.team_id '
            . 'ORDER BY v.vote_num DESC';
        $rows = $this->getORM()->queryAll($sql, array());

注意，此时的表需要使用全名，即自带前缀。这样也可以实现更自由的关联查询。

(10)事务操作

关于事务操作，可以参考 NotORM官网 的说明：

$db->transaction = $command Assign 'BEGIN', 'COMMIT' or 'ROLLBACK' to start or stop transaction

即：

//第一步：先指定待进行事务的数据库（通过获取一个notorm表实例来指定；否则会提示：PDO There is no active transaction）
$user = DI()->notorm->user;

//第二步：开启事务开关（此开关会将当前全部打开的数据库都进行此设置）
DI()->notorm->transaction = 'BEGIN';

//第三步：进行数据库操作
$user->insert(array('name' => 'test1',));
$user->insert(array('name' => 'test2',));

//第四：提交/回滚
DI()->notorm->transaction = 'COMMIT';
//DI()->notorm->transaction = 'ROLLBACK';

推荐使用PhalApi的事务操作方式

PhalApi一开始对事务这块考虑不周，后来发现很多同学、很多项目都需要用到数据库事务操作。
基于此，在不破坏原来的代码基础上，我们决定在PhalApi_DB_NotORM上添加对数据库维度的事务操作支持。

示例简单如下：

    public function testTransactionCommit()
    {
        //Step 1: 开启事务
        DI()->notorm->beginTransaction('db_demo');

        //Step 2: 数据库操作
        DI()->notorm->user>insert(array('name' => 'test1'));
        DI()->notorm->user>insert(array('name' => 'test2'));

        //Step 3: 提交事务
        DI()->notorm->commit('db_demo');

    }

温馨提示： 以上操作，须PhalApi 1.3.1 及以上版本才能支持。

(11)扩展对非MySQL数据库的支持

PhalApi使用的是NotORM来进行数据库操作，而NotORM底层则是采用了PDO。目前，NotORM支持： MySQL, SQLite, PostgreSQL, MS SQL, Oracle (Dibi support is obsolete)。

但需要注意的是，PhalApi本身对NotORM进行了修改，需要调整一下代码才能更好地支持除MySQL外的数据库。 即使NotORM不支持的数据库，你也可以轻松通过添加扩展的方式来支持。如：

首先，定制自己的数据库连接的PDO。

class Common_MyDB extends PhalApi_DB_NotORM {

    protected function createPDOBy($dbCfg) {
        /* Connect to an ODBC database using driver invocation */
    $dsn = 'uri:file:///usr/local/dbconnect';
    return new PDO($dsn, $dbCfg['user'], $dbCfg['password']);
    }
}

随后，在初始化文件init.php中重新注册DI()->notorm即可，如：

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new Common_MyDB(DI()->config->get('dbs'), $debug);
};

1.15.3 可选的Model基类

(1)表数据入口模式

我们一直在考虑，是否应该提供数据库的基本操作支持，以减少开发人员重复手工编写基本的数据操作。

最后，我们认为是需要的。然后就引发了新的问题：是以继承还是以委托来支持？

委托有助于降低继承的层级，但仍然需要编写同类的操作然后再次委托。所以，这里提供了基于NotORM的Model基类：PhalApi_Model_NotORM。

然而提供这个基类还是会遇到一些问题，例如：如何界定基本操作？如何处理分表存储？如何支持定制化？

由于我们这里的Model使用了 “表数据入口” 模式，而不是“行数据入口”，也不是“活动纪录”，也不是复杂的“数据映射器”。所以在使用时可以考虑是否需要此基类。即使这样，你也可以很轻松转换到“行数据入口”和“活动纪录”模式。这里，PhalApi中的Model是更广义上的数据源层（后面会有更多说明），因此对应地PhalApi_Model_NotORM基类充当了数据库表访问入口的对象，处理表中所有的行。

(2)规约层的CURD

在明白了Model基类的背景后，再来了解其具体的操作和如何继承会更有意义。

而具体的操作则与数据表的结构相关，在“约定编程”下：即每一个表都有一个主键（通常为id，也可以自由配置）以及一个序列化LOB字段ext_data。我们很容易想到Model接口的定义（注释已移除，感兴趣的同学可查看源码）：

interface PhalApi_Model {

    public function get($id, $fields = '*');

    public function insert($data, $id = NULL);

    public function update($id, $data);

    public function delete($id);
}

上面的接口在规约层上提供了基于表主键的CURD基本操作，在具体实现时，需要注意两点：一是分表的处理；另一点则是LOB字段的序列化。

(3)不使用Model基类的写法

由于我们使用了NotORM进行数据库的操作，所以这里也提供了基于NotORM的基类：PhalApi_Model_NotORM。下面以我们熟悉的获取用户的基本信息为例，说明此基类的使用。

为唤醒记忆，下面贴上Model_User类原来的代码：

// $ vim ./Demo/Model/User.php

<?php

class Model_User {

    public function getByUserId($userId) {
        return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
    }
}

对应的调用：

$model = new Model_User();
$rs = $model->getByUserId($userId);

(4)继承Model基类的写法

若继承于PhalApi_Model_NotORM，则是：

// $ vim ./Demo/Model/User.php

<?php

class Model_User extends PhalApi_Model_NotORM {

    protected function getTableName($id) {
        return 'user';
    }
}

从上面的代码可以看出，基类已经提供了基于主键的CURD操作，但我们需要钩子函数以返回对应的表名。相应地，外部调用则调整为：

$model = new Model_User();
$rs = $model->get($userId);

再进一步，我们可以得到其他的基本操作：

$model = new Model_User();

//查询
$row = $model->get(1);
$row = $model->get(1, 'id, name'); //取指定的字段
$row = $model->get(1, array('id', 'name')); //可以数组取指定要获取的字段

//更新
$data = array('name' => 'test', 'update_time' => time());
$model->update(1, $data); //基于主键的快速更新

//插入
$data = array('name' => 'phalapi');
$id = $model->insert($data);
//$id = $model->insert($data, 5); //如果是分表，可以这样指定

//删除
$model->delete(1);

1.15.4 定制化你的Model基类

正如上面提及到的两个问题：LOB序列化和分表处理。所以，如果PhalApi现有就此两问题的解决方案不能满足项目的需求，可作定制化处理。

(1)LOB序列化

先是LOB序列化，考虑到有分表的存在，当发生数据库变更时（特别在线上环境）会有一定的难度和风险，因此引入了扩展字段ext_data。当然，此字段也应对数据库变更的同时，也可以作为简单明了的值对象的大对象。序列化LOB首先要考虑的问题是使用二进制（BLOB）还是文本（CLOB），出于通用性、易读性和测试性，我们目前使用了json格式的文本序列化。所以，如果考虑到空间或性能问题（在少量数据下我认为问题不大，如果数据量大，应该及时重新调整数据库表结构），可以重写formatExtData() & parseExtData()。

如改成serialize序列化：

abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    /**
     * 对LOB的ext_data字段进行格式化(序列化)
     */
    protected function formatExtData(&$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = serialize($data['ext_data']);
        }
    }

    /**
     * 对LOB的ext_data字段进行解析(反序列化)
     */
    protected function parseExtData(&$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = unserialize($data['ext_data'], true);
        }
    }

    // ...
}

将Model类继承于Common_Model_NotORM后，

// $ vim ./Demo/Model/User.php

<?php
class Model_User extends Common_Model_NotORM {
   //...
}

就可以轻松切换到序列化，如：

$model = new Model_User();

//带有ext_data的更新
$extData = array('level' => 3, 'coins' => 256);
$data = array('name' => 'test', 'update_time' => time(), 'ext_data' => $extData);
$model->update(1, $data); //基于主键的快速更新

(2)分表处理

其次是分表处理，同样考虑到分表的情况，以及不同的表可能配置不同的主键表，而基于主键的CURD又必须要先知道表的主键名才能进行SQL查询。所以，问题就演变成了如何找到表的主键名。这里可以自动匹配，也可以手工指定。自动匹配是智能的，因为当我们更改表的主键时，可以自动同步更新而不需要担心遗漏（虽然这种情况很少发生）。手工指定可以大大减少系统不必要的匹配操作，因为我们开发人员也知道数据库的主键名是什么，但需要手工编写一些代码。在这里，提供了可选的手工指定，即可重写getTableKey($table)来指定你的主键名。

如，当我们的表的主键都固定为id时：

abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    protected function getTableKey($table) {
        return 'id';
    }
}

上一章 文档首页 下一章#[1.16]-配置读取：内外网环境配置的完美切换

上一章 文档首页 下一章

Experience by itself teaches nothing... Without theory, experience has no meaning. Without theory, one has no questions to ask. Hence, without theory, there is no learning. – Edwards Deming

1.16.1 配置的简单读取

通常，我们会有以下三个配置文件：

dogstar@ubuntu:Config$ tree
.
├── app.php
├── dbs.php
└── sys.php

其中app.php为项目应用配置；dbs.php为分布式存储的数据库配置；sys.php为不同环境下的系统配置。

在入口文件，注册配置组件服务后：

//$ vim ./Public/init.php

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

假设有这样的app.php配置：

return array(

    'version' => '1.1.1',

    'email' => array(
        'address' => 'chanzonghuang@gmail.com',
    );
);

我们就可以分别这样根据需要获取配置：

//app.php里面的全部配置
DI()->config->get('app');

//app.php里面的单个配置
DI()->config->get('app.version');  //返回：1.1.1

//app.php里面的多级配置
DI()->config->get('app.email.address');  //返回：'chanzonghuang@gmail.com'

其他配置文件类似，你也可以随意添加新的配置文件。

1.16.2 内外环境配置管理的策略

可以说，在项目开发中，除了我们的代码、数据和文档外，配置也是一块相当重要的组成部分，而且占据着非常重要的位置。最为明显的是，如果配置一旦出错，就很可能影响到整个系统的运行，并且远比修改代码再上线发布速度快（假设我们不用文件存储配置而是以分布式存储在各服务器内存中）。

但这里将讨论另外一个同样重要的问题，即： 不同环境下不同配置的管理和切换 。

现在将不同的策略分说如下。

(1)支持整包发布的环境变量配置

此种策略的主要方法就是在PHP代码中读取php-fpm中的ENV环境配置，再对应到linux环境下的profile环境变量，即：

PHP代码  -->    $_ENV环境配置  -->   Linux服务器环境变量/etc/profile

这样的好处莫过于可以支持项目代码的整包发布，而不需要在各个环境（开发环境、测试环境、回归环境、预发布环境、生产环境）来回修改切换配置，同时运维可以更好地保护服务器的账号和密码而不让开发知道。

而这样的不足则是，在对项目进行初次部署时，需要添加以上一系列的配置，而且后期维护也比较复杂麻烦，特别当机器多时。这时可以通过pupple/stacksalt这些运维工具进行自动化管理。但对于开发来说，依然会觉得有点烦锁。

(2)不同环境，不同入口

当服务器的账号和密码也是由开发来掌控时，则可以使用这种在代码层次控制的策略。

我们可以在Public下提供不同的入口，一如：

$ mkdir ./Public/myapi
$ myapi$ tree
.
├── index.php
└── test.php

我们有这样不同的入口，客户端在测试时，只需要将入口路径改成：/myapi/test.php?service=XXX.XXX，而在打包发布时只需要将入口路径改成：/myapi/?service=XXX.XXX 即可。也就是将test.php去掉。

而在服务端，仅需要在这些不同的入口文件，修改一下配置文件目录路径即可：

//$ vim ./Public/myapi/test.php
DI()->config = new PhalApi_Config_File(API_ROOT . '/ConfigTest');

另外，也可以通过客户端在请求时带参数来作区分，如带上&env=test或者&env=prod。

(3)持续集成下的配置管理

但个人最为建议的还是在 持续集成 下作配置管理。

因为首先，持续集成中的发布应该是经常性的，且应该是自动化的。所以，既然有自动化的支持，我们也应该及早地将配置纳入其中管理。

配置文件不同只要是环境不同，而环境不同所影响的配置文件通常只有sys.php和dbs.php；为此，我们为测试环境和生产环境准备了各自的配置文件，而在发布时自动选择所需要的配置文件。一般地，我们建议生产环境的配置文件以 .prod 结尾。所以，这时我们的配置文件可能会是这样：

dogstar@ubuntu:Config$ tree
.
├── app.php
├── dbs.php
├── dbs.php.prod
├── sys.php
└── sys.php.prod

多了生产环境的配置文件：dbs.php.prod和sys.php.prod。

再通过发布工具，我们就可以对不同环境的配置文件进行快速选择了。这里以phing为例，说明一下相关的配置和效果。

如下，在Phing的配置文件build.xml中，在生产环境发布过程中，我们将配置文件进行了替换。

//$ vim ./build.xml

        <copy
            file="./Config/dbs.php.prod"
            tofile="./Config/dbs.php"
            overwrite="true" />
        <copy
            file="./Config/sys.php.prod"
            tofile="./Config/sys.php"
            overwrite="true" />

执行phing发布后，将会看到对应的这样提示：

     [copy] Copying 1 file to /home/phapapi/Config
     [copy] Copying 1 file to /home/phapapi/Config

1.16.3 主从数据库的配置

通常一般而言，数据库支持分表分库配置，且只有一份，即：

./Config/dbs.php

然后，我们这样指定数据库的配置：

DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

如果需要从库时，要以参考./Config/dbs.php，创建一个从库的配置拷贝，即：

cp ./Config/dbs.php ./Config/dbs_slave.php

然后，注册一个从库的notorm：

DI()->notormSlave = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs_slave'), $debug); //注意：配置不同
};

最后使用此从库的服务DI()->notormSlave即可完成对从库的读取操作，用法同DI()->notorm，这里不再赘述。

1.16.4 扩展你的配置读取

虽然上面有不同的配置文件管理策略，但很多实际情况下，我们的配置需要可以随时更改、下发和调整。并且在海量访问，性能要求高的情况下快速读取配置。

这就要求我们的项目既可以方便维护即时修改，又需要能够快速同步一致更新下发和读取。这样就涉及到了配置更高层的管理： 统一集中式的配置管理，还是分布式的配置管理？ 文件存储，还是DB存储，还是MC缓存，还是进驻内存？

个人认为，性能最优的莫过于配置进驻内存并分布式存储。毕竟作为前端机的PHP内存这块还是相对充裕且可以利用的。但需要把这一条线打通，有点难度。

这里不过多地谈论配置更多的内容，但在PhalApi框架中，当你根据自己项目的需要实现了自己的配置读取方式后，再次简单的在入口文件重新注册即可。

1.16.5 使用Yaconf扩展快速读取配置

首先，非常感谢 @豹 同学的提议：使用Yaconf扩展快速读取配置。

虽然现在是冬天，有点冷（特别是在北京的同学），而且现在还是凌晨1点25分。但我们希望能继续将开源的热情继续下去。

以下是刚刚这几个小时努力的成果。

(1)前期准备：PHP7 的安装

具体可以参考：Linux环境PHP7.0安装

(2)前期准备：Yaconf扩展的安装

具体可以参考：laruence/yaconf

(3)使用

和正常的配置一样，简单如下：

DI()->config = new PhalApi_Config_Yaconf();

var_dump(DI()->config->get('foo')); //相当于var_dump(Yaconf::get("foo"));

var_dump(DI()->config->has('foo')); //相当于var_dump(Yaconf::has("foo"));

以上，假设我们已经有了这样的配置文件：

$ vim ./test.ini
name="PhalApi"
version="1.3.1"

需要注意的是，如果需要使用Yaconf扩展，需要PHP7的版本支持，但使用基本上和原来PhalApi的思路是完全一样的。
但还有所区别的是，配置文件的目录路径（当然也可以配置成和PhalApi的配置目录一致）、以及配置文件的格式。

Just have fun!

上一章 文档首页 下一章#[1.17]-日记纪录：简化版的日记接口

上一章 文档首页 下一章

PHP日记接口规范

关于日记系统，PHP规范给出了相当好的说明和定义，请见： Logger Interface

1.17.1 简化版

但在用过这么多开源框架进行各种项目开发，以及内部框架的开发后，发现实际上日记的分类并没有使用得这么丰富，而通常只是频繁地几类。因此，为了减少不必要的复杂性，这里特地精简为三种，只有：

  • error 系统异常类
  • info 业务纪录类
  • debug 开发调试类

现分说如下。

(1)error 系统异常类

此类是后台开发同学认为 不应该发生却发生的事情 ，即我们通常所说的系统异常。
如：调用app store支付的接口失败了，我们需要纪录一下当时的场景，以便复查和给用户补尝其损失。
又如：写入一条纪录到数据纪录，但失败了，我们需要纪录一下，以便排查。

用法很简单，如：

//只有描述
DI()->logger->error('fail to insert DB');

//描述 + 简单的信息
DI()->logger->error('fail to insert DB', 'try to register user dogstar');

//描述 + 当时的上下文数据
$data = array('name' => 'dogstar', 'password' => '123456');
DI()->logger->error('fail to insert DB', $data);

上面三条纪录，效果类如：

dogstar@ubuntu:dev.phalapi.com$ tailf ./Runtime/log/201502/20150207.log
2015-02-07 20:37:55|ERROR|fail to insert DB
2015-02-07 20:37:55|ERROR|fail to insert DB|try to register user dogstar
2015-02-07 20:37:55|ERROR|fail to insert DB|{"name":"dogstar","password":"123456"}

截图效果： apic

(2)info 业务纪录类

此类通常为业务需要进行必要的操作纪录，以便发生系统问题后的回滚处理、问题排查以及数据统计。
如在有缓存的情况下，可能数据没及时写入数据库而导致数据丢失或者回档，这里可以通过日记简单查看是否可以恢复。以及说明一下操作发生的背景或原由，如通常游戏中用户的经验值添加：

//假设：10 + 2 = 12
DI()->logger->info('add user exp', array('name' => 'dogstar', 'before' => 10, 'addExp' => 2, 'after' => 12, 'reason' => 'help one more phper'));

//对应的LOG
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}

但当哪天我们看到以下的LOG是就会发现系统存在隐藏的BUG：

//WTF? 10 + 2 = 11 ???
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":11,"reason":"help one more phper"}

而当用户玩家来投诉客服时，客服妹子来找到我们后台开发时，我们可以证明得了是系统原因造成了用户丢失1点经验值。
特别地，若我们看到以下的LOG时，不难看出有人在用非法的渠道刷经验：

2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}
2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":12,"addExp":2,"after":14,"reason":"help one more phper"}

....

2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":998,"addExp":2,"after":1000,"reason":"help one more phper"}

所幸我们有日记并及时发现了，随后如何处理就视具体的项目而定了。但当产品来追问时，我们可以及时给出反馈和做出处理。

还有更为重要的是 数据统计 。这块就App数据分析和统计这块已经有了很好的第三方服务支持，一如：友盟。但我们仍然可以轻松实现自己的数据统计，以便二次确认和定制化。毕竟，总是依赖第三方不是那么轻便，而且存在敏感数据安全问题。
这里简单提供一个上报接口，如：

    public function getRules() {
        return array(
            'report' => array(
                'username' => array('name' => 'username', 'require' => true),
                'msg' => array('name' => 'msg', 'require' => true),
            ),
        );
    }

    public function report() {
        DI()->logger->info($this->username, $this->msg);
    }

客户端在需要的场景，如用户打开应用，请求：

http://dev.phalapi.com/demo/?service=DogstarTest.Report&username=dogstar&msg=enter%20app

即可看到：

2015-02-07 21:01:13|INFO|dogstar|enter app

到后期，若我们需要统计用户的登录情况时，可以这样统计：

$ cat ./Runtime/log/201502/20150207.log | grep "enter app" | awk -F '|' '{print $3}' | sort | uniq -c
     11 dogstar
      5 King
      2 Tom

(3)debug 开发调试类

此类为开发调试用，用法如上，这里不再赘述。以下是一些简单的示例：

DI()->logger->debug('just for test');

DI()->logger->debug('just for test', '一些其他的描述 ...');

DI()->logger->debug('just for test', array('name' => 'dogstar', 'password' => '******'));

1.17.2 使用DI()->logger->log()进行更灵活的分类

若上面的error/info/debug都不能满足项目的需求时，可以这样更灵活地进行日记纪录：

DI()->logger->log('demo', 'add user exp', array('name' => 'dogstar', 'after' => 12));
DI()->logger->log('test', 'add user exp', array('name' => 'dogstar', 'after' => 12));

//对应的日记
2015-02-07 21:13:27|DEMO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}
2015-02-07 21:15:39|TEST|add user exp|{"name":"dogstar","after":12}

注意到，DI()->logger->log()第一个参数为日记分类的名称，在写入日记时会自动转换为大写。其接口函数签名为：

    /**
     * 日记纪录
     *
     * 可根据不同需要，将日记写入不同的媒介
     *
     * @param string $type 日记类型，如：info/debug/error, etc
     * @param string $msg 日记关键描述
     * @param string/array $data 场景上下文信息
     * @return NULL
     */
    abstract public function log($type, $msg, $data);

1.17.3 别忘记了日记的级别设置

上面的三类日记分别对应的标识为：

  • error 系统异常类：PhalApi_Logger::LOG_LEVEL_ERROR
  • info 业务纪录类：PhalApi_Logger::LOG_LEVEL_INFO
  • debug 开发调试类：PhalApi_Logger::LOG_LEVEL_DEBUG

根据不同的项目在不同环境下的需要，我们可以这样有选择性地将需要的日记分类写入保存：

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

多个日记级别需要使用或运算进行组合。

1.17.4 扩展你的日记存储

普遍情况下，我们认为将日记存放在文件是比较合理的，因为便于查看、管理和统计。当然，如果你的项目需要将日记纪录在数据中，也可以快速扩展实现。如实现数据库的存储思路：

//$ vim ./Apps/Common/Logger/DB.php

class Common_Logger_DB extends PhalApi_Logger {

    public function log($type, $msg, $data) {
        //TODO 数据库的日记写入 ...
    }

随后，利用DI注册一下即可：

//日记纪录 - DB
DI()->logger = new Common_Logger_DB($dbConfig,
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

上一章 文档首页 下一章#[1.18]-快速函数：人性化的关怀

上一章 文档首页 下一章

1.18.1 提供快速函数的缘由

很多人，很多框架都在说遵循的是OOP，实际上很多都是面向过程的开发，或者是很呆板的面向对象。

而且你也会发现，面向对象和面向过程在某方面是相互补充的。而且，面向对象里面很痛苦的改动在面向过程中可以很容易实现，反之亦然。如面向对象中添加一个类成员变量（新数据）很简单，但在抽象父类中添加一个抽象方法（新方法）却很痛苦；而在面向过程中添加一个全局变量（新数据）很痛苦，但要添加一个新方法却很简单。

所以，没有绝对的好与不好，关键在于如何设计和如何使用。而PhalApi则遵循敏捷开发的思想，为了更多的接口开发人员减少代码编写和记忆的痛苦、体验开发的乐趣，我们思虑衡量再三，提供了以下的快速函数。

1.18.2 目前已有的快速函数

(1)获取全局DI实例的函数：DI()

为了定位获得全部服务资源，我们需要一个统一的入口，在这里即为PhalApi_DI的全局实例。
我们可以这样原始获取：

$di = PhalApi_DI::one();

也可以这样快速获取：

$di = DI();

也就是说：

function DI() {
    return PhalApi_DI::one();
}

(2)国际化翻译函数：T()

在真正需要进行翻译的项目中，翻译函数的调用显然会到处可见。为了方便编写代码，这里取Translator的首写字母T作为快速函数的名字，在需要进行翻译时只需要调用T()，即可：

$msg = T('hello world');

$msg = T('hello {name}', array('name' => 'dogstar'));

对应地：

function T($msg, $params = array()) {
    return PhalApi_Translator::get($msg, $params);
}

上一章 文档首页 下一章#[1.19]-DI服务速查：各资源服务一览表

上一章 文档首页 下一章

“完成，好过完美。” -- 脸谱书文化之一

1.19.1 一览表

变量名称 | 是否启动时自动注册 | 是否必须 | 接口/类 | 作用说明
loader | 否 | 是 | PhalApi_Loader | 自动加载：负责PEAR风格下类的自动加载，需要手动注册，指定项目路径
config | 否 | 是 | PhalApi_Config | 配置：负责项目配置的读取，需要手动注册，指定存储媒介，默认是PhalApi_Config_File
logger | 否 | 是 | PhalApi_Logger | 日记纪录：负责日记的写入，需要手动注册，指定日记级别和存储媒介，默认是PhalApi_Logger_File
request | 是 | 是 | PhalApi_Request | 接口参数请求：用于收集接口请求的参数
response | 是 | 是 | PhalApi_Response | 结果响应：用于输出返回给客户端的结果，默认为PhalApi_Response_Json
notorm | 否 | 推荐 | PhalApi_DB_NotORM | 数据操作：基于NotORM的DB操作，需要手动注册，指定数据库配置
cache | 否 | 推荐 | PhalApi_Cache | 缓存：实现缓存读写，需要手动注册，指定缓存
filter | 否 | 推荐 | PhalApi_Filter | 拦截器：实现签名验证、权限控制等操作
crypt | 否 | 否 | PhalApi_Crypt | 对称加密：实现对称加密和解密，需要手动注册
curl | 否 | 否 | PhalApi_CUrl | CURL请求类：通过curl实现的快捷方便的接口请求类，需要手动注册
cookie | 否 | 否 | PhalApi_Cookie | COOKIE的操作
debug | 否 | 否 | boolean | 应用级的调试开关，通常可从配置读取，暂时框架未用到
_formatterArray | 否 | 否 | PhalApi_Request_Formatter_Array | 数组格式化服务（系统内部使用）
_formatterBoolean | 否 | 否 | PhalApi_Request_Formatter_Boolean | 布尔值格式化服务（系统内部使用）
_formatterCallable | 否 | 否 | PhalApi_Request_Formatter_Callable | 回调格式化服务（系统内部使用）
_formatterDate | 否 | 否 | PhalApi_Request_Formatter_Date | 日期格式化服务（系统内部使用）
_formatterEnum | 否 | 否 | PhalApi_Request_Formatter_Enum | 枚举格式化服务（系统内部使用）
_formatterFile | 否 | 否 | PhalApi_Request_Formatter_File | 上传文件格式化服务（系统内部使用）
_formatterFloat | 否 | 否 | PhalApi_Request_Formatter_Float | 浮点数格式化服务（系统内部使用）
_formatterInt | 否 | 否 | PhalApi_Request_Formatter_Int | 整数格式化服务（系统内部使用）
_formatterString | 否 | 否 | PhalApi_Request_Formatter_String | 字符串格式化服务（系统内部使用）

1.19.2 DI服务是否已注册的判断误区

(1)错误的判断方法

当需要判断一个DI服务是否已被注册，出于常识会这样判断：

if (isset(DI()->cache)) {

但这样的判断永远为false，不管注册与否。

追其原因在于，DI类使用了魔法方法的方式来提供类成员属性，并存放于PhalApi_DI::$data中。

这就导致了如果直接使用isset(DI()->cache)的话，首先不会触发魔法方法 PhalApi_DI::__get($name)的调用，其次也确实没有PhalApi_DI::$cache这个成员属性，最终判断是否存在时都为false。

简单来说，以下两种判断，永远都为false：

//永远为false
var_dump(isset(DI()->XXX));
var_dump(!empty(DI()->XXX));

(2)正确判断的写法：先获取，再判断

正确的用法应该是：

//先获取，再判断
$XXX = DI()->XXX;
var_dump(isset($XXX));
var_dump(!empty($XXX));

这里，可能会对大家带来一定的困惑，所以在这里特别进行说明。也对框架中这一点不合理的设计深感抱歉。

上一章 文档首页 下一章

[1.20]-DB操作：数据库基本操作速查

上一章 文档首页 下一章

一花一世界，
一沙一天国，
君掌盛无边，
刹那含永劫。

-- 《天真的预兆》

1.20.1 前提

为了让大家更为明确数据库NotORM的使用，我们假设有以下数据库表：

-- ----------------------------
-- Table structure for `tbl_user`
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user`;
CREATE TABLE `tbl_user` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  `age` int(3) DEFAULT NULL,
  `note` varchar(45) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tbl_user
-- ----------------------------
INSERT INTO `tbl_user` VALUES ('1', 'dogstar', '18', 'oschina', '2015-12-01 09:42:31');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', '21', 'USA', '2015-12-08 09:42:38');
INSERT INTO `tbl_user` VALUES ('3', 'King', '100', 'game', '2015-12-23 09:42:42');

并且，假设我们已获得了tbl_user表对应的notorm实例$user，此NotORM表实例可从两种方式获得：

  • 1、使用原生态的notorm，即：$user = DI()->notorm->user
  • 2、使用PhalApi_Model_NotORM基类的方式，即：$user = $this->getORM()，如：
<?php
class Model_User extends PhalApi_Model_NotORM {
    protected function getTableName($id) {
        return 'user';
    }

    public function doSth() {
        $user = $this->getORM(); //获取NotORM表实例
    }
}

写在前面的话

NotORM的实例是有内部状态的，因为在开发过程中，需要特别注意何时需要保留状态（使用同一个实例）、何时不需要保留状态。即：

保留状态的写法：

$user = $notorm->user;  //获取一个新的实例
$user->where('age > ?', 18);
$user->where('name LIKE ?', '%dog%');  //相当于age > 18 AND name LIKE '%dog%'

不保留状态的写法：

$user = $notorm->user;  //获取一个新的实例
$user->where('age > ?', 18);

$user = $notorm->user;  //重新获取新的实例
$user->where('name LIKE ?', '%dog%');  //此时只有 name LIKE '%dog%'

关于这两者的使用场景，下面在进行说明时会特别提及。
下面，就让我们结合实例来尝试一下数据库的操作吧！

调试模式

(1)打印SQL语句

在请求接口时，当需要查看当前接口所执行的SQL语句时，可以添加参数&__sql__=1。如请求：

http://demo.phalapi.net/?service=User.getBaseInfo&user_id=1&__sql__=1

会返回类似这样：

[1 - 0.00035s]SELECT * FROM tbl_user WHERE (id = ?); -- 1<br>
{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}

(2)显示数据库连接错误的详细信息

普通情况下，数据库连接失败时会这样提示：

{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: 数据库db_demo连接失败"
}

考虑到生产环境不方便爆露服务器的相关信息，故这样简化提示。当在开发过程中，需要定位数据库连接失败的原因时，可使用debug调试模式。
开启调试模式的方法有两种：

  • 1、在请求接口时添加 &__debug__=1，开启本次调试
  • 2、将配置文件 ./Config/sys.php中的debug设置为true，开启全局调试

开启后，当再次失败时，会看到类似这样的提示：

{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: 数据库db_demo连接失败，异常码：1045，错误原因：SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost' (using password: NO)"
}

然后，便可根据具体的错误提示进行排查解决。

默认表名与指定表名

请注意：PhalApi v1.3.4 版本以上有效。

当继承PhalApi_Model_NotORM时，如编写Model_User类：

<?php
class Model_User extends PhalApi_Model_NotORM {
}

则默认的表名为：user。默认表名的自动匹配规则是：取Model_后面部分的字符全部转小写。转化后框架会另外加上表前缀。

在以下场景下，需要重写此方法以指定表名：

  • 自动匹配的表名与实际表名不符
  • 存在分表
  • Model类名不含有Model_

如，当Model_User类对应的表名为：my_user表时，可使用getTableName($id)重新指定表名：

<?php
class Model_User extends PhalApi_Model_NotORM {
    protected function getTableName($id) {
        return 'my_user';
    }
}

1.20.2 基本操作

(1)SELECT

单个字段：

// SELECT id FROM `tbl_user`
$user->select('id')

多个字段获取：

// SELECT id, name, age FROM `tbl_user`
$user->select('id, name, age')

字段别名获取：

// SELECT id, name, MAX(age) AS max_age FROM `tbl_user`
$user->select('id, name, MAX(age) AS max_age')

全部字段（缺省）获取：

// SELECT * FROM `tbl_user`
$user->select('*')

(2)WHERE

单个字段查询：

// WHERE id = 1
$user->where('id', 1)
$user->where('id = ?', 1)
$user->where(array('id', 1))

多个字段查询：

// WHERE id > 1 AND age > 18
$user->where('id > ?', 1)->where('age > ?', 18)
$user->and('id > ?', 1)->and('age > ?', 18)
$user->where('id > ? AND age > ?', 1, 18)
$user->where(array('id > ?' => 1, 'age > ?' => 10))

// WHERE name = 'dogstar' AND age = 18
$user->where(array('name' => 'dogstar', 'age' => 18))

// WHERE name = 'dogstar' OR age = 18
$user->or('name', 'dogstar')->or('age', 18)

// WHERE ((name = ? OR id = ?)) AND (note = ?) -- 'dogstar', '1', 'xxx'
#实现方式1：使用and拼接
$user->where('(name = ? OR id = ?)', 'dogstar', '1')->and('note = ?', 'xxx')
#实现方式2：只使用where，不用数组传参
$user->where('(name = ? OR id = ?) AND note = ?', 'dogstar', '1', 'xxx')
#实现方式3：只使用where，用下标为顺序数字的数组传参
$user->where('(name = ? OR id = ?) AND note = ?', array('dogstar', '1', 'xxx'))
#实现方式4：只使用where，用下标为标识符的数组传参
$user->where('(name = :name OR id = :id) AND note = :note', array(':name' => 'dogstar', ':id' => '1', ':note' => 'xxx'))

IN查询：

// WHERE id IN (1, 2, 3)
$user->where('id', array(1, 2, 3))

// WHERE id NOT IN (1, 2, 3)
$user->where('NOT id', array(1, 2, 3))

// WHERE (id, age) IN ((1, 18), (2, 20))
$user->where('(id, age)', array(array(1, 18), array(2, 20)))

模糊匹配查询：

// WHERE name LIKE '%dog%'
$user->where('name LIKE ?', '%dog%')

NULL判断查询：

// WHERE (name IS NULL)
$user->where('name', null)

对于非空的判断，则可以：

// WHERE (name IS NOT ?) LIMIT 1; -- NULL
$user->where('name IS NOT ?', null)

(3)ORDER BY

单个字段排序：

// ORDER BY age
$user->order('age')

// ORDER BY age DESC
$user->order('age DESC')

多个字段排序：

// ORDER BY id, age DESC
$user->order('id')->order('age DESC')
$user->order('id, age DESC')

(4)LIMIT

按数量限制：

// LIMIT 10
$user->limit(10)

按数量和偏移量限制（请注意：先偏移量、再数量）：

// LIMIT 2,10
$user->limit(2, 10)

(5)GROUP BY和HAVING

不带HAVING：

// GROUP BY note
$user->group('note')

带HAVING：

// GROUP BY note HAVING age > 10
$user->group('note', 'age > 10')

1.20.3 CURD之查询类（Retrieve）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
fetch() | 循环获取每一行 | while($row = $user->fetch()) { //... } | 否
fetchOne() | 只获取第一行 | $row = $user->where('id', 1)->fetchOne(); | 等效于fetchRow() | 是
fetchRow() | 只获取第一行 | $row = $user->where('id', 1)->fetchRow(); | 等效于fetchOne() | 是
fetchPairs() | 获取键值对 | $row = $user->fetchPairs('id', 'name'); | 第二个参数为空时，可取多个值，并且多条纪录 | 否
fetchAll() | 获取全部的行 | $rows = $user->where('id', array(1, 2, 3))->fetchAll(); | 等效于fetchRows() | 是
fetchRows() | 获取全部的行 | $rows = $user->where('id', array(1, 2, 3))->fetchRows(); | 等效于fetchAll() | 是
queryAll() | 复杂查询下获取全部的行，默认下以主键为下标 | $rows = $user->queryAll($sql, $parmas); | 等效于queryRows() | 是
queryRows() | 复杂查询下获取全部的行，默认下以主键为下标 | $rows = $user->queryRows($sql, $parmas); | 等效于queryAll() | 是
count() | 查询总数 | $total = $user->count('id'); | 第一参数可省略 | 否
min() | 取最小值 | $minId = $user->min('id'); | 否
max() | 取最大值 | $maxId = $user->max('id'); | 否
sum() | 计算总和 | $sum = $user->sum('age'); | 否

循环获取每一行（多个字段）：

// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $notorm->user->select('id, name')->where('age > 18');
while($row = $user->fetch()) {
     var_dump($row);
}

// 输出
array(2) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(3) "Tom"
}
array(2) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(4) "King"
}

循环获取每一行（单个字段）：

// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $notorm->user->select('id, name')->where('age > 18');
while($row = $user->fetch('name')) { // 指定获取name这列，但此字段须在select里
     var_dump($row);
}

// 输出
string(3) "Tom"
string(4) "King"

循环获取每一行（错误的用法，注意！会死循环）：

while($row = $notorm->user->select('id, name')->where('age > 18')->fetch('name')) {
     var_dump($row);
}

只获取第一行（多个字段）：

// SELECT id, name FROM tbl_user WHERE (age > 18) LIMIT 1;
$rs = $user->select('id, name')->where('age > 18')->fetchOne();  //等同fetchRow()
var_dump($rs);

// 输出
array(2) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(3) "Tom"
}

只获取第一行（单个字段）：

var_dump($user->fetchOne('name'));  // 输出 string(3) "Tom"
var_dump($user->fetchRow('name'));  // 等同输出 string(3) "Tom"

获取键值对（多个字段）：

// SELECT id, name, age FROM tbl_user LIMIT 2;
$rs = $user->select('name, age')->limit(2)->fetchPairs('id'); //指定以ID为KEY
var_dump($rs);

// 输出
array(2) {
  [1]=>
  array(3) {
    ["id"]=>
    string(1) "1"
    ["name"]=>
    string(7) "dogstar"
    ["age"]=>
    string(2) "18"
  }
  [2]=>
  array(3) {
    ["id"]=>
    string(1) "2"
    ["name"]=>
    string(3) "Tom"
    ["age"]=>
    string(2) "21"
  }
}

获取键值对（单个字段）：

// SELECT id, name FROM tbl_user LIMIT 2
var_dump($user->limit(2)->fetchPairs('id', 'name')); //通过第二个参数，指定VALUE的列

// 输出
array(2) {
  [1]=>
  string(7) "dogstar"
  [2]=>
  string(3) "Tom"
}

获取全部的行：

// SELECT * FROM tbl_user
var_dump($user->fetchAll());  //全部表数据输出，输出结果略，相当于$user->fetchRows()

复杂查询下获取全部的行（1.3.1及以前版本默认下以主键为下标，1.3.1以后的版本则默认采用数组方式）：

// SELECT name FROM tbl_user WHERE age > :age LIMIT 1
$sql = 'SELECT name FROM tbl_user WHERE age > :age LIMIT 1';
$params = array(':age' => 18);
$rs = $user->queryAll($sql, $params);
var_dump($rs);

// 输出
array(1) {
  [0]=>
  array(1) {
    ["name"]=>
    string(3) "Tom"
  }
}

// 也可以这样。。。
$sql = 'SELECT name FROM tbl_user WHERE age > ? LIMIT 1';  //使用问号表示变量
$params = array(18);
$rs = $user->queryRows($sql, $params); //使用queryRows()别名
var_dump($rs);

请注意：使用上面这种方式进行查询，需要手动填写完整的表名字，并且需要通过某个表的实例来运行。

取最小值：

// SELECT MIN(age) FROM tbl_user
var_dump($user->min('age'));

// 输出
string(2) "18"

取最大值：

// SELECT MAX(age) FROM tbl_user
var_dump($user->max('age'));

// 输出
string(3) "100"

计算总和：

// SELECT SUM(age) FROM tbl_user
var_dump($user->sum('age'));

// 输出
string(3) "139"

1.20.4 CURD之插入类（Create）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
insert() | 插入数据 | $user->insert($data); | 原生态操作需要再调用insert_id()获取插入的ID | 否
insert_multi() | 批量插入 | $user->insert_multi($rows); | 可批量插入 | 否
insert_update() | 插入/更新 | 接口签名：insert_update(array $unique, array $insert, array $update = array() | 不存时插入，存在时更新 | 否

插入数据

// INSERT INTO tbl_user (id, name, age, note) VALUES (4, 'PhalApi', 1, 'framework')
$data = array('id' => 4, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$user->insert($data);
$id = $user->insert_id(); //必须是同一个实例，方能获取到新插入的行ID，且表必须设置了自增
var_dump($id); //新增的ID

//使用Model的写法
$model = new Model_User();
var_dump($model->insert($data)); //输出新增的ID

批量插入：

// INSERT INTO tbl_user (name, age, note) VALUES ('A君', 12, 'AA'), ('B君', 14, 'BB'), ('C君', 16, 'CC')
$rows = array(
    array('name' => 'A君', 'age' => 12, 'note' => 'AA'),
    array('name' => 'B君', 'age' => 14, 'note' => 'BB'),
    array('name' => 'C君', 'age' => 16, 'note' => 'CC'),
);
$rs = $user->insert_multi($rows);
var_dump($rs);

// 输出
int(3) //成功插入的条数

插入/更新：

// INSERT INTO tbl_user (id, name, age, note) VALUES (8, 'PhalApi', 1, 'framework') ON DUPLICATE KEY UPDATE age = 2
$unique = array('id' => 8);
$insert = array('id' => 8, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$update = array('age' => 2);
$rs = $user->insert_update($unique, $insert, $update);
var_dump($rs); //输出影响的行数

1.20.5 CURD之更新类（Update）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
update() | 更新数据 | $user->where('id', 1)->update($data); | 更新异常时返回fals，数据无变化时返回0，成功更新返回1 | 否

更新数据：

// UPDATE tbl_user SET age = 2 WHERE (name = 'PhalApi');
$data = array('age' => 2);
$rs = $user->where('name', 'PhalApi')->update($data);
var_dump($rs);

// 输出
int(1) //正常影响的行数
int(0) //无更新，或者数据没变化
boolean(false) //更新异常、失败

更新数据（+1）：

// UPDATE tbl_user SET age = age + 1 WHERE (name = 'PhalApi')
$rs = $user->where('name', 'PhalApi')->update(array('age' => new NotORM_Literal("age + 1")));
var_dump($rs); //输出影响的行数

1.20.6 CURD之删除类（Delete）

操作 | 说明 | 示例 | 备注 | 是否PhalApi新增
delete() | 删除 | $user->where('id', 1)->delete(); | 禁止无where条件的删除操作 | 否

按条件删除，返回影响的行数：

// DELETE FROM tbl_user WHERE (id = 404);
$user->where('id', 404)->delete();

注意： 如果是全表删除，框架将会禁止，并抛出异常。如：

// Exception: sorry, you can not delete the whole table
$user->delete();

参考

更多请参考 NotORM官网接口说明

上一章 文档首页 下一章

[1.21]-类的自动加载：遵循PEAR包的命名规范

上一章 文档首页 下一章

1.21.1 PhalApi自动加载的机制

首先，PhalApi的自动加载是简单的；其次，PhalApi不强制只使用我们框架的机制。

有些框架，单单在类文件的自动加载这一块就弄得非常复杂，以致开发同步需要在这些框架添加一些自己的类代码时，往往不知要怎么引入！甚至明明用引入了却又不见得生效！

而在PhalApi，我们则是： 简单、统一、规范。

1.21.2 PEAR包命名规范

PEAR包的类文件路径和类名映射非常简单，如下图：

0215
或者查看（里面有很好的代码风格编写说明）： Autoloading Standard

而PhalApi则更简单，我们暂时还不使用命名空间，所以namespace这一块省去。例如：

Api_User
Domain_User
Model_User

对应以下路径的文件：

.
|-- Api
|   `-- User.php
|-- Domain
|   `-- User.php
|-- Model
|   `-- User.php

再举一个模拟的示例，如类Api_Game_User_Equitment对应以下文件路径：

./Api/Game/User/Equitment.php

此外，还需注意区分大小写。

1.21.3 挂靠式自动加载

然后在我们准备了这些类和文件后，我们怎么才能让这些类被框架所自动加载呢？

我们提供的方式是： 挂靠式自动加载 。

熟悉Linux系统的同学可能很容易明白，还没接触到Linux的同学也是可以很快理解的。这里稍微说明一下。

所谓的 挂靠 就是把你代码的目录放在项目的目录下（或者其他任何一个地方），随后简单地将此目录添加到自动加载器即可。如上面的：

DI()->loader->addDirs('Demo');

当有多个目录时，可以这样：

DI()->loader->addDirs(array('Demo', 'Demo1', 'Demo2'));

需要注意的是，上面相对路径的都需要放置在应用项目的目录API_ROOT下面。如果是单个文件的引入，则可以使用绝对路径。下面分别简单说明。

(1)相对路径的目录路径挂靠

通过PhalApi_Loader::addDirs()方式挂靠的路径，都是强制在目录API_ROOT下面，即下面的三种方式是等效的（在Linux系统上）：

//路径：API_ROOT/Demo
DI()->loader->addDirs('Demo');

//路径：API_ROOT/./Demo
DI()->loader->addDirs('./Demo');

//路径：API_ROOT/Demo
DI()->loader->addDirs('/Demo');

如果需要挂靠的目录不在项目目录下，在Linux可以通过软链来解决。

(2)单个文件允许绝对路径引入

对于单个文件的引入，可以通过PhalApi_Loader::loadFile()来引入。注意以下两种写法的区别：

//路径：API_ROOT/Demo/Tool.php
DI()->loader->loadFile('Demo/Tool.php');

//路径：/path/to/Demo/Tool.php
DI()->loader->loadFile('/path/to/Demo/Tool.php');

上一章 文档首页 下一章#[1.22]-签名验证：自定义签名规则

上一章 文档首页 下一章

1.22.1 过滤器服务

正如我们核心思想DI里面所说的，我们把后台很多功能资源都称为服务，所以在PhalApi框架中我们已经系统规定了DI()->filter为过滤器服务，以实现接口请求时的一些拦截操作，一如现在要说明的签名验证。

在接口进行初始化时，会自动调用已注册的过滤器服务 DI()->filter ，关键的代码如下：

//$vim ./PhalApi/PhalApi/Api.php
    public function init()
    {
        $this->createMemberValue();

        $this->filterCheck();

        $this->checkStatus();
    }

    protected function filterCheck()
    {
        $filter = DI()->filter;

        if (isset($filter)) {
            $filter->check();
        }
    }

默认直接可用的接口验证

基于很多同学对接口签名验证比较陌生，从框架推出签名验证以来，很长一段时间内，很多同学对于如何实现一个接口签名依然不知如何下手。
为了给大家更好的便利性，我们提供了一个基本版的接口验证服务。

主要是基于md5进行的签名生成，这个只能作为一般性的参考。大家可以在此基础上进行调整延伸。

默认情况下，可以去掉注释开启使用PhalApi_Filter_SimpleMD5进行接口验证，即：

//签名验证服务
DI()->filter = 'PhalApi_Filter_SimpleMD5';

其验签的算法如下（如注释所示）：

1、排除签名参数（默认是sign）
2、将剩下的全部参数，按参数名字进行字典排序
3、将排序好的参数，全部用字符串拼接起来
4、进行md5运算

以下面的示例参数为例，即：

1、排除签名参数（默认是sign）
?service=Default.Index&username=dogstar

2、将剩下的全部参数，按参数名字进行字典排序
service=Default.Index
username=dogstar

3、将排序好的参数，全部用字符串拼接起来
"Default.Indexdogstar" = "Default.Index" + "dogstar"

4、进行md5运算
sign = 35321cc43cfc1e4008bf6f1bf9b7e3b8 = md5("Default.Indexdogstar")

5、请求时，加上签名参数
?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8

下面是两个调用示例，错误请求下（即签名失败）：

http://localhost/phalapi/public/demo/?service=Default.Index&username=dogstar

返回：
{
    "ret": 406,
    "data": [],
    "msg": "非法请求：签名错误"
}

温馨提示：
签名错误情况下，可以查看日记获得正确的sign，如：

2015-10-23 23:16:16|DEBUG|Wrong Sign|{"needSign":"35321cc43cfc1e4008bf6f1bf9b7e3b8"}

正常请求下（带sign签名）：

http://localhost/phalapi/public/demo/?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8

如果不想使用sign作为关键的签名参数，可以在注册时指定，如使用缩写s：

DI()->filter = new PhalApi_Filter_SimpleMD5('s');

1.22.2 微信签名示例

所以，如果我们需要实现签名验证，只需要简单的两步即可：

  • 1、实现过滤器接口 PhalApi_Filter::check()；
  • 2、注册过滤器服务 DI()->filter；

下面以大家熟悉的 微信验签 为例，进行示例说明。

(1)实现过滤器接口 PhalApi_Filter::check()

通常我们约定返回ret = 402表示验证失败，所以当签名失败时，我们可以返回ret = 402以告知客户端签名不对。根据微信的检验signature的PHP示例代码，我们可以快速实现自定义签名规则，如：

//$ vim ./Demo/Common/SignFilter.php
<?php

class Common_SignFilter implements PhalApi_Filter
{
    public function check()
    {
        $signature = DI()->request->get('signature');
        $timestamp = DI()->request->get('timestamp');
        $nonce = DI()->request->get('nonce');

        $token = 'Your Token Here ...';
        $tmpArr = array($token, $timestamp, $nonce);
        sort($tmpArr, SORT_STRING);
        $tmpStr = implode( $tmpArr );
        $tmpStr = sha1( $tmpStr );

        if ($tmpStr != $signature) {
            throw new PhalApi_Exception_BadRequest('wrong sign', 1);
        }
    }
}

(2)注册过滤器服务 DI()->filter

随后，我们只需要再简单地注册一下过滤器服务即可，在init.php初始化文件最后追加：

//$ vim ./Public/init.php
//签名验证服务
DI()->filter = 'Common_SignFilter';

(3)运行效果

当我们再次请求接口时，如默认的服务：/demo/?service=Default.Index，即会出现以下的错误： apic

即：

{
    "ret": 401,
    "data": [
    ],
    "msg": "非法请求：wrong sign"
}

如果符合接口签名的验证，则会正常返回我们熟悉的内容，如：

/demo/?service=Default.Index&signature=b75e0a1b574d4e111a1d6ed3c9cfbe2ccdc09404&timestamp=123&nonce=123

会返回：

{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1423055188
    },
    "msg": ""
}

1.22.3 特殊的场景

(1)个别接口不需要验签？

在注册好统一的接口验签的过滤器拦截服务后，是会存在这样一种情况：即个别的接口不需要签名。

而这种情况，我们也是有考虑到的。所以在提供了公共的功能的情况下，我们是可以快速灵活地进行定制化和扩展。

当我们个别的接口不需要签名验证时，只需要简单地在接口子类里面重定义过滤器的检测即可，如在我们熟悉的默认服务器取消签名验证：

//vim ./Demo/Api/Default.php
class Api_Default extends PhalApi_Api
{
    //....

    protected function filterCheck()
    {
    }
}

1.22.4 更好地建议

通常关于接口签名这块，我们还需要：

  • 1、为不同的接入方定义不同的密钥和私钥；

  • 2、如果业务需要，为各个接口、各个接入方分配调用权限；

  • 3、统一签名参数的规则，可以配置在./Config/app.php中的，如上面的签名需要的参数，我们可以追加统一的参数规则：
    /**
 * 应用接口层的统一参数
 */
'apiCommonRules' => array(
    'signature' => array('name' => 'signature', 'require' => true),
    'timestamp' => array('name' => 'timestamp', 'require' => true),
    'nonce' => array('name' => 'nonce', 'require' => true),
),

上一章 文档首页 下一章#[1.23]-请求和响应：GET和POST两者皆可得及超越JSON格式返回

上一章 文档首页 下一章

1.23.1 请求

把我们后台的API想象成一个函数，那么请求的参数就是我们的参数列表；而接口响应的数据则对应函数返回的结果。

对于请求，正如前面所看到的，我们可以使用$_GET，也可以使用$_POST，也可以两者都使用，还可以在测试时自己指定模拟的请求数据包。

或者，在实际项目开发中，我们还需要根据自身的需求，跟我们自己的客户端作一些约定。如通常地，我们会要求客户端 service参数必须用GET方式 ，以便服务器返回500时定位接口服务位置。对此，简单的我们把$_POST['service']去掉即可，如在入口文件前面添加：

unset($_POST['service']);   //只接收GET方式的service参数

(1)在index.php入口处指定数据源

很多时间，不同的项目对数据接收有不同的需求。如简单地，如果客户端是通过JSON后再传递的话，我们可以这样在入口处进行代码调整：

//vim ./index.php

$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : "{}";
DI()->request = new PhalApi_Request(array_merge($_GET,json_decode($HTTP_RAW_POST_DATA, true)));

(2)单元测试时指定数据源

在进行单元测试时，我们需要模拟接口的请求动作，也需要提供接口参数。这时的参数的指定更为灵活。可通过以下代码来实现，即：

//数据源
$data = array(...);

DI()->request = new PhalApi_Request($data);

或者使用PhalApi封装的测试类来快速模拟调用接口：

    public function testIndexByRunner()
    {
        //Step 1. 构建请求URL
        $url = 'service=Default.Index&username=dogstar';
        $params = array();

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url, $params);
    }

(3)接口数据的加密传送

有时，出于安全性的考虑，项目需要对请求的接口参数进行对称加密传送。这时可以通过重载PhalApi_Request::genData()来轻松实现。
假设，我们现在需要把全部的参数base64编码序列化后通过$_POST['data']来传递，则相应的解析代码如下。

第一步，先定义自己的扩展请求类，在里面完成对称解析的动作：

<?php

class Common_Request extends PhalApi_Request {

    public function genData($data) {
        if (!isset($data) || !is_array($data)) {
            $data = $_POST; //改成只接收POST
        }

        return isset($data['data']) ? base64_decode($data['data']) : array();
    }
}

第二步，在index.php入口文件中重新注册请求类（即添加以下代码）：

//重新注册request
DI()->request = 'Common_Request';

然后，就可以轻松实现了接口参数的对称加密传送。
至此，你也许已经发现：指定数据源和对称加密是可以结合来使用的。

1.23.2 响应

当前默认使用JSON的格式返回，但项目需要其他返回格式也是可以的。只需要实现PhalApi_Response抽象中的formatResult($result)格式化返回结果，然后也是重新注册DI()->response服务即可。如：

(1)JSONP的返回

在H5页面中，我们可能会需要用到JSONP的返回，所以这里默认提供了这种格式的支持。

在入口文件中，添加：

//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

在接口访问时再带上&callback=XXX参数即可。

这里在创建响应服务时，可以看到是用了回调函数的名字进行初始化。考虑到会存在XSS（跨站脚本攻击），对回调函数要进行相应的过滤，可以用黑名单或者白名单的方式。黑名单方式暂时还没提供，白名单相对简单但需要项目自己实现，如：

class Common_JsonP extends PhalApi_Response_JsonP {

    protected function clearXss($callback) {
        return in_array($callback, array('fun1', 'func2', 'func3')) ? $callback : '';
    }
}

(2)其他格式返回

如上面所述，当需要返回一种当前PhalApi没提供的格式时，可以：

  • 1、实现PhalApi_Response抽象中的formatResult($result)格式化返回结果
  • 2、重新注册DI()->response服务

这里以扩展XML返回格式为例，简单说明。

首先，添加实现一个XML响应子类：

<?php

class Common_Response_XML extends PhalApi_Response {

    protected function formatResult($result) {
        return ArrayToXML::toXml($result);
    }
}

关于ArrayToXML，请查看： 将PHP数组转成XML

然后，重新注册：

DI()->response = new Common_Response_XML();

done!

上一章 文档首页 下一章#[1.24]-缓存策略：更灵活地可配置化的多级缓存

上一章 文档首页 下一章

1.24.1 论缓存的重要性

在很多情况下，我们都需要缓存。如对重复获取但变化不大的数据进行缓存以提供服务器的响应能力，又如当需要为数据库服务器减少QPS时等等。但同时缓存也因为有延时导致数据不能实时更新，或者在需要更新时不能被更新。如在接口调试、单元测试或者预览时，但这些都可以通过一些技巧来获得。这里，将从简单的缓存、再到高速缓存、多级缓存逐步进行说明。

1.24.2 简单缓存

这里所指的简单缓存，是指文件缓存。因为实现简单，且部署方便。但其缺点也是明显的，如文件I/O读写导致性能低，不能支持分布式。所以在没有集群服务器下是适用的。

当需要使用文件缓存时，先对文件缓存进行DI注册，便可使用。

//$ vim ./Public/init.php
DI()->cache = new PhalApi_Cache_File(array('path' => API_ROOT . '/Runtime', 'prefix' => 'demo'));

// 设置
DI()->cache->set('thisYear', 2015, 600);

// 获取
echo DI()->cache->get('thisYear');

// 删除
DI()->cache->delete('thisYear');

1.24.3 高速缓存

高速缓存通常具备分布式的能力，并能进驻内存，因此性能高。但同样此优点所带来的则是需要另外部署缓存扩展，如常见的Memcached、Redis。若需要考虑缓存落地，还要进一点筛选和配置。

如使用Memcached：

DI()->cache = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211, 'prefix' => 'demo_'));

// 设置
DI()->cache->set('thisYear', 2015, 600);

// 获取
echo DI()->cache->get('thisYear');

// 删除
DI()->cache->delete('thisYear');

从上面示例代码注意到，我们使用了前缀配置，以防同一台MC服务器同一端口下key名冲突。当然，更好是使用配置文件来配置缓存的地址和端口，以及前缀。

1.24.4 多级缓存

然而，很多时候，我们需要多级缓存来承载更大的访问量。以便结合本地低速缓存和分布式高速缓存来获得更好的用户体验和服务器吞吐率。这时，则可以使用多级缓存策略。

在切换到多级缓存时，我们明显希望原有的代码调用不需要同步调整便能获得升级后的多级缓存，并且我们后台开发人员也希望可以轻便在原有的单点缓存的基础上进行扩展。所以这就引出了一个有趣的问题：我们该如何组织多级缓存？

作为一个框架，除了考虑上述的原有调用、单点缓存复用外，还需要考虑到多级缓存的组装。部分框架，一如我最喜欢的Phalcon则是使用了配置的形式来实现。但仍然，我们需要了解其配置的各个格式要求才能更好掌握和使用，这点带来了学习的成本。因此，PhalApi则是使用了简单的组合模式来实现。

正如你在源代码中看到的PhalApi_Cache_Multi类，通过此类的实例可以利用PhalApi_Cache_Multi::addCache()快速添加一个缓存节点，而节点的优先级则按开发同学添加的顺序来确定。显然在添加时，我们应该先添加分布式的高速缓存，再添加本地的低速缓存（希望不会有人先添加文件缓存再添加MC缓存）。而各个节点的初始化，则是我们之前所熟悉的。只是简单添加即可。

以下是结合文件缓存和MC缓存的多级缓存示例：

$cache = PhalApi_Cache_Multi();

$mcCache = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211, 'prefix' => 'demo_'));
$cache->addCache($mcCache);

$fileCache = new PhalApi_Cache_File(array('path' => API_ROOT . '/Runtime', 'prefix' => 'demo'));
$cache->addCache($fileCache);

DI()->cache = $cache;

然后，就可像之前那样设置、获取和删除缓存，而不需考虑是单点缓存，还是多级缓存：

// 设置
DI()->cache->set('thisYear', 2015, 600);

// 获取
echo DI()->cache->get('thisYear');

// 删除
DI()->cache->delete('thisYear');

1.24.5 静态结构图

对应地，我们可以得出清晰明了的UML静态结构图：

结构层次非常简单，但主要分为三大类：左边是多级缓存；中间突出的是特殊情况，即：空对象模式下的空缓存；右边是目前已提供或者后期扩展的具体缓存实现。

上一章 文档首页 下一章#[1.25]-国际化翻译：为走向国际化提前做好翻译准备

上一章 文档首页 下一章

Your new understanding has made you powerful. Please use your new powers for good.

1.25.1 固定的中文写法

一直以来，我们都是硬编码方式返回中文的文案或者提示，如：

$rs['msg'] = '用户不存在';

这种写法在项目根本不需要考虑国际化翻译时，是完全没问题的。

1.25.2 通用的翻译写法

当我们需要进行翻译时，可以这样进行调整：

$rs['msg'] = T('user not exists');

然后在对应的翻译文件中（如中文对应文件是：./Language/zh_cn/common.php）添加对应的翻译即可：

// $vim ./Language/zh_cn/common.php
return array(
    'user not exists' => '用户不存在',
);

(1)当存在动态变量时？

有时，我们需要动态返回一些值，这里可以用 大括号 将需要动态替换的值包住并提供替换的参数即可，一如：

echo T('hello {name}', array('name' => 'dogstar'));

如果对应的中文翻译是：

'hello {name}' => '您好，{name}',

将会看到输出：

您好，dogstar

(2)当多个动态变量时，还可以这样省略变量名

如果要简化动态变量的传递，可以这样：

echo T('{0} I love you because {1}', array('PhalApi', 'no reasons'));

如果对应的中文翻译是：

'{0} I love you because {1}' => '{0} 我爱你因为{1}',

将会看到输出：

PhalApi 我爱你因为no reasons

(3)当翻译不存在时？

翻译不存在，有两种场景：一是指定的语言包不存在；二是语言包存在但翻译不存在。无论何种情况，找不到翻译时，都会用代码硬编码的内容返回。

1.25.3 语言的设定

当我们拥有了多种语言时，则可以在入口/初始化文件中，选择设定需要的语言。

SL('zh_cn');    //SL()函数等效于PhalApi_Translator::setLanguage()

参数即为语言包的路径名，如下面的en, zh_cn：

.
|-- en
|   `-- common.php
`-- zh_cn
    `-- common.php

此处，也可以通过客户端传递参数来自行选择语言。简单地：

SL($_GET['lan']);

1.25.4 添加语言翻译包

有时需要手动添加语言翻译包，因为对应的翻译不一定在项目的./Language下，如扩展类库。

当需要添加额外的语言翻译包时，可以这样添加：

        PhalApi_Translator::addMessage(API_ROOT . '/Library/User');

在User这个目录下的翻译包类似地，为：

./Language/
└── zh_cn
    └── common.php

1 directory, 1 file

相当于追加一个额外的项目路径，其他的加载过程和原来的一样。

1.25.5 建议

所以，你可以轻松看到，所谓的翻译也只是通过数组下标找一下对应的内容而已，没有太多的技术性，也没有过多的性能问题。

但正是有这样提前周到的国际化准备，我们可以对外（如像产品、BOSS和外界）传送这样一个隐喻： 我们的项目可以快速支持国际翻译 。
这听起来多么高大尚啊！因为那些不懂技术的人，根本不在乎是用PHP的数组来存放还是什么技术，而在于能不能走向国际化。

SO？既然翻译”无伤大雅“（指对性能的影响和对代码编写的阻碍），统一使用翻译的写法是值得推荐的。

即使项目没有机会用到真正的翻译，但至少有两点我认为也是有用的：

  • 1、便于产品维护接口返回的提示文案；
  • 2、被同行问到时，你们有支持i18n吗？我们也可以笑着回答：有 ^_^ ；

上一章 文档首页 下一章#[1.26]-数据安全：数据对称加密方案

上一章 文档首页 下一章

1.26.1 对称加密和非对称加密

关于数据加密和安全这一块，只作简单说明。

首先，对称加密是指数据可以加密成密文也可以解密还原，共用同一个密钥，而非对称是则公钥和私钥，两者皆使用可逆加密算法。不可逆加密不存在密钥，只有salt，用来增加可变性和随机性，如md5。而在加密里面，又数块算法加密的方式最为完善，随附图一张：

1.26.2 PHP的mcrypt加密扩展

在PhalApi中，同样也是使用了mcrypt作为底层的数据加密技术方案。请查看： PHP 手册 函数参考 加密扩展

1.26.3 加解密的使用

在单元测试中，我们可以快速找到加密和解密的使用，这里再简单举一例：

$mcrypt = new PhalApi_Crypt_Mcrypt('12345678');

$data = 'The Best Day of My Life';
$key = 'phalapi';

$encryptData = $mcrypt->encrypt($data, $key);
var_dump($encryptData);

$decryptData = $mcrypt->decrypt($encryptData, $key);
var_dump($decryptData);

上面将会输出(有乱码)：

1.26.4 更富弹性和便于存储的加密方案

上面看到，mcrypt下的加密在两点不足：

  • 1、有乱码，不能很好地永久化存储；
  • 2、只针对文本字符串的加密，不支持数组等，且无法还原类型；

为此， 我们提供了更富弹性和便于存储的加密方案，即：序列化 + base64 + mcrypt的多重加密方案。

以下是上面的示例-多重加密版：

$mcrypt = new PhalApi_Crypt_MultiMcrypt('12345678');

$data = 'The Best Day of My Life';
$key = 'phalapi';

$encryptData = $mcrypt->encrypt($data, $key);
var_dump($encryptData);

$decryptData = $mcrypt->decrypt($encryptData, $key);
var_dump($decryptData);

对应的输出（这里使用了文字结果输出，是因为没了乱码）：

string(44) "rmFMdhvszAkHhOdzwt/APBACk/Mn/SqhV1Ahp1xT0Gk="
string(23) "The Best Day of My Life"

1.26.5 RSA的支持与超长字符串的应对方案

基于项目有使用RSA进行加密和解密的需求，这里特扩展对RSA的支持。同时针对到RSA对字符串长度的限制，提供了分段处理的方案。RSA加密模块的静态类结构UML如下：
rsa-PhalApi

(1)原生态的通信加密和解密

此部分只是简单地封装了openssl相关函数的操作，可以实现与其他语言和客户端下RSA的加密通信。
唯一需要注意的是，对于 “私钥加密，公钥解密” 和 “公钥加密，私钥解密” 这两种情况下key的互换和对应问题。不要混淆。

(2)超长字符串的分段处理

这里重点说明一下超长字符串通信加密的问题。
解决方案主要涉及两点：一是分段的处理，二是中间层转换。分段是指将待加密的字符串分割成允许最大长度117（有用户反馈说是127）内的数组，再各自处理；中间层转换是为了稳定性、通用性和方便落地存储，使用了json和base64的结合编码。

虽然此方案解决了超长字符串的问题，但需要特别指出的是， 不能与其他语言、或者PHP其他框架和客户端进行原生态的RSA通信 。
我们突破了长度的限制，但失去了通用性。这里罗列一下各个场景和对应的处理方式：

支持：PhalApi项目A <--> PhalApi项目A

支持：PhalApi项目A <--> PhalApi项目B，PhalApi项目C，PhalApi项目D，...

不支持：PhalApi项目 <--> 非PhalApi项目的PHP项目

解决方案：将以下RSA模块相关的代码以包的形式拷贝到需要的PHP项目。

.
├── Crypt
│   └── RSA
│       ├── KeyGenerator.php
│       ├── MultiBase.php
│       ├── MultiPri2Pub.php
│       ├── MultiPub2Pri.php
│       ├── Pri2Pub.php
│       └── Pub2Pri.php
└── Crypt.php

不支持：PhalApi项目 <--> 非PHP语言的项目

解决方案：参考PhalApi对RSA超长字符串的处理，同步实现。

不支持：PhalApi项目 <--> 客户端（iOS/Android/Windows Phone, etc）

解决方案：参考PhalApi对RSA超长字符串的处理，同步实现。

(3)使用示例：2块2毛2和故事

这是一个有趣的故事，发生在我一个很好的朋友身上。

在去年暑假的时候，有位师妹通过几层关系找到我朋友，想他帮她支付回家的火车票。本着“调试”（调戏）的精神，我朋友爽快地答应了并让她把支付宝帐号发过来。然后，不一会，我朋友收到了一条有支付宝帐号的短信，立马打了2.22元过去，并附言： 别问我是谁，请叫我雷锋！ 后来证实师妹没发来过短信，原来收到的那条是诈骗的短信。。。。你能想象得到骗子收到这2.22元和看到这条留言时的表情么，哈哈~~

以下测试代码，以上面故事为背景，并演示了RSA的使用示例：

    public function testDecryptAfterEncrypt()
    {
        $keyG = new PhalApi_Crypt_RSA_KeyGenerator();
        $privkey = $keyG->getPriKey();
        $pubkey = $keyG->getPubKey();

        DI()->crypt = new PhalApi_Crypt_RSA_MultiPri2Pub();

        $data = 'AHA! I have $2.22 dollars!';

        $encryptData = DI()->crypt->encrypt($data, $privkey);

        $decryptData = DI()->crypt->decrypt($encryptData, $pubkey);

        $this->assertEquals($data, $decryptData);
    }

1.26.6 建议

在上面的加密中，接口项目在开发时，需要自定义两个值：加密向量和私钥。

为了提高数据加密的安全度，建议：

  • 加密向量项目统一在./Config/app.php中配置；
  • 各模块业务数据加密所用的Key则由各业务点自定义；

这样，可以对不同的数据使用不同的加密私钥，即使破解了某一个用户的数据，也难以破解其他用户的。

1.26.7 扩展你的对称加密

尤其对于加密方案和算法，我们在项目开发决策时，更应该优先考虑使用现在行业内成熟公认的加密方案和算法，而不是自己去从头研发。

但如果你项目确实有此需要，或者需要在mcrypt的基础上再作一些变通，也是可以很快地实现和注册使用。

首先，请先实现下面的加密接口：

// $vim ./PhalApi/PhalApi/Crypt.php

interface PhalApi_Crypt {

    public function encrypt($data, $key);

    public function decrypt($data, $key);
}

然后，重新注册加密服务即可。

上一章 文档首页 下一章#[1.27]-精益开发：更富表现力的Model层和重量级数据获取的应对方案

我们能够有意识地推断我们想要在哪一条想法的溪流中遨游，然而此后与那些想法的接触会潜在地塑造我们的习惯和信仰。 -- 《智慧社会》

1.27.1 写在前面的话

此篇章有点长，但我认为是值得一读的。因为这里我将逐步讲述如何在已有的基础上演变扩展出更高层次的代码结构和系统架构，而不致于因目前频繁的需求变更而导致代码凌乱不堪。更为重要的是，你将能从中发现，如何在一个框架中持续演变，最终体验浮现式设计的乐趣。如果你的项目亦能如此，我相信你会找到编程如同搭建积木般轻便明了的感觉。

1.27.2 更富表现力的Model层

接口不尽相同，主要区别在于领域业务数据的处理。而数据的来源则更为广泛，可能是来自数据库，可能来自第三方平台接口，可能存放于内存。所以，PhalApi这里的Model层，则是 广义上的数据源层 ，用于获取原始的业务数据，而不管来自何方，何种存储媒介。这也是为什么我们没有将Model层打造成活动纪录或者数据映射器的原因。当然，如果你确实需要，也可以自行调整。

如果数据来源于数据库，我们则需要考虑到数据库服务器的感受，保证不会有过载的请求而导致它罢工。对此，我们可以结合缓存来进行性能优化。

如，一般地：

    // 版本1：简单的获取
        $model = new Model_User();
        $rs = $model->getByUserId($userId);

这种是没有缓存的情况，当发现有性能问题并且可以通过缓存来解决时，我们可以在调用时简单引入缓存：

        // 版本2：使用单点缓存/多级缓存 (应该移至Model层中)
        $key = 'userbaseinfo_' . $userId;
        $rs = DI()->cache->get($key);
        if ($rs === NULL) {
                $rs = $model->getByUserId($userId);
                DI()->cache->set($key, $rs, 600);
        }

但不建议在领域Domain层中引入缓存，因为会导致混淆和不便进行测试。更好是将缓存的处理移至Model，保持数据获取的透明性：

class Model_User extends PhalApi_Model_NotORM {

    public function getByUserIdWithCache($userId) {
        $key = 'userbaseinfo_' . $userId;
        $rs = DI()->cache->get($key);
        if ($rs === NULL) {
            $rs = $this->getByUserId($userId);
            DI()->cache->set($key, $rs, 600);
        }
            return $rs;
    }

对应地，外部的调用调整成：

    // 版本2：使用单点缓存/多级缓存 (应该移至Model层中)
        $model = new Model_User();
        $rs = $model->getByUserIdWithCache($userId);

至此，Model层对于上层如Domain来说，负责获取源数据，而不管此数据来自于数据库，还是远程接口，抑或是缓存包装下的数据。这正是我们使用数组在Model层和Domain层通讯的原因，因为数组更加通用，不需要额外添加实体。

1.27.3 重量级数据获取的应对方案

纵使更富表现力的Model很好地封装了源数据的获取，但是仍然会遇到一些尴尬的问题。特别地，当我们大量地进行缓存读取判断时，会出现很多重复的代码，这样既不雅观也难以管理，甚至会出现一些简单的人为编写错误而导致的BUG。另外，当我们需要进行预览、调试或测试时，我们是不希望看到缓存的，即我们能够手工指定是否需要缓存。

这里再稍微简单回顾总结一下我们现在的问题：我们希望通过缓存策略来优化Model层的源数据获取，特别当源数据获取的成本非常大时。但我们又希望我们可以轻易控制何时需要缓存，何时不需要，并且希望原有的代码能在OCP的原则下不需要修改，但又能很好地传递源数据获取的复杂参数。归纳一下，则可分为三点：缓存的控制、源数据的获取、复杂参数的传递。

(1)缓存的控制

不管是单点缓存，还是多级缓存，都希望使用原有已经注册的cache组件服务。所以，应该使用委托。委托的另一个好处在于使用外部依赖注入可以获得更好的测试性。

(2)源数据的获取

源数据的获取，作为源数据获取的主要过程和主要实现，需要进行缓存的控制（可细分为：是否允许读缓存、和是否允许写缓存）、 获取缓存的key值和有效时间，以及最终原始数据的获取。明显，这里应该使用模板方法，然后提供钩子函数给具体子类。

这里，我们提供了Model代理抽象类PhalApi_ModelProxy。

之所以使用代理模式，是因为实际上并不一定会真正调用到最终源数据的获取，因为往往源数据的获取成本非常高，故而我们希望通过缓存来拦截数据的获取。

由于Model代理被上层的Domain领域层调用，但又依赖于下层Model层获得原始数据，所以处于Domain和Model之间。为了保持良好的项目代码层级，如果需要创建PhalApi_ModelProxy子类，建议新建一个ModelProxy目录。

如对用户基本信息的获取，我们添加了一个代理：

class ModelProxy_UserBaseInfo extends PhalApi_ModelProxy {

    protected function doGetData($query) {
        $model = new Model_User();

        return $model->getByUserId($query->id);
    }

    protected function getKey($query) {
        return 'userbaseinfo_' . $query->id;
    }

    protected function getExpire($query) {
        return 600;
    }
}

其中，doGetData($query)方法由具体子类实现，委托给Model_User的实例进行源数据获取。另外，实现钩子函数以返回缓存唯一key，和缓存的有效时间。

这里只是作为简单的示例，更好的建议是应该将缓存的时间纳入配置中管理，如 配置四个缓存级别：低(5 min)、中(10 min)、高(30 min)、超(1 h) ，然后根据不同的业务数据使用不同的缓存级别。这样，即便于团队交流，也便于缓存时间的统一调整。

(3)复杂参数的传递

敏锐的读者会发现，上面有一个$query查询对象，这就是我们即将谈到的复杂参数的传递。

$query是查询对象PhalApi_ModelQuery的实例。我们强烈建议此类实例应当被作为 值对象 对待。虽然我们出于便利将此类对象设计成了结构化的使用。但你可以轻松通过new PhalApi_ModelQuery($query->toArray())来拷贝一个新的查询对象。

此查询对象，目前包括了四个成员变量：是否读缓存、 是否写缓存、主键id、时间戳。

很多时候，这四个基本的变量是满足不了各项目的实际需求的，因此你可以定义你的查询子类， 以支持丰富的数据获取。如调用优酷平台接口获取用户最近上传发布的视频时，需要用户昵称、获取的数量、排序种类等。

(4)最终的调用

在完成了上面的工作后，让我们看下最终呈现的效果：

        // 版本3：缓存 + 代理
        $query = new PhalApi_ModelQuery();
        $query->id = $userId;
        $modelProxy = new ModelProxy_UserBaseInfo();
        $rs = $modelProxy->getData($query);

在领域层中，我们切换到了Model代理获取数据，而不再是原来的Model直接获取。其中新增的是代理具体类 ModelProxy_UserBaseInfo，和可选的查询类。

(5)UML静态图

至此，我们很好地在源数据的获取基础上，统一结合缓存策略。你会发现： 缓存节点可变、具体的源数据可变、复杂的查询亦可变 。

将此图简化一下，可得到：

这样的设计是合理的，因为缓存节点我们希望能在项目内共享，而不管是哪块的业务数据；对于具体的源数据获取明显也是不尽相同，所以也需要各自实现，同时对于同一类业务数据（如用户基本信息）则使用一样的缓存有效时间和指定格式的缓存key（通常结合不同的id组成唯一key）；最后在前面的缓存共享和同类数据的基础上，还需要支持不同数据的具体获取，因此需要查询对象。也就是说，你可以在不同的层级不同的范畴内进行自由的控制和定制。

如果退回到最初的版本，我们可以对比发现，Model Proxy就是Domain和Model间的桥梁，即：中间层。因为每次直接通过Model获取源数据的成本较大，我们可以通过Model Proxy模型代理来缓存获取的数据来减轻服务器的压力。

1.27.4 细粒度和可测试性

这无疑是细粒度的划分，但对于支撑复杂的领域业务却发挥着重要的作用。一来是如此清楚明了，二来则是带来了可测试性。

正如前面提及到的，我们在预览、调试、单元测试或者后台计划任务时，不希望有缓存的干扰。在细粒度划分的基础上，可轻松用以下方法实现而不必担心会破坏代码的简洁性。

(1)取消缓存的方法1： 外部注入模拟缓存

在构造Model代理时，默认情况下使用了DI()->cache作为缓存，当需要进行单元测试时，我们可以两种途径在外部注入模拟的缓存而达到测试的目的：替换全局的DI()->cache，或单次构造注入。对于计划任务则可以在统一的后台任务启动文件将DI()->cache设置成空对象。

(2)取消缓存的方法2： 查询中的缓存控制

在项目层次，我们可以统一构造自己的查询基类，以实现对缓存的控制。

如：

class Common_ModelQuery extends PhalApi_ModelQuery {

    public function __construct($queryArr = array()) {
        parent::__construct($queryArr);

        if (DI()->debug) {
            $this->readCache = FALSE;
            $this->writeCache = FALSE;
        }
    }
}

至于DI()->debug的设置，则可以在入口文件中根据约定的接口参数设定，简单地如：

if (isset($_GET['debug']) && $_GET['debug'] == 1) {
    DI()->debug = true;
}

这样便可以获得了接口预览和调试的能力。

1.27.5 何时使用此方案？

可以看到，此方案是在缓存策略（包括单点缓存、低高速缓存、多级缓存）和广义Model层基础上扩展的，以便应对重量级的业务数据获取。此方案有一定的优势，但作为代价则是额外的代码编写以及层级复杂性。并且，我们还没谈及到数据变更时的处理。

所以，请在确切需要统一封装高成本的数据获取时，才使用此方案。

1.27.6 扩展：多接口参数传递的优雅处理方案

当接口的查询参数过多时，我们需要手工重复地将接口参数从Api层传递到Domain层，再通过Query对象传递到Model层，这中间任何一个环节的缺失或遗漏都会造成一个BUG。

为此，项目可以考虑使用一种更为优雅的方案来进行整合，并实现自动化参数获取，但又保留接口原来的参数验证。

假设，我们需要以下多个接口参数：

function getRules() {
    return array(
        'getList' => array(
            'keyword' => array(...),
            'filed' => array(...),
            'page' => array(...),
            'perpage' => array(...),
            'order' => array(...),
        ),
    );
}

为避免出现以下这样的手工调用（而且也不符合值对象的特征）：

$query = new Query_Demo();
$query->keyword = $this->keyword;
$query->filed = $this->filed;
$query->page = $this->page;
$query->perpage = $this->perpage;
$query->order = $this->order;

$domain = new Domain_Demo();
$list = $domain->getList($query);

我们首先需要提取出一个层超类：

class Query_Demo extends PhalApi_ModelQuery {
    public $keyWord;
    public $filed;
    public $page;
    public $perpage;
    public $order;

    public function __construct($api) {
        //按需获取，自动初始化
        $vars = get_object_vars($api);
        foreach ($vars as $key => $var) {
            if (isset($api->$key)) {
                $this->$key = $api->$key;
            }
        }
    }
}

然后，在接口Api中对Domain层的调用就会简化成：

$query = new Query_Demo($this); //自动初始化

$domain = new Domain_Demo();
$list = $domain->getList($query); //通过查询对象传递众多参数

这样的好处在于：

  • 1、更方便职能的划分
  • 2、易于测试
  • 3、实现简单（可提取一个Query的层超类来完成自动填充）
  • 4、便于IDE时的参数提示，同时可以提供默认值

    [1.28]-COOKIE：对COOKIE原生态的支持及记忆加密升级版

    上一章 文档首页 下一章

只要你能把握自己，你就能把握任何东西。 -- 古谚语

1.28.1 原生态的支持

应开发同学的需求，这里补充对COOKIE的支持。

因为，确实在项目的开发过程中，有时是需要对COOKIE进行处理的。所以，这里提供了简单的原生态支持。

下面通过代码示例来简明如何使用。

(1)注册COOKIE服务

如同其他的服务一样，我们在使用前需要对COOKIE进行注册。不需要用到COOKIE的同学，可以忽略此服务的注册。

在初始化文件init.php中添加如下注册代码：

//COOKIE
DI()->cookie = 'PhalApi_Cookie';

(2)使用

查看PhalApi_Cookie提供的接口，可以得到针对COOKIE的三种操作：

//设置COOKIE服务
DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

//获取
echo DI()->cookie->get('name');  //输出 phalapi

//删除
DI()->cookie->delete('name');

1.28.2 记忆加密升级版

实际情况，项目的使用的情况更为复杂。

比如，需要对数据进行加解密，或者需要突破COOKIE的限制（设置后需要下一次请求才能获取，不过这也正是COOKIE的特点），想记住这一次设置的内容。所以，额外提供了这个升级版本。

(1)注册COOKIE服务

和原生态的初始化，有些许不同，在于需要配置指定加解密的服务。如下：

$config = array('crypt' => $crypt, 'key' => 'a secrect');
DI()->cookie = new PhalApi_Cookie_Multi($config);

上面中的crypt即为加密解服务，未指定时使用DI()->crypt，需要实现PhalApi_Crypt接口；key则为对应的密钥。

(2)使用COOKIE服务

使用同原生态一样，客户端感知不到区别，这里不再赘述。

(3)一个简单的实现示例

假设我们有这样一个简单的加解密类（真的很简单的）：

class Cookie_Crypt_Mock implements PhalApi_Crypt {

    public function encrypt($data, $key) {
        return base64_encode($data);
    }

    public function decrypt($data, $key) {
        return base64_decode($data);
    }
}

随后，写一个简单的接口测试一下（因为涉及到COOKIE的操作，需要与浏览器可视化测试，所以没用单元测试）：

    public function cookieTest() {
        $rs = array();

        $config = array('crypt' => new Cookie_Crypt_Mock(), 'key' => 'a secrect');
        DI()->cookie = new PhalApi_Cookie_Multi($config);

        $rs['aEKey'] = DI()->cookie->get('name');
        DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

        return $rs;

    }

运行效果如下：

从上图中可以看出，COOKIE在浏览器已经被我们做了“加密”，哈哈~

1.28.3 一如其他框架的建议

  • 1、敏感数据不要存到COOKIE，以保证数据安全
  • 2、大的数据不要存到COOKIE，以保证瘦的客户端

1.28.4 扩展你的COOKIE

如需扩展，请重载PhalApi_Cookie基类。

上一章 文档首页 下一章#[1.29]-开放与封闭：多入口和统一初始化

上一章 文档首页 下一章

美国著名棒球运动员约格.贝拉说过：“通过仔细地观察，你可以了解很多事情。”

1.29.1 绝对的开放，还是绝对的固化？

框架应该都会考虑这样一个问题：到底是应该给应用项目提供统一固定的入口和初始化流程呢，还是应该给他们完全自由的空间？

但我发现，很多PHP的框架都提供了一个绝对的固化流程。也就是你能作出改动的地方很少，虽然这样应用不需要过多地去考虑。

然而我觉得，这样做，弊大于利。特别在现在项目需求背景各有各的不同时。所以，我为PhalApi框架选择了开放式的初始化做法。考虑到若开放的度太大，项目可能会迷茫，所以又结合了统一的初始化。

下面分别说明这两点：开放式入口和封闭式的初始化。

1.29.2 开放式的入口

作为一个接口系统，是需要为不同的终端、不同的开放人群，甚至不同的版本提供不同的服务的。

如：

  • 按不同的终端：iOS设备、Android设备、PC版、网页版
  • 按不同的开放人群：手机客户端、内部管理后台、内部其他系统、公共开放平台
  • 按不同的版本：版本从V1、V2到VN
  • 按不同的项目：同一个应用下可能会存在多个项目，当然更推荐每个项目单独一个系统

基于此，为不同的维度提供不同的入口就很有现实实用场景了。

更为重要的是，这些不同的入口都应该尽可能简单，并能统一共享公共的组件资源、数据库、日记系统这些。一个可能的入口示例如下：

<?php
/**
 * Demo 统一入口
 */

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()->loader->addDirs('Demo');

/** ---------------- 响应接口请求 ---------------- **/

$api = new PhalApi();
$rs = $api->response();
$rs->output();

简单解读一下上面的代码，首先要加载统一初始化文件，其实装载挂靠的接口项目（对应你的项目目录的名称），最后创建一个PhalApi接口实例进行响应、输出结果。

下面来看下统一初始化文件需要做的事情。

1.29.3 封闭式的初始化

不言而喻，尽管我们有按不同维度划分入口的需要，但统一初始化的过程更是必不可少的。

不同的入口提供了各维度定制的机会，统一的初始化则为应用提供了统一定制的机会。

(1)常规入口代码

常规的入口，需要以下初始化操作：

<?php
/**
 * 统一初始化
 */

/** ---------------- 根目录定义，自动加载 ---------------- **/

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

/** ---------------- 注册&初始化服务组件 ---------------- **/

//自动加载
DI()->loader = $loader;

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

//调试模式，$_GET['__debug__']可自行改名
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

//翻译语言包设定
SL('zh_cn');

(2)常规代码解读

上面是框架执行所需的基础服务注册和配置，一般直接可用，但也可以根据需要作些细微的调整。如日记的级别设定、调试的参数修改（改成一个只有自己知道的参数名字，别让外界知道！）等。

出于让大家对初始化过程有一个更理性的认识，这里补充一下各代码的作用。

最开始是利用了PHP原生态的时区设置和宏定义：

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

接着，便开始引入PhalApi框架的类自动加载器：

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

这样，我们就可以从原生态的PHP开发，切入到了PhalApi接口开发模式。但在捲起袖口准备大干一场前，我们还需要注册一些必备的服务：

//自动加载
DI()->loader = $loader;

//配置
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

上面的自动加载、配置、日记和数据库操作通常而言，对于一个项目都是必须的。
但配置文件的路径可自行指定，日记的存储类型也可以自由组合（多个类型采用或运算），还可以选择你合适的数据库配置。 注意到，PhalApi_DB_NotORM初始化时，除了配置文件外，还有一个debug参数，此参数的作用是用于控制是否打印显示执行的SQL语句及对应消耗的时间。

至此，我们已经为项目完成了绝大部分的基础服务注册，且上面的初始化顺序建议保留不变。因为，前后有依赖关系。

但为了让我们的项目更有活力、更具生气、更国际化，我们还可以多加这么两行代码：

//调试模式，$_GET['__debug__']可自行改名
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

//翻译语言包设定
SL('zh_cn');

这里，也有一个debug参数，之所以和数据库的分开，是因为如果混在一起会导致返回结果解析失败（如不再是JSON格式）。
此debug的来源，默认来自环境的系统配置文件（如区分生产环境和测试环境）；也可以来自某个请求的手动设置，这样，开发同学便可以快速进行在线调试了。 而这个参数，则是框架代码、项目代码以及扩展类库所共用的调试开关，至于各个场景使用的效果，视各环节而定。

SL()是一个快速函数，作用是设定翻译语言包。如果觉得中文下的UTF-8查看不直观，可以自行加个参数修改，如：

SL(isset($_GET['__lan__']) ? $_GET['__lan__'] : 'zh_cn');

(3)更多定制注册代码

上面的操作，涵盖了大部分项目的需要。除此之外，还有一些额外的服务，可根据自身的情况，定制处理：

/** ---------------- 以下服务组件就根据需要定制注册 ---------------- **/

//缓存 - MC
/**
DI()->cache = function() {
    $mc = new PhalApi_Cache_Memcached(DI()->config->get('sys.mc'));
    return $mc;
};
 */

//签名验证服务
//DI()->filter = 'Common_SignFilter';

//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

(4)更多定制解读

此部分的注册，非项目必须部分。可根据需要，自行定制。

如上面出现的第一个，即缓存服务，使用的是Memcached：

DI()->cache = function() {
    $mc = new PhalApi_Cache_Memcached(DI()->config->get('sys.mc'));
    return $mc;
};

接下来的，是重要的接口签名验证服务。之所以没有提供这个服务的实现，是出于更高安全性考虑而建议项目各自制定签名规则并实现。然后这样简单注册即可被框架自动调用：

//签名验证服务
//DI()->filter = 'Common_SignFilter';

在有些需要支持JsonP返回的场景，可以使用PhalApi_Response_JsonP返回格式开放callback操作：

//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

1.29.4 用一张图来表示

上一章 文档首页 下一章#[1.30]-保持的力量：接口开发最佳实践

上一章 文档首页 下一章

神啊，求你赐给我平静的心，去接受我无法改变的事；赐给我勇气，去做我能改变的事；赐给我智慧，去分辨两者的不同。 --平静之祷

1.30.1 论保持的力量

追到一个心仪的女生不难，难于如何保持和培养一份真挚的感情；获得一时的财富也不难，难于如何长久保持收益；创业的公司很容易博得一时媒体的关注以及某次天使的投资，但难于如何排除各种障碍、充分利用各方资源发展成中企业及至上市公司。

同样，提供一时的接口很容易，但当我们需要不断为接口提供升级，以及当我们维护提供一整套接口时，面临的困难和问题会越来越大。
所以，这是一场持久的战役。需要我们用稳重的心态、专业的能力在背后持久支撑、推动。

值得庆幸的是，这些都是问题而不是限制，都是可以被解决的。
以下是结合 @郭了个浩浩 同学提供的apigee.web_api.pdf文档，以及我们多年来的项目实际开发经验为新手提供的一些建议，对老同学相信也会有所帮助。
每个建议通常会包括三部分： 现在主流的做法、PhalApi的做法以及项目的选取。

1.30.2 最佳实践建议

为了大家查阅和翻看，这里先罗列本章的全部建议：

  • (1)接口风格和协议的选择 - HTTP
  • (2)接口域名 - 使用api单独域名
  • (3)异常处理 - 200/400/500三大接口结果状态码
  • (4)对外的命名规则 - 使用小写加下划线
  • (5)对内的命名规则 - 使用驼峰法和遵循PEAR命名
  • (6)安全与验证 - 使用接口签名和token登录态双重机制
  • (7)返回结果格式 - JSON
  • (8)URL规则与路由映射 - 统一service接口服务，可一个文件一个接口
  • (9)SDK包 - 给客户端自由的调用空间和自由
  • (10)接口文档 - 使用markdown快速编写
  • (11)测试驱动开发 - 坚持单元测试

1.30.3 建议细说

(1)接口风格和协议的选择 - HTTP

目前，后台接口开发可以用RESTFull风格，也可以用Web Service；可以用SOAP协议、RPC协议，也可以用HTTP协议；可以用短链接，也可以使用长链接。如果我们希望继续进行划分，还可以分为同步或异步、单个或批量、是否有SDK包、内部接口还是开放接口平台等。

主流的做法

现在看来，大部分大型的企业以及大多数的小公司使用的都是HTTP协议下的接口开发，部分使用RESTFull，但Web Service较少。如：

  • 优酷开放平台，示例请点击
  • 微信公众号-服务器接口协议，示例请点击
  • 新浪微博开放平台，使用REST风格，示例请点击
  • Amazon ，采用Web Service、SOAP、REST等多种风格和协议提供服务，示例请点击

PhalApi的做法

我们选取了HTTP的协议，在于其无论是客户端接入、开发调试，还是部署构建上都很容易实现，而且也符合主流，因为大家都比较熟悉。
这一点是非常重要的：因为简单，后台接口开发的同学才会更容易上手；因为容易，客户端接入才会更加无压力而不用担心处处受挫。

项目的选取

根据项目不同的项目背景和需求，可以选择你合适的风格或者协议。但是即使出于安全、性能或者其他技术或非技术的原因而不采用HTTP协议的情况下，你也可以在PhalApi原有的接口开发实现时，轻松扩展你需要的协议。如使用SOAP，PHPRpc或者swoole下的TCP协议。其中，部分协议已有扩展类库提供支持。

(2)接口域名 - 使用api单独域名

首先，有一点是可以肯定的。
接口系统应该有自己单独的域名，而不应该附属于网站或者管理后台。

主流的做法

显然，主流做法也是这样做的。如：

  • 优酷开放平台：https://openapi.youku.com
  • 微信公众号： https://api.weixin.qq.com
  • 新浪微博： https://api.weibo.com

项目的选取

如果可以，尽量让接口系统使用独立的域名，并且使用api作为一级域名。如：

//你的网站为：
http://www.demo.com

//则对应的接口为：
http://api.demo.com

(3)异常处理 - 200/400/500三大接口结果状态码

对于接口的异常处理，在使用HTTP协议下，可以通过HTTP本身的响应状态码来进行区分。 在非HTTP协议并有SDK包的情况下，异常的处理手段则会更为多样。

主流的做法

优酷接口采用了HTTP响应状态码加结果返回的形式，如：

Request URL:https://openapi.youku.com/v2/videos/show_basic.json
Request Method:GET
Status Code:400 Bad Request

{"error":{"code":1004,"type":"SystemException","description":"Client id null"}}

新浪微博也一样：

Request URL:https://api.weibo.com/2/statuses/mentions/ids.json
Request Method:GET
Status Code:403 Forbidden

{"error":"auth by Null spi!","error_code":21301,"request":"/2/statuses/mentions/ids.json"}

微信接口则采用了统一200的形式，如：

Request URL:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=
Request Method:GET
Status Code:200 OK

{"errcode":41002,"errmsg":"appid missing"}

PhalApi的做法

为了与HTTP保持一致性，同时降低不必要的复杂性，我们采用了200/400/500三大接口结果状态码。

注意，这里所说的三大状态码，是指接口返回结果中的状态码，而不是HTTP的响应状态。
也就是说接口全部的结果返回都应该是200，除非接口服务有内部未捕获的异常，即：

Status Code:200 OK

返回结果状态码刚是以下几种：

//正常返回
{
    "ret": 200,
    "data": {
        //...
    },
    "msg": ""
}

//客户端非法请求
{
    "ret": 400,
    "data": [],
    "msg": "非法请求：接口服务Default.Test不存在"
}

//服务端内部错误
{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: can not connect to database db_demo"
}

####项目的选取
你可以根据你的需要，扩展400和500这两系列的错误，如401表示登录失败等。
此外，在data里面，你也可以添加一个code来表示业务级的操作码，以及客户端根据不同的业务场景做出不同和反应、交互或引导提示。

(4)对外的命名规则 - 使用小写加下划线

对外的命名，是指外部看得到的命名，如接口参数的名字，接口返回的结果节点名字，以及数据库的表名、字段名。

主流的做法

新浪微博采用了小写加下划线的做法，如：

//URL
https://c.api.weibo.com/2/friendships/followers/trend_count.json

//请求参数
source
access_token

//返回结果
{
    "uid": 10438,
    "result": [
        {
            "days": "2012-04-04",
            "follower_count_online":"15",  //粉丝数
            "active_follower":"14", //活跃粉丝数
            "loyal_follower":"0"   //互动粉丝数
        },
        ....
    ]
}

Amazon采用了首字母大写且无下划线的做法，如：

//Responses
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2009 12:00:00 GMT
Connection: close
Server: AmazonS3

<?xml version="1.0" encoding="UTF-8"?>
<BucketLoggingStatus xmlns="http://doc.s3.amazonaws.com/2006-03-01">
  <LoggingEnabled>
    <TargetBucket>mybucketlogs</TargetBucket>
    <TargetPrefix>mybucket-access_log-/</TargetPrefix>
    <TargetGrants>
      <Grant>
        <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:type="AmazonCustomerByEmail">
          <EmailAddress>user@company.com</EmailAddress>
        </Grantee>
        <Permission>READ</Permission>
      </Grant>
    </TargetGrants>
  </LoggingEnabled>
</BucketLoggingStatus>

PhalApi的做法

我们提倡使用全部小写加下划线的命名，因为这样更符合客户端的使用，如： 接口参数：

//正确的
&user_id=888

//错误的
&userId=888

返回字段：

//正确的
"device_type": "cube",

//错误的
"deviceType": "cube",

数据库字段：

//正确的
`user_id` bigint(20) DEFAULT '0' COMMENT '创建者的用户ID',

//错误的
`userId` bigint(20) DEFAULT '0' COMMENT '创建者的用户ID',

项目的选取

不管是使用全部小写，还是全部大写，项目都应该保持一致的命名风格，而不是混合凌乱的风格。

(5)对内的命名规则 - 使用驼峰法和遵循PEAR命名

与对外命名对应的则是对内的命名规则，这里又回归到了老生常谈的PHP代码风格。
这里不作过多的说明，只是稍作提及。

PhalApi的做法

我们建议使用PEAR包的命名风格，和驼峰法，如下为一个接口示例：

$ vim ./Api/Default.php
<?php
/**
 * 默认接口服务类
 *
 * @author: dogstar <chanzonghuang@gmail.com> 2014-10-04
 */

class Api_Default extends PhalApi_Api {

    public function getRules() {
        return array(
            'index' => array(
                'username'  => array('name' => 'username', 'default' => 'PHPer', ),
            ),
        );
    }

    public function index() {
        return array(
            'title' => 'Default Api',
            'content' => T('Hello {name}, Welcome to use PhalApi!', array('name' => $this->username)),
            'version' => PHALAPI_VERSION,
            'time' => $_SERVER['REQUEST_TIME'],
        );
    }
}

项目的选取

你可以选择你喜欢的风格，但团队应该保持一致。

即便你不喜欢PhalApi约定的PEAR命名，你也可以自行实现内部的类加载机制。
当有多个项目或者多个模块并存时，可以添加模块名前缀来作区分，如：

$ tree
.
├── Demo
│   └── Api
│       └── DUser.php
├── MyApp
│   └── Api
│       └── MUser.php
└── Task
    └── Api
        └── TUser.php

$ head */*/*
==> Demo/Api/DUser.php <==
<?php
class Api_DUser extends PhalApi_Api {
}

==> MyApp/Api/MUser.php <==
<?php
class Api_MUser extends PhalApi_Api {
}

==> Task/Api/TUser.php <==
<?php
class Api_TUser extends PhalApi_Api {
}

其它的Domain层和Model层等也类似，这样可以避免类名冲突，或者IDE开发环境下的混淆。

(6)安全与验证 - 使用接口签名和token登录态双重机制

既然采用HTTP协议，那么安全方面就需要接口自身进行保证。

所幸，现在可用的加密手段有多种选择。
对于接口签名，我们可以使用非对称的验签方式，如md5；也可以用对称的方式，如RSA。
最后，为每一个接入的客户端分配app_key和app_secrect即可。

当然，更好的安全是接口系统再提供登录态的验证，即通常所说的token。 这两者的相合，会为接口增加更好的安全保障。

主流的做法

  • 七牛云存储，采用Access Key/Secret Key，并且在需要时添加相应的凭证
  • 微信公众号，采用由AppID(应用ID)和AppSecret(应用密钥)生成的ACCESS_TOKEN
  • 优酷开放平台，采用应用Key client_id

PhalApi的做法

我们不提供具体的接口签名方案，是因为把这种决策移交给项目应用本身进行定制。
而定制也是非常简单的，只需要简单的两步即可：

  • 1、实现过滤器接口 PhalApi_Filter::check()；
  • 2、注册过滤器服务 DI()->filter；

对于token，虽然框架没有提供内置的实现，但可以从PhalApi的扩展类库寻找这种支持，这一点已经User扩展类库支持。

项目的选取

正如PhalApi提供的自由空间，项目可以自行实现接口签名，和根据需要是否采用User扩展类库，或者自行实现token的处理。

(7)返回结果格式 - JSON

在 [1.14.1 统一返回的格式]一节中，已经对JSON的返回格式作了说明，这里不再赘述，也只是稍作提及。

主流的做法

目前采用了JSON的格式返回的有：

  • 新浪微博
  • 优酷开放平台
  • 腾讯开放平台
  • 微信接口

采用了XML格式返回的有：

  • Amazon

PhalApi的做法

我们默认采了JSON的格式返回。

项目的选取

项目可以轻松扩展成其他格式的返回。

(8)URL规则与路由映射 - 统一service接口服务，可一个文件一个接口

先从项目内部的文件划分说起，通常最为常见的情况是，很多开发人员都喜欢把很多很多很多接口都塞到一个接口文件里面。

这样的文件，通常会有2K到3K左右。
我觉得这是一种极端，而且是一种不好的极端。因为文件过大的话，会带来很多问题。

但与之对立的有另一种做法，即一个文件，一个接口。
这一点，在我之前就职的一家出名的游戏公司中得到了广泛的认可和遵循。如：

//?service=UserInfo.Go
<?php
class Api_UserInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}

//?service=GroupInfo.Go
<?php
class Api_GroupInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}

虽然也是一种极端，但却很好地做到了接口隔离，即不用担心修改此接口的实现而影响到其他接口服务。

最后，我们再来聊URL规则，就更顺畅了。如果我们采用一个文件对应一个接口，则我们可以省略Action（全部都为go()方法），简写成：?service=XXX。
再进一步，我们可以利用接口服务器（如Nginx）的规则Rewrite来提供更好的URL规则，同时尽量隐藏我们的接口内部实现细节，如：

//原始地
http://api.demo.com/?service=UserInfo.Go

//简化地
http://api.demo.com/?service=UserInfo

//再进一步
http://api.demo.com/UserInfo

//或者
http://api.demo.com/UserInfo.json

还有一点需要关注的就是接口的版本，当有v1，v2，v3等不同的版本时，我们也需要在接口URL中体现这些版本的不同。

主流的做法

  • 新浪微博的一个接口URL：https://c.api.weibo.com/2/friendships/followers/trend_count.json
  • 优酷平台的一个接口URL：https://openapi.youku.com/v2/videos/show_basic.json
  • 微信的一个接口URL：https://api.weixin.qq.com/device/get_stat

PhalApi的做法

目前而言，PhalApi在URL规则和路由这块还比较欠缺，没有像其他网站一样提供强大的路由支持。
但我们在代码实现的层面，可以提供不同的入口，以开放给不同的终端（内部的或者外部的）， 以及不同的版本支持。如：

$ tree Public/
Public/
├── v1
│   └── index.php
├── v2
│   └── index.php
└── v3
    └── index.php

3 directories, 3 files

则对应的版本URL则可以为：

//v1版本
http://api.demo.com/v1/?service=Default.Index

//v2版本
http://api.demo.com/v2/?service=Default.Index

//v3版本
http://api.demo.com/v3/?service=Default.Index

项目的选取

项目可以结合不同的入口，以及接口服务器的URL规则Rewrite作一些自定的URL路由。

(9)SDK包 - 给客户端自由的调用空间和自由

目前移动开发主要有iOS、Android、Windowns Phone、网站等不同的终端，各种终端又有不同的语言，如果我们需要提供SDK包，不仅仅需要考虑到纵向的版本升级，还需要维护横向的多样性。
而且，如果我们使用的是HTTP协议，则不必要担心这些维护的成本，同时给客户端提供一个自由的空间进行调用 -- 即客户端可以自己编写本身的接口客户端。

主流的做法

很多国内的开放平台接口都是不提供SDK包的，但有些安全度高的则会，如支持宝。

以下是一些提供了SDK的平台 ：

  • 支付宝SDK
  • Yahoo! SDKs
  • Amazon丰富的开发工具包

PhalApi的做法

我们暂时没有提供SDK包，但对于PHP，有一个简单的客户端类，可见： [1.13]-统一的接口请求方式

项目的选取

出于公司产品簇的项目考虑，项目可以内部提供SDK给同类的客户端使用，如分为iOS版的客户端SDK，以及Android版的客户端SDK。

小故事：与SDK包的一个真实的痛苦经历

有一点是非常重要的，千万不要让不懂PHP语言的人去开发提供PHP的SDK包，更不要使用所谓的工具自动转换生成SDK包代码。
在我曾经做过的一个项目中，因为需要接入一个接口系统，而这个接口是由专业的JAVA团队维护的，但他们对PHP语言则是非常薄弱，以致他们使用了工具来生成PHP语言的SDK包。

这就导致了我在接入一个简单的接口时，却开发联调耗费了两天、测试联调时耗费了在接口调用超时问题排查上。
而最后找到的原因却是因为app_key不对而导致服务端异常，而在SDK包却隐藏了这一异常错误信息，反而给出了time out超时的提示，严重误导了排查的方向！
而当我尝试深入去调试SDK时，得到却又是既没有code又没有message的异常！最让人难以忍受的是，他们提供的SDK包竟然和JAVA的企业系统一样复杂的结构（正如他们是使用工具来生成转换的）！

想象一下，PHP代码下有\com\sina\webo\sdk\Constants.php这样类似JAVA的文件结构，PHP的同学会作何感想？用JAVA的世界的方式来开发PHP，显然是走不通的啊！
而执意要走的话，到最后就是各种接入的痛苦，稍微按奈不住的同学难免就会因为情绪问题而大开争论了。而这一切，只是因为非PHP人员使用了自动生成工具。
我觉得，这是一种不负责任的做法，希望大家不要效仿。

(10)接口文档 - 使用markdown快速编写

（场外音：通过沐浴法理清了头绪，继续回来执笔编写）。
就我个人经历而言，markdown就是一个开始你会拒绝，接着你会越来越喜欢，到最后会爱不释手的一个工具。

如果你或者你的团队还在使用邮件或者work文档来传递共享接口文档，那就太不应该了；如果你正在使用某个WIKI系统进行文档的维护但却不喜欢它的编辑或者展示方式时，你可以尝试使用一下markdown。
正如你现在正在查看的文档也是通过markdown编写的。

主流的做法

作为开放接口平台，文档肯定是以网站的形式提供。但很多时候，对于我们内部的接口或者小项目来说，显然这样的成本太大了。
接口，从简单开始。
我们理应一直坚持这一点，所以文档也是一样，我们应该寻求一种在内部快速共享最新接口文档的途径。如：

  • 1、使用内部WIKI
  • 2、使用开源中国或者其他站点的WIKI（这时可以通过在线编辑或者GIT更新）

项目的选取

你可以根据项目的需要，或者公司以往的做法，但至少不要再使用邮件或者word文档。

(11)测试驱动开发 - 坚持单元测试

单元测试，在PhalApi里面不只一次提到了，这里再次进行说明，是希望能引起大家的关注，去尝试体验一下。

我们都知道，在开发一个新功能时、新接口时，修复一个BUG或者作一些大的调整或者重构工作，我们是毫无压力的，而且这时的成本很低，仅在于开发人员本身的时间和精力的消耗。
当提测后进入测试阶段，测试人员发现一个BUG后，有些团队会以禅道或者Bugzilla或其他方式来纪录和追踪BUG。这时我们开发会觉得一个这么小的问题还需要去纪录、去登记很不值得。然后，我们应当注意到这时修复一个BUG会涉及到测试人员资源的开销。
当进入了回归测试阶段，特别是多系统交互、跨团队合作时，一个BUG就会从一个人传到另一个人，从这个团队流到那个团队，这时成本就会逐渐增大。

最后，上线后，当一个奇怪的问题出现后，我们需要定位原因就更加困难重重了。
我曾经就经历这样一番：有用户发现游戏的道具减少了。我们一开始以为是某些运营配置、或者数据以及用户的等级限制所引发的，但在排除了各种业务的问题后，到最后却发现是PHP中array使用“+”运算而引发的血案！
在正常情况下，我们都知道array_merge()函数对于数值的下标则会追加并重新生成下标序列，即会合并；而数组+则会去掉相同下标的元素。

但实际情况下，线上BUG所产生的影响不在于排查和修复的时间成本，而在于在这段时间内所损失的金额、数据等成本。

当然，从测试的角度上看，测试并不能保证我们的系统没有BUG，只能说暂时未发现BUG。
单元测试也一样，作为开发人员，我们应当在最低成本的时期就及时发现我们直觉觉得可能会出现的问题并进行修复。
对我们亲手所编写的代码负责，并且用客观的方式来证明我们的代码目前未发现问题，而不是主观认为“我写的代码没有问题”。更不应该一次又一次地犯下各种低级或者重复的错误，而让团队其他成员对我们丧失信任。

PhalApi一直很注重单元测试，也很注重自动化，为了减轻大家重复编写单元测试骨架代码的痛苦，我们提供了一个可以生成单元测试代码的脚本。
假设我们有这么一个类：

<?php

class Api_Default extends PhalApi_Api {

        public function index() {
             //TODO
        }
}

那么，我们可以这样生成测试代码：

$ cd .//Demo/Tests
$ phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php
<?php
/**
 * PhpUnderControl_ApiDefault_Test
 *
 * 针对 ../Api/Default.php Api_Default 类的PHPUnit单元测试
 *
 * @author: dogstar 20150514
 */

require_once dirname(__FILE__) . '/test_env.php';

if (!class_exists('Api_Default')) {
    require dirname(__FILE__) . '/../Api/Default.php';
}

class PhpUnderControl_ApiDefault_Test extends PHPUnit_Framework_TestCase
{
    public $apiDefault;

    protected function setUp()
    {
        parent::setUp();

        $this->apiDefault = new Api_Default();
    }

    protected function tearDown()
    {
    }

    /**
     * @group testGetRules
     */
    public function testGetRules()
    {
        $rs = $this->apiDefault->getRules();
    }

    /**
     * @group testIndex
     */
    public function testIndex()
    {
        $rs = $this->apiDefault->index();
    }

}

温馨提示：

  1. 可以先执行：ln -s /path/to/PhalApi/phalapi-buildtest /usr/bin/phalapi-buildtest
  2. test_env.php为测试环境初始化文件，可以在里面引用init.php文件，并作一些调整
  3. 输出的测试代码可以重定向到./Demo/Tests/Api/Api_Default_Test.php，让测试代码与产品代码对齐

最后，我们就可以这样执行单元测试了：

$ phpunit ./Api_Default_Test.php

上一章 文档首页 下一章#[1.31]-新型计划任务：以接口形式实现的计划任务

上一章 文档首页 下一章

1.31.1 这里所说的计划任务

计划任务主要负责处理一些耗时的操作，或者非用户触发的作业。

有些人会称它为后台任务，或者推送作业，又或者定时任务。这时则统称为：计划任务。

例如，当你发布一条微信朋友圈后需要通知上百个好友时；当一条后台的推荐资讯需要推送到每个用户的客户端时；当需要将本地的静态资源如图片同步到CDN时。
显然这些动则需要分钟级别的操作，不应该在客户端调用接口时同步处理（但让我惊讶的是现实真的有人会这么做！），又或者非用户触发而需要后台处理（但更让我惊讶的是竟然也有系统是在用户请求时附带进行处理，而且还是国内某个知名的会员中心！）。

这里不仅仅是提供实现计划任务的约束和机制，更多的是引导大家更好地应对此类问题。

1.31.2 计划任务的关键环节

(1)触发

首先，是何时何地由何用户产生一条待执行的计划任务，我们可以把这个场景点称为一个触发点。
通常的做法，我们会先纪录下此触发点的场景信息，并放入到一个队列里面，以便等待计划任务消费。

(2)调度

其次，是通过何种机制进行计划任务的调度。
这里不仅有技术层面的问题，还有业务的问题，如每次批量处理多少，间隔多少，是否需要失败重试等等？

(3)消费

最后，则是具体的计划任务执行，以完成必要的操作，也称为消费。
很多传统的做法，都是把这些操作和接口混在一起的，而这里，PhalApi则会以一种更为明朗的方式来实现，从而自底而上，支持更多的调度方式和触发机制。

1.31.3 传统的计划任务

如果以一图而鳖之，上图虽然简化，但可以很好地说明传统计划任务的结构体系。
即：很多项目都是使用内嵌的方式来包含计划任务，这样明显会把接口服务系统和后台计划任务混在一起，增加了系统间的耦合性。
虽然小项目可以忍受或者适合这种混合，但是出于长远考虑，进行有意识地分解还是很有好处的。

而且这种混合潜意识下又让开发人员不加判断就进行调用，这会严重增加接口的反应时间。
我曾目睹一个接口耗时了近36秒之久，在对这个旧系统的接口进行一番排查后，原来是这个接口在发布后对上百个好友做了通知推送导致产生了上百条insert语句。

(1)传统的调度方式

我们重点关注一下传统计划任务的调度方式，在过去，我们通常会有两种方式：一种是启动死循环的进程，另一种是启动一个crontab之类的定时任务。
当然，上述的在接口请求时同步进行调度也算一种方式，但不是正规的做法。

如果采用死循环的方式，我们还需要考虑代码更新升级后，对脚本的重启，以便载入新的代码。如果是sh循环调用PHP脚本，则可以忽略。

1.31.4 新型的计划任务

(1)以接口的形式提供计划任务服务

PhalApi中最具特色的做法是，将计划任务的执行消费实现，以接口形式来提供。
这样的好处在于，我们作为接口开发人员，可以以熟悉的方式来进行计划任务的开发。
但更大的得益在于，将计划任务通过接口的形式提供后，我们会看到更为广阔的使用场景：我们可以使用MQ队列消费，可以同步请求也可以异步请求。

(2)系统架构

我们所做的，不仅仅只是把原来混合型的代码作简单分解，如下： a pic

而是以一种更为正统的做法，为此我们添加了一些必要的节点来设计此构架。新的实现方式下的体系结构如下：
a pic

节点说明

在上图中，应用节点还是我们的接口系统；MQ队列则是用于存放待消费的场景信息，同其他的MQ一样；计划任务则可以分为两部分，API接口实现和任务调度。
计划任务这两部分，物理部署上可以合在一起，也可以分开，这取决于应用系统是采用分布式的做法，还是单一的服务器。

执行流程

由上图可以看出，一个完整的计划任务流程为：

  • 1、应用产生一条新的计划任务，并存放于MQ队列
  • 2、计划任务定时或者不停扫描新的计划任务；若有，则进行调度
  • 3、计划任务API完成需要的工作，并将结果返回调度器

(3)单个添加，批量处理

这里只支持单个MQ添加，而处理则是批量的，且每批处理的数据可指定配置。

(4)MQ共享

无论是分布式还是本地一体化，MQ队列都应该是可以共享访问的，以便为应用节点、计划任务调度节点所访问，如下图所示： a pic

首选redis MQ

因为MQ作为频繁读写的媒介，应该优先使用高效缓存来提高系统的吞吐率以及增加并发的能力。此外，作为临时一次性的数据，使用高效缓存也是大有好处的（但我们也需要考虑到数据丢失的情况）。
而且，为了支持 单个添加，批量处理，第三方缓存应该很好地支持队列的操作。
所以，redis是一个不错的选择。

如下，是redis简单的队列操作：

$redis = new Redis();
$redis->connect('127.0.0.1', 6300);

$redis->lpush('test_key', 'www');
$redis->lpush('test_key', 'phalapi');
$redis->lpush('test_key', 'net');

echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";

数据库MQ

如果考虑到redis扩展不好安装，或者应用喜欢使用数据库来存放MQ，也是可以的。只需要用SQL的一些基本的操作语句便可做到FIFO。

文件MQ

文件MQ也是一种方式，但很少使用。

(5)更丰富的调度方式

接口同步调度

虽然也是同步调度，但是我们将计划任务隔离后，便于日后发现此同步的计划任务影响到接口的响应时间时，可以及时轻松地切换到后台异步处理的方式。

回归传统的调度

我们也可以沿用传统的做法，即使用死循环的脚本调度，或者crontab类的定时任务。

MQ队列消费

既然我们以接口服务的形式提供计划任务的操作，那么可以把同一接口的调度放置到同一队列中进行维护和消费。

接口异步调度

当计划任务以接口服务提供后，我们可以使用另一种免MQ的做法，即使用接口的异步调度。如下：
a pic

这样既可以避免死循环带来的性能负载问题，也可以避免定时任务带来的延时问题，可以说异步调度是一种折中完美的做法。
但这也可能是一种不负责任或者不安全的做法，因为我们无法跟进异步计划任务的结果。

本地调度和远程调度

本地调度是指在执行过程中构建模拟接口的调用而无须经过网络请求，远程调度则是通过远程接口请求来实现。
如果把本地调度和远程调度，跟同步/异步组合起来，我们可以得到以下三种有意义的组合：

  • 本地同步调度
  • 远程同步调度
  • 远程异步调度

(6)计划任务的划分

service即类型

明显地，接口服务名称service即可作为计划任务划分的依据。

不同的service作为不同的队列，不同类型的计划任务；而相同的service则作为相同的队列相同的计划任务。

接口参数即参数

接口参数即可计划任务执行时所需要的上下文信息。

1.31.5 PhalApi中计划任务的核心设计解读

(1)桥接模式 - 数据与行为独立变化

为了给计划任务一个执行的环境，我们提供了 计划任务调度器 ，即：Task_Runner。
每个计划任务需要调度的接口是不一样的，即不同的接口服务决定不同的行为；每个行为需要的数据也不一样，即不同的接口参数决定不同的数据。

自然而言的，Task_Runner按照桥接模式，其充当的角色如下：
a pic

然后，我们就可以这样各自实现： a pic

(2)适配器模式 - 对象适配器和类适配器

在对MQ进行实现时，我们提供的Redis MQ队列、文件MQ队列和DB MQ队列，都使用了适配器模式，以重用框架已有的功能。
其中，Redis MQ队列和文件MQ队列是属于对象适配器，DB MQ队列是类适配器。 对于对象适配器，我们也提供了外部注入，以便客户端在使用时可以轻松定制扩展，当然也可以使用默认的缓存。

如下： a pic

这样以后，我们可以这样根据创建不同的MQ队列：

//Redis MQ队列
$mq = Task_MQ_Redis();
//或
$mq = Task_MQ_Redis(new PhalApi_Cache_Redis(array('host' => '127.0.0.1', 'port' => 6379)));

//文件MQ队列
$mq = new Task_MQ_File();
//或
$mq = new Task_MQ_File(new PhalApi_Cache_File(array('path' => '/tmp/cache')));

//DB MQ队列
$mq = new Task_MQ_DB();

//Array MQ队列
$mq = new Task_MQ_Array();

(3)模板方法 - 本地和远程两种调度策略

在完成底层的实现后，我们可以再来关注如何调度的问题，目前可以有本地调度和远程调度两种方式。

  • 本地调度：是指本地模拟接口的请求，以实现接口的调度
  • 远程调度：是指通过计划任务充当接口客户端，通过请求远程服务器的接口以完成接口的调度

为此，我们的设计演进成了这样： a pic

上图多了两个调度器的实现类，并且远程调度器会将远程的接口请求功能委托给连接器来完成。

(4)设计审视

好了！让我们再回头审视这样的设计。

首先，我们在高层，也就是规约层得到了很好的约定。
不必过多地深入理解计划任务内部的实现细节，我们也可以轻松得到这样的概念流程：
计划任务调度器(Task_Runner)从MQ队列(Task_MQ)中不断取出计划任务接口服务(PhalApi_Api)进行消费。

再下一层，则是具体的实现，即我们所说的实现层。
客户可以根据自己的需要进行选取使用，他们也可以扩展他们需要的MQ。重要的是，他们需要自己实现计划任务的接口服务。

根据爱因斯坦说的，要保持简单，但不要过于简单。
所以，为了更好地理解计划任务的运行过程，我们提供了简单的时序图：
a pic

上图主要体现了两个操作流程：加入MQ和MQ消费。
其中，注意这两个流程是共享同一个MQ的，否则不能共享数据。同时调度是会进行循环式的调度，并且穷极之。

(5)没有引入工厂方法的原因

我们在考虑是否需要提供工厂方法来创建计划任务调度器，或者MQ。
但我们发现，设计是如此明了，不必要再引入工厂方法来增加使用的复杂性，因为存在组合的情况。而且，对于后期客户端进行扩展也不利。

当我们需要启动一个计划任务时，可以这样写：

$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);

$runner->go('MyTask.DoSth');

上面简单的组合可以有：4种MQ * 2种调度 = 8种组合。

所以，我们最后决定不使用工厂方法，而是把这种自由组合的权利交给客户端。

(6)失败重试与并发问题

除了对计划任务使用什么模式进行探讨外，我们还需要关注计划任务其他运行时的问题。

一个考虑的是失败重试，这一点会发生在远程调度中，因为接口请求可能会超时。这时我们采用的是失败轮循重试。
即，把失败的任务放到MQ的最后，等待下一批次的尝试。连接器在进行请求时，也会进行一定次数的超时重试。这里主要是为了预防接口服务器崩溃后的计划任务丢失。

另一个则是并发的问题。这里并没有过多地进行加锁策略。
而是把这种需要的实现移交给了客户端。因为加锁会使得计划任务更为复杂，而且有时不一定需要使用，如一个计划任务只有一个进程时，也就是单个死循环的脚本进程的情况。

(7)客户端的使用

最后，客户端的使用就很简单了：

$mq = new Task_MQ_Redis();
$taskLite = new Task_Lite();

$taskLite->add('MyTask.DoSth', array('id' => 888));

上一章 文档首页 下一章#[2.11]-核心思想：DI依赖注入-让资源更可控

上一章 文档首页 下一章

一个没有绝对答案的世界，却拥有绝对的丰富。 --《沈奇岚：我愿生命从容》

2.11.1 定义

(1) 关于依赖注入

即控制反转，目的是了减少耦合性，简单来说就是使用开放式来获取需要的资源。

(2) 关于资源

这里说的资源主要是在开发过程中使用到的资源，包括配置项；数据库连接、Memcache、接口请求等系统级的服务；以及业务级使用到的实例等。

引入依赖注入的目的不仅是为了增加一个类，而是为了更好的对资源进行初始化、管理和维护。下面将进行详细的说明。

2.11.2 一个简单的例子

很多时候，类之间会存在依赖、引用、委托的关系，如A依赖B时，可以这样使用：

class A {
    protected $_b;

    public function __construct()
    {
        $this->b = new B();
    }
}

这种方式在A内限制约束了B的实例对象，当改用B的子类或者改变B的构建方式时，A需要作出调整。这时可以通过依赖来改善这种关系：

class A {
    protected $_b;

    public function __construct($b)
    {
        $this->b = $b;
    }
}

再进一步，可以使用DI对B的对象进行管理：

class A {
    public function __construct()
    {
    }

    public function doSth()
    {
        //当你需要使用B时
        $b = $di->get('B');
    }
}

这样的好处？

一方面，对于使用A的客户（指开发人员），不需要再添加一个B的成员变量，特别不是全部类的成员函数都需要使用B类服务时。另一方面在外部多次初始化A实例时，可以统一对B的构建。

2.11.3 依赖注入的使用示例

为方便使用，调用的方式有：set/get函数、魔法方法setX/getX、类变量$fdi->X、数组$fdi['X']，初始化的途径有：直接赋值、类名、匿名函数。

/** ------------------ 创建与设置 ------------------ **/
//获取DI
$di = DI();
//演示的key
$key = 'demoKey';

/** ------------------ 设置 ------------------ **/
//可赋值的类型：直接赋值、类名赋值、匿名函数
$di->set($key, 'Hello DI!');
$di->set($key, 'Simple');
$di->set($key, function(){
    return new Simple();
});
//设置途径：除了上面的set()，你还可以这样赋值
$di->setDemoKey('Hello DI!');
$di->demoKey = 'Hello DI!';
$di['demoKey'] = 'Hello DI!';

/** ------------------ 获取 ------------------ **/
//你可以这样取值
echo $di->get('demoKey'), "\n";
echo $di->getDemoKey(), "\n";
echo $di->demoKey, "\n";
echo $di['demoKey']. "\n";

/**
 * 演示类
 */
class Simple
{
    public function __construct()
    {
    }
}

2.11.4 依赖注入的好处

(1)减少对各个类编写工厂方法以单例获取的开发量

DI相当于一个容器，里面可以放置基本的变量，也可以放置某类服务，甚至是像文件句柄这些的资源。在这容器里面，各个被注册的资源只会存在一份，也就是当被注册的资源为一个实例对象时，其效果就等于单例模式。

因此，保存在DI里面的类，不需要再编写获取单例的代码，直接通过DI获取即可。

例如很多API的服务组件以及其他的一些类，都实现了单例获取的方式。分别如：

微博接口调用：

<?php
class Weibo_Api
{
    protected static $_instance = null;

    public static function getInstance()
    {
        if (!isset(self::$_instance)) {
            self::$_instance = new Weibo_Api();
        }
        return self::$_instance;
    }

    //....
}

七牛云存储接口调用：

class Qiniu_Api {
    private static $_instance = null; //实例对象

    public static function getInstance()
    {
        if (self::$_instance ===null) {
            self::$_instance = new Qiniu_Api();
        }
        return self::$_instance;
    }
}

QQ开放平台接口调用：

class QQ_Api {
    private static $_instance = null; //实例对象

    public static function getInstance()
    {
        if (self::$_instance ===null) {
            self::$_instance = new QQ_Api();
        }
        return self::$_instance;
    }
}

如果使用DI对上面这些服务进行管理，则上面三个类乃至其他的类对于单例这块的代码都可以忽略不写。注册代码如下：

$di->sStockApi = 'Weibo_Api';
$di->sDioAopi = 'Qiniu_Api';
$di->sShopApi = 'QQ_Api';

上面是通过类名来进行延迟加载，但需要各个类提供public的无参数的构造函数。如果各个服务需要进行初始化，可以将初始化的工作放置在onInitialize()函数内，DI在对类实例化时会回调此函数进行初始化。

(2)统一资源注册，便于后期维护管理

这里引入DI，更多是为了“一处创建，多处使用”， 而不是各自创建，各自使用。

创建和使用分离

考虑以下场景：假设有这样的业务数据需要缓存机制，所以可注册一个实现缓存机制的实例：

$di->set('cache', new FileCache());

然后提供给多个客户端使用：

$di['cache']->set('indexHtml', $indexContent);   //缓存页面
$di['cache']->set('config', $config);  //缓存公共配置
$di['cache']->set('artistList', $artistList);   //缓存数据

当需要切换到MC或者Redis缓存或者多层缓存时，只需要修改对缓存机制的注入即可，如：

$di->set('cache', new RedisCache());

依赖注入的一个很大的优势就在于可以推迟决策，当需要用到某个对象时，才对其实例化。可以让开发人员在一开始时不必要关注过多的细节实现，同时也给后期的扩展和维护带来极大的方便。

再上一层，假设未来我们需要更高级的缓存服务，那么我们可以在不影响客户端使用的情况下，轻松升级。
未来的可配置化的多级缓存策略

以下是一个模拟的使用场景，但依然对现在的项目有一定的帮助。假设我们现在有一个MC集群的缓存且引入了DI，使用如下：

<?php

//初始化
$di = PhalApi_DI::one();
$di->cache = new Memcache();
$di->cache->connect('localhost', 11211);

//不同文件的多处使用 ...
echo $di->cache->get('key');
echo $di->cache->get('key2');
echo $di->cache->get('key3');
...

假设现在发现一层缓存存在穿透情况，为保证服务器的稳定性，我们已开发实现了多层缓存策略，并且可以通过简单配置即可实现，只需要对DI容器里面的cache实例进行升级，其他客户端的调用即可马上享受到缓存升级的优质服务。升级涉及改动的代码如下：

<?php

//初始化
$di = new PhalApi_DI();
$di->cache = function () {
$ultraFastFrontend = new DataFrontend(array(
    "lifetime" => 3600
));

$fastFrontend = new DataFrontend(array(
    "lifetime" => 86400
));

$slowFrontend = new DataFrontend(array(
    "lifetime" => 604800
));

return new Multiple(array(
    new ApcCache($ultraFastFrontend, array(
        "prefix" => 'cache',
    )),
    new MemcacheCache($fastFrontend, array(
        "prefix" => 'cache',
        "host" => "localhost",
        "port" => "11211"
    )),
    new FileCache($slowFrontend, array(
        "prefix" => 'cache',
        "cacheDir" => "../app/cache/"
    ))
));
};

备注：关于多级缓存策略，后续会提供源代码和重用库，或者期待读者的分享。

(3)延迟式加载，提高性能

延迟加载可以通过DI中的类名初始化、匿名函数和参数配置（未实现）三种方式来实现。

延迟加载有时候是非常有必要的，如在初始化项目的配置时，随着配置项的数据增加，服务器的性能也将逐渐受到影响，因为配置的内容可能是硬编码，可能来自于数据库，甚至需要通过接口从后台调用获取， 特别当很多配置项不需要使用时。而此时，支持延时加载将可以达到很好的优化，而不用担心在需要使用的时候忘记了初始化。从而很好的提高服务器性能，提高响应速度。

如对一些耗时的资源先进行匿名函数的初始化：

$di['hightResource'] = function() {
    //获取返回耗性能的资源
    //return $resource;
}

(4)以优雅的方式取代滥用的全局变量

在我看来，PHP里面是不应该使用全局变量（global和$_GLOBALS），更不应该到处使用。

用了DI来管理，即可这样注册：

$di->set('debug', true);

然后这样使用：

$debug = $di->get('debug');

也许有人会想：仅仅是换个地方存放变量而已吗？其实是换一种思想使用资源。

以此延伸，DI还可用于改善优化另外两个地方：通过include文件途径对变量的使用和变量的多层传递。

变量的多层传递，通俗来说就是漂洋过海的变量。

2.11.5 DI思想的来源与推荐参考

Dependency Injection/Service Location

上一章 文档首页 下一章#[2.12]-海量数据：可配置的分库分表

上一章 文档首页 下一章

2.12.1 背景

为了应对产品海量用户的愿景需求，这里将设计一个分布式的数据库存储方案，以便能满足数据量的骤增、云服务的横向扩展、后台接口开发的兼容性，以及数据迁移等问题，避免日后因为全部数据都存放在单台服务器上的限制。

2.12.2 主要思想

  • 1、分库分表
  • 2、路由规则
  • 3、扩展字段
  • 4、可配置
  • 5、SQL语句自动生成

(1)分库分表

是指将不需要进行必要关联查询的表分开存放，如存放事件推送的weili_event_pushto和存放标签的weili_tag；同时，对于 同一个表，因为存放的数据量是可预见式的暴增，如上述的weili_event_pushto，每时每刻都会产生大量的来自用户发布的事件，因此为了突破 MySQL单表的限制以及其他问题，需要将此表同时创建N份。

(2)路由规则

在上面进行了分库分表后，开发人员在读取时，就需要根据相应的规则找到对应 的数据库和数据库表，这里建议每个表都需要有int(11)类型的id字段，以便作为分表的参考。

(3)扩展字段

在完成了分库分表和制定路由规则后，考虑到日后有数据库的DB变更，为减少DB变更对现有数据库表的影响，这里建议每个表都增加text类型的extra_data字段，并且使用json格式进行转换存储。

(4)可配置

在有了N台数据库服务器以及每个表都拆分成M张表后，为减少后台接口开发人员的压力，有必须在后台接口框架提供可配置 的支持。即：数据库的变更不应影响开发人员现有的开发，也不需要开发人员作出代码层面的改动，只需要稍微配置一下即可。关于这块，请见下面的框架实现部 分。

(5)SQL语句自动生成

对于相同表的建表语句，可以通过脚本来自动生成，然后直接导入数据即可。

2.12.3 PhalApi框架的实现方案

PhalApi框架主要需要实现的是路由这一层的映射，并且通过可配置的方式进行控制，同时还应支持生产环境和测试环境的异同，如在测试环境我们明显不需要1000张数据库的表。为此，需要提供一种 表名 + id 映射到 数据库服务器 + 具体哪张表 的规则。 show 如上图所示，表名会统一加上前缀，并且将id按一定的表总数进行取模，最后再根据得到的具体表名，通过映射表查找到对应 的数据库服务器进行操作。其中，model层为开发实现，数据库表的映射由接口框架实现支持。

2.12.4 使用示例

(1)配置数据库的路由配置

修改./Config/dbs.php文件，以下是参考的示例配置。其中servers为DB服务器，包括数据库的账号信息等，tables为数据库表的映射关系，其中default下标为缺省的数据库路由。

在每个数据库表里面，可以配置多个数据库表，通过开始的下标start和结束的下标end来对表进行分布式存放，并且如果没有start和end的，则视为不需要拆分存放，同时也是当找不到合适时的拆分表时所采用的默认配置。

return array(
    /**
     * avaiable db servers
     */
    'servers' => array(
        'db_demo' => array(
            'host'      => 'localhost',             //数据库域名
            'name'      => 'test',                  //数据库名字
            'user'      => 'root',                  //数据库用户名
            'password'  => '123456',                //数据库密码
            'port'      => '3306',                  //数据库端口
        ),
    ),

    /**
     * custom table map
     */
    'tables' => array(
        '__default__' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
            ),
        ),
        'demo' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 2, 'db' => 'db_demo'),
            ),
        ),
    ),
);

上面示例配置的意思是：

表名   DB服务器
  tbl_demo    db_demo
  tbl_demo_0      db_demo
  tbl_demo_1      db_demo
  tbl_demo_2      db_demo

(2)准备需要创建表的基本SQL语句

这里说的基本SQL语句是指：仅是这个表所特有的字段，排除已固定公共有的自增主键id，和扩展字段ext_data。下面是一个示例：

`name` varchar(11) DEFAULT NULL,

(3)生成并导入SQL语句

由于拆分后的数据库表数量众多，这里提供了一个快捷的脚本工具来生成所需要创建的数据库表。

$ php ./build_sqls.php
Usage: ./build_sqls.php <table> [engine=InnoDB]

执行上面的脚本，输入数据库表参数后：

 php ./build_sqls.php demo

将会从配置文件 里面寻找所需要创建的表，并生成类似以下的SQL语句：

/**
 * DB: localhost  db_demo
 */

CREATE TABLE `demo` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/**
 * DB: localhost  db_demo
 */

CREATE TABLE `tpl_demo_0` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_1` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_2` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(4)使用与代码开发

在将上面的SQL语句导入数据库后，即可以像之前那样操作数据库。下面是一些示例：

DI()->notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()->config->get('dbs'), true);

DI()->notorm->demo->where('id', '1')->fetch();

用到了拆分表的代码示例，假设event表被拆分成了3个表，则客户端在调用里，需要根据（id % 3 ）来拼接合适的数据库表名，其他使用不变。

DI()->notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()->config->get('dbs'), true);

$row = DI()->notorm->demo_0->where('id', '3')->fetch();
$row = DI()->notorm->demo_1->where('id', '10')->fetch();
$row = DI()->notorm->demo_2->where('id', '2')->fetch();

使用Model基类的情况

更好的写法，应该是继承于PhalApi_Model_NotORM，并统一实现分表的操作，如：

<?php

class Model_Demo extends PhalApi_Model_NotORM {

    protected function getTableName($id) {
        $tableName = 'demo';
        if ($id !== null) {
            $tableName .= '_' . ($id % 3);
        }
        return $tableName;
    }
}

然后，上面的查询分别对应：

$model = new Model_Demo();

$row = $model->get('3', 'id');
$row = $model->get('10', 'id');
$row = $model->get('2', 'id');

更进一步，我们可以通过$this->getORM($id)来获取分表的实例进行分表的操作，如：

<?php

class Model_Demo extends PhalApi_Model_NotORM {

    //... ...

    public function getNameById($id) {
        $row = $this->getORM($id)->select('name')->fetchRow(); //假设$id为3，则 $this->getORM($id) 等效于 DI()->notorm->demo_0
    return !empty($row) ? $row['name'] : '';
    }

}

2.12.5 多个数据库的配置方式

当需要使用多个数据库时，可以先在servers中可以配置多组数据库的信息，然后在tables为不同的数据库表指定不同的数据库服务器。

假设我们有两台数据库服务器，分别叫做db_A、db_B，即：

return array(
    /**
     * DB数据库服务器集群
     */
    'servers' => array(
        'db_A' => array(                              //db_A
            'host'      => '192.168.0.1',             //数据库域名
            // ... ...
        ),
        'db_B' => array(                              //db_B
            'host'      => '192.168.0.2',             //数据库域名
            // ... ...
        ),
    ),

    //... ...

若db_A服务器中的数据库有表a_table_user、a_table_friends，而db_B服务器中的数据库有表b_table_article、b_table_comments，则：

<?php
return array(

    //... ...

    /**
     * 自定义路由表
     */
    'tables' => array(
        //通用路由
        '__default__' => array(
            'prefix' => 'a_',  //以 a_ 为表前缀
            'key' => 'id',
            'map' => array(
                array('db' => 'db_A'),  //默认，使用db_A数据库
            ),
        ),

        'table_article' => array(                                     //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_article表使用db_B数据库
            ),
        ),

        'table_comments' => array(                                    //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_comments表使用db_B数据库
            ),
        ),
    ),

如果项目存在分表的情况，可结合上述的分表的说明进行配置。
这里为了让大家更为明了，假设db_A服务器中的数据库有表a_table_user、a_table_friends_0到a_table_friends_9（共10张表）， 而db_B服务器中的数据库有表b_table_article、b_table_comments_0到b_table_comments_19（共20张表），则结合起来的完整配置为：

<?php
return array(
    /**
     * DB数据库服务器集群
     */
    'servers' => array(
        'db_A' => array(                              //db_A
            'host'      => '192.168.0.1',             //数据库域名
            // ... ...
        ),
        'db_B' => array(                              //db_B
            'host'      => '192.168.0.2',             //数据库域名
            // ... ...
        ),
    ),

    /**
     * 自定义路由表
     */
    'tables' => array(
        //通用路由
        '__default__' => array(
            'prefix' => 'a_',  //以 a_ 为表前缀
            'key' => 'id',
            'map' => array(
                array('db' => 'db_A'),  //默认，使用db_A数据库
            ),
        ),

        'table_friends' => array(                                     //分表配置
            'prefix' => 'a_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_A'),                                // b_table_comments表使用db_B数据库
                array('start' => 0, 'end' => 9, 'db' => 'db_A'),      //分表配置（共10张表）
            ),
        ),

        'table_article' => array(                                     //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_article表使用db_B数据库
            ),
        ),

        'table_comments' => array(                                    //表b_table_article
            'prefix' => 'b_',                                         //表名前缀
            'key' => 'id',                                            //表主键名
            'map' => array(                                           //表路由配置
                array('db' => 'db_B'),                                // b_table_comments表使用db_B数据库
                array('start' => 0, 'end' => 19, 'db' => 'db_B'),     //分表配置（共20张表）
            ),
        ),
    ),
);

2.12.6 与主从数据库的有机结合

虽然这是专门为海量数据设计的存储方案，但也是可以结合主从配置来获得更庞大强壮的方案，当然为之付出的是复杂性的引入。

简单地，可以将dbs.php复制一份dbs_slave.php出来给从库使用，然后注册一个从库的服务：

DI()->slaveNotorm = new PhalApi_DB_NotORM(DI()->config->get('slave_dbs'));

最后，在需要使用从库来读取时，使用slaveNotorm 服务即可。

2.12.7 不足与注意点

这样的设计是有明显的灵活性的，因为在后期如果需要迁移数据库服务器，我们可以在框架支持的情况下轻松应对，但依然需要考虑到一些问题和不足。

(1)DB变更

DB变更，这块是必不可少的，但一旦数据库表被拆分后，表数量的骤增导致变更执行困难，所以这里暂时使用了一个折中的方案，即提供了一个ext_data 扩展字段用于存放后期可能需要的字段信息，建议采用json格式，因为通用且长度比序列化的短。但各开发可以根据自己的需要决定格式。即使如此，扩展字段 明显做不到一些SQL的查询及其他操作。

(2)表之间的关联查询

表之间的关联查询，这个是分拆后的最大问题。虽然这样的代价是我们可以得到更庞大的存储设计， 而且很多表之间不需要必须的关联的查询，即使我们需要，我们也可以通过其他手段如缓存和分开查询来实现。这对开发人员有一定的约束，但是对于可预见性的海 量数量，这又是必须的。

上一章 文档首页 下一章

[2.13]-接口调试：在线SQL语句查看与性能优化

上一章 文档首页 下一章

后台接口绝大数情况下，都需要与数据库进行交互，以获取业务数据或者接收保存客户端上报的数据。为方便后台开发同学进行调试，以及实时查看全部执行的SQL语句，这里简单地对全部执行的SQL语句进行调试模式下输出。

2.13.1 开启SQL调试

开启调试模式很简单，但这里和通常的框架不一样，我们不是全部统一地开启调试模式，因为在接口正常调用情况下返回非法的JSON会导致接口结果解析失败。故我们通过添加调试参数来控制是否开启SQL调试。如下：

//$vim ./Public/init.php

//数据操作 - 基于NotORM
DI()->notorm = function() {
    $debug = isset($_GET['debug']) ? true : false;
    return new PhalApi_DB_NotORM(DI()->config->get('dbs'), $debug);
};

特别注意： 通常，我们的调试参数不应都简单地使用&debug=1，而是各自定义，如 复杂一点：&__phalapi_debug__=1 ，或者再添加一个简单的验签，额外带个参数校验，如：&phalapisign__=202cb962ac59075b964b07152d234b70。减少暴露SQL的风险。

2.13.2 调试示例

回到前面获取用户基本信息接口 /demo/?service=User.GetBaseInfo 的示例。

(1)正常情况下

请求：

http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=1

返回：

{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}

(2)带&debug=1调试下

请求：

http://dev.phalapi.com/demo/?service=User.GetBaseInfo&user_id=1&debug=1

返回：

[1 - 0.00057s]SELECT * FROM tbl_user WHERE (id = ?); -- 1
{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}

2.13.3 一个错误的接口开发

有时，在进行接口开发时，会需要进行批量获取的功能，如列表。但很多开发的同学可能会因为时间赶或者没有意识去对SQL查询进行优化，或者甚至不知道自己的接口背后隐藏着多少问题。下面是一个错误的开发示例。

(1)新增的批量获取接口

假设我们在开发一个国际的项目，并且运行良好，BOSS说因业务需要，要加多一个接口以支持批量获取用户的基本信息，提供给国外某知名的社交平台调用。

于是乎，我们很快就根据原来的单个获取接口实现了新的接口：

//$vim ./Demo/Api/User.php
<?php

class Api_User extends PhalApi_Api {

    public function getRules() {
        return array(
            //...
            'getMultiBaseInfo' => array(
                'user_ids' => array('name' => 'user_ids', 'type' => 'array', 'format' => 'explode', 'require' => true),
            ),
        );
    }

    //...

    public function getMultiBaseInfo() {
        $rs = array('code' => 0, 'msg' => '', 'list' => array());

        $domain = new Domain_User();
        foreach ($this->user_ids as $userId) {
            $rs['list'][] = $domain->getBaseInfo($userId);
        }

        return $rs;
    }
}

(2)运行调用一下

显然，我们可以很清楚地调用新增的接口：

http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3

可返回：

{
    "ret": 200,
    "data": {
        "code": 0,
        "msg": "",
        "list": [
            {
                "id": "1",
                "name": "dogstar",
                "note": "oschina"
            },
            {
                "id": "2",
                "name": "Tom",
                "note": "USA"
            },
            {
                "id": "3",
                "name": "King",
                "note": "game"
            }
        ]
    },
    "msg": ""
}

假设我们已经有了这样的数据库表数据：

INSERT INTO `tbl_user` VALUES ('1', 'dogstar', 'oschina');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', 'USA');
INSERT INTO `tbl_user` VALUES ('3', 'King', 'game');

(3)这样的问题？

这样的问题，在对外黑盒调用的客户端同学是发现不了的，对于测试人员来说也是无法感知的。但所犯的错误也是显然易见的，就是没有进行SQL的批量查询优化，造成了很多不必要的重复查询。
这里，根据后台接口开发人员提供的调试参数（假设为：&debug=1），则我们可以快速发现存在的问题：

http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&user_ids=1,2,3&debug=1

如下返回，我们看到了很多重复类似的查询语句。

[1 - 0.0005s]SELECT * FROM tbl_user WHERE (id = ?); -- 1
[2 - 0.00042s]SELECT * FROM tbl_user WHERE (id = ?); -- 2
[3 - 0.00038s]SELECT * FROM tbl_user WHERE (id = ?); -- 3
{"ret":200,"data":{"code":0,"msg":"","list":[{"id":"1","name":"dogstar","note":"oschina"},{"id":"2","name":"Tom","note":"USA"},{"id":"3","name":"King","note":"game"}]},"msg":""}

上面输出的调试信息，简单补充一个格式：

[序号 - 所耗时间]SQl语句 -- [参数1, 参数2]

(4)如何改进？

这是一个很基本的问题，当然在实际项目中不会普通存在，这里只是作为一个示例加以说明。但让人失望的是，实际项目确实存在为数不少的这样的情况。可能是新人的技术和意识问题，也有可能是老同学的态度问题。所以，优化这么一个接口的批量SQL查询不难，难的是如何才能让新、老同学都注重这块的SQL查询优化呢？而不是等到线上服务器异常崩溃后再来推托责任。
具体的代码改进，留给读者自己实践了。毕竟，看了，实践了，才会真正深刻地掌握。

2.13.4 由此引申

  • 这里不专门讲述SQL的优化，但也顺便提供一些SQL查询优化的建议：
  • 使用批量查询，而不是N次循环查询！
  • 重复的数据，不要重复获取；
  • 根据需要，按需要获取表字段，而不是SELECT *；
  • 针对频繁的搜索字段，建立必要的索引，以加快查询速度；
  • 使用关联查询，而不是粗暴地类似：where uid IN (... 这里是成千上W个用户ID ...)；
  • 针对单条SQL语句执行时间超过1秒的，重点优化；

2.13.5 最后最后

奉上我们坚持TDD开发下的单元测试代码：

    public function testGetMultiBaseInfo()
    {
        $str = 'service=User.GetMultiBaseInfo&user_ids=1,2,3';
        parse_str($str, $params);

        DI()->request = new PhalApi_Request($params);

        $api = new Api_User();
        //自己进行初始化
        $api->init();
        $rs = $api->getMultiBaseInfo();

        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('msg', $rs);
        $this->assertArrayHasKey('list', $rs);

        foreach ($rs['list'] as $item) {
            $this->assertArrayHasKey('id', $item);
            $this->assertArrayHasKey('name', $item);
            $this->assertArrayHasKey('note', $item);
        }
    }

执行单元测试的效果：

dogstar@ubuntu:Tests$ phpunit --filter testGetMultiBaseInfo ./Api/Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

.

Time: 23 ms, Memory: 6.25Mb

OK (1 test, 13 assertions)

搞定，收工，开饭！

上一章 文档首页 下一章#[2.14]-测试驱动开发：意图导向编程下的接口开发

上一章 文档首页 下一章

有的时候宁愿付钱让你一周在床上待着，也不想让你用这周剩下的时间去调试你在周一所写的代码。 --丹·所罗门

2.14.1 测试驱动开发

做正确的事，比把事情做正确更为重要。

当明确需要做何事后，再通过事先编写单元测试来准确表达我们将要实现的功能，是相当具有指导意义的。你会发现接下来你的开发历程就是：单元测试-设计-重构，而且这种正向循环是很有创造性的，并且进行到一定程度后会慢慢体会到浮现式设计的乐趣。

关于测试驱动开发TDD，有很多资料已进行了说明，这里不再赘述。

如果还没了解PHPUnit，可先阅读：PHPUnit 手册
如果还没了解PHPUnit，可先阅读：PHPUnit 手册
如果还没了解PHPUnit，可先阅读：PHPUnit 手册

2.14.2 意图导向编程

在编写代码前，先写测试代码，更容易提高 关注点 。

因为，在开发过程中， 大多时候会被外界打断（如需求沟通、线上问题处理、临时会议等），而通过单元测试则可以让你“几乎忘却需要做什么”的情况下重新让你回到之前的状态，特别在并行开发多个不同项目的需求时尤其重要。

除此之外，遵循“红-绿-重构”这样的流程，我们可以在更高的层面关注需要实现的功能需求，并自顶而下地进行设计优化，精益代码。

2.14.3 编写测试的原则、模式和指导

首先应该意识到，测试代码和生产代码一样重要。其次，测试代码也应该和生产代码一样被同步维护更新，这样才能保持生气，更大地发挥作用。只有当不断地对测试的代码进行修修补被，我们才能保持自动化测试这张“安全网”常新。

2.14.4 F.I.R.S.T.原则

  • 快速 Fast
  • 独立 Independent
  • 可重复 Repeatable
  • 自足验证 Self-validating
  • 及时 Timely

2.14.5 构造-操作-检验（BUILD-OPERATE-CHECK）模式

这个模式也可以理解成：“当... 做...应该...”。其中，构造包括测试环境的搭建、测试数据前期的准备；操作是指对被测试对象的调用， 以及被测试对象之间的通信和协助交互；最后检验则是对业务规则的断言、对功能需求的验证。

2.14.6 如何编写高效测试代码

  • 1、与产品代码分开，与测试代码对齐
  • 2、利用测试骨架（phpunit-skelgen或者自定义生成器）自动生成测试代码
  • 3、使用测试替身、测试桩构建昂贵资源、制造异常情况
  • 4、每个测试一个概念

2.14.7 PhalApi开发下的单元测试

我们推荐在各自的项目代码中平行编写单元测试，并逐渐完善、保持同步。以下是进行单元测试的参考。

(1)Api接口层的单元测试

Api接口层，是我们后台开发的主要切入点，也是直接对外提供服务的入口，属于更高层次的概念并拥有指定的业务功能，更是后台开发的关注点。所以在对新接口进行开发前，编写单元测试是非常有意义的。

为了可以自动生成测试代码，我们可以先简单定义好接口的函数签名（以获取用户基本信息接口为例）：

//$ vim ./Demo/Api/User.php
<?php

class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }

}

随后，自动生成测试代码骨架：

$ mkdir ./Demo/Tests/Api -p
$ cd ./Demo/Tests/Api
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php > ./Demo/Tests/Api/Api_User_Test.php

根据接口的需要，验证接口返回的格式，以及业务数据的正确性。

//$ vim ./Demo/Tests/Api/Api_User_Test.php
    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&user_id=1';

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url);

        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('msg', $rs);
        $this->assertArrayHasKey('info', $rs);

        $this->assertEquals(0, $rs['code']);

        $this->assertEquals('dogstar', $rs['info']['name']);
        $this->assertEquals('oschina', $rs['info']['note']);
    }

上面的验证意思简单明了，结合 构造-操作-检验（BUILD-OPERATE-CHECK）模式 加以说明一下。

构造：构建请求URL

        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&user_id=1';

此参数即对应接口请求的URL参数，我们将此参数追加在接口入口并在浏览器打开可以得到同样的接口执行效果。但这样的好处更在于通过单元测试帮我们记住了各种接口测试的业务场景。而不再是像以前那样打开N个浏览器窗口人工进行调试，也不用像以前那样苦苦寻找浏览器记录。

如果接口需要POST数据，或者其他更多参数，可以使用$params来传递更多参数，一如：

        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&user_id=1';
        $params = array(); //更多参数

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url, $params); //通过第二个参数，传送更多参数

操作：执行请求

这里的操作，显然就是对应我们接口的调用。简单地如：

        //Step 2. 执行请求
        $rs = PhalApi_Helper_TestRunner::go($url);

这样，便可以在服务端模拟进行一次接口的请求调度，注意这里是在服务端进行的接口请求，而不是客户端。
此外，如果需要传递更多参数，可以参考前面的示例。这里简单补充一下PhalApi_Helper_TestRunner测试辅助类的接口签名说明：

<?php
class PhalApi_Helper_TestRunner {

    /**
     * @param string $url 请求的链接
     * @param array $param 额外POST的数据
     * @return array 接口的返回结果
     */
    public static function go($url, $params = array()) {

        ... ...

检验：验证

在对接口返回的结果中，我们可以这样依次进行正确性的验证：

  • 1、先验证接口返回的格式是否正确，有无字段遗漏；
  • 2、返回的业务数据是否正确；
        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('msg', $rs);
        $this->assertArrayHasKey('info', $rs);

        $this->assertEquals(0, $rs['code']);

        $this->assertEquals('dogstar', $rs['info']['name']);
        $this->assertEquals('oschina', $rs['info']['note']);

由于测试环境的数据变动频繁，所以我们可以针对个别的接口进行更精确的验证，而对类似列表获取这样的大批量的数据，则校验其结构格式。
除此之外，还有一种情况也是需要纳入检验，即除了上面的正常请求情况下的 异常请求 。

接下来的即是之前文档里面所说的单元测试执行和接口开发，此处略。

(2)Domain层和Model层的单元测试

下面继续简单补充一下之前没谈及到的Domain层和Model层的单元测试。

显然，这两层的开发，已经在前面的接口测试驱动开发的指导下很好地完成了。现在可以快速追加对这两层的单元测试。得益于我们的生成测试骨架的脚本，操作如下：

$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Domain/User.php Domain_User > ./Demo/Tests/Domain/Domain_User_Test.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Model/User.php Model_User > ./Demo/Tests/Model/Model_User_Test.php

接着，修改一下测试环境 test_env.php的引用路径：

//$ vim ./Demo/Tests/Domain/Domain_User_Test.php
//$ vim ./Demo/Tests/Model/Model_User_Test.php

require_once dirname(__FILE__) . '/../test_env.php';

各自完善一下单元测试：

//$ vim ./Demo/Tests/Domain/Domain_User_Test.php

    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        $userId = '1';

        $rs = $this->domainUser->getBaseInfo($userId);

        $this->assertArrayHasKey('id', $rs);
        $this->assertArrayHasKey('name', $rs);
        $this->assertArrayHasKey('note', $rs);

        $this->assertEquals('dogstar', $rs['name']);
    }

执行一下：

$ phpunit ./Demo/Tests/Domain/Domain_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

.

Time: 49 ms, Memory: 6.25Mb

OK (1 test, 4 assertions)

(3)Model层的单元测试

Model层的单元测试类似，不再赘述。

2.14.7 更进一步的单元测试套件

到目前为止，我们有了如下的产品代码：

dogstar@ubuntu:Demo$ tree
.
├── Api
│   └── User.php
├── Domain
│   └── User.php
├── Model
│   └── User.php

并拥有了与之平行对应的单元测试：

dogstar@ubuntu:Tests$ tree
.
├── Api
│   └── Api_User_Test.php
├── Domain
│   └── Domain_User_Test.php
├── Model
│   └── Model_User_Test.php
└── test_env.php

这样是一个很好的开始，但若我们每次测试都分别调用三次这些不同层次的单元测试，显然有点不科学。所以，利用PHPUnit的配置文件，我们可以轻松管理我们的测试套件，如：

dogstar@ubuntu:Tests$ vim ./phpunit_user_getbaseinfo.xml

<?xml version="1.0" encoding="UTF-8"?>

<phpunit backupGlobals="false"
    ...

    <testsuites>
        <testsuite name="Test Suite">
            <file>./Api/Api_User_Test.php</file>
            <file>./Domain/Domain_User_Test.php</file>
            <file>./Model/Model_User_Test.php</file>
        </testsuite>
    </testsuites>
</phpunit>

啊哈！终于，当需要调用这些分布在不同目录位置的单元测试时，只需要这么简单的一行命令：

dogstar@ubuntu:Tests$ phpunit -c ./phpunit_user_getbaseinfo.xml
PHPUnit 4.3.4 by Sebastian Bergmann.

.....

Time: 54 ms, Memory: 7.25Mb

OK (5 tests, 28 assertions)

2.14.8 这样的好处？

上面的过程，细节较多，而且需要实际操作的部分也比较多。对于之前没有接触过单元测试这块的同学，可能会有点迷茫，对于不愿意接受单元测试的同学来说更加枯燥。

然而，然而。 当我们把越痛苦的事情越早完成后，我们后面就顺畅多了。正如在某一次培训中的某一位敏捷开发的专家所说的： 要逐步对小问题做优化，而不是要等到大问题到来时再做变革 。

那这样的好处在于哪里呢？

这里不就理论回答，而是以我个人的经历来简单说明。

首先，正如上面所说的，单元测试帮你很好地记住并整理了各种接口测试的场景，而不用再像以前那样打开N个浏览器窗口逐个人工校对。
其次，在单元测试的论证下我们可以更有信心地跟测试说、跟产品说、跟发布说我们的代码没问题，因为我们通过严格的单元测试，而不是人为主观上的想当然应该不会有问题吧。
最后，也是最重要的，在后期的接口升级、改动和维护中，单元测试再一次为我们提供了保护，犹如一张安全网，涵盖我们改动的每一处代码。与此同时，对于重构也亦然。

但单元测试所带给你的，不仅仅是上面所说的简单这几点。更多地完全不一样的开发历程，而其中滋味和令人兴奋的体现，只有当你亲自去尝试才会明白其中滋味。So, try it by yourself.

上一章 文档首页 下一章#[2.15]-演进：新型计划任务续篇

上一章 文档首页 下一章

计算机科学中的任何问题都可以用另外的间接层解决，但是这通常会引发另一个问题。 -- David Wheeler

2.15.1 新型计划任务回顾

在 [1.31]-新型计划任务：以接口形式实现的计划任务 一章中，我们讨论了PhalApi中对计划任务的设计和底层实现。

但对于很多应用，很多项目，或者很多同学来说，仍然比较广泛，不能直接使用。
这一章则专门为此而进行演进，并提供最终可用的计划任务调度，同时我们也会阐明如何进行扩展定制。

也就是说，这一章将提供Task扩展类库的统一调度方式，以便在启动crontab任务后，可以通过数据库简单配置，即可执行各种任务。

2.15.2 最终调度的方式：crontab

出于对业务的考虑，我们首先需要明确此crontab调度方式所支持的功能，它应该包括但不限于：

  • 1、通过简单的数据库配置，即可启动一个新的任务
  • 2、具备循环调度的能力，并能初步防止并发调度
  • 3、可以对异常的任务进行修复
  • 4、优先执行太远未执行的任务
  • 5、支持本地和远程两种调度方式、三种MQ类型，以及扩展的能力

2.15.3 核心时序图与分层

在原来的时序图基础上，我们可以进行演进的设计，追加了统一的调度后如下所示：
a pic

通过上面详细的时序图，我们可以发现里面的设计是出于这样的分层考虑：

序号 | 层 | 关键操作 | 说明 | 如何使用
1 | 启动脚本 | crontab.php | 操作crontab执行的脚本 | 客户端可以进行必要的初始化工作
2 | 进程级 | Task_Progress::run() | 根据进程配置的数据库表，进行循环调度 | 不需要改动，直接使用
3 | 触发器 | Task_Trigger::fire() | 进行计划任务调度的上下文环境，用于指定runner和mq类型 | 客户端也可进行定制扩展，进行必要的操作
4 | MQ消费与调度 | Task_MQ::pop()和Task_Runner::go() | 不断消费MQ队列，并依次进行调度 | 不需要改动，直接使用，也可扩展
5 | 计划任务服务 | PhalApi_Api::doSth() | 执行计划任务服务 | 由客户端按接口形式实现

虽然上面的层级，初看起来有点多，但我们再次验证了计算机那个伟大的定论：计算机的任何问题都可以通过一个中间层来解决。

由此看出，上面的层级其实相当于：

客户端初始化 --> 直接使用 --> 自由组合与操作 --> 直接使用 --> 任务服务实现

2.16.4 进程配置的数据库表设计

CREATE TABLE `phalapi_task_progress` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(200) DEFAULT '' COMMENT '任务标题',
  `trigger_class` varchar(50) DEFAULT '' COMMENT '触发器类名',
  `fire_params` varchar(255) DEFAULT '' COMMENT '需要传递的参数，格式自定',
  `interval_time` int(11) DEFAULT '0' COMMENT '执行间隔，单位：秒',
  `enable` tinyint(1) DEFAULT '1' COMMENT '是否启动，1启动，0禁止',
  `result` varchar(255) DEFAULT '' COMMENT '运行的结果，以json格式保存',
  `state` tinyint(1) DEFAULT '0' COMMENT '进程状态，0空闲，1运行中，-1异常退出',
  `last_fire_time` int(11) DEFAULT '0' COMMENT '上一次运行时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

对此表的关键字段说明如下：

字段 | 说明 | 示例
trigger_class | 触发器的类名 | 须实现Task_Progress_Trigger::fire($params)接口
fire_params | 触发器的参数 | 加传给Task_Progress_Trigger::fire()函数的参数，格式为：service&MQ类名&runner类名
interval_time | 执行间隔 | 单位为秒
enable | 是否启动 | 此字段禁止时，将不再执行
state | 进程状态 | 当此状态一直为异常或者运行且超过1天时，系统会进行修复，即重置为空闲状态

其中，对于fire_params参数，MQ类名和runner类名可选，以下是一些示例：

//示例1：完整的配置
//fire_params=Task_Demo.DoSth&Task_MQ_DB&Task_Runner_Local
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq);
$runner->go('Task_Demo.DoSth');

//示例2：使用默认的Runner
//fire_params=Task_Demo.DoSth&Task_MQ_DB
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq);  //默认使用本地Runner
$runner->go('Task_Demo.DoSth');

//示例3:使用默认的MQ和默认的Runner
//fire_params=Task_Demo.DoSth
$mq = new Task_MQ_Redis(); //默认使用redis的MQ
$runner = new Task_Runner_Local($mq);  //默认使用本地Runner
$runner->go('Task_Demo.DoSth');

//示例4：使用自定义的MQ和Runner
//fire_params=Task_Demo.DoSth&My_MQ&My_Runner
class My_MQ implements Task_MQ {
     // ...
}
class My_Runner extends Task_Runner {
     // ...
}
$mq = new My_MQ();
$runner = new My_Runner($mq);
$runner->go('Task_Demo.DoSth');

2.15.5 运行效果

最终的效果就是，我们通过这样两行简单的代码，即可实现一系列复杂的任务调度：

$progress = new Task_Progress();
$progress->run();

让我们来看下这样设计的运行效果吧！看下这两行代码背后所产生的魔力。

首先，我们先添加两条计划任务：

INSERT INTO `phalapi_task_progress` VALUES ('1', 'test demo', 'Task_Progress_Trigger_Common', 'Task_Demo.DoSth&Task_MQ_File&Task_Runner_Local', '300', '1', '', '0', '0');
INSERT INTO `phalapi_task_progress` VALUES ('2', 'test ok', 'Task_Progress_Trigger_Common', 'Default.Index&Task_MQ_DB&Task_Runner_Local', '100', '1', '', '0', '0');

然后，伪造一些MQ：

INSERT INTO `phalapi_task_mq_0` VALUES ('8', 'Default.Index', '', '0', '');

最后，生成单元测试：

<?php
class PhpUnderControl_TaskProgress_Test extends PHPUnit_Framework_TestCase
{
    public $taskProgress;

    protected function setUp()
    {
        parent::setUp();

        $this->taskProgress = new Task_Progress();
    }

    /**
     * @group testRun
     */
    public function testRun()
    {
        $rs = $this->taskProgress->run();
    }

}

并执行之：

$ phpunit ./Task_Progress_Test.php

[1 - 0.06666s]SELECT id, title FROM phalapi_task_progress WHERE (state != ?) AND (last_fire_time < ?) AND (enable = ?) ORDER BY last_fire_time ASC; -- 0, 1431965153, 1<br>
[2 - 0.07002s]SELECT id, title, trigger_class, fire_params FROM phalapi_task_progress WHERE (state = 0) AND (interval_time + last_fire_time < ?) AND (enable = ?); -- 1432051553, 1<br>
[3 - 0.06549s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '1');<br>
[4 - 0.07432s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '1');<br>
[5 - 0.06469s]UPDATE phalapi_task_progress SET result = '{\"total\":0,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '1');<br>
[6 - 0.06746s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '2');<br>
[7 - 0.07043s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '2');<br>
[8 - 0.06673s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;<br>
[9 - 0.48185s]DELETE FROM phalapi_task_mq_0 WHERE (id IN ('8'));<br>
[10 - 0.06514s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;<br>
[11 - 0.50694s]UPDATE phalapi_task_progress SET result = '{\"total\":1,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '2');<br>

Time: 1.98 seconds, Memory: 6.50Mb

OK (1 test, 0 assertions)

查看对比一下数据库，目前发现运行良好！

提交代码，保存文档，收工睡觉！

2.15.6 演进的乐趣

得益于前期良好的设计以及底层支持，我们发现，在提供这样一种统一的调度方式是非常方便的。

不仅如此，如果你明白了其中的设计，需要进行定制和扩展也是非常方便的。也就是说，我们不仅提供了一种具体实际可用的方式，也提供了广阔自由的扩展空间。具体与抽象，两者仍可得。

然而，这一切不仅依赖于良好的设计，还依赖于测试驱动开发下的浮现式设计。

上一章 文档首页 下一章#[2.16]-领域驱动设计：应对复杂领域业务的Domain层

上一章 文档首页 下一章

温馨提示：此篇章需要比较长的时间才能最终定稿，因为我还要寻找最合适的方式和语言来表述。

2.16.1 领域驱动设计

很多框架关心性能，而不关心人文；很多项目关心技术，而不关注业务。

就这造成了复杂的领域业务在项目中得不到很好地体现和描述，也没有统一的规则，更没有释意的接口。最终导致了在“纯面向对象”框架里面凌乱的代码编写，为后期的维护扩展、升级优化带来很大的阻碍。这就变成了，框架只关注性能，项目只关心技术，而项目却可怜地失去了演进的权利，慢慢地步履维艰，最终牵一发而动全身。

很多人都不知道该如何真正应对和处理领域的业务 ，尽管领域业务和单元测试都是如此重要并被广泛推崇。正如同表面上我们都知道单元测试却没有具体真实地接触过，并且一旦到真正需要编写一行单元测试的代码时就傻眼了。

这里不是发明一些新技术，也不是提供一些新的模式，而是继续将前人、大神和顶级大师关于领域驱动设计这方面的思想结合真实后台接口开发进行分享，进而推广之。

2.16.2 讲述故事

很多人，都喜欢听故事。像我以前中学的时候，就很喜欢看《故事会》。

如果，我们能让代码也像小说一样，在讲述某个故事时，将会更加吸引“读者”（也就是其他开发同学），从而易于理解和维护。

最近，我在做一个项目时，再一次发现了这种讲述故事的威力。

(1)一个第三方登录的写法

我们先以F项目来命名这个项目，在F项目中，我们跟其他App一样，需要接入第三方登录，其中包括：微信登录、微博登录和QQ登录、邮箱登录等。

以下，则是我根据 讲述故事 的方式，为微信登录编写的代码：

<?php

class Api_User_Login extends PhalApi_Api {

    public function getRules() {
        return array(
            'weixin' => array(
                'openId' => array('name' => 'wx_openid', 'require' => true, 'min' => 1, 'max' => 28),
                'token' => array('name' => 'wx_token', 'require' => true, 'min' => 1, 'max' => 150),
                'expiresIn' => array('name' => 'wx_expires_in', 'require' => true, 'min' => 1),
                'nickname' => array('name' => 'name', 'default' => '',),
                'avatar' => array('name' => 'avatar', 'default' => '',),
            ),
        );
    }

    public function weixin()
    {
        $rs = array('code' => 0, 'info' => array(), 'msg' => '');

        $domain = new Domain_User_Login_Weixin();
        $isFirstBind = $domain->isFirstBind($this->openId);

        $userId = 0;
        if ($isFirstBind) {
            $userId = Domain_User_Generator::createUserForWeixin(
                   $this->openId, $this->nickname, $this->avatar);

            $domain->bindUser($userId, $this->openId, $this->token, $this->expiresIn);
        } else {
            $userId = $domain->getUserIdByWxOpenId($this->openId);
        }

        $token = Domain_User_Session::generate($userId, $this->client);

        $rs['info']['user_id'] = $userId;
        $rs['info']['token'] = $token;
        $rs['info']['is_new'] = $isFirstBind ? 1 : 0;

        return $rs;
    }
}

温馨提示：
以下代码为我正在参与开发的一个项目的源代码，已征得项目负责人同意。同时出于对项目的尊重，已省去部分代码。

(2)登录场景的故事

细细品读上面的代码，其实就是在描述登录场景的故事：
当用户进行微信登录时，先查看用户是否首次登录；如果是，则为用户自动生成了一个帐号并绑定，如果不是，则获取已绑定的用户ID；最后，生成一个登录态的token。

当然，这里为了突出故事的主线，已去除了很多异常情况的处理。

(3)有趣的开发体验

更为有趣的是，此次参与F项目开发的还有另外一位同学。
这位同学拥有多年资深的iOS开发经验，但对PHP开发还是首次接触，但他在参考微信登录的写法后，很快就交付了微博和QQ登录这两个接口服务。

但令我为之惊讶和兴奋的不是他的速度，而是他所编写的代码是如此的优雅美丽，犹如出自资深PHP开发人员之手。

这让我再一次相信，使用 讲述故事 的方式来开发接口，不仅能让代码更易于传送业务逻辑，也能为更多的同学乃至新手接受并快速上手。

(4)与TDD的结合

讲述故事 有一个很明显的特点就是，全部的操作都是处于同一抽象级别的，即都是释意接口下的领域业务规则和操作。

但对于如何引出这个业务场景，很多人用传统的方式都是写一个接口，然后在浏览器调试。

其实，这并不是最好的开发体验。因为，使用这种传统的开发方式，你难免会落入技术缠绕的纠结中，比如在想使用哪些类型的数据库表字段。也就是说，你在丢失关注点。

而通过测试驱动，则会先引导你做正确的事，再将你的关注引导到领域业务上，最后将自然而然地就知道应用使用什么技术了。

讲故事，是针Domain领域层外部使用的说明。下面，我们将走进Domain层内部，阐明我们应该如何为讲故事做好准备。

2.16.3 表达规则

(1)释意接口

释意接口的作用是很大的，这可以使得后来的同学在看待一个接口时，无须深入内部实现即可明白它的用意和产生的影响。
如一个get系列的操作，我们可以推断出它是无副作用的。但如果当时的开发者不遵守约定，在里面作了一些“手脚”，则会破坏我们这些“望文生义”的推断。

在我曾经就职的一个游戏公司里面，我常根据接口的命名来推断它的作用，但往往会倍受伤害。因为以前的开发人员没有遵守这些约定，当时的team leader还责怪我不能太相信这些接口的命名。然而我想，如果我们都不能相信我们团队其他人员的接口，我们又能相信谁呢？我们是否应该反思，是否应该遵守约定编程所带来的好处？
任何一个问题，都不是个人的问题，而是一个团队的问题。如果我们经常不断地发生一生项目的问题而要去指责某个人时，我们又为何不从一开始就遵守约定而去避免呢？

简单来说，释意接口会将“命令-查询”分离、会将多个操作分解成更小粒度的操作而保持同一层面的处理。根据《领域驱动设计》一书的说法：
类型名、方法名和参数名一起构成了一个释意接口（Intention-Revealing Interface），以解释设计意图，避免开发人员需要考虑内部如何实现，或者猜测。

如下面的家庭组成员领域业务类：

<?php
class Domain_Group_Member {

    public function joinGroup($userId, $groupId) {
        //TODO
    }

    public function hasJoined($userId, $groupId) {
        //TODO
    }

}

我们可以知道，Domain_Group_Member::joinGroup()用于加入家庭组，会产生副作用，是一个命令操作；Domain_Group_Member::hasJoined()用于检测用户是否已加入家庭组，无副作用，则是一个查询操作。

(2)业务规则的描述

规则出现且仅出现一次。

当代码出现重复时，我们都知道会面临维护的高成本。而当规则多次出现时，我们更知道当规则发生变化时所带来的各种严重的问题，这也正是为什么总有一些这样那样的BUG的原因。
系统出现问题，大多数上都是业务的问题。而业务的问题在于我们不能把规则收敛起来，汇集于一处。

在以往的开发中，我都很注意对这些规则统一的重构工作。这使得我可以非常相信我所提供业务的稳定性，以及在给别人解讲时的信心。
如有一次，我们有一个大型的系统中的一个页面跳转链接的生成规则，后来系统进行了调整，需要对URL生成规则作出调整。我跟另一位新来的同事说只需改一处时，他仍然很惊讶地问我怎么可能？！因为他看到是这么多场景，如此多的页面，怕会有所遗漏。然而，事实证明，我们确实只需要改动一处就可以了。

类似这样的URL拼接规则，我们可以这样表示：

<?php
class Domain_Page_Helper {

    public static function createUrl($userId) {
        return DI()->config->get('app.web.host') . '/u/' . $userId;
    }
}

正如你看到了，我们使用了static静态方法，是因为这个规则生成可以当作一个工具方法来使用。我们不反对使用static方法，但推荐只在合适的时候使用。

规则出现且仅出现一次，可以说是一个知易行难的做法，因为我们总会有不经意间重复实现规则。有时我们会忽略已有的规则，有时我们会出于当前紧张开发进度的考虑，有时我们可能懒得去统一。
但把规则的实现统一起来，再重复调用，会让你在今后的项目开发中，长期收益。没错，真的会长期收益。

2.16.4 不可变值与无状态操作

(1)在开源中国上翻译的两点收获

首先，让我们简单来了解一下PHP语言的运行机制。
PHP是一个运行于服务端的脚本解析语言，每一个HTTP请求都会触发一个php-fpm进程来响应，所以不同于其他长时间运行的语言或者系统，不用过多地考虑内存的回收或者对实体的管理和共享。

这样是有明显的好处，作为PHP开发人员，由于每一次请求所消耗的内存都会在本次释放，即使运行错误也不会影响其他的调用。从而，我们可以放心快速地开发。
但我们也应该看到这样的便利给很多开发同学所带来的误导。正因为不用再担心一些传统的问题（如内存管理），他们变得更无限制。当这种无限制日积月累而引发诸多项目的问题时，他们会开始责怪PHP这门语言。

其实，语言本身没有对错，关键在于我们怎么使用。

先前，在开源中国进行翻译时，我从翻译的文章中收获了两点。

  • 第一点是，学习不同的语言，你将会获得不同的灵感。比如，你是OC的开发人员，可以从GO语言中获得OC的开发灵感；反之亦然。
  • 第二点是，不可变值的使用。

这里，我将尝试说明如何在PhalApi现有的分层机制基础上，结合不可变值和无状态，应对复杂的领域业务开发。

(2)不可变值

通常，我们在程序中处理的变量可以分为：值和实体。简单来说，值是一些基本的类型，如整数、布尔值、字符串；实体则是类对象，有自己内部的状态。当一个实体表示一个值的概念时（如坐标、金额、日期等），我们可以称之为值对象。
明显地，系统的复杂性不在于对值的处理，而在于对一系列实体以及与其关联的另一系列实体间的处理。

如同其他语言一样，如果我们也在PHP遵循 不可变值 与 无状态 这两个用法，我们的系统乃至业务都可以从中获益。

不可变值 是指一个实体在创建后，其内部的状态是不可变更的，这样就能在系统内放心地流通使用，而无须担心有副作用。

举个简单的例子，在我们国际交易系统中有一个金额为100RMB的对象，表示用户此次转账的金额。如果此对象是不可变值，那么我们在系统内，无论是计算手费、日记纪录，还是转账事务或其他，我们都能信任此对象放心使用，不用担心哪里作了篡改而导致一个隐藏的致使BUG。

也就说，当你需要修改此类对象时，你需要复制一个再改之。有人会担心new所带来的内存消耗，但实际上，new一个只有一些属性的对象消耗很少很少。

要明白为什么在修改前需要再创建新的对象，也是很容易理解的。首先，我们保持了和基本类型一致的处理方式；其次，我们保持了概念的一致性，如坐标A（1，2）和坐标B（1，3）是两个不同的坐标。
当坐标A发生改变，坐标A就不再是原来的坐标A，而是一个新的坐标。从哲学角度上看，这是两个不同的概念。

在PhalApi中，我们可以看到不可变值在Query对象中的应用：

$query1 = new PhalApi_ModelQuery();
$query1->id = 1;

$query2 = new PhalApi_ModelQuery($query1->toArray());
$query2->id = 2;

这样以后，我们就不再需要小心翼翼维护“漂洋过海”的值对象了，而是可以轻松地逐层传递，这有点像网络协议的逐层组装。

这又让我想起了《领域驱动设计》一书中较为中肯的说法：
把值对象看成是不可变的。不要给它任何标识，这样可以避免实体的维护工作，降低设计的复杂性。

(3)无状态操作

前面提到了PHP的运行机制，不同于长时间运行的语言或系统，PHP很少会在不同的php-fpm进程中共享实体，最多也只是在同一次请求中共享。

这样，当我们在一次请求中需要处理两个或两个以上的用户实体时，可以怎么应对呢？
关于对实体的追踪和识别，可以使用ORM进行实体与关系数据库映射，但PhalApi弱化了这种映射，取而代之的是更明朗的处理方式，即： 无状态操作 。

因为PhalApi都是通过“空洞”的实体来获得数据，即实体无内部属性，对数据库的处理采用了 表数据入口模式 。
当我们需要获取两个用户的信息时，可以这样：

$model = new Model_User();
$user1 = $model->get(1);  //$user1是一个数组
$user2 = $model->get(2);

//而不是
$user1 = new Model_User(1);  //$user1是一个对象
$user2 = new Model_User(2);

//或者可以这样批量获取
$users = $model->multiGet(array(1, 2));  //$users是一个二维数组，下标是用户的ID

这样做，没有绝对的对错，可以根据你的项目应用场景作出调整。但我觉得无状态在PhalApi应用，可以更简单便捷地处理各种数据以及规则的统一，以实现操作的无状态。因为：

  • 1、可以按需取得不同的字段，多个获取时可以使用批量获取
  • 2、在单次请求处理中，简化对实体的追踪和维护
  • 3、换种方式来获得不可变值性的好处，因为既然没有内部状态，就没有改变了

(4)引申到Domain层

Domain层作为ADM（Api-Domain-Model）分层中的桥梁，主要负责处理业务规则。

将值对象与无状态操作引申到Domain层，同样有处于简化我们对数据和业务规则的处理。

我们可以根据上述的家庭组成员领域类来完成类似下面功能场景的业务需求：

$domain = new Domain_Group_Member();

if (!$domain->hasJoined(1, 100)) {
    $domain->joinGroup(1, 100);
}
if (!$domain->hasJoined(2, 100)) {
    $domain->joinGroup(2, 100);
}
if (!$domain->hasJoined(3, 100)) {
    $domain->joinGroup(3, 100);
}

即：如果用户1还没加入过组100，那么就允许他加入。用户2、用户3也以此类推。

当我们把业务规则，划分为更细的维度时，我们可以轻松上在业务层组装不同的功能，讲述不同的故事。

2.16.5 越痛苦的事情，越早做

在有一次敏捷开发分享会上，有位前辈说：要对小问题不断进行优化迭代，而不要等到大问题来了再作变革。
也就是我们常听说的黄金法则： 演变优于剧变 。

同样，在持续集成中，也提倡着类似的理念，即：越痛苦的事情，越早做。

有时，对自己狠一点，是会有所收获的。

我们都赞扬美好的事物，但我们很少也会那样去做，因为我们知道美好需要付出更多的努力，意味着有所牺牲。

如很多女生，都很喜欢苗条的身材，却总忍不住零食的诱惑，也很难坚持锻炼。
我们很多人都喜欢优雅的代码，自已却也会写下一些临时性的代码，而没多及时清理代码的异味，也没有尝试去重构，更没有坚持单元测试。

从另外一个角度说，如果一个项目的问题，我们在前期及时沟通并解决的话，根本不值得过多去关注。但若我们因为团队关系或者心烦意乱有意识去抵触多变的需求时，一个很小的问题，到了上线后，就可能会演变成一个灾难。
到了那时，即使只是一行代码的改变，也会涉及到开发、测试、产品、运维、用户、商务、老板等等一系列的干涉人。为了修复上线，我们还要走一系列的发布流程，事后还需要为这样的故障买单。

既然如此，明明知道当初一个不确定的需求时，为什么没去及时处理呢？
我知道，我相信我们大家也知道，程序员总会有一些很烦很想抵触的时候，这时我们会拒绝改变，拒绝去做一些正确的事情。

但越痛苦的事情，越早做，不仅仅需要我们增强对自身的情商控制，更让我们能很好地应对高价值的系统。
试想，谁会把一个影响到千千万万用户、涉及到动则百万金额的系统交给一个动不动就发脾气的人呢？

所以，对自己“狠”一点吧，明天的你，将会感谢今天努力的你。

2.16.6 收篇

领域驱动设计所提供的思想、概念和设计非常广泛，这里不能一一说明。本章仅仅是摘取其中的部分内容进行再传播，以唤醒入门同学的注意，培养一种约定编程的意识。

更为重要的是，要懂得去学习，学习后应用到自己当前的工作或项目中，慢慢地你将体会到开发编程的乐趣。

上一章 文档首页 下一章#[2.17]-微服务：Api接口服务层

上一章 文档首页 下一章

我发现，我越是努力，就越发幸运。 -- Thomas Jefferson

2.17.1 微服务

Martin Fowler（我喜欢和敬仰的大师）曾发表了上面这一段话。这段话也出现在了2015年QCon分享会上，并加了一张PPT“什么是微服务”加以说明。

里面提到了 微服务 这个概念，在PhalApi框架中即对应我们的Api接口服务层，只是我们不是称之为微服务，而是接口服务。
不管何种说法，我们都应该关注里面提及到的这几点重要特质：

  • 小，且专注于做一件事情
  • 独立的进程中
  • 轻量级的通信机制
  • 松耦合、独立部署

这里不过多地讨论微服务相关的分享，而是重温接口服务层Api与领域驱动和单元测试之间的关系，以及如何开发一个优雅、稳定又简单的接口。

2.17.2 层级调用的顺序

整体上讲根据从Api接口层、Domain领域层再到Model数据源层的顺序进行开发。

在开发过程中，需要注意不能 越层调用 也不能 逆向调用 ，即不能Api调用Model。而应该是 上层调用下层，或者同层级调用 ，也就是说，我们应该：

  • Api层调用Domain层
  • Domain层调用Domain层
  • Domain层调用Model层
  • Model层调用Model层

如果用一张图来表示，则是：
a pic

为了更明确调用的关系，以下调用是 错误 的：

  • 错误的做法1：Api层直接调用Model层
  • 错误的做法2: Domain层调用Api层，也不应用将Api层对象传递给Domain层
  • 错误的做法3: Model层调用Domain层

这样的约定，便于我们形成统一的开发规范，降低学习维护成本。
比如需要添加缓存，我们知道应该定位到Model层数据源进行扩展；若发现业务规则处理不当，则应该进入Domain层探其究竟；如果需要对接口的参数进行调整，即使是新手也知道应该找到对应的Api文件进行改动。

2.17.3 接口服务的定义

现实项目开发过程中，绝大部分我们编写的接口是给别人使用的，或许给Android客户端同学使用，或者给iOS客户端同学使用，抑或提供给其他后台系统的同学使用。
为了提高并行开发的速度，我们不能等待接口完全开发完成后才提供接口文档，而且他们也不能忍受这么漫长的等待。

所以，客户端同学时常会问我们：什么时候可以提供接口文档？

我们提倡“接口先行”，如果有时不能很好地做到这一点（毕竟多变的需求促发多变的情境），我们可以快速提供接口的定义。
这有点像规约层对接口的定义一样，在PhalApi中定义一个接口，再具体一点即：

  • 1、创建一个接口服务类和声明函数
  • 2、配置接口参数规则
  • 3、提供接口返回格式的注释

简单来说，就是创建一个类，写个函数，定义参数和返回结果。

下面以 开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动） 中的团队参赛接口为例，说明这三步操作的过程。

(1)创建一个接口服务类和声明函数

//$ vim ./Vote/Api/Act.php

<?php
class Api_Act extends PhalApi_Api {

    public function joinIn() {
    }
}

(2)配置接口参数规则

<?php
class Api_Act extends PhalApi_Api {

    public function getRules() {
        return array(
            'joinIn' => array(
                'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
            ),
        );
    }

    public function joinIn() {
    }
}

(3)提供接口返回格式的注释

<?php
class Api_Act extends PhalApi_Api {

    public function getRules() {
        return array(
            'joinIn' => array(
                'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
            ),
        );
    }

    /**
     * 团队参赛接口
     *
     * @return int code 0，参赛成功；1，队名已存在
     * @return int team_id 新建的团队ID
     */
    public function joinIn() {
    }
}

(4)在线查看一下效果

在完成上面的动作后，我们可以通过在线工具来看下实时的效果，在浏览打开后访问：

http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn

可以看到：
a pic

到了这里，即使我们未完成接口的开发，也未提供更完善的接口文档，但接口客户端同学也可以根据这个在线的接口参数进行开发了。

2.17.4 在ATDD下讲述故事

我们一直推崇测试驱动开发，但在对于Api接口开发，更准确来说是ATDD，即：验收测试驱动开发（Acceptance Test Driven Development）。

在前面Domain层文档中，我们提到了Api层是讲述故事的场景。因此，为了验证我们的业务场景是否正确，我们应该事先编写好单元测试，以不断引导我们前往正确的目的地。

我们可以使用脚本来快速生成测试骨架：

$ pwd
$ /path/to/api.vote.phalapi.com/Vote/Tests/Api
$ phalapi-buildtest ../../Api/Act.php Api_Act ../test_env.php > Api_Act_Test.php

然后，稍微修改完善测试场景：

    /**
     * @group testJoinIn
     */
    public function testJoinIn()
    {
        //Step 1. 构建请求URL
        $url = 'service=Act.JoinIn';
        $params = array(
            'sign' => 'phalapi',
            'team_name' => 'test team name',
            'user_id' => '1',
            'token' => '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731',
        );

        DI()->notorm->team->where('team_name', $params['team_name'])->delete();

        //Step 2. 执行请求
        $rs = PhalApiTestRunner::go($url, $params);
        //var_dump($rs);

        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('team_id', $rs);
        $this->assertEquals(0, $rs['code']);
        $this->assertGreaterThan(0, $rs['team_id']);

        //create again
        $rs = PhalApiTestRunner::go($url, $params);
        $this->assertEquals(1, $rs['code']);
    }

从上面测试的代码可以看出，我们先后进行了两次报名。明显地，第一次报名应该是成功的，第二次则应该提示不能重复报名。

单元测试的好处，不但在于可以引导我们做正确的事情，还可以提高我们的关注点，不致于在开发过程中被各种事务（如临时性的会议）打断后回来却不知刚才开发到哪了。
然而，更多的是为后期的维护、扩展提供可验证的业务场景。这点是很重要的。因为每一个测试场景，都保存了对应场景的模拟信息，这样不仅仅在后面的扩展，还是突如其来的BUGFIXED，我们都可以快速证明我们的修改是正确的，至少不会影响到原来的业务流程。
试想一下，如果原来可以下单支付的接口，突然被影响到而导致支付不成功，这是何等的损失！

我现在慢慢地，每当需要修改别人的代码时，我都会看下有没有对应的单元测试。如果没有，我会先补回，这样能增强我修改别人代码的信心。

2.17.5 精益接口开发

传统的接口开发，由于没有很好的分层结构，而且热衷于在一个文件里面完成绝大部分事情，最终导致了臃肿漫长的代码，也就是通常所说的意大利面条式的代码。

在PhalApi中，我们针对接口领域开发，提供了新的分层思想：ADM（Api - Domain - Model）。
即便这样，如果项目在实际开发中，仍然使用原来的做法，纵使使用再好的接口开发框架，也还是会退化到原来的局面。

为了能让大家更为明确Api接口层的职责所在，我们建议：

(1)Api接口层应该做：

  • 应该：对用户登录态进行必要的检测
  • 应该：控制业务场景的主流程，创建领域业务实例，并进行调用
  • 应该：进行必要的日记纪录
  • 应该：返回接口结果

(2)Api接口层不应该做：

  • 不应该：进行业务规则的处理或者计算
  • 不应该：关心数据是否使用缓存，或进行缓存相关的直接操作
  • 不应该：直接操作数据库
  • 不应该：将多个接口合并在一起

在明确了上面应该做的和不应该做的，并且也完成了接口的定义，还有验收测序驱动开发的场景准备后，相信这时，即使是新手也可以编写出高质量的接口代码。
因为他会受到约束，他知道他需要做什么，主要他按照限定的开发流程和约定稍加努力即可。

如果真的这样，相信我们也就慢慢能体会到 精益开发 的乐趣。

最后，让我们一起来看下上述团队参赛接口开发的代码实现：

    /**
     * 团队参赛接口
     *
     * @return int code 0，参赛成功；1，队名已存在
     * @return int team_id 新建的团队ID
     */
    public function joinIn() {
        $rs = array('code' => 0, 'team_id' => 0);

        DI()->userLite->check(true);

        $domain = new Domain_Team();
        if ($domain->isExists($this->teamName)) {
            $rs['code'] = 1;
            return $rs;
        }

        $teamId = $domain->joinIn($this->teamName);
        $rs['team_id'] = $teamId;

        return $rs;
    }

可以看出，上面的代码短小达意，简单清晰。

上一章 文档首页 下一章#[2.19]-扩展库：可重用的扩展类库

上一章 文档首页 下一章

PhalApi框架扩展类库

-- 致力于与开源项目一起提供企业级的解决方案！

此部分类库为 PhalAPi框架 下的扩展类库包，各个扩展包各自独立，可以根据需要自动下载安装。

虽然此部分的扩展很多都是基于已有的第三方开源实现，或者开发同学自己实现分享的，但我们所做的不只是代码的搬运工，更多的是在系统架构上的组件库重用，以及在此引入 防腐层 ，避免进入 供应商锁定（Vendor Lock-In） 。

除此之外，可以在PhalApi框架的基础上，快速引入符合我们国内实际项目开发需要的各种扩展。如时，您会发现，原来编程本来就是一件如此简单的事情，就像搭积木一样。

正如我们一直推荐的：

接口，从简单开始！

目前已提供的扩展类库 - 针对使用者

1、微信开发

此扩展可用于微信的服务号、订阅号、设备号等功能开发，则PhalApi框架下简单配置即可开发使用。

更多请点这里查看使用文档

2、代理模式下phprpc协议的轻松支持

此扩展可用于phprpc协议的调用，服务端只需要简单添加入口即可完美切换。

更多请点这里查看使用文档

3、基于PHPMailer的邮件发送

此扩展可用于发送邮件。

更多请点这里查看使用文档

4、优酷开放平台接口调用

此扩展可用于调用优酷开放平台的接口。

更多请点这里查看使用文档

5、七牛云存储接口调用

此扩展可以用于将图片上传到七牛CDN，或者其他七牛接口的调用。

更多请点这里查看使用文档

6、用户、会话和第三方登录集成

此类库主要特点有：

  • 1、可以和第三方登录集成，包括：微信登录、新浪登录、QQ登录
  • 2、为客户端提供了直接可以调用的登录接口
  • 3、为服务端提供了直接可以检测用户登录态的操作
  • 4、支持token落地、高效缓存和分布式的数据库存储

  • 5、展示了如何开发一个项目级的类库、包括数据库配置、翻译等

    更多请点这里查看使用文档

7、swoole支持下的长链接和异步任务实现

目前，此扩展类库提供了：

  • 长链接的接口调用

  • 异步计划任务的调用

    更多请点这里查看使用文档

8、新型计划任务

此扩展类型用于后台计划任务的调度，主要功能点有：

  • 1、提供了Redis/文件/数据库三种MQ队列
  • 2、提供了本地和远程两种调度方式
  • 3、以接口的形式实现计划任务

更多请点这里查看使用文档

9、Auth 权限扩展 (由@黄苗笋提供)

实现了基于用户与组的权限认证功能，与RBAC权限认证类似，主要用于对服务级别的功能进行权限控制，主要功能点有：

  • 1、提供了接口服务维度的权限验证
  • 2、提供了可配置的组与规则
  • 3、支持免检用户

更多请点这里查看使用文档

11、FastRoute快速路由

此扩展基于 FastRoute 实现，需要 PHP 5.4.0 及以上版本，可以通过配置实现自定义路由配置，从而轻松映射到PhalApi中的service接口服务。，主要有：

  • 1、基于FastRoute实现
  • 2、需要PHP 5.4.0 及以上版本
  • 3、通过配置文件来实现自定义路由，并映射到service

  • 4、可兼容无路由的历史URI

    更多请点这里查看使用文档

12、基于PhalApi的DB集群拓展DB_Cluster (由@喵了个咪提供)

为应对海量数据分析与统计，提供针对分表分库统一封装的数据库操作接口，主要用于解决大量数据写入分析的问题。请注意：V0.1bate版本,很多功能尚不完善,只提供技术交流使用,请不要用于生产环境。主要特点有：

  • 1、适用于大量select和大量的insert的业务场景
  • 2、基于架构思维的实现
  • 3、分表分库算法介绍

  • 4、基准测试对比

    更多请点这里查看使用文档

13、基于PhalApi的Redis_Base拓展 (由@喵了个咪提供)

主要提供更丰富的Redis操作,并且进行了分库处理可以自由搭配

  • 1、适用于对Redis需要其他数据类型操作的业务

  • 2、可以用于队列脚本,封装了队列处理

    更多请点这里查看使用文档

扩展开发指南 - 针对开发者

为了统一扩展类库的风格、便于用户更容易使用，这里建议：

  • 代码：统一放置在Library目录下，各扩展包各自一个目录，尽量Lite.php入口类，遵循PEAR包命名规范；
  • 配置：统一放置在DI()->config->get('app.扩展包名')中，避免配置冲突；
  • 文档：统一提供WIKI文件对扩展类库的功能、安装和配置、使用示例以及运行效果进行说明；

1.19.4 从微架构到扩展类库的演进

在应用项目的实际开发，我们也可以有意识地将一些通用的工具和操作与业务分离，以便可以在项目内更好地重用。当抽离成工具或者通用类后，则可以进一步推广到公司内其他项目，即组件复用。如果觉得可以，则发扬开源精神，分享给社区。这也是符合从微架构到应用构架、系统架构、乃至企业架构的演进之路。

上一章 文档首页 下一章#[2.20]-约定编程：架构明显的编程风格

上一章 文档首页 下一章

2.20.1 前言

我们一直强调推荐让后台接口开发更简单。

所以，我们提供了PhalApi开发框架和WIKI文档。然后，这仅仅是个开始。

因为，真正发挥作用，实现价值的还是来自项目实际开发中的源代码。

但纵使我们提供了好的框架作为基础，但不得不承认的一个事实是：架构模型和项目代码随着各自的演进逐渐产生分歧。而这种分歧，如果没有注意、管理和约束，则会产生越来越混乱的代码。

在说明如何编写简洁的项目代码之前，让我们先了解一下PhalApi的架构思想以及主要的设计意图。

2.20.2 开发-配置-使用 模式 (develop-config-use pattern)

开发-配置-使用 模式即：开发实现-配置注册-客户使用 模式。

现分说如下。

(1)开发实现

开发实现的主要内容是组件、公共服务或者基础设施的功能实现，此部分主要针对高级开发工程师，或者有经验的PHP同学。

例如对项目的接口签名的验证拦截、一个完成了对七牛云存储接口调用的扩展、又或者是项目内部加密的方案等，这些以包或者接口提供，为外部使用提供了配置说明、使用示例和文档说明。更为重要的是，应该提供了配套的单元测试，并有着很高的代码覆盖率。

此类实现应该是稳定的，即没有明显或者隐藏的BUG。即使有，原作者也可以快速进行定位和解决，以及后期的扩展和升级。

(2)配置注册

一旦上面的接口被实现后，不同的项目都可以轻松引入和使用。这块通常由项目的负责人，或者主程来操作，因为在对项目进行构建部署、组件和环境装配时，需要考虑到哪些组件需要被用到，以何种方式进行初始化和装载。

但使用的方式，应该是简明的。如简明的安装，简明的配置。所以，这里自然而言，就涉及到了 依赖注入 ，DI。

通过DI，项目的负责人，可以轻松地将已通过严格测试的组件/服务注册进来。完成此步骤后，一切都整装待发，剩下的就是使用的问题了。

(3)客户使用

项目会不断有新的需求出来，而团队也会因此同步增加吸纳新开发同学进来负责新模块新功能的开发。而新的同学，往往会是一些开发新手，他们需要使用已有的功能，快速实现一些具体的业务逻辑、规则和功能。

但如果他们还需要实现一些基础重要的功能，又要考虑如何与现在项目整合，会分散他们的关注点。而且，即使放手给他们去做，他们也会常常因为考虑不周或者编程风格各异而产出一些与项目期望不符的代码。

若换一种工作的方式，即如果新手使用已有的组件进行一些特定领域业务的开发，会是怎样？

我想，会有很大的改观。

比如，我们对新来的同学说，你使用DI()->logger就可以写一条日记了，如：

DI()->logger->debug('app enter');

新手可能很喜欢追问一些问题，他可能会问及到，那怎么将一些参数（当时日记的上下文）也进行纪录呢？你可以很骄傲地说：也是可以的，你只需要这样写就可以了：

DI()->logger->debug('app enter', array('device' => 'iOS', 'version' => '1.1.0'));

(4)创建和使用分离

开发-配置-使用 模式 也符合了创建和使用分离的思想。

不同的项目，不同的应用，需要的初始化服务不一样；不同的规模，对不同的技术解决方案也不一样；不同的环境，配置也不一样。

但即使是这样，新手还是可以一如既往地使用之前注册的服务（也就是不需要修改任何调用代码）。也就是上层的调整或者环境变更这些，对新手的使用都是透明的。为了更好地理解这些概念，这里补充一些案例场景。

以日记为例

假设我们有个项目A，分别部署到内网测试环境和外网生产环境，显然内外网环境的配置是不一样的。我们希望在内网环境为日记开启debug模式以方便开发人员进行调试，在外网则希望将其关闭以减少系统的性能开销。在一开始使用文件作为日记存储方案时，对应的内网环境初始化代码如下：

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

在外网，我们只需要去掉PhalApi_Logger::LOG_LEVEL_DEBUG即可：

//日记纪录
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

随着项目的不断发展，我们有了一批又一批的新用户。产品经理为此很开心，也请我们开发吃了好几顿大餐。但谨慎的我们发现了现在文件日记的一些限制。如即时文件读写带来了I/O瓶颈，而且不能将分布式的日记文件自动收集起来。所以，我们决定对logger进行更深层次的探索。。。

至于最后是使用了Hive还是Hadoop，还是MC异步后台队列的方式实现，我们这里不具体指定。假设新的logger研发成功后，我们便可以轻松对原有的文件日记组件进行升级，实现完美切换：

//升级后的日记纪录
DI()->logger = new My_Logger(PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

这不仅是几行代码上的区别，而是针对不同问题不同技术解决方案的抉择。这也是有经验的开发和新手之间的区别，因为你选择的技术解决方案要和面临的风险相匹配。例如用牛刀来杀鸡，就是一个不匹配的做法，就如同使用高级的Hive来实现单一小项目的日记存储一样。

这是令人值得兴奋的。在很多旧的项目里面，当遇到瓶颈时，会请一些外部的专家来指导或优化。但即使拥有着各种“法宝”以及知道何时该使用哪种方案的专家，对于这种残留的代码也会步履维艰基于束手无策。因为，各种初始化和调用的代码，分遍在项目的“全国各地，四面八方”。即使你优化了，你会发现还要手动一个个地进行切换升级。更重要的是，很多时候不是你想优化就能优化的。

我曾经遇到过这样一个旧系统。它是在UcHome基础上做二次开发，但对于它的数据库使用，开发人员没有过多地优化，如：没有使用缓存，没有进行批量合并查询优化，重复查询相同的数据，没有建立索引等等，等等。这样的后果就是，请求一次接口，会触发150条到500条SQL语句不等。我后来，在底层添加了在线查看调试SQL语句的功能，尝试进行了一些合并查询，但当我想为数据库的表添加索引时，发现它用的却是虚拟表 -- 视图！

(5)扩展类库对“开发-配置-使用”模式的应用

如果说DI服务是微观上对“开发-配置-使用”模式的使用，那么PhalApi的扩展类库则是宏观上的应用。

扩展类库也是由第三方（可能是PhalApi开发团队、他人或者你）开发实现的，然后再通过简单配置（或者免配置），就可以使用扩展类库的功能了。如邮件发送、phprpc协议。
之所以提供扩展类库的形式，是因为DI服务更适合于单个类以及几个操作接口，而扩展类库则提供更丰富的功能操作和一系列的接口。

这样以后，项目就可以简单快速共享各种扩展类库了。难道这不是一件令人兴奋的事情吗？
因为“哈啊！我又找到了一个可以直接用的代码类库”，而不是“唉，又要写一堆代码，还要测试、联调。。。”。

(6)回顾Yii框架的发现

程序、系统和框架，其作用太多数都体现在动态的功能上，而不是静态有限的功能。而动态的功能则很大程序上依赖于各种配置，如Tomcat下各层级xml配置。有些框架对配置这块提供了丰富的支持，但为此的代码是，配置难以掌控。

就拿Yii框架为例（Yii确实是一个很了不起的框架，这里只是以事论事），当你需要在视图渲染一个数据表格时，你可以使用 CGridView ，并类似这样配置：

$columns = array(
    array('name' => 'mId', 'header' => '序号'),
    array('name'=>'id', 'header'=>'事件ID'),
    array('name'=>'title', 'header'=>'标题'),
    array('name'=>'content', 'header'=>'内容', 'type' => 'html'),
);

$this->widget('bootstrap.widgets.TbGridView', array(
    'type'=>'striped bordered condensed',
    'dataProvider'=>$dataProvider,
    'columns'=> $columns,
));

更为复杂的情况可以是：

$columns = array(
    // ...
    array('class' => 'CDataColumn', 'header' => '内容', 'type' => 'html', 'name' => 'content', 'htmlOptions' => array('width' => '200px')),
    array(
        'class'=>'CButtonColumn',
        'template'=>'{showEvent}<br/><br/>{deleteEvent}',
        'header'=>'操作',
        'buttons'=>array
        (
            'showEvent' => array(
                'label' => '查看',
                'url' => '"?r=DailyOperations/eventManagerShow&user_iduser_id=' . $userId  . '&eventId=". $data["id"];',
                'options' => array('target' => '_blank'),
            ),
            'deleteEvent' => array(
                'label'=>'删除',
                'url'=>'"javascript:void(0)"',
                'imageUrl'=>'/images/delete_24.png',
                'deleteConfirmation'=>"js:'Record with ID '+$(this).parent().parent().children(':first-child').text()+' will be deleted! Continue?'",
                'click'=>'js:function(){if (confirm("此操作将删除：ID = " + $(this).parent().parent().children(\':first-child\').text() + " \n是否确定？")) {deleteEvent($(
this).parent().parent().children(\':first-child\').text());};}',
            ),
        ),
    ),
);

// ...

然后，对于我这么笨的人来说，不管是简单的配置，还是复杂的配置，每次当我需要使用时，我都非常害怕且需要从以下三方便获取帮助：

  • 找曾经写过类似的代码并拷贝过来修改；
  • “耐心”（耐着心）查看官方的文档；
  • 网上搜索相关的例子

因为，每次我都记不住这些配置，但我又不得不承认它的效果很好。然后我觉得其缺点至少有两点：

  • 缺点1：尽管是很简单的功能也需要用配置来实现，从而导致配置羞涩难懂；
  • 缺点2：配置太复杂，对人的记忆要求太高；

这是我对Yii框架配置的体会。

自己工作的回顾

最初，感受到配置式的开发，是在大学的时候做一个OutLook的插件。这个插件需要同步本地和远程服务器的联系人，其中当有冲突时，就有这么几种策略：冲突时以本地为准、冲突时以远程为准、冲突时提醒我、忽略冲突。

这是当时写的博客，感兴趣可以看看 配置编程： 让项目开发从多样到统一

这有点像我们常用的SVN的处理方式。然而当我在尝试开发实现时，我发现过程很复杂，但处理又是如此相似。这里的区别很微妙，特别这些策略又是由外部用户指定时。最后，我惊讶地发现，如果我使用配置来做的话，会非常简单且明了！

但那时，只是初体会。

现在，经过了几年的开发，我才慢慢发现，可以把这种开发模式总结为：开发-配置-使用模式。
不知是否有其他模式和此新发现的模式类似？

2.20.3 框架外延

框架外延是指跨项目，与业务无关的抽象特性与代码实现。

这些从简单到复杂，有：通用功能，定制，扩展类库和产品簇框架。

(1)通用功能

有些功能是常用的，也是通用的。一般以函数或者工具类的形式提供，如一个生成随机数的方法。这些通用的功能，开发人员都可以在项目间流通使用。

(2)定制

当发现PhalApi现在的框架不支持某些接口下具体方案的实现时，或者支持但不满足当前项目的开发需要时，可以进行一些定制化。

之前有一个项目的开发同学，给我提供了一个很好的建议。他说他的项目为了能够获得更高的安全性，对客户端传递的参数进行了整包加密，然后在服务商进行整包解密再通过getRules()获取。所以对应的定制大概实现可以是这样的：

<?php
class My_Request extends  PhalApi_Request {

     protected function genData($data){
         $needData = array();

        if (!isset($data) || !is_array($data)) {
            $data = $_POST;
        }

        $needData['service'] = isset($data['service']) ? $data['service'] : '';
        //整包加密后的数据包
        $params = isset($data['params']) ? $data['params'] : '';
        //TODO: 对称解密 ...

        $needData = array_merge($params, $needData);

        return $needData;
    }
}

(3)扩展类库

有时候，我们在项目中需要使用到一组功能操作，这时可以结合外观模式，将这些需要用到的接口以包的形式都封装到一个扩展里面。因为扩展这个概念是容易理解的，也是开发人员所喜爱的。

在PhalApi的Issues里面，有位同学就提到了对新浪或者百度云空间的支持，就是对应的一个场景。

现在PhalApi提供了部分的扩展类库，但由于个人时间有限，而且也不可能知悉全部项目需要用到的全部扩展。所以当发现需要的扩展类库还没有提供时，简单的方法：自动写一个。

这也是对框架进行外延的一种途径。伴随着这种途径经历的次数越多，你会突然有一天发现你所搭建的这一切结合起来后，已经可以很好地应对了公司内的目前全部项目的开发。

(4)产品簇框架

这个时候，就来了产品簇框架这一阶段。

PhalApi只是提供了一个基础的框架，一个项目实际开发的基础框架，更是一个产品簇框架的铺垫。如果说我们关注项目的快速交付，不如说我们更关注 如何提供一个底层框架以支持不同项目的产品簇框架开发，从而最终支持项目的快速交付 。

也只有这样，你辛苦付出的代码，你才会更加珍惜和不断为之冥想、维护和改进。
也只有这样，你公司里面的其他项目才会更愿意和信赖使用，因为框架是你直接提供的。
也只有这样，代码才得以永恒，因为这种思想在你、我、各个项目团队间不断传递，共存在每个开发人员的脑里和心中，而不是为某个自私的人把持着。

2.20.4 项目内涵

框架外延 是针对特定领域项目以外的问题。然而，在使用PhalApi进行项目开发时，我们如果只关注这些是无用的，我们还要关注项目的实际开发，如何编写代码，即：项目内涵。

项目内涵是指完成特定领域功能所需要的前置条件、基础设施和工作流程。

这里面着重讲解复杂的领域和更广义的数据源这两方面。不是说其他的方面不重要，而是这两方既重要但又常常为人们所误解误用。

(1)复杂的领域业务层

在一个项目架构里面，有三个主要模型：设计模型、领域模型和代码模型。设计模型在选择PhalApi时已大体确定，领域模式则需要项目干系人员消化、理解并表达出来。对于开发人员，代码模型则是他们表达的媒介。

这一层，主要关注的是领域业务规则的处理。所以，我们抛开外界客户端接口调用的签名验证、参数获取、安全性等问题，也不考虑数据从何而来、存放于何处，而是着重关注对领域业务数据的处理上。

根据这么年来的工作、项目开发和学习，这里有一些建议。

规则出现且出现一次

领域之所以复杂，在于规则众多。如果不能很好地把控这些规则，当规则发生变化时，就会出现很大的问题。在开发过程中，要注意对规则进行提炼并且放置在一个指定的位置。如对游戏玩家的经验计算等级时，这样一个规则就要统一好。不要到处都有类型相同的计算接口。

释意接口

领域的逻辑是对现实业务场景的再解释。现实的因素充满变数并且由人为指定，所以不能简单的在计算机中“推导”出领域逻辑。在开发过程中，要特别对这些领域逻辑准确并很好的解释，以便后面接手的同学可以更容易理解和明白这些流程、限制和规则。

其中一个有力的指导就是释意接口。对接口签名甚至是对变量命名的仔细推敲都是很有益处的，因为名字能正名份，不至于混淆或者含糊不清。

代码保持在同一高度

领域层关注的是流程、规则，所以当你进行用户个性化分流和排序时，不应该把底层网络接口请求的细节也放到这里流程里面。把底层技术实现的细节和业务规则的处理分开是很有好处的，这样便于更清晰领域逻辑的表达，也助于单元测试时的测试桩模拟。

(2)更广义的数据源层

领域层固然重要，但如果没有数据源层，领域层就是一个空中楼阁。

但不应把数据源就理所当然地对等成数据库。因为这种观念很常见但也很狭隘。首先，很多项目在对数据存储时，不一定会落地存储，即使落地也不一定使用数据库。我曾经在一家游戏公司任职时，就看到他们使用了文件来存放。相信，你也看到过。其次，在现在多客户端多系统的交互背景下，很多系统都需要进行数据共享和通信，为了提高服务器的性能也会使用到缓存。这些场景下，会导致数据是通过接口来获取，或者来源于缓存。可以看出，如果把数据源就看作是MySql，是非常局限的。

我们在PhalApi中继续使用了Model层，因为受MVC模式的影响，大家都对Model层非常熟悉。但我们却为它赋予了新的诠释和活力。

Model导获取的数据，可以是来自数据库的读取，也可以是通过开放平台接口获取的数据，也可以是不落地直接存放于缓存的数据。

2.20.5 测试先行和真实的测试

框架外延和项目内涵，分别是对 开发-配置-使用 模式 前半部分和后半部分的诠释。

剩下的问题就在于，如何评判我们编写的代码是好的，是美的，并且能够按期望地工作？

如果只是主观地判断，显然是不可靠的也不是完全可信的。例如“我觉得我这样没问题啊！”，即使代码能够正常运行，也不并代表在其他临界或者极端的情况也能如期运行，也不代表这些代码就具备了一些好的品质，更不代表这些代码遵循了我们的架构明显的编程风格：编写人容易理解的代码。

既然主观不可靠，就应该转到客观上的标准。

静态的代码分析，是很有用的，但对于最终需要动态执行的代码，还是需要单元测试才能更好的探知代码内部每个角落的状况。

这里，我们强烈推荐测试先行，也就是测试驱动开发。可能你已对此耳熟能详，也可能你略知一二。

但请相信，测试先行是值得的。关于真实的测试，即编写PHPUnit单元测试，关于PHPUnit的使用，请查看： PHPUnit Manual – 第 1 章 自动化测试 。

2.20.6 PhalApi架构的设计意图

(1)设计意图

PhalApi是开放式的框架，这里不仅仅体现在源代码开放，产品开放，还表现在思想开放。

这样说，可能有些抽象。若落实到代码层次，你会发现PhalApi提供了接口上的约束，并为每块接口提供了很好的扩展机制。也就是说，一旦你发现已提供的功能不满足你项目的需要，你可以轻松定制、扩展和升级。

这就是我们的设计意图。

对于新手，你可以快速使用这个框架；对于老手，你则可以定制它。

(2)协同合作和关注点

开发-配置-使用 模式 并不是为了传递 “君君臣臣、父父子子”这样的封建等级观念。而是为了资源更好的调配，最终更好地完成项目的开发。

架构系统有分层的思想，这里也一样。通过高级开发、开发工程师和新手各尽其才、各施其职，能够更好的提高各自的关注点，并发挥他们应有的能力和价值。

比如对于高级开发，我们希望他们能够抽离业务，并且产出一些公司内可以使用的通用组件、核心技术甚至是产品簇开发框架。而对于他们来说，也许这也正是他们所喜爱的，因为他们在攻关一些技术难点，又或者在解决一些他们觉得有挑战的工作。

对于项目负责人，也就是开发工程师，他们更关注的是整个项目的运行和所能提供的功能。而这一些功能需要各业务规则在代码上的体现，最终又会直接或间接落到某些基础设施的支持上，一如数据库的查询操作。显然，他们不希望每次都为这样通用的技术支持重复开发。如果有已经能够直接拿来使用的代码，那该多好。网上虽然资源众多，但符合公司项目使用的，少之甚少，又或许会有这样那样的限制。如果有公司内部可重用的组件库，这种情况会大为改观。

对于新手，我们要求明显会低很多。简单来说，他们会使用就可以了。当然，我们也推荐新手在熟练使用的情况下，再深化到底层，慢慢过渡到大工。

(3)这只是一个开始

尽管 我们提供了这样架构明显的编程风格，但仍然需要你以及你的团队来遵循。

正如前面所说的，项目的每一行代码和每一个命名都来自你的思考和双手的输入。也正如此，你的付出让你更深刻体会到编程的乐趣，特别是项目发挥和实现了有价值的业务功能时。这时，你会发现维护别人的代码不再是一件痛苦的事件，与团队的合作也变得更加融洽，因为和你一起奋斗努力的是一支精英团队。

请记住，这只是一个开始，一个起点。

上一章 文档首页 下一章

[2.21]-服务器统一部署方案简明版：CentOs---Nginx---php-fpm---MySql-[--Memcached]

上一章 文档首页 下一章

2.21.1 前言

目前服务器架构通常采用：CentOs + Nginx + php-fpm + MySql [+ Memcached]，现将部署方案安装方法、遇到的问题和解决方法、启动方式总结如下，以便后期服务器快速部署。

以下文档的是经过几个深夜整理并在阿里云服务器真实部署通过，运行良好，整个过程熟悉后约共半小时，可以进一步考虑做成自动化部署脚本。
如是，请把脚本也邮件发我一份。^_^

2.21.2 统一约定

  • 全部使用编译安装
  • 安装路径路径：/usr/local/

  • 安装包存放位置：/home/apps/web/

    特别注意：请尽量不要使用root账号操作服务器，特别是对外服务器，谢谢。

2.21.3 依赖库和开发工具

yum -y install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers

# nginx
yum -y install pcre-devel zlib-devel

# php
yum -y install gd-devel libjpeg-devel libpng-devel freetype-devel libxml2-devel curl-devel freetype-devel

# mysql
yum -y install bison gcc gcc-c++ autoconf automake zlib* libxml* ncurses-devel libtool-ltdl-devel* mysql-devel

# sz rz
yum install lrzsz

2.21.4 安装Nginx 1.6.0

参考官方网站

http://wiki.nginx.org/Install

(1)安装

./configure --prefix=/usr/local/nginx
make
make install

(2)启动

/usr/local/nginx/sbin/nginx

(3)重启

/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload

(4)更多配置

server {
    listen 80;
    server_name test.wetime.com;
    root /home/apps/projects/test.wetime.com;
    charset utf-8;
    access_log logs/test.wetime.com.access.log;
    error_log logs/test.wetime.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

2.21.5 安装PHP-FPM 5.3.5

(1)参考

http://blog.csdn.net/dc_726/article/details/9519619

(2)安装

./configure --prefix=/usr/local/php --with-mcrypt --with-zlib --with-curl --with-gd --disable-debug --enable-fpm --enable-mbstring --enable-sockets --enable-zip --enable-xml --enable-pdo

温馨提示

如果提示以下错误：

If configure fails try --with-jpeg-dir=<DIR>
configure: error: libpng.(a|so) not found.

则需要执行以下命令：

cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so

yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

(3)启动

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

(4)重启

/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini -t
killall php
/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini

(5)错误处理与依赖

cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so
yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

我们可以在命令下输入：

yum localinstall –nogpgcheck http://mirrors.ustc.edu.cn/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/free/el/updates/6/x86_64/rpmfusion-free-release-6-1.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/nonfree/el/updates/6/x86_64/rpmfusion-nonfree-release-6-1.noarch.rpm

安装这3个东东后，我们就可以直接使用yum install libmcrypt来安装啦～

(6)php扩展安装

#安装mysql扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysql=/usr/local/mysql/
make
make install

#安装mysqli扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysqli=/usr/local/mysql/bin/mysql_config
make
make install

扩展包位置在./php-5.3.5/ext/mysql，需要使用到 /usr/local/php/bin/phpize，扩展so应该放到：/usr/lib/php/modules/

此外，

#PHP之pdo_mysql扩展安装步骤

# http://www.smallbigcake.com/2012/08/05/1572/
# http://pecl.php.net/get/PDO_MYSQL-1.0.2.tgz

./configure --with-php-config=/usr/local/php/bin/php-config --with-pdo-mysql=/usr/local/mysql

如果提示“libmysqlclient.so.18: cannot open shared object file: No such file or directory in ”错误则参考 ：http://blog.itpub.net/26230597/viewspace-1161322/，
或进行软链：
ln -s /usr/local/mysql/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.18

2.21.6 安装MYSQL 5.6.10

(1)参考

http://www.cnblogs.com/xiongpq/p/3384681.html

(2)mysql-5.6.10.tar.gz下载

http://pan.baidu.com/share/link?shareid=3729862999&uk=1327790029

(3)前期清理和依赖

rpm -e mysql   //普通删除模式
rpm -e --nodeps mysql    // 强力删除模式，如果使用上面命令删除时，提示有依赖的其它文件，则用该命令可以对其进行强力删除
yum -y install make gcc-c++ cmake bison-devel  ncurses-devel

(4)安装

cmake \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc \
-DWITH_MYISAM_STORAGE_ENGINE=1 \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_MEMORY_STORAGE_ENGINE=1 \
-DWITH_READLINE=1 \
-DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock \
-DMYSQL_TCP_PORT=3306 \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DEXTRA_CHARSETS=all \
-DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci

make && make install

(5)ERROR! The server quit without updating PID file

#去掉干扰的配置
mv /etc/my.cnf /etc/my.cnf.bak

#添加日记纪录
vim /usr/local/mysql/my.cnf
log-error=/var/log/mysqld.log
innodb_force_recovery=1

2.21.7 [可选]MEMCACHE 1.4.20

(1)参考官网

http://www.memcached.org/downloads

(2)更多参考

http://crtlib.blog.163.com/blog/static/133158013201392661912837/

(3)先安装依赖包

./configure --prefix=/usr/local/libevent/
make
make install

(4)安装

wget http://memcached.org/latest tar -zxvf memcached-1.x.x.tar.gz cd memcached-1.x.x ./configure && make && make test && sudo make install

(5)最后成功安装后启动

/usr/local/memcached/bin/memcached -d -m 64 -u root -l 127.0.0.1 -p 11211 -c 128 -P /tmp/memcached.pid

(6)memcached 扩展

#http://blog.sina.com.cn/s/blog_66ac09390101drxc.html
#http://www.linuxidc.com/Linux/2013-07/87743.htm

#准备依赖包
./configure --prefix=/usr/local/libmemcached --with-memcached
make
make install

#安装 扩展
/usr/local/php/bin/pecl install memcached
接着输入
libmemcached directory [no] : /usr/local/libmemcached

上一章 文档首页 下一章#[2.22]-更多工具：精益项目和团队建设

上一章 文档首页 下一章

2.22.1 PHPUnit：自动化单元测试

目前核心框架单元测试的覆盖率（高达90%以上！！！此处应该有掌声~）： a pic

注意，此测试截图并不是最新的，但我们一直都在致力坚持单元测试。
这一点，你可以在单元测试的代码中找到证明。

2.22.2 Phing：一键部署、快速发布

无论如何，都应该走自动化发布流程，避免人工地打包、上传、解压、改生产配置这些重复性的人工操作。

在自动化发布中，Phing是个不错的尝试。

以下是我某个项目中的使用配置，出于对项目的保护，部分数据已删除，但仍然可以参考。

(1)发布配置 - build.xml

主要的操作有：

    1. 备份当前代码，并删除一天前的备份
    1. 从SVN签出最新的发布代码
    1. 执行配置检测脚本
    1. 代码发布切换，并替换使用线上配置文件
    1. 移除单元测试初始化文件，以免误执行
<?xml version="1.0" encoding="UTF-8"?>

<!-- ============================================  -->
<!-- PhalApi                       -->
<!-- @dogstar 20141221                             -->
<!-- ============================================  -->

<project name="demo.phalapi.com" default="build">

    <property
        name="backup_path"
        value="/home/apps/backup/demo.phalapi.com"
        override="true" />
    <property
        name="backup_prefix"
        value="demo.phalapi.com_phing_backup_"
        override="true" />
    <property
        name="svn_todir"
        value="/home/apps/svn/demo.phalapi.com"
        override="true" />

    <!-- ============================================  -->
    <!-- Target: prepare                               -->
    <!-- ============================================  -->
    <target name="prepare">
        <mkdir dir="./Runtime" />
        <mkdir dir="${svn_todir}" />
        <mkdir dir="${backup_path}" />
    </target>

    <!-- ============================================  -->
    <!-- Target: svn checkout                          -->
    <!-- ============================================  -->
    <target name="svncheckout">
        <svncheckout
            repositoryurl="svn://127.0.0.1/PhalApi/demo/release"
            username="test"
            password="123456"
            nocache="true"
            todir="${svn_todir}" />
    </target>

    <!-- ============================================  -->
    <!-- Target: svn update                            -->
    <!-- ============================================  -->
    <target name="svnup" depends="svncheckout">
        <svnupdate
            repositoryurl="svn://127.0.0.1/PhalApi/demo/release"
            username="test"
            password="123456"
            nocache="true"
            todir="${svn_todir}" />
    </target>

    <!-- ============================================  -->
    <!-- Target: check config                          -->
    <!-- ============================================  -->
    <target name="checkconfig">
    <php expression="include('${svn_todir}/Tools/check_config.php')" />
        <php
            function="checkProd"
            class="Tools_Check_Config"
            returnProperty="checkProdErrorMsg" />

        <fail msg="${checkProdErrorMsg}" if="checkProdErrorMsg" />
    </target>

    <!-- ============================================  -->
    <!-- Target: backup                                -->
    <!-- ============================================  -->
    <target name="backup">
        <php
            expression="date('Ymd', strtotime('-1 day'))"
            returnProperty="backup_version_yesterday" />
        <php
            expression="date('Ymd')"
            returnProperty="backup_version_today" />
        <delete>
            <fileset dir="${backup_path}">
                <exclude name="${backup_prefix}${backup_version_yesterday}*" />
                <exclude name="${backup_prefix}${backup_version_today}*" />
            </fileset>
        </delete>

        <php
            expression="date('YmdHis')"
            returnProperty="backup_version" />
        <zip destfile="${backup_path}/${backup_prefix}${backup_version}.zip" basedir="." >
            <fileset dir=".">
                <include name="**/**" />

                <exclude name="./Runtime" />
                <exclude name="./Runtime/**" />
                <exclude name="./.svn" />
                <exclude name="./.svn/**" />
            </fileset>
        </zip>
        <copy
            file="${backup_path}/${backup_prefix}${backup_version}.zip"
            tofile="${backup_path}/${backup_prefix}lastest.zip"
            overwrite="true" />
    </target>

    <!-- ============================================  -->
    <!-- Target: build                                 -->
    <!-- ============================================  -->
    <target name="build" depends="prepare,svnup,checkconfig,backup">
        <copy todir="." overwrite="true" >
            <fileset dir="${svn_todir}">
                <include name="**/**" />

                <exclude name="${svn_todir}/Config/dbs.php" />
                <exclude name="${svn_todir}/Config/sys.php" />
                <exclude name="${svn_todir}/Test" />
                <exclude name="${svn_todir}/Test/**" />
            </fileset>
        </copy>

        <copy
            file="./Config/dbs.php.prod"
            tofile="./Config/dbs.php"
            overwrite="true" />
        <copy
            file="./Config/sys.php.prod"
            tofile="./Config/sys.php"
            overwrite="true" />

        <!-- 避免在生产环境执行PHPUnit，故把测试环境的初始文件移开 -->
        <move
            file="./Test/test_env.php"
            tofile="./Test/test_env.php.bak"
            overwrite="true"/>
    </target>

</project>

(2)回滚配置 - rollback.xml

主要操作：

    1. 回滚到上一个版本
<?xml version="1.0" encoding="UTF-8"?>

<!-- ============================================  -->
<!-- PhalApi                       -->
<!-- @dogstar 20141222 冬至                        -->
<!-- ============================================  -->

<project name="demo.phalapi.com" default="rollback">

    <property
        name="backup_path"
        value="/home/apps/backup/demo.phalapi.com"
        override="true" />
    <property
        name="backup_prefix"
        value="demo.phalapi.com_phing_backup_"
        override="true" />
    <property
        name="svn_todir"
        value="./__svn__"
        override="true" />

    <!-- ============================================  -->
    <!-- Target: rollback                              -->
    <!-- ============================================  -->
    <target name="rollback" >
        <unzip file="${backup_path}/${backup_prefix}lastest.zip" todir="." >
            <fileset dir=".">
                <include name="*.zip"/>
            </fileset>
        </unzip>
    </target>

</project>

2.22.3 autobench：接口压力测试与可视化图表

可以通过以下的脚本来进行，使用示例：

$ ./autobench.sh
Usage: ./autobench.sh <host> <uri>

 - ./autobench.sh www.baidu.com /index.php

参数可以自行调整。

(1)利用bench2graph生成可视化图表

纯PHP访问 - 入口欢迎接口

mysql访问 - 事件获取接口

带MC缓存的访问 - 应用入口 - 带缓存的身份token验证

(2)详细的数据报表

dem_req_rate    req_rate_demo.phalapi.com     con_rate_demo.phalapi.com     min_rep_rate_demo.phalapi.com avg_rep_rate_demo.phalapi.com    max_rep_rate_demo.phalapi.com stddev_rep_rate_demo.phalapi.com      resp_time_demo.phalapi.com       net_io_demo.phalapi.com       errors_demo.phalapi.com
5       5.0     5.0     4.8     5.0     5.2     0.1     22.4    2.9     0
25      25.0    25.0    25.0    25.0    25.0    0.0     21.6    14.3    0
45      45.0    45.0    45.0    45.0    45.0    0.0     22.3    25.7    0
65      64.9    64.9    64.7    64.7    64.7    0.0     25.4    37.0    0
85      84.8    84.8    84.6    84.6    84.6    0.0     28.6    48.3    0
105     104.6   104.6   0.0     0.0     0.0     0.0     34.4    59.7    0
125     124.0   124.0   0.0     0.0     0.0     0.0     42.2    70.7    0
145     143.8   143.8   0.0     0.0     0.0     0.0     59.2    82.0    0
165     147.4   147.4   0.0     0.0     0.0     0.0     262.9   84.1    0
185     151.1   151.1   0.0     0.0     0.0     0.0     429.7   86.2    0

(3)附脚本

#!/bin/bash

if [ $# -eq 0 ]; then
    echo "Usage: $0 <host> <uri>"
    echo ""
    echo " - $0 www.baidu.com /index.php"
    echo ""
    exit
fi

DM=$1
URL=$2

#--signle_host 只测单机
#--host1 测试主机地址
#--uri1 host1 测试URI
#--quiet 安静模式
#--low_rate 测试时最低请求数(指 httperf)
#--hight_rate 测试时最高请求数
#--rate_step 每次测试请求数增加步长
#--num-call 每连接中发起联接数，一般是1
#--num_conn 测试联接数
#--file 测试结果输出的 tsv文件

autobench \
--single_host \
--host1=$DM \
--port1=80 \
--uri1=$URL \
--low_rate=5 \
--high_rate=200 \
--rate_step=20 \
--num_call=1 \
--num_conn=500 \
--timeout=10 \
--file ./$DM.tsv

2.22.4 xhprof：性能分析工具

xhprof是一个不错的内部性能分析工具，这里不过多的讲述此工具的特点和使用，但会以对PhalApi进行的一个性能测试展示它的分析效果。

(1)测试的接口服务

http://api.phalapi.com /demo/?service=Default.Index&username=test

(2)Overall Summary

Total Incl. Wall Time (microsec):     7,873 microsecs
Total Incl. CPU (microsecs):    7,999 microsecs
Total Incl. MemUse (bytes):    304,456 bytes
Total Incl. PeakMemUse (bytes):    306,616 bytes
Number of Function Calls:        338

(3)Top 10耗时

Function Name | Calls | Calls% | Incl. Wall Time(microsec) | IWall%(microsec) | Excl. Wall Time | EWall%
PhalApi_Loader::loadClass | 10 | 3.00% | 3,020 | 38.40% | 1,038 | 13.20%
PhalApi_Loader::loadClass1 | 4 | 1.20% | 785 | 10.00% | 398 | 5.10%
run_init::Public/init.php | 1 | 0.30% | 3,915 | 49.70% | 327 | 4.20%
file_exists | 18 | 5.30% | 249 | 3.20% | 249 | 3.20%
main() | 1 | 0.30% | 7,873 | 100.00% | 248 | 3.20%
load::zh_cn/common.php | 2 | 0.60% | 237 | 3.00% | 237 | 3.00%
PhalApi_Loader::load | 9 | 2.70% | 3,380 | 42.90% | 226 | 2.90%
PhalApi_DI::get | 9 | 2.70% | 1,268 | 16.10% | 223 | 2.80%
PhalApi_Translator::addMessage | 2 | 0.60% | 522 | 6.60% | 136 | 1.70%
DI | 15 | 4.40% | 645 | 8.20% | 134 | 1.70%
class_exists | 11 | 3.30% | 1,383 | 17.60% | 130 | 1.70%
load::Config/sys.php | 1 | 0.30% | 120 | 1.50% | 120 | 1.50%

对应的图表如下：
a pic
从中可以看出，主要的耗时途径在于文件的加载，下面将进一步探讨。

(4)Top 1耗时深入

所加载的文件如下：
a pic

2.22.5 Jenkis和Sonar：持续集成和静态代码分析

PhalApi从来不会隐藏自己的设计，当然，我们也不会隐藏我们内部的各个细节以及存在的问题。
但和其他隐藏了技术债务的框架相比，PhalApi敢于展示自己的静态代码分析报告，并且它也是做得相当出色的。

以下截图来自 GIT@OSC 上的静态代码分析： a pic

当你的项目很重要时，也可以使用Jenkis或者Sonar进行静态代码的分析。
不要个人主观地觉得你的代码风格写得好，而是交由专业的分析工具进行解剖，并理解各个数据报表背后的含义，然后改进之。

2.22.6 Git/SVN：更多的版本控制

2.22.7 WIKI：团队沟通与文档交流

(1)markdown接口模板

#3.2 接口文档模板
##1、功能说明
_请在这里放置简短的接口功能说明。_

##2、接口URL
/?service= _接口服务名称_  + 公共参数(是否免登录态？)

##3、接口参数 [跳转](http://demo.phalapi.com/demo/checkApiParams.php?service=)

参数|必须|默认值|说明
---|---|---|---
user_id|1||用户id

##4、返回结果

###返回字段
参数|类型|说明
---|---|---
data.username|string|用户名

###结果示例
{
    "ret": 200,
    "data": {
       ....  //更多结果的说明

        "msg": ""
    },
    "msg": ""
}

###请求示例
_请放置一个接口请求的链接。_

http://demo.phalapi.com/demo/?service=

2.22.8 静态代码分析工具 - phpmetrics

以下是针对本框架核心代码所做的分析报告：

PS：从报告的评估可以看出，我们的框架明显具有相当 高的可维护性。

上一章 文档首页 下一章#[3.1]-扩展类库：微信开发

上一章 文档首页 下一章

3.1.1 扩展类库：微信开发

此扩展可用于微信的服务号、订阅号、企业号等功能开发，在PhalApi框架下简单配置即可开发使用。

3.1.2 安装和配置

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 Wechat 微信包，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 Wechat 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/Wechat/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(2)扩展包配置

为了让微信扩展包能接收来自微信服务器的信息以及返回信息给用户，我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 插件注册 - 项目级重用
     */
  'Wechat' => array(
    'plugins' => array(
        Wechat_InMessage::MSG_TYPE_TEXT => array('Plugin_Menu',),
        Wechat_InMessage::MSG_TYPE_IMAGE => array(),
        Wechat_InMessage::MSG_TYPE_VOICE => array(),
        Wechat_InMessage::MSG_TYPE_VIDEO => array(),
        Wechat_InMessage::MSG_TYPE_LOCATION => array(),
        Wechat_InMessage::MSG_TYPE_LINK => array(),
        Wechat_InMessage::MSG_TYPE_EVENT => array(),
        Wechat_InMessage::MSG_TYPE_DEVICE_EVENT => array(),
        Wechat_InMessage::MSG_TYPE_DEVICE_TEXT => array(),
    ),
  )

简单说明一下上面配置的作用，很明显，Key为对应的消息类型，如：文本、位置、语音、图片等；然后是各种消息类型对应的处理的类名，可以有多个，从上到下依次处理。
如果觉得配置很多，可以只配置需要用到的消息类型。另外，如果你的配置是其他途径存放的，也亦然。

3.1.3 入门使用

(1)微信入口文件

可以按将下面的入口代码得到你的项目下：

//$ vim ./Public/weixin/index.php

<?php

// echo $_GET['echostr'];
// die();

if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
    die('Access denied!');
}

require_once dirname(__FILE__) . '/../init.php';

//装载项目代码和扩展类库
DI()->loader->addDirs(array('Demo', 'Library'));

/** ---------------- 微信轻聊版 ---------------- **/

$robot = new Wechat_Lite('YourTokenHere...', true);
$rs = $robot->response();
$rs->output();

特别地 ，当首次接入微信时，需要将开头的两句注释去掉，以便通过微信的验证，即：

// echo $_GET['echostr'];
// die();

3.1.4 示例：让红包飞

(1)业务场景

现在，快要过年了，各大企业都在派红包，这里，我们也模拟一下微信服务号上红包的派发。

通常地，当我们需要添加一个新的微信服务号的功能时，可以两步走： 先开发插件，再注册插件 。

(2)开发插件

很简单地：

// $vim ./Demo/Plugin/Money.php

<?php

class Plugin_Money implements Wechat_Plugin_Text {

    public function handleText($inMessage, &$outMessage) {
        $outMessage = new Wechat_OutMessage_News();

        $item = new Wechat_OutMessage_News_Item();
        $item->setTitle('让红包飞~')
            ->setDescription(sprintf('您已领取到一个%d元红包~', rand(1, 100)))
            ->setPicUrl('http://webtools.qiniudn.com/172906_61c8663a_121026.jpeg')
            ->setUrl('http://git.oschina.net/dogstar/PhalApi-Library');

        $outMessage->addItem($item);
    }
}

(3)注册插件

开发好插件后，需要在配置添加对哪种消息开启使用该新的插件。在 ./Config/app.php 配置文件中追加以下配置：

        // 前面配置略 ...

        // 在前面追加红包插件，处理顺序由左到右
        Wechat_InMessage::MSG_TYPE_TEXT => array('Plugin_Money', 'Plugin_Menu', ),

(4)运行效果

3.1.5 微信开发调试

为了便于进行微信开发的调试，我们专门提供了一个快速模拟微信请求的脚本。以下是对上面发红包的模拟请求：

$php ./Library/Wechat/Tests/send_wechat_text.php http://dev.wechat.com/weixin/ demo

<xml>
<ToUserName><![CDATA[oWNXvjipYqRViMpO8GZwXxE43pUY]]></ToUserName>
<FromUserName><![CDATA[gh_43235ff1360f]]></FromUserName>
<CreateTime>1423821388</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>1</ArticleCount>
<Articles>
<item>
<Title><![CDATA[让红包飞~]]></Title>
<Description><![CDATA[您已领取到一个85元红包~]]></Description>
<PicUrl><![CDATA[http://webtools.qiniudn.com/172906_61c8663a_121026.jpeg]]></PicUrl>
<Url><![CDATA[http://git.oschina.net/dogstar/PhalApi-Library]]></Url>
</item>
</Articles>
<FuncFlag>0</FuncFlag>
</xml>

上一章 文档首页 下一章#[3.2]-扩展类库：代理模式下phprpc协议的轻松支持

上一章 文档首页 下一章

3.2.1 扩展类库：代理模式下phprpc协议的轻松支持

我们一直都建议在我们的项目中使用设计模式以便让代码更优雅，但我们更推荐只在合适的场景才使用合适的设计模式，而不是为了“显学”而生硬套用。设计模式又需要更高层的设计原则和工程思想作指导，才能更正确被使用发挥其作用。

在PhalApi，正如前面所说的，我们引入并应用了很多设计原则，一如单一职责原则SPR、开放-封闭原则OCP等。因此，在这里我们可以在phprpc的基础上，利用代理模式优雅地扩展实现phpcpr协议。

在需要使用phprpc协议进行接口调用的项目中，我们可以快速利用此扩展类库。并且你会发现，服务端的接口已有的代码不需要做任何改动，只需要增加此扩展包和添加一个新入口便可完美切换。特别在项目中途BOSS说我们要提高接口调用的安全性而改用phprpc协议时。

3.2.2 安装

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 PHPRPC 微信包，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 PHPRPC 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/PHPRPC/ ./PhalApi/Library/ -R

到此安装完毕！

3.2.3 入门使用

(1)phprpc协议入口

可以参考下面的入口代码编写：

//$ vim ./Public/phprpc/index.php

<?php

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()->loader->addDirs(array('Demo', 'Library'));

$server = new PHPRPC_Lite();
$server->response();

3.2.4 示例

这里以?service=User.GetBaseInfo为例进行说明。

(1)浏览器访问失败

在使用了phprpc协议后，我们再以浏览器（HTTP协议）访问调用接口时，会预期地出现以下失败信息：

(2)通过phprpc协议调用

如果通过phprpc协议调用，我们则可以得到接口返回的 源数据 。
假设请求的接口为：?service=User.GetBaseInfo&user_id=1，则可以得到我们熟悉的：

array (
  'ret' => 200,
  'data' =>
  array (
    'code' => 0,
    'msg' => '',
    'info' =>
    array (
      'id' => '1',
      'name' => 'dogstar',
      'note' => 'oschina',
    ),
  ),
  'msg' => '',
)

3.2.5 phprpc协议调试

同样，为了方便进行phprpc协议下接口调用的调试，我们提供了一个脚本，如上面的对?service=User.GetBaseInfo&user_id=1调用，使用脚本即为：

$ php ./Library/PHPRPC/check.php http://dev.phprpc.com/phprpc/?service=User.GetBaseInfo&userId=1
array(3) {
  ["ret"]=>
  int(200)
  ["data"]=>
  array(3) {
    ["code"]=>
    int(0)
    ["msg"]=>
    string(0) ""
    ["info"]=>
    array(3) {
      ["id"]=>
      string(1) "1"
      ["name"]=>
      string(7) "dogstar"
      ["note"]=>
      string(7) "oschina"
    }
  }
  ["msg"]=>
  string(0) ""
}

3.2.6 对客户端的调整

虽然服务端不需要作出太多的改动，但对于客户端来说，仍然需要进行三方面的调整以进行phprpc协议的调用以及参数的传递和返回结果的获取。

现分说如下。

(1)调用方式的改变

首当其冲的就是调用方式的改变，但值得开心的是，phprpc对很多语言都有支持。具体可以查看phprpc官网。

(2)POST参数传递方式的改变

其实对POST参数传递的改变。考虑到phprpc协议中对POST的数据有一定的复杂性，这里统一作了简化。 正如我们下面的代码所示：

    public function response($params = NULL) {
        $paramsArr = json_decode($params, TRUE);
        if ($paramsArr !== FALSE) {
            DI()->request = new PhalApi_Request(array_merge($_GET, $paramsArr));
        }

我们约定： 通过第一个参数用JSON格式来传递全部原来需要POST的数据 。

当POST的数据和GET的数据冲突时，以POST为准。

所以，相应地，当需要传递POST参数时，客户需要这样调整（如PHP下）：

$client->response(json_encode($params)))

如无此POST参数，则可以忽略不传。

(3)返回结果格式的改变

最后，就是返回结果格式的改变。

在phprpc协议下，因为可以更轻松地获取接口返回的源数据，所以这里也同样不再通过字符串流式的序列返回（如原来的JSON或XML），而是直接返回接口的 源数据 。如上面示例中所看到的结果一样。

这点，需要特别注意。

3.2.7 参考

phprpc官网：http://www.phprpc.org/zh_CN/

上一章 文档首页 下一章#[3.3]-扩展类库：基于PHPMailer的邮件发送

上一章 文档首页 下一章

3.3.1 扩展类库：基于PHPMailer的邮件发送

此扩展可用于发送邮件。

3.3.2 安装和配置

从 PhalApi-Library 扩展库中下载获取 PHPMailer 包，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 PHPMailer 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/PHPMailer/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(1)邮箱配置

需要在 ./Config/app.php 配置文件中追加以下配置：

  'PHPMailer' => array(
    'email' => array(
        'host' => 'smtp.gmail.com',
        'username' => 'XXX@gmail.com',
        'password' => '******',
        'from' => 'XXX@gmail.com',
        'fromName' => 'PhalApi团队',
        'sign' => '<br/><br/>请不要回复此邮件，谢谢！<br/><br/>-- PhalApi团队敬上 ',
    ),
  ),

3.3.3 使用

如下代码示例：

require_once './init.php';

DI()->loader->addDirs('Library');

$mailer = new PHPMailer_Lite(true);
$mailer->send('chanzonghuang@gmail.com', 'Test PHPMailer Lite', 'something here ...');

稍候将会收到：

如果需要发送邮件给多个邮箱时，可以使用数组，如：

$addresses = array('chanzonghuang@gmail.com', 'test@phalapi.com');
$mailer->send($addresses, 'Test PHPMailer Lite', 'something here ...');

上一章 文档首页 下一章#[3.4]-扩展类库：优酷开放平台接口调用

上一章 文档首页 下一章

3.4.1 扩展类库：优酷开放平台接口调用

此扩展可用于对优酷开放平台提供的接口进行快速调用，并将失败的情况以debug方式写入日记。

3.4.2 安装和配置

从 PhalApi-Library 扩展库中下载获取 YoukuClient 优酷接口客户端，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 YoukuClient 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/YoukuClient/ ./PhalApi/Library/ -R

到此安装完毕！

3.4.3 使用

可以先在初始化文件init.php中引入扩展类库，并注册一个youkuClient的服务：

//$ vim ./Public/init.php
$loader->addDirs('Library');

DI()->youkuClient = function() {
    return new YoukuClient_Lite('https://openapi.youku.com', 'b043a60fbef8aed0');
};

然后，就可以在项目中使用youkuClient这个服务来实现对优酷平台接口的调用。

通常，我们会在Model层包装这一层数据来源的获取。但这里只是了为演示的效果，全部将代码放到Api里面，如：

//$ vim ./Demo/Api/Youku.php

<?php

class Api_Youku extends PhalApi_Api {

    public function showBasic()
    {
        $uri = '/v2/videos/show_basic.json';
        $params = array(
            'video_url' => 'http://v.youku.com/v_show/id_XOTA4ODU4NjA0.html'
        );
        $timeoutMs = 3000;

        $info = DI()->youkuClient->get($uri, $params, $timeoutMs);

        return array('id' => $info['id'], 'title' => $info['title']);
    }
}

请求一下对应的接口，便可以获得以下的运行效果：

对应的优酷视频是： 【双语字幕】Apple Watch 动手玩 - The Verge

上一章 文档首页 下一章#[3.5]-扩展类库：七牛云存储接口调用

上一章 文档首页 下一章

3.5.1 扩展类库：七牛云存储接口调用

此扩展可用于将图片上传到七牛云存储，或者七牛提供的其他功能。

3.5.2 安装和配置

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 Qiniu 七牛扩展包，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 Qiniu 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/Qiniu/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(2)扩展包配置

我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 七牛相关配置
     */
    'Qiniu' =>  array(
        //统一的key
        'accessKey' => '*****',
        'secretKey' => '****',
        //自定义配置的空间
        'space_bucket' => '自定义配置的空间',
        'space_host' => 'http://XXXXX.qiniudn.com',
    ),

你也可以不使用配置文件，直接在初始化注册DI()->qiniu时指定配置。

3.5.3 入门使用

(1)入口注册

//$ vim ./Public/init.php

// 其他代码....

DI()->qiniu = new Qiniu_Lite();

根据上面所说的，你可以也可以指定配置。

//$ vim ./Public/init.php

// 其他代码....

$qiniuConfig = array( /* 格式参照配置文件 *);
DI()->qiniu = new Qiniu_Lite($qiniuConfig);

3.5.4 示例：图片上传

(1)模拟客户端

先简单写个测试文件：

// vim ./Public/test_qiniu.html

<html>
    <form method="POST" action="/demo/?service=Qiniu_CDN.uploadFile" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit">
    </form>
</html>

效果如下（通常是提供给客户端上传的）：
p

(2)文件上传接口

随后，我们使用此七牛扩展提供的编写文件上传接口，通过查看在线接口文档，可以看到：

(3)运行一下：

缺少上传文件时：

{"ret":200,"data":{"code":1,"url":"","msg":"miss upload file"},"msg":""}

上传失败时：

{"ret":200,"data":{"code":2,"url":"","msg":"fail to upload file"},"msg":""}

可以看到对应的日记（如果不想看到此日记，可通过在初始化日记服务时去掉调试日记）：

2015-03-18 00:07:02|DEBUG|failed to upload file to qiniu|{"Err":"bad token","Reqid":"u1EAAG73DB3PVMwT","Details":"","Code":401}

上传成功时：

{"ret":200,"data":{"code":0,"url":"图片链接","msg":""},"msg":""}

上一章 文档首页 下一章#[3.6]-扩展类库：新型计划任务

上一章 文档首页 下一章

3.6.1 扩展类库：新型计划任务

此扩展类型用于后台计划任务的调度，主要功能点有：

  • 1、提供了Redis/文件/数据库三种MQ队列
  • 2、提供了本地和远程两种调度方式
  • 3、以接口的形式实现计划任务
  • 4、提供统一的crontab调度

更多内容，请查看：[1.31] 新型计划任务：以接口形式实现的计划任务

3.6.2 安装

(1)内置扩展包

此安装略，已内置于PhalApi框架中。

温馨提示：
PhalApi需要PhaApi 1.2.0及以上版本。

(2)配置

我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 计划任务配置
     */
    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'file' => array(
                'path' => API_ROOT . '/Runtime',
                'prefix' => 'phalapi_task',
            ),
            'redis' => array(
                'host' => '127.0.0.1',
                'port' => 6379,
                'prefix' => 'phalapi_task',
                'auth' => '',
            ),
            'mc' => array(
                'host' => '127.0.0.1',
                'port' => 11211,
            ),
        ),

        //Runner设置，如果使用远程调度方式，请加此配置
        'runner' => array(
            'remote' => array(
                'host' => 'http://library.phalapi.net/demo/',
                'timeoutMS' => 3000,
            ),
        ),
    ),

以上内容看情况需要而配置。

(3)数据库表

当需要使用数据库MQ列队时，还需要将以下数据库的配置追加到 ./Config/dbs.php 中：

    'tables' => array(

    //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中，未配置的表将使用默认路由

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'task_mq' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 9, 'db' => 'db_demo'),
            ),
        ),
    )

同时，需要将 ./Data/phalapi_task_mq.sql 文件的SQL建表语句导入到你的数据库。你也可以将利用脚本来生成。

3.6.3 入门使用

(1)加入MQ队列

首先，我们需要在入口文件进行对Task的初始化：

DI()->loader->addDirs('Library');
$mq = new Task_MQ_Redis();  //可以选择你需要的MQ
DI()->taskLite = new Task_Lite($mq);

然后，这样即可添加一个新的计划任务到MQ：

DI()->taskLite->add('Task.DoSth', array('id' => 1));

(2)计划任务的启动

在启动计划任务前，我们需要编写简单的脚本，一如这样：

#!/usr/bin/env php
<?php
require_once '/path/to/Public/init.php';

DI()->loader->addDirs('Demo');

if ($argc < 2) {
    echo "Usage: $argv[0] <service> \n\n";
    exit(1);
}

$service = trim($argv[1]);

$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);
$rs = $runner->go($service);

echo "\nDone:\n", json_encode($rs), "\n\n";

然后使用nohup或者crontab启动即可。

(3)续篇 - 统一的crontab计划任务

上面第二点的启动是通用、自定义、也是自由的启动方式。

这里再提供一种具体的、统一的启动方式，即使用crontab的方式。

首先，创建以下表(或见./Library/Task/Data/phalapi_task_progress.sql文件)：

CREATE TABLE `phalapi_task_progress` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `title` varchar(200) DEFAULT '' COMMENT '任务标题',
      `trigger_class` varchar(50) DEFAULT '' COMMENT '触发器类名',
      `fire_params` varchar(255) DEFAULT '' COMMENT '需要传递的参数，格式自定',
      `interval_time` int(11) DEFAULT '0' COMMENT '执行间隔，单位：秒',
      `enable` tinyint(1) DEFAULT '1' COMMENT '是否启动，1启动，0禁止',
      `result` varchar(255) DEFAULT '' COMMENT '运行的结果，以json格式保存',
      `state` tinyint(1) DEFAULT '0' COMMENT '进程状态，0空闲，1运行中，-1异常退出',
      `last_fire_time` int(11) DEFAULT '0' COMMENT '上一次运行时间',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

接着，添加crontab计划任务：

$ crontab -e

*/1 * * * * /usr/bin/php /path/to/PhalApi/Library/Task/crontab.php >> /tmp/phalapi_task_crontab.log 2>&1

然后，实现你的计划任务服务接口：

<?php
class Api_Task_Demo extends PhalApi_Api {

      public function doSth() {
            // ...
      }
}

最后，配置计划任务：

INSERT INTO `phalapi_task_progress`(title, trigger_class, fire_params, interval_time)  VALUES('你的任务名字', 'Task_Progress_Trigger_Common', 'Task_Demo.DoSth&Task_MQ_File&Task_Runner_Local', '300');

注意，在配置时，需要指明MQ和Runner的类型。

3.6.4 更多说明

(1)依赖的资源服务

名称 | 类 | 没有时是否自动创建
curl | PhalApi_CUrl | 是
request | PhalApi_Request | 强制每次初始化，用于本地调度时
response | PhalApi_Response_Json | 强制每次初始化，用于本地调度时

(2)MQ

Redis MQ队列

Redis MQ队列需要的配置为：

    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'redis' => array(
                'host' => '127.0.0.1',
                'port' => 6379,
                'prefix' => 'phalapi_task',
                'auth' => '',
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
host | 否 | 127.0.0.1 | redis的HOST
port | 否 | 6379 | redis的端口
prefix | 否 | phalapi_task | key的前缀
auth | 否 | redis的验证，不为空时执行验证

可以这样创建Redis MQ队列：

//方法一：使用app.Task.mq.redis配置
$mq = new Task_MQ_Redis();

//方法二：外部依赖注入
$redisCache = new PhalApi_Cache_Redis(array('host' => '127.0.0.1'));
$mq = new Task_MQ_Redis($redisCache);

文件MQ队列（通常不能共享，队列大小不限制，有效期一年）

文件MQ需要的配置为：

    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'file' => array(
                'path' => API_ROOT . '/Runtime',
                'prefix' => 'phalapi_task',
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
path | 否 | API_ROOT/Runtime | 缓存的文件目录
prefix | 否 | phalapi_task | key的前缀

可以这样创建文件MQ队列：

//方法一：使用app.Task.mq.file配置
$mq = new Task_MQ_File();

//方法二：外部依赖注入
$fileCache = new PhalApi_Cache_File(array('path' => '/tmp/cache'));
$mq = new Task_MQ_File($fileCache);

Memcached/Memcache MQ队列（通常队列大小不能超过1M，有效期29天）

MC MQ需要的配置为：

    'Task' => array(
        //MQ队列设置，可根据使用需要配置
        'mq' => array(
            'mc' => array(
                'host' => '127.0.0.1',
                'port' => 11211,
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
host | 否 | 127.0.0.1 | MC的host
port | 否 | 11211 | MC端口

可以这样创建文件MQ队列：

//方法一：使用app.Task.mq.mc配置
$mq = new Task_MQ_Memcached();

//方法二：外部依赖注入
$mc = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211));
$mq = new Task_MQ_File($mc);

数据库MQ队列

数据库MQ队列需要的配置为：

    'tables' => array(

        //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中，未配置的表将使用默认路由

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'task_mq' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 9, 'db' => 'db_demo'),
            ),
        ),
    )

可以这样创建数据库MQ队列：

$mq = new Task_MQ_DB();

数组MQ队列

数组MQ队列是将MQ存放在PHP的数组里面，用于单元测试或者是一次性、临时性的计划任务调度。

可以这样创建数据库MQ队列：

$mq = new Task_MQ_Array();

(3)本地和远程调度

本地调度的创建

$runner = new Task_Runner_Local($mq, 10);  //10表示每批次弹出10个进行处理

需要注意的是，每次执行一个计划任务，都会重新初始化必要的DI资源服务。
且此调度方式不能用于接口请求时的同步调用。

远程调度的创建

首先需要以下配置：

    /**
     * 计划任务配置
     */
    'Task' => array(
        //Runner设置，如果使用远程调度方式，请加此配置
        'runner' => array(
            'remote' => array(
                'host' => 'http://library.phalapi.net/demo/',
                'timeoutMS' => 3000,
            ),
        ),
    ),

其中：

选项 | 是否必须 | 默认值 | 说明
host | 是 | 接口域名链接
timeoutMS | 否 | 3000 | 接口超时时间，单位毫秒

可以这样创建：

//使用默认的连接器 - HTTP + POST的方式
$runner = new Task_Runner_Remote($mq, 10); //10表示每批次弹出10个进行处理

//或者，指定连接器
$connector = new Task_Runner_Remote_Connector_Impl();
$runner = new Task_Runner_Remote($mq, 10, $connector); //10表示每批次弹出10个进行处理

上一章 文档首页 下一章#[3.8]-扩展类库：用户、会话和第三方登录集成

上一章 文档首页 下一章

“你应该明白，我的朋友，
宇宙从来无所谓起始，
它也永远不会有结局，
因为邦迪，戈尔德，还有我，
直到我们头发掉精光，
对这一点都深信不疑！”

--《物理世界奇遇记》

3.8.1 扩展类库：用户、会话和第三方登录集成

首先，特别感谢@Aevit ，允许我将些扩展类库进行开源。原来此类库的功能只是当前开发项目中的功能，我现将其抽离成可配置使用的扩展类库，以供大家共享。

此类库主要特点有：

  • 1、可以和第三方登录集成，包括：微信登录、新浪登录、QQ登录
  • 2、为客户端提供了直接可以调用的登录接口
  • 3、为服务端提供了直接可以检测用户登录态的操作
  • 4、支持token落地、高效缓存和分布式的数据库存储
  • 5、展示了如何开发一个项目级的类库、包括数据库配置、翻译等

温馨提示：
此扩展类库还在开发完善中，但已有项目在使用，感兴趣的同学可以尝试使用。

3.8.2 安装

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 User 用户包，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 User 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/User ./PhalApi/Library/ -R

(2)数据库表导入

默认下，User类库会使用默认的数据库配置，但有追加一些需要用到的表。因此需要导入以下表：

$ cd /Library/User
$ tree

├── Data
│   ├── user_login_qq.sql
│   ├── user_login_sina.sql
│   ├── user_login_weixin.sql
│   ├── user_session.sql
│   └── user.sql

温馨提示：
导入前，可以自行调整表的前缀。

(3)数据库配置

将 PhalApi-Library/User/Config/dbs.php 里面的配置拷贝到你的项目配置：

    //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'user_session' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 9, 'db' => 'db_demo'),
            ),
        ),

(4)入口注册

//必须显式注册，以便可以让服务自行初始化
DI()->userLite = new User_Lite();

(5)其他可选配置

  • 为了体现高效缓存的好处，建议先注册DI()->cache
  • 为了接口更明朗的参数规则说明，建议将下而规则追加到配置文件./Config/app.php中：
return array(

    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' => array(
        //其他原来的参数配置
        ...

        //登录信息
        'userId' => array(
            'name' => 'user_id', 'type' => 'int', 'default' => 0, 'require' => false,
        ),
        'token' => array(
            'name' => 'token', 'type' => 'string', 'default' => '', 'require' => false,
        ),
    ),

(6)checkApiParams.php的初始化

同样，需要在checkApiParams.php文件显式注册，才能让User这个扩展类库完成自我的必要初始化：

//装载你的接口
DI()->loader->addDirs(array('Demo', 'Library'));
DI()->userLite = new User_Lite();

否则会导致service不存在。

3.8.3 入门使用

(1)对于客户端

(1-1)微信登录

1、功能说明

实现通过微信登录，并且首次登录时，自动创建用户并绑定。

2、接口URL

/?service=User_User_Login.weixin + 公共参数(免登录态)

3、接口参数
参数 | 必须 | 默认值 | 说明
wx_openid | 1 | 微信OPENID
wx_token | 1 | 微信TOKEN
wx_expires_in | 1 | 微信失效时间
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
token | string | 用户TOKEN
is_new | int | 是否为首次登录，0不是，1是
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Login.weixin&wx_openid=wx_122348561111&wx_token=ASDF&wx_expires_in=130000000&name=weixinName&avatar=phpunit.png

(1-2)QQ登录

1、功能说明

实现通过QQ登录，并且首次登录时，自动创建用户并绑定。

2、接口URL

/?service=User_User_Login.qq + 公共参数(免登录态)

3、接口参数
参数 | 必须 | 默认值 | 说明
qq_openid | 1 | QQ的OPENID
qq_token | 1 | QQ的TOKEN
qq_expires_in | 1 | QQ的失效时间
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
token | string | 用户TOKEN
is_new | int | 是否为首次登录，0不是，1是
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Login.qq&qq_openid=xxxx&qq_token=ASDF&qq_expires_in=130000000&name=sinaName&avatar=http://dev.phalapi.com/no_avatar.png

(1-3)新浪微博登录

1、功能说明

实现通过新浪微博登录，并且首次登录时，自动创建用户并绑定。

2、接口URL

/?service=User_User_Login.sina

3、接口参数
参数 | 必须 | 默认值 | 说明
sina_openid | 1 | 新浪微博OPENID
sina_token | 1 | 新浪微博TOKEN
sina_expires_in | 1 | 新浪微博失效时间
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
token | string | 用户TOKEN
is_new | int | 是否为首次登录，0不是，1是
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Login.weixin&wx_openid=wx_122348561111&wx_token=ASDF&wx_expires_in=130000000&name=weixinName&avatar=phpunit.png

(1-4)用户信息

1、功能说明

获取用户个人信息

2、接口URL

/?service=User_User_Info.getUserInfo + 公共参数

3、接口参数
参数 | 必须 | 默认值 | 说明
other_user_id | 1 | 用户id
4、返回结果
返回字段
参数 | 类型 | 说明
info | object | 登录成功后的相关信息
user_id | int | 用户ID
username | string | 用户名
nickname | string | 用户昵称
avatar | string | 用户头像url
结果示例
{
    "ret": 200,
    "data": {
        "code": 0,
        "info": {
            "username": "aevit",
            "nickname": "test",
            "avatar": "",
            "user_id": 1
        },
        "msg": ""
    },
    "msg": ""
}
请求示例

/service=User_User_Info.getUserInfo&other_user_id=1

(2)对于服务端

(1)温柔式检测登录态

$isLogin = DI()->userLite->check();

(2)强制式检测登录态

DI()->userLite->check(true);

(3)更多操作

当需要对用户进行退出登录时，可以在接口中执行：

DI()->userLite->logout();

当需要为用户新建一个会话时，可以在接口中执行：

DI()->userLite->generateSession(1); //假设用户ID为1

当需要为用户延长登录态有效时间时（默认为一个月），可以在接口进行心跳操作，从而再延长一个月有效时间：

DI()->userLite->heartbeat();

3.8.4 示例

(1)假设在我们的应用的接口中：

<?php

class Api_Test extends PhalApi_Api {

    public function go() {
        Di()->userLite->check(true);

        return 'ok';
    }
}

(2)先调用微信的登录：

/user/?service=User_User_Login.Weixin&wx_openid=wx_122348561111&wx_token=ASDF&wx_expires_in=130000000&name=weixinName&avatar=phpunit.png

返回：

{
    "ret": 200,
    "data": {
        "code": 0,
        "info": {
            "user_id": 94,
            "token": "8509BE9C80B67009C559E6A2C4B88E02C885DA6B9FEFFC2DA080999377D08FBF",
            "is_new": 0
        },
        "msg": ""
    },
    "msg": ""
}

(3)再调用我们应用 的接口：

/user/?service=Test.go&user_id=94&token=CE1218493B3A441FCF630E24E98FD3640133EBA6D734C90A620A11C07CE35323

返回：

{"ret":200,"data":"ok","msg":""}

(4)未登录下，返回：

{"ret":401,"data":[],"msg":"\u975e\u6cd5\u8bf7\u6c42\uff1auser not login"}

上一章 文档首页 下一章#[3.9]-扩展类库：swoole支持下的长链接和异步任务实现

上一章 文档首页 下一章

遵守规则可以让你远离选择烦恼，无论是在坐车还是参加鸡尾酒会的时候。 --《选择的悖论》

写在前面的话

此PhalApi扩展类库只是初步开发完成，建议有swoole扩展经验或非保守派的同学使用，也欢迎你来丰富完善此扩展类库。

3.9.1 扩展类库：swoole支持下的长链接和异步任务实现

swoole官网请见： Swoole: PHP的异步、并行、分布式扩展

在这里，首先需要非常感谢swoole。

因为Swoole给我们提供了很多解决以往因PHP本身限制而产生的难题的灵感，但与此同时，也给我们带来了从没遇到过的挑战，特别是并发和长时间运行。

这需要我们更为小心地进行编码，因为以往可以忽略的小问题，在新的解决方案背景下都可能成为一个大问题。此扩展类库更多是作为一种尝试，并且可以作为扩展类库重要的一个转换点。因为在今后的日子里， PhalApi将会争取与其他开源项目一起，提供企业级的解决方案 。

目前，此扩展类库提供了：

  • 长链接的接口调用
  • 异步计划任务的调用

3.9.2 安装

(1)安装swoole扩展类库 安装过程可以参考swoole官网，这里稍以linux系统简单说明一下编译安装。

unzip ./swoole-src-swoole-1.7.16-beta.zip
cd swoole-src-swoole-1.7.16-beta
phpize
./configure
make && make install

安装好后，添加swoole扩展：

extension=swoole.so

重启PHP后，如果看到有swoole扩展，则说明安装成功：

#php -m | grep swoole
swoole

(2)扩展包下载

从 PhalApi-Library 扩展库中下载获取 Swoole 包，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 Swoole 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/Swoole/ ./PhalApi/Library/ -R

到此安装完毕！

温馨提示：
此扩展类库需要PhaApi 1.1.4及以上版本。

(3)配置

将以下配置追加到./Config/app.php：

    /**
     * Swoole扩展类库
     */
    'Swoole' => array(
        //服务
        'server' => array(
            'ip' => '127.0.0.1',
            'port' => 9501,
            'worker_num' => 1,
        ),
        //计划任务
        'task' => array(
            'ip' => '127.0.0.1',
            'port' => 9502,
            'worker_num' => 1,
        ),
    ),

3.9.3 入门使用

(1)长链接入口

在使用长链接入口对外提供接口服务后，由于不再是HTTP协议，所以入口建议放置在新的目录./Server，而不再是./Public。
入口文件的编写，如同我们以往一样，很简单：

//$ vim ./Server/run_server.php
<?php

require_once dirname(__FILE__) . '/../Public/init.php';

DI()->loader->addDirs(array('Library', 'Demo'));

$swooleLite = new Swoole_Lite();
$swooleLite->runServer();

启动、重启和关闭服务

启动可以用：

php ./Server/run_server.php

关闭可以用：

ps -ef | grep run_server | grep -v grep | awk '{print $2}'|xargs kill -9

(2)异步计划任务

异步计划任务是新型的做法，即：也通过接口服务调用的方式来完成计划任务的调度，其启动文件如同长链接入口一样简单：

//$ vim ./Server/run_task.php
<?php

require_once dirname(__FILE__) . '/../Public/init.php';

DI()->loader->addDirs(array('Library', 'Demo'));

$swooleLite = new Swoole_Lite();
$swooleLite->runTask();

启动、重启和关闭服务

启动可以用：

nohup php ./Server/run_task.php > ./Server/run_task.log 2>&1 &

(3)客户端调用

在扩展类库里有一个测试的脚本，可以用来进行PHP客户端的请求。

(1)默认接口调用

$ php ./check.php 127.0.0.1 9501 Default.Index username=swoole
Send: {"service":"Default.Index","username":"swoole"}
Received: {"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430620911},"msg":""}
Connection close

(2)带数据库的调用

$ php ./check.php 127.0.0.1 9501 User.getBaseInfo userId=1
Send: {"service":"User.getBaseInfo","userId":"1"}
Received: {"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","username":"aevit","nickname":"test","password":"DE4CA99150F44B26F0D320DCA6E4B7629C43B6","salt":"wefewfew","reg_time":"0","avatar":"http:\/\/image.famillelab.com\/no_avatar.png","UUID":""}},"msg":""}
Connection close

(3)异步计划任务的调度

$ php ./check.php 127.0.0.1 9502 Default.Index username=swoole
Send: {"service":"Default.Index","username":"swoole"}
Connection close

假设Default.Index为一个计划任务的接口，但目前发现一个问题是，首次请求异步计划任务不会主动结束，而需要工作强制ctrl + c结束，再请求，则正常。

对应的log，可以看到：

2015-05-03 12:24:57|DEBUG|asynctask(0) dispath in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(0) start in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(0) finish in swoole|{"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430627097},"msg":""}
2015-05-03 12:24:57|DEBUG|asynctask(1) dispath in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(1) start in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(1) finish in swoole|{"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430627097},"msg":""}
...

3.9.4 对客户端调整

(1)调用方式的改变

改用长链接。

(2)POST参数传递方式的改变

统一使用json发送数据包。

3.9.5 对服务端的影响

(1)DI资源服务的调整

变量名称 | 是否全局通用 | 是否每次请求新建 | 可否使用 | 备注
loader | 是 | 否 | 可使用
config | 是 | 否 | 可使用
logger | 是 | 否 | 可使用
notorm | 是 | 否 | 可使用 | 每次响应后，关闭此次全部数据库链接
cache | 是 | 否 | 可使用 | 需要查看使用的链接是否支持长链接
filter | 是 | 否 | 可使用
crypt | 是 | 否 | 可使用
curl | 是 | 否 | 可使用
request | 否 | 是 | 可使用
response | 否 | 是 | 可使用
cookie | --- | --- | 不可使用 | 长链接下不应进行COOKIE的操作

(2)关于swoole扩展类库的自问自答

  • 1、数据库使用长链接吗？ -- 不使用，每次响应后手动关闭数据链接
  • 2、内存问题？-- 通过类成员方法的作用域控制内存
  • 3、并发的问题？-- TODO
  • 4、代码更新后如何同步？需要重启服务器吗？ -- 需要重启
  • 5、PhalApi_Response::formatResult()的访问权限，是框架的调整，还是个别自我提高？ -- 扩展自我提高
  • 6、注册错误回调函数？ -- 来自swoole的建议，已注册
  • 7、启动和关闭、重启脚本文件？ -- 提供参考命令
  • 8、一直运行？ -- TODO

    (3)特别注意！

    由于服务在启动后，已经完成了大部分的类加载、配置读取以及PHP文件的解析，所以在对项目代码（包括配置）修改后，需要重启服务，方能生效。

上一章 文档首页 下一章#[3.11]-扩展类库：基于FastRoute的快速路由

上一章 文档首页 下一章

3.11.1 扩展类库：基于FastRoute的快速路由

此扩展基于 FastRoute 实现，需要 PHP 5.4.0 及以上版本，可以通过配置实现自定义路由配置，从而轻松映射到PhalApi中的service接口服务。

3.11.2 安装和配置

(1)扩展包下载

从 PhalApi-Library 扩展库中下载获取 FastRoute 扩展包，如使用：

git clone https://git.oschina.net/dogstar/PhalApi-Library.git

然后把 FastRoute 目录复制到 ./PhalApi/Library/ 下，即：

cp ./PhalApi-Library/FastRoute/ ./PhalApi/Library/ -R

到此安装完毕！接下是插件的配置。

(2)扩展包配置

我们需要在 ./Config/app.php 配置文件中追加以下配置：

    /**
     * 扩展类库 - 快速路由配置
     */
    'FastRoute' => array(
         /**
          * 格式：array($method, $routePattern, $handler)
          *
          * @param string/array $method 允许的HTTP请求方式，可以为：GET/POST/HEAD/DELETE 等
          * @param string $routePattern 路由的正则表达式
          * @param string $handler 对应PhalApi中接口服务名称，即：?service=$handler
          */
        'routes' => array(
            array('GET', '/user/get_base_info/{user_id:\d+}', 'User.GetBaseInfo'),
            array('GET', '/user/get_multi_base_info/{user_ids:[0-9,]+}', 'User.GetMultiBaseInfo'),
        ),
    ),

(3)nginx的协助配置（省略index.php）

如果是使用nginx的情况下，需要添加以下配置：

    if (-f $request_filename) {
        expires max;
        break;
    }

    if (!-e $request_filename) {
        rewrite ^/(.*)$ /index.php/$1 last;
    }

然后重启nginx。

3.11.3 入门使用

(1)入口注册

//$ vim ./Public/index.php

$loader->addDirs('Library');

// 其他代码....

//显式初始化，并调用分发
DI()->fastRoute = new FastRoute_Lite();
DI()->fastRoute->dispatch();

/** ---------------- 响应接口请求 ---------------- **/

$api = new PhalApi();
$rs = $api->response();
$rs->output();

3.11.3 调用效果及扩展

(1)通过新的路由正常访问

在完成上面的配置后，我们就可以这样进行页面访问测试：

  http://library.phalapi.com/user/get_base_info/1
  等效于：http://library.phalapi.com/?service=User.GetBaseInfo&user_id=1

  http://library.phalapi.com/user/get_multi_base_info/1,2
  等效于：http://library.phalapi.com/?service=User.GetMultiBaseInfo&user_ids=1,2

(2)非法访问

当请求的HTTP方法与配置的不符合时，就会返回405错误，如我们配置了：

array('POST', '/user/{id:\d+}/{name}', 'handler2'),

但是通过GET方式来访问，即：

http://library.phalapi.com/user/123/name

则会返回：

{
    "ret": 405,
    "data": [],
    "msg": "快速路由的HTTP请求方法错误，应该为：POST"
}

(3)路由配置错误

当在./Config/app.php的文件里配置错误的路由时，会直接抛出FastRoute\BadRouteException异常，以及时提示开发人员修正。

(4)异常错误处理器

我们也可以实现FastRoute_Handler接口来自定义我们自己的错误异常处理回调函数。如：

class FastRoute_Handler_App implements FastRoute_Handler {

    public function excute(PhalApi_Response $response) {
        // ... ...
    }
}

然后，在分发时指定handler：

DI()->fastRoute->dispatch(new FastRoute_Handler_App());

3.11.4 更多路由配置说明

请访问 FastRoute ，查看其官方说明。

上一章 文档首页 下一章#[4.2]-开发实战2：模拟优酷开放平台接口项目开发

上一章 文档首页 下一章

4.2.1 模拟的业务场景

在这个开发实战中，我们将模拟实现优酷的开放平台接口，即： http://open.youku.com/docs

但这里不涉及具体的内部开发（我们也确实不得而知），而是只从外部的角度，通过PhalApi框架搭建类似的平台接口架构。

4.2.2 优酷URL分析与路由Rewrite

以下是优酷平台的部分接口URL：

#单条视频基本信息(videos/show_basic)
https://openapi.youku.com/v2/videos/show_basic.json

#我的详细信息(users/myinfo)
https://openapi.youku.com/v2/users/myinfo.json

#评论创建(comments/create)
https://openapi.youku.com/v2/comments/create.json

#搜索节目通过关键词(searches/show/by_keyword)
https://openapi.youku.com/v2/searches/show/by_keyword.json

从上面的接口URL，我们可以明显发现一些规律。即：

URL =  接口域名 + 版本 + 相对路径.json

与PhalApi接口规则的冲突

但在PhalApi框架中，我们是通过&service=XXX参数来指定需要的服务的。为此，我们需要在服务端配置一些Rewrite规则以支持这些URL。 简单地，我们可以这样在nginx配置：

    if ( !-f $request_filename )
    {
        rewrite ^/v2/(.*)/(.*).json /v2/?service=$1.$2;
    }

并为了更兼容PhalApi的风格，我们在入口文件将接收到的service首字母强制为大写，即：

//$ vim ./Public/v2/index.php

if (isset($_REQUEST['service'])) {
    $_REQUEST['service'] = ucwords($_REQUEST['service']);
}

重启一下nginx后，我们可以有浏览器，试着访问：

https://openapi.youku.com/v2/videos/show_basic.json

我们将会看到：

{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：接口服务Videos.show_basic不存在"
}

即表明Rewrite规则已生效，并能正常工作了，哈哈！

以下是更完整的nginx配置：

server {
    root /path/to/openapi.youku.com/Public;
    index index.html index.htm index.php;

    error_log /var/log/nginx/.error_log;
    access_log /var/log/nginx/openapi.youku.com.access_log;

    server_name openapi.youku.com;

    location / {
        try_files $uri $uri/ /index.html;
    }

    if ( !-f $request_filename )
    {
        rewrite ^/v2/(.*)/(.*).json /v2/?service=$1.$2;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        fastcgi_pass 127.0.0.1:9000;
        include fastcgi_params;
    }

}

4.2.3 项目的主要目录结构

首先，我们建立了一个Youku的项目目录，并按不同的版本划分了不同的模块，如：

$ tree ./Youku/
./Youku/
└── V2
    ├── Api
    │   └── Default.php
    ├── Domain
    ├── Model
    └── Tests
        ├── Api
        ├── Domain
        ├── Model
        ├── phpunit_user_getbaseinfo.xml
        ├── phpunit.xml
        └── test_env.php

8 directories, 4 files

然后，为不同的版本，提供不同的入口，如：

$ tree ./Public/
./Public/
├── checkApiParams.php
├── index.php
├── init.php
└── v2
    ├── checkApiParams.php
    ├── index.php
    └── listAllApis.php

1 directory, 6 files

4.2.4 简单的模拟实现

现在，到了接口具体开发的环节，我们将模拟开发 单条视频基本信息(videos/show_basic)

首先，我们可以定义接口：

//$ vim ./Youku/V2/Api/Videos.php

<?php

class Api_Videos extends PhalApi_Api {

    public function getRules() {
        return array(
            'show_basic' => array(
                'clientId' => array('name' => 'client_id', 'require' => true, 'desc' => '应用Key'),
                'videoId' => array('name' => 'video_id', 'desc' => '视频ID'),
                'videoUrl' => array('name' => 'video_url', 'desc' => '视频播放页URL'),
            ),
        );
    }

    /**
     * 单条视频基本信息(videos/show_basic)
     *
     * @return string id 视频唯一ID
     * @return string title 视频标题
     * @return string link 视频播放链接
     * @return string thumbnail 视频截图
     * @return int duration 视频时长，单位：秒
     * @return ... ...
     */
    public function show_basic() {

    }
}

然后，使用浏览器在线访问接口文档，访问：

http://openapi.youku.com/v2/checkApiParams.php?service=Videos.show_basic

可以看到：
a pic
可以看出，这与优酷平台上的接口文档是非常相近的。

最后，我们可以简单模拟实现：

    public function show_basic() {
        $rs = '{
            "id" : "XMjg1MTcyNDQ0",
                "title" : "泡芙小姐的灯泡 11",
                "link" : "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
                "thumbnail" : "http://g4.ykimg.com/0100641F464E1FC...",
                "duration" : "910",
                "category" : "原创",
                "state" : "normal",
                "published" : "2011-07-15 09:00:42",
                "description" : "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
                "player" : "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
                "public_type" : "all",
                "copyright_type" : "original",
                "user" :
                {
                    "id" : 58921428,
                    "name" : "泡芙小姐",
                    "link" : "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
                },
                "operation_limit": ["COMMENT_DISABLED"],
                "streamtypes" : ["flv","flvhd","hd"]
        }';

        return json_decode($rs, true);
    }

4.2.5 接口调用效果

虽然是模拟返回（其实是直接强制返回优酷开放平台上的示例数据），但我们还是可以来看下在模拟实现后的接口调用效果。

首先，是缺少client_id时的非法请求：

#请求
http://openapi.youku.com/v2/videos/show_basic.json

#返回
{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：缺少必要参数client_id"
}

然后，尝试一个合法的请求：

#请求
http://openapi.youku.com/v2/videos/show_basic.json?client_id=test

#返回
{
    "ret": 200,
    "data": {
        "id": "XMjg1MTcyNDQ0",
        "title": "泡芙小姐的灯泡 11",
        "link": "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
        "thumbnail": "http://g4.ykimg.com/0100641F464E1FC...",
        "duration": "910",
        "category": "原创",
        "state": "normal",
        "published": "2011-07-15 09:00:42",
        "description": "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
        "player": "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
        "public_type": "all",
        "copyright_type": "original",
        "user": {
            "id": 58921428,
            "name": "泡芙小姐",
            "link": "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
        },
        "operation_limit": [
            "COMMENT_DISABLED"
        ],
        "streamtypes": [
            "flv",
            "flvhd",
            "hd"
        ]
    },
    "msg": ""
}

很好，目前运行效果相当流畅。
虽然如此，但我们明显看到了问题所在。

4.2.6 返回格式的自行调整

在上一节中，我们很明显看到了返回格式与优酷现有的不一样，因为PhalApi框架多了一层。
其实，这些调整对于不同的项目来说，都是非常简单。

当项目需要返回的格式有定制化需求时，可以先自实现response服务，再注册。
在此场景，即：

先自定义response服务

我们先创建一个公共的目录./Youku/Common，再创建项目需要的特定响应类：

//$ vim ./Youku/Common/Response.php

<?php

class Common_Response extends PhalApi_Response_Json {

    public function getResult() {
        return $this->data;
    }
}

注册response服务

接着，我们对response进行注册：

//$ vim ./Public/v2/index.php

//装载你的接口
DI()->loader->addDirs(array('Youku', 'Youku/V2'));

DI()->response = 'Common_Response';

这里需要稍微注意一下，我们要在装载Youku目录后，才能注册DI()->response。

再次返回

回到刚才那个请求链接，我们可以发现，当再次调用时，会返回：

{
    "id": "XMjg1MTcyNDQ0",
    "title": "泡芙小姐的灯泡 11",
    "link": "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
    "thumbnail": "http://g4.ykimg.com/0100641F464E1FC...",
    "duration": "910",
    "category": "原创",
    "state": "normal",
    "published": "2011-07-15 09:00:42",
    "description": "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
    "player": "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
    "public_type": "all",
    "copyright_type": "original",
    "user": {
        "id": 58921428,
        "name": "泡芙小姐",
        "link": "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
    },
    "operation_limit": [
        "COMMENT_DISABLED"
    ],
    "streamtypes": [
        "flv",
        "flvhd",
        "hd"
    ]
}

4.2.7 签名验证

以上我们调整了返回格式，这使得我们的项目开发，越来越达到优酷开放平台接口的标准了（当然，是假设）。

但有一点，我们是不能忽视的，在很多项目中同样是不能忽视的。那就是：接口的签名验证。

我们可以先来看下优酷开放平台是怎么处理接口签名的。

简单地，优酷会为每一个接入方提供一个client_id，然后在每次接口请求时，通过都需要传递此参数。

为此，我们针对这个client_id编写一个简单的客户端验证服务。如：

//$ vim ./Youku/Common/ClientCheck.php

<?php

class Common_ClientCheck implements PhalApi_Filter {

    public function check() {
        $clientId = DI()->request->get('client_id');

        $allCliendIds = array(
            'phalapi',
            'oschina'
        );

        if (!in_array($clientId, $allCliendIds)) {
            throw new PhalApi_Exception_BadRequest('illegal client id');
        }
    }
}

然后，在入口处注册一下：

//$ vim ./Public/v2/index.php

DI()->filter = 'Common_ClientCheck';

当我们，再次打开刚才那个链接：

http://openapi.youku.com/v2/videos/show_basic.json?client_id=test

我们会看到空的返回：

[]

这说明，对客户端的非法请求已拦截成功，但这样用户体验明显不好，因为没有任何的错误提示输出。

为此，我们需要回到刚才自定义的响应类，修改一下：

//$ vim ./Youku/Common/Response.php

<?php

class Common_Response extends PhalApi_Response_Json {

    public function getResult() {
        if ($this->ret != 200) {
            return array(
                'error' => array(
                    'code' => $this->ret,
                    'type' => 'SystemException',
                    'description' => $this->msg,
                ),
            );
        }

        return $this->data;
    }
}

再刷新一下，可以看到和优酷平台接口近似的返回了！

{
    "error": {
        "code": ,
        "type": "SystemException",
        "description": "非法请求：illegal client id"
    }
}

如需要能返回数据，我们只需要传递正确的client_id（目前是固定的两个）即可：

http://openapi.youku.com/v2/videos/show_basic.json?client_id=phalapi

4.2.8 尾声

当然，此次的优酷接口模拟开发，我们都只是很简单地表面说明。

这样的目的，不是为了让大家真的去了解优酷接口的内部实现，而是向大家展示，通过PhalApi框架，我们可以更灵活地实现各种业务需求和非功能性的需求。

希望对大家有帮助，夜已深，安。

源代码请访问：

https://git.oschina.net/dogstar/PhalApi-Demo-Youku.git

上一章 文档首页 下一章#[4.3]-开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动）

上一章 文档首页 下一章

Give it some time, everything will be okay. -- 《那些年我们疯狂的青春/青春洋溢色彩/This Youth is Crazy》

4.3.1 模拟的业务场景

假设我们需要为“奔跑吧兄弟”综艺节目开发一套手机App的投票接口，以供用户在观看电视的同时，可以进行投票活动参与互动。

下面将以此模拟的业务场景，提供一个接口开发实战的过程。

4.3.2 源代码下载

PhalApi-Demo-Vote

4.3.3 接口总列表

以下是我们根据业务需求整理出来的接口：

  • 1、用户可以通过微信、QQ、新浪微博等渠道进行第三方登录
  • 2、用户可以创建团队进行参赛，但队名不能重复
  • 3、用户可以对已参赛的团队进行投票，且每个用户每天投票最多不能超过3次，支持可配置
  • 4、获取已参赛团队的得票排行榜

4.3.4 主要涉及技术功能点

  • 1、使用User扩展类库实现第三方登录操作
  • 2、使用缓存存放用户每天投票的次数（为方便起见，使用文件缓存，不落地）
  • 3、对接口进行签名验证（为方便起见，固定sign签名）
  • 4、数据库的基本操作
  • 5、自动化脚本的使用

4.3.4 快速开发流程

（题外音：整个示例的开发，我个人在单元测试驱动开发下，只用了两个多小时，其中还包括对模板场景的业务构思、建表、编写单元测试代码等）。

(1)创建项目和部署环境

把PhalApi最新的框架代码下载后，并将User扩展类库按文档说明配置后，将项目部署到了以下接口域名：

http://api.vote.phalapi.com

测试一下：

http://api.vote.phalapi.com/vote/?sign=phalapi

//返回
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.4",
        "time": 1431796924
    },
    "msg": ""
}

Good! 下面是简明的开发过程。

(2)单元测试驱动开发

在定好接口后：

<?php
class Api_Act extends PhalApi_Api {

    public function joinIn() {
    }

    public function showList() {
    }

    public function vote() {
    }
}

便可使用脚本，快速生成单元测试的骨架代码：

$ cd ./Vote/Tests/Api
$ phalapi_buildtests ../../Api/Act.php Api_Act ../test_env.php  > Api_Act_Test.php

(3)快速开发

开发过程此处略，但在单元测试驱动的引导下，很快就产出了以下高质量的代码：

.
├── Api
│   ├── Act.php
│   └── Default.php
├── Common
│   └── SignFilter.php
├── Domain
│   ├── Team.php
│   └── Vote.php
├── Model
│   ├── Team.php
│   ├── UserVoteRecord.php
│   └── Vote.php
└── Tests
    ├── Api
    │   ├── Api_Act_Test.php
    │   └── Api_Default_Test.php
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php

(4)单元测试全部通过了！

为了方便大家查看，已省略了部分的调试内容，但保留了测试过程中全部执行的SQL语句，如下：

$ phpunit ./Api_Act_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.

[1 - 0.06911s]DELETE FROM phalapi_team WHERE (team_name = 'test team name');<br>
[2 - 0.06487s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[3 - 0.06553s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');<br>
[4 - 0.0653s]INSERT INTO phalapi_team (team_name) VALUES ('test team name');<br>
[5 - 0.0699s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[6 - 0.06555s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');<br>
[9 - 0.06778s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[10 - 0.06603s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[11 - 0.06825s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[12 - 0.07374s]UPDATE phalapi_vote SET team_id = 3, vote_num = 22 WHERE (team_id = 3);<br>
[13 - 0.07012s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[14 - 0.06682s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[15 - 0.07433s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[16 - 0.07283s]UPDATE phalapi_vote SET team_id = 3, vote_num = 23 WHERE (team_id = 3);<br>
[17 - 0.07307s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[18 - 0.07501s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[19 - 0.07135s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[20 - 0.07653s]UPDATE phalapi_vote SET team_id = 3, vote_num = 24 WHERE (team_id = 3);<br>
[21 - 0.07215s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[22 - 0.06722s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[23 - 0.06506s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[24 - 0.06732s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 404);<br>

Time: 1.97 seconds, Memory: 7.00Mb

OK (4 tests, 42 assertions)

(5)运行效果 - Part 1

在我们通过第三方登录后，我们就可以这样进行接口操作了。

首先，让我们添加两个参赛团队：

//奔跑吧兄弟（蓝队）
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E8%93%9D%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "team_id": "5"
    },
    "msg": ""
}

//奔跑吧兄弟（红队）
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "team_id": "6"
    },
    "msg": ""
}

然后，让我们进行疯狂地投票：

//第一次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":1},"msg":""}

//第二次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":2},"msg":""}

//第三次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":3},"msg":""}

//第四次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回 - 注意此返回！
{"ret":200,"data":{"code":2,"vote_num":0},"msg":""}

最后，看一下排行榜：

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.ShowList&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "teams": [
            {
                "id": 5,
                "team_name": "奔跑吧兄弟（蓝队）",
                "vote_num": 3
            },
            {
                "id": 6,
                "team_name": "奔跑吧兄弟（红队）",
                "vote_num": 0
            }
        ]
    },
    "msg": ""
}

至此，我们已经可以把接口交付给客户端同学使用啦！
当然，我们还需要稍微整理输出WIKI文档~~~
部分接口返回的结果可能与你实际看到的不一样，因为数据会在变化而且有单元测试的测试数据。

(6)运行效果 - Part 2

接口服务，不仅仅需要提供正常的业务功能，还需要考虑到各种客户端使用的情况，包括非法的请求，或者不合的调用，比如防刷票。

这一部分，主要展示接口在各种异常情况下的响应能力。

签名失败

http://api.vote.phalapi.com/vote/?sign=XXX&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：wrong sign"
}

无登录态

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=XXX&user_id=1
//返回
{
    "ret": 401,
    "data": [
    ],
    "msg": "非法请求：user need to login again"
}

重复参赛

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 1,
        "team_id": 0
    },
    "msg": ""
}

当天投票次数已达最大

//第四次投票后
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回 - 注意此返回！
{
    "ret": 200,
    "data": {
        "code": 2,
        "vote_num": 0
    },
    "msg": ""
}

投票的团队不存在

http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=404&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 1,
        "vote_num": 0
    },
    "msg": ""
}

在进行上面的非法请求后，我们可以同时关注后台的日记：

$ tailf ./Runtime/log/201505/20150517.log
2015-05-17 01:35:32|DEBUG|user not login|{"userId":null,"token":null}
2015-05-17 01:36:01|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:37:52|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:45:25|DEBUG|user need to login again|{"expiresTime":0,"userId":"1","token":"XXX"}
2015-05-17 01:48:13|DEBUG|user can not vote today|{"userId":1,"teamId":5}

(7)需要的数据库表

以下为关键的表，其他表，可以通过脚本自动生成，然后导入。

-- ----------------------------
-- Table structure for `phalapi_team`
-- ----------------------------
DROP TABLE IF EXISTS `phalapi_team`;
CREATE TABLE `phalapi_team` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `team_name` varchar(100) DEFAULT '' COMMENT '队名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of phalapi_team
-- ----------------------------
INSERT INTO `phalapi_team` VALUES ('3', 'egg team');
INSERT INTO `phalapi_team` VALUES ('5', '奔跑吧兄弟（蓝队）');
INSERT INTO `phalapi_team` VALUES ('6', '奔跑吧兄弟（红队）');
INSERT INTO `phalapi_team` VALUES ('17', 'test team name');

INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');

INSERT INTO `phalapi_user_login_weixin` VALUES ('1', 'wx_122348561111', 'ASDF', '130000000', '1');

INSERT INTO `phalapi_user_session_1` VALUES ('1', '1', '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731', '', '1', '1431790647', '1934382647', null);

4.3.4 脚本的使用

(1)创建项目脚本 - phalapi_buildapp

此脚本可用于快速创建项目，其使用如下：

$ ./PhalApi/phalapi-buildapp
Usage: ./PhalApi/phalapi-buildapp <app>

此脚本会根据Demo示例，快速生成一个新的项目。

(2)自动生成单元测试骨架脚本 - phalapi_buildtest

这是一个用来自动生成单元测试骨架的脚本，其使用如下：

$ phalapi-buildtest

Usage:
        php /usr/bin/phalapi-buildtest <file_path> <class_name> [bootstrap] [author = dogstar]

Demo:
        php ./build_phpunit_test_tpl.php ./Demo.php Demo > Demo_Test.php

然后，我们就可以使用辅助的PhalApiTestRunner::go()进行单元测试：

    public function testShowList()
    {
        //Step 1. 构建请求URL
        $url = 'service=Act.ShowList&sign=phalapi&user_id=1&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731';

        //Step 2. 执行请求
        $rs = PhalApiTestRunner::go($url);
        //var_dump($rs);

        //Step 3. 验证
        $this->assertNotEmpty($rs);
        $this->assertArrayHasKey('code', $rs);
        $this->assertArrayHasKey('teams', $rs);
        $this->assertEquals(0, $rs['code']);

        $this->assertNotEmpty($rs['teams']);
        foreach ($rs['teams'] as $team) {
            $this->assertArrayHasKey('id', $team);
            $this->assertArrayHasKey('team_name', $team);
            $this->assertArrayHasKey('vote_num', $team);

            $this->assertGreaterThanOrEqual(0, $team['vote_num']);
        }
    }

(3)自动生成分表建表SQL语句 - phalapi_buildsqls

此脚本用于根据dbs.php配置和./Data/*.sql文件生成完整的建表语句，其使用如下：

$ ./PhalApi/phalapi-buildsqls
Usage: ./PhalApi/phalapi-buildsqls <dbs.php> <table> [engine=InnoDB]

假设我们需要对vote进行分表，拆分成3个表，可以这样配置：

//$ vim ./Config/dbs.php

        'vote' => array(
            'prefix' => 'phalapi_',
            'key' => 'id',
            'map' => array(
                array('start' => 0, 'end' => 2, 'db' => 'db_vote'),
            ),
        ),

然后准备基本的建表语句：

//$vim ./Data/vote.sql

  `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',

最后，执行生成脚本：

$ phalapi-buildsqls ./Config/dbs.php vote

/**
 * DB: phalapi_vote
 */

CREATE TABLE `phalapi_vote_0` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_vote_1` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_vote_2` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4.3.5 在线接口参数查看

可以通过以下链接，在线实时查看接口参数：

//参赛接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn

//投票接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.Vote

//排行榜接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.ShowList

其中，投票接口的参数如下：
a pic

上一章 文档首页 下一章#[6.1]-基于接口查询语言的SDK包

上一章 文档首页 下一章

6.1.1 用一句话来描述接口请求

为了给客户端统一接口请求调用的规范性、流畅性和简单易懂，我们特别为此使用了内部领域特定语言： 接口查询语言 （Api Structured Query Language） 。

从外部DSL的角度来看待接口查询的操作

create

withHost host
withFilter filter
withParser parser

reset   #特别注意：重复查询时须重置请求状态

withService service
withParams paramName1 paramValue1
withParams paramName2 paramValue2
withParams ... ...
withTimeout timeout

request

根据此设计理念，各客户端语言都可以实现此接口请求的操作。

6.1.2 接口查询语言设计理念与示例

(1)文法：create -> with -> request

所用到的查询文法如下（通常从上往下依次操作，顺序不强制）：

操作 | 参数 | 是否必须 | 是否可重复调用 | 作用说明
create | 无 | 必须 | 可以，重复调用时新建一个实例，非单例模式 | 需要先调用此操作创建一个接口实例
withHost | 接口域名 | 必须 | 可以，重复时会覆盖 | 设置接口域名，如：http://demo.phalapi.net/
withFilter | 过滤器 | 可选 | 可以，重复时会覆盖 | 设置过滤器，与服务器的DI().filter对应，需要实现PhalApiClientFilter接口
withParser | 解析器 | 可选 | 可以，重复时会覆盖 | 设置结果解析器，仅当不是JSON返回格式时才需要设置，需要实现PhalApiClientParser接口
reset | 无 | 通常必须 | 可以 | 重复查询时须重置请求状态，包括接口服务名称、接口参数和超时时间
withService | 接口服务名称 | 通常必选 | 可以，重复时会覆盖 | 设置将在调用的接口服务名称，如：Default.Index
withParams | 接口参数名、值 | 可选 | 可以，累加参数 | 设置接口参数，此方法是唯一一个可以多次调用并累加参数的操作
withTimeout | 超时时间 | 可选 | 可以，重复时会覆盖 | 设置超时时间，单位毫秒，默认3秒
request | 无 | 必选 | 可以，重复发起接口请求 | 最后执行此操作，发起接口请求

(2)JAVA示例

以JAVA版本为例，演示如何调用：
最简单的调用，也就是默认接口的调用：

PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")  //接口域名
       .request();                            //发起请求

通常的调用，即有设置接口服务名称、接口参数和超时：

PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withService("Default.Index")          //接口服务
       .withParams("username", "dogstar")     //接口参数
       .withTimeout(3000)                     //接口超时
       .request();

高级复杂调用，即设置了过滤器、解析器的操作：

PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withService("Default.Index")
       .withParser(new PhalApiClientParserJson()) //设置JSON解析，默认已经是此解析，这里仅作演示
       .withParams("username", "dogstar")
       .withTimeout(3000)
       .request();

6.1.3 更好的建议

(1)不支持面向对象的实现方式

此接口查询的用法是属于基础的用法，其实现与宿主语言有强依赖关系，在不支持面向对象语言中，如javascript，可以使用函数序列的方式，如：

create();
withHost('http://demo.phalapi.net/');
withService('Default.Index');
withParams('username', 'dogstar');
withTimeout(3000);
var rs = request();

(2)封装自己的接口实例

通常，在一个项目里面我们只需要一个接口实例即可，但此语言没默认使用单例模式，是为了大家更好的自由度。
基于此，大家在项目开发时，可以再进行封装：提供一个全局的接口查询单例，并组装基本的接口公共查询属性。

即分两步：初始化接口实例，以及接口具体的查询操作。

如第一步先初始化：

PhalApiClient client = PhalApiClient.create()
     .withHost("http://demo.phalapi.net/")
     .withParser(new PhalApiClientParserJson());

第二步进行具体的接口请求：

PhalApiClientResponse response = client.reset()  #重复查询时须重置
     .withService("Default.Index")
     .withParams("username", "dogstar")
     .withTimeout(3000)
     .request();

这样，在其他业务场景下就不需要再重复设置这些共同的属性（如过滤器、解析器）或者共同的接口参数。

(3)超时重试

当接口请求超时时，统一返回 ret = 408，表示接口请求超时。此时可进行接口重试。

如：

PhalApiClient client = PhalApiClient.create()
     .withHost("http://demo.phalapi.net/")

PhalApiClientResponse response = client.request();

if (response.getRet() == 408) {
     response = client.request(); //请求重试
}

上一章 文档首页 下一章#[6.2]-SDK包（JAVA版）

上一章 文档首页 下一章

6.2.1 使用说明

将框架目录下的 ./SDK/JAVA/net 目录中的全部代码拷贝到项目里面即可使用。如：

6.2.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

//FullscreenActivity.java
import net.phalapi.sdk.*;

然后，准备一个子线程调用，并在此线程中实现接口请求：

    /**
     * 网络操作相关的子线程
     */
    Runnable networkTask = new Runnable() {

        @Override
        public void run() {
            // TODO
            // 在这里进行 http request.网络请求相关操作

            PhalApiClient client = PhalApiClient.create()
                        .withHost("http://demo.phalapi.net/");

            PhalApiClientResponse response = client
                        .withService("Default.Index")
                        .withParams("username", "dogstar")
                        .withTimeout(3000)
                        .request();

            String content = "";
            content += "ret=" + response.getRet() + "\n";
            if (response.getRet() == 200) {
                try {
                    JSONObject data = new JSONObject(response.getData());
                    content += "data.title=" + data.getString("title") + "\n";
                    content += "data.content=" + data.getString("content") + "\n";
                    content += "data.version=" + data.getString("version") + "\n";
                } catch (JSONException ex) {

                }
            }
            content += "msg=" + response.getMsg() + "\n";

            Log.v("[PhalApiClientResponse]", content);

            Message msg = new Message();
            Bundle data = new Bundle();
            data.putString("value", content);
            msg.setData(data);
            handler.sendMessage(msg);
        }
    };

接着，实现线程回调的hander：

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Bundle data = msg.getData();
            String val = data.getString("value");
            Log.i("mylog", "请求结果为-->" + val);
            // TODO
            // UI界面的更新等相关操作
        }
    };

最后，在我们需要的地方启动：

    View.OnClickListener mDummyBtnClickListener = new View.OnClickListener() {

        @Override
        public void onClick(View arg0) {
            // 开启一个子线程，进行网络操作，等待有返回结果，使用handler通知UI
            new Thread(networkTask).start();

            // ....
        }
    };

再一次调用和异常请求

当我们需要再次使用同一个接口实例进行请求时，需要先进行重置，以便清空之前的接口参数，如：

//再一次请求
response = client.reset() //重置
        .withService("User.GetBaseInfo")
        .withParams("user_id", "1")
        .request();

content = "";
content += "ret=" + response.getRet() + "\n";
if (response.getRet() == 200) {
    try {
        JSONObject data = new JSONObject(response.getData());
        JSONObject info = new JSONObject(data.getString("info"));

        content += "data.info.id=" + info.getString("id") + "\n";
        content += "data.info.name=" + info.getString("name") + "\n";
        content += "data.info.from=" + info.getString("from") + "\n";
    } catch (JSONException ex) {

    }
}
content += "msg=" + response.getMsg() + "\n";

Log.v("[PhalApiClientResponse]", content);

异常情况下，即ret != 200时，将返回错误的信息，如：

//再来试一下异常的请求
response = client.reset()
        .withService("XXX.XXXX")
        .withParams("user_id", "1")
        .request();

content = "";
content += "ret=" + response.getRet() + "\n";
content += "msg=" + response.getMsg() + "\n";

Log.v("[PhalApiClientResponse]", content);

6.2.3 运行效果

运行后，查询log，可以看到：

可以注意到，在调试模式时，会有接口请求的链接和返回的结果日记，如：

10-17 07:40:55.268: D/[PhalApiClient requestUrl](1376): http://demo.phalapi.net/?service=User.GetBaseInfo&user_id=1
10-17 07:40:55.364: D/[PhalApiClient apiResult](1376): {"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","from":"oschina"}},"msg":""}

6.2.4 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

class MyFilter implements PhalApiClientFilter {

        public void filter(String service, Map<String, String> params) {
            //TODO ...
        }
}

然后设置过滤器：

PhalApiClientResponse response = PhalApiClient.create()
           .withHost("http://demo.phalapi.net/")
           .withFilter(new MyFilter())
           // ...
           .request();

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，可以重新实现此接口。

当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。

6.2.5 特别注意：Android之NetworkOnMainThreadException异常

由于此SDK包是使用HttpURLConnection发起请求时，所以在主线程调用时会触发NetworkOnMainThreadException异常，具体可参考： Android之NetworkOnMainThreadException异常

所以，需要使用子线程来发起请求，或者重新继承改用异步的请求。

上一章 文档首页 下一章#[6.3]-SDK包（PHP版）

上一章 文档首页 下一章

6.3.1 使用说明

将框架目录下的 ./SDK/PHP/PhalApiClient 目录中的全部代码拷贝到项目里面即可使用。

6.3.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

<?php

require_once dirname(__FILE__) . '/PhalApiClient.php';

$client = PhalApiClient::create()
        ->withHost('http://demo.phalapi.net/');

$rs = $client->reset()
    ->withService('Default.Index')
    ->withParams('username', 'dogstar')
    ->withTimeout(3000)
    ->request();

var_dump($rs->getRet());
echo "\n";
var_dump($rs->getData());
echo "\n";
var_dump($rs->getMsg());

6.3.3 运行效果

运行后，可以看到：

int(200)

array(4) {
    ["title"]=>
    string(12) "Hello World!"
    ["content"]=>
    string(36) "dogstar您好，欢迎使用PhalApi！"
    ["version"]=>
    string(5) "1.2.1"
    ["time"]=>
    int(1444925238)
}

string(0) ""

6.3.4 更多调用

当需要重复调用时，需要先进行 重置操作 ，如：

//one more time
$rs = $client->reset()
    ->withService("User.GetBaseInfo")
    ->withParams("user_id", "1")
    ->request();

var_dump($rs->getRet());
echo "\n";
var_dump($rs->getData());
echo "\n";
var_dump($rs->getMsg());

当请求有异常时，返回的 ret!= 200，如：

//illegal request
$rs = $client->reset()
    ->withService("XXX.XXXXX")
    ->withParams("user_id", "1")
    ->request();

var_dump($rs->getRet());
echo "\n";
var_dump($rs->getData());
echo "\n";
var_dump($rs->getMsg());

以上的输出为：

--------------------
int(200)

array(3) {
  ["code"]=>
  int(0)
  ["msg"]=>
  string(0) ""
  ["info"]=>
  array(3) {
    ["id"]=>
    string(1) "1"
    ["name"]=>
    string(7) "dogstar"
    ["from"]=>
    string(7) "oschina"
  }
}

string(0) ""

--------------------
int(400)

array(0) {
}

string(45) "非法请求：接口服务XXX.XXXXX不存在"

6.3.5 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

<?php

class MyFilter implements PhalApiClientFilter {

        public function filter($service, array &$params) {
            //TODO ...
        }
}

然后设置过滤器：

<?php

$rs = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withFilter(new MyFilter())
       // ...
       .request();

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，可以重新实现此接口。

当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。

上一章 文档首页 下一章#[6.4]-SDK包（Objective-C版）

上一章 文档首页 下一章

该文档由 @Aevit 提供

6.4.1 使用说明

将框架目录下的 ./SDK/Objective-C/ 目录中的全部代码拷贝到项目里面即可使用。如：

请求方式，可以使用系统的NSURLSession，或第三方的网络请求库等
这里我们使用第三方的AFNetworking，进行二次封装（https://github.com/AFNetworking/AFNetworking）
如需使用其他网络请求方式（如官方的NSURLSession），请继承自PhalApiClient，重写request方法即可，可参考AFNPhalApiClient

6.4.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

#import "AFNPhalApiClient.h"

然后，准备按以下方法实现接口请求即可：

// 待POST的参数
NSDictionary *params = @{@"demo_key_1": @"your_key", @"demo_key_2": @"1.0"};

// 使用AFNPhalApiClient
[[[[[AFNPhalApiClient sharedClient] withHost:@"http://api1.aevit.xyz/"] withService:@"Default.Index"] withParams:params] requestWithFormDataBlock:^(id formData) {
    // 如需上传文件（图片等），请参照如下格式
    [formData appendPartWithFileData:UIImageJPEGRepresentation([UIImage imageNamed:@"head.JPG"], 1) name:@"file" fileName:@"image.jpg" mimeType:@"image/jpeg"];
} completeBlock:^(id resultObject) {
    PALog(@"resultObject: %@", resultObject);
} failureBlock:^(NSError *error) {
    PALog(@"error: %@", error);
}];

// 打印url查看
PALog(@"total url: %@", [[AFNPhalApiClient sharedClient] printTotalUrlStr]);

6.4.3 运行效果

运行后，查询log，可以看到：

可以注意到，调用完成后，会有接口请求的链接和返回的结果，如：

2015-10-19 18:28:01.503 PhalApiClientDemo[23161:1199740] total url: http://api1.aevit.xyz/?service=Default.Index&amp;demo_key_2=1.0&amp;demo_key_1=your_key

2015-10-19 18:28:01.536 PhalApiClientDemo[23161:1199740] resultObject: {
    data =     {
        content = "PHPer\U60a8\U597d\Uff0c\U6b22\U8fce\U4f7f\U7528PhalApi\Uff01";
        time = 1445250481;
        title = "Hello World!";
        version = "1.2.1";
    };
    msg = "";
    ret = 200;
}

6.4.4 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

@interface MyFilter : PhalApiClientFilter
@end

@implementation PhalApiClientFilter
/**
 *  接口过滤器
 *  可用于接口签名生成
 *
 *  @param service 接口服务名称
 *  @param params  接口参数，注意是mutable变量，可以直接修改
 */
- (void)filter:(NSString*)service params:(NSMutableDictionary*)params {
    // 在此对接口进行过滤
}
@end

然后设置过滤器：

[[[[[[AFNPhalApiClient sharedClient] withHost:@"http://api1.aevit.xyz/"] withService:@"Default.Index"] withParams:params]
      withFilter:[MyFilter new]] // filter
     requestWithFormDataBlock:^(id formData) {
    } completeBlock:^(id resultObject) {
    } failureBlock:^(NSError *error) {
}];

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，如XML，请上google搜索“AFNetworking XML”相关资料即可。

上一章 文档首页 下一章

[6.5]-SDK包（javascript版）

todo#[6.6]-SDK包（Ruby版）

上一章 文档首页 下一章

推荐使用Gem

推荐使用：PhalApi SDK for Ruby 。

6.6.1 使用说明

将框架目录下的 ./SDK/Ruby/PhalApiClient 目录中的全部代码拷贝到项目里面即可使用。

6.6.2 代码示例

如下是使用的代码场景片段。

首先，我们需要导入SDK包：

#demo.rb

require_relative './PhalApiClient/phalapi_client'

a_client = PhalApi::Client.create.withHost('http://demo.phalapi.net')
a_response = a_client.withService('Default.Index').withParams('username', 'dogstar').withTimeout(3000).request()

puts a_response.ret, a_response.data, a_response.msg

6.6.3 运行效果

运行后，可以看到：

200
{"title"=>"Hello World!", "content"=>"dogstar您好，欢迎使用PhalApi！", "version"=>"1.2.1", "time"=>1445741092}

6.5.4 更多调用

当需要重复调用时，需要先进行 重置操作 ，如：

#one more time
a_response = a_client.reset \
    .withService("User.GetBaseInfo") \
    .withParams("user_id", "1") \
    .request

puts a_response.ret, a_response.data, a_response.msg

当请求有异常时，返回的 ret!= 200，如：

#illegal request
a_response = a_client.reset.withService('XXXX.noThisMethod').request

puts a_response.ret, a_response.data, a_response.msg

以上的输出为：

--------------------
400
非法请求：接口服务XXXX.noThisMethod不存在

6.6.5 扩展你的过滤器和结果解析器

(1)扩展过滤器

当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。

当需要扩展时，分两步。首先，需要实现过滤器接口：

class MyFilter < PhalApi::ClientFilter
        def filter(service, *params)
            #TODO ...
        end
}

然后设置过滤器：

a_response = PhalApi::Client.create.withHost('http://demo.phalapi.net') \
       .withFilter(MyFilter.new) \
       # ... \
       .request

(2)扩展结果解析器

当返回的接口结果不是JSON格式时，可以重新实现此接口。

当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。

6.6.6 一如既往的单元测试

最后，附一张单元测试运行的效果图：

上一章 文档首页 下一章#[5.1]-架构与思想：PhalApi核心设计和思想解读

上一章 文档首页 下一章

设计软件有两种方法：一种是简单到明显没有缺陷，另外一种复杂到缺陷不那么明显。 --托尼.霍尔

5.1.1 前言

在软件工程这一学科和行业里，关于软件工程的解说有很多。有人说开发是一门艺术；有人说开发是一种技艺；也有人说开发是一门哲学。但个人认同从实用主义和理性的角度去理解。

例如一个框架，我们之所以认为它好是因为我们发现这个框架遵循了编程规范、适当地使用了设计模式、巧妙地结合了设计原则、有着稳定的依赖、代码复杂度低、并且有着很高代码覆盖率的单元测试等等。

也就是说，好的框架都是可以被解释的。既然可以被解释量化，也就可以学习、参考和借鉴。

5.1.2 共性和可变性分析

关于共性和可变性分析，在《设计模式解析》一书中有着非常到位的讲解。

CVA是一种很容易的理念，按我的理解即： 抽离共性、隔离变化 。有点类似易经里面的“变”与“不变”。

诚然，在过去的教育中（包括大学在内的），对于软件开发都着重谈论面向对象开发，即OOD，以致于很多人都对面向对象开发产生了很大的误解。而这种误解所带来的实际情况就是： 我们都在进行面向对象开发，但却是标准呆板的面向对象开发，缺少生气，缺少活力 。

很多人，都没有把我们开发人员作为专业人士看待，甚至连我们自己都否认我们是专业的。所以很多时候当产品提出需求时，我们提供的开发周期往往会被外界以讲价的方式削减。何以？为什么医生给出的手术时间病人没有讨价呢？因为很简单，在病人的眼里，医生是专业的。

若我们也想达到专业的层次时，何以为？学习、思考和实践，我认为至少这三者是必不可少的。

所以，当我们在对PhalApi进行设计时，我们进行了一次又一次地酝酿、尝试、思考。我们在思考：这些功能是否真的会在实际项目中被使用？开发人员是否可能很好地进行扩展？此种决策是否便于单元测试、从思路上减少代码异味？。。。

我们谨记敏捷开发，不过度设计。但我们也确实需要一种思想上的指导。正好，我们看到了 共性和可变性的分析（commonality and variability analysis, CVA） 。

5.1.3 CVA和三种视角、抽象类之间的关系

引用一下《设计模式解析》一书中的图表：

在这种理念的指导下，我们会更愿意将接口开发过程的共性抽离统一起来，而可变性部分的则可以由开发人员根据不同的项目情况进行快速定制实现。

5.1.4 不稳定性与抽象度分布

除了常谈及到的“低耦合、高内聚”外，在对代码进行静态分析和衡量其可维护度时，还有一个值得注意的值，即：不稳定的度量。

不稳定的度量可以根据以下公式计算获得：

I = 离心耦合 / (离心耦合 + 向心耦合)

因此从宏观上，我们的代码结构，从上层到下层，应该向着稳定的方向递增，也就是说越底层越应稳定。对应 稳定依赖原则的规则（SDP），包之间的依赖应该朝着稳定的方向：不稳定的包应该依赖于更稳定的包。

又结合不稳定性与抽象分布图，我们PhalApi框架的代码 应该大部分分布在上图中的抽象稳定区以实现框架高层的建设、少部分分布在具体不稳定区以提供一些基础的功能 。

5.1.5 创建和使用分离

在框架的特性中，包括：可重用、IoC Container以及SOLID五条原则的运用等。这里就部分SOLID原则的运用简单说明一下。

(1)S：单一职责原则

这是我们一直都坚持遵守的原则，因为，我们也坚持 短而美 的写法， 致力于编写优雅的代码、编写人容易理解的代码 。

(2)O：开放-封闭原则

我们首先希望的是在进行接口开发过程中，当需要新增一个接口时是开放的，对已有的响应调用流程是封闭的。即我们只需要实现新接口逻辑即可，不需要改动其他过程的调用。因此在OCP原则的指导下，我们通过结合工厂方法封装了对接口的初始化和调用。

(3)D：依赖倒置原则

PhalApi框架，最大的特色莫过于 它提供了一种如何快速进行接口开发的机制，但它不强制你使用不必要的功能，甚至它还鼓励你通过它来尝试研发自己的框架 。更进一步，PhalApi引入了新颖明确的概念，一如服务。我们把客户端调用的接口称之为接口服务，把服务端用到的资源称之为资源服务。对于后者，PhalApi提供了灵活的DI机制，以支持各项目定制化的开发。

5.1.6 PhalApi核心架构图

显然，到目前为止，从核心架构图所折射出PhalApi的结构和代码是如此的 简单明了、统一规范。至少我是这么认为的，也是一直这样努力的。

从上图的核心架构图可以看出，中间红色部分的DI处于汇点位置，提供各种资源服务的定位、创建、管理和提供。

而左上角的代码示例则表达本系统框架运行的主流程： 创建一个接口实例，运行响应。

右上解黄色部分则为多变的接口应用开发的代码，这里特意罗列了两组接口，意在表明可以在此框架下挂靠多套接口。

最下面是接口开发过程中所用到的各种基础设施和技术，如日记、配置读取、缓存、加密、请求和响应等。同样，除各应用项目中形式多变的接口开发外，这块的底层技术亦支撑不一而足的需求。因为，PhalApi只是作了共性的抽离，即提供一级抽象且稳定的接口或者抽象类，以约定规约视角中接口的函数签名，不作过多的具体实现。同时以DI作为辅助，支持快速扩展。

5.1.7 PhalApi核心执行流程

和其他框架不同，除了有文档对基本使用有说明外，我们还提供了我们框架核心的设计和思想，以便大家洞明其中的原理从而进一步优化扩展。

这里，扼要说明一下PhalApi框架中接口请求背后的核心执行流程。

PhalApi-接口处理主流程 - 0227

从上图的时序图中可以看出，在PhalApi中，一个接口的请求处理，只要分为两个环节： 接口服务初始化 和 接口服务调用 。

(1)接口服务初始化

在Web Service中，往往需要对服务进行注册发布后，才能开放请求。这里免去这一层，但遵循 创建和使用分离 的原则，我们将接口服务的初始化进行了封装，以便可以统一进行初始化、异常处理和一些权限ACL的控制

，甚至接口访问的统计等操作，更为重要的是接口开发人员可以进行无绪开发，而不需要过多知道如何合法创建接口服务。

在1.2. 步骤中，UML时序图中的::generateService()表示对静态函数的调用，即对应代码：

PhalApi_ApiFactory::generateService();

随后，可以看到（假设我们这次请求的服务为：?service=Demo.DoSth），我们创建了一个指定接口的实例（此接口类须继承于PhalApi_Api基类），并以变量a返回实例。

如果请求的服务非法，则会以 400非法请求 返回给客户端。而正确创建接口服务a后，则会进行接口的初始化，其中有接口参数规则的解析和注册了过滤器服务后的检测操作。

当这一系列的操作都成功执行后，我们将会得到一个接口服务实例a返回。
因此，在接口服务的创建过程中，我们没有过多地限制，而是预留了很大的空间给到接口项目定制开发。

至此，接口服务的创建完成。

(2)接口服务调用

在完成复杂的创建工作后，客户端（备注：这里指的是服务端开发的开发客户端）只需要简单调用需要进行的操作即可。

而这一块，则需要接口项目具体开发实现，也是我们项目级的核心部分。

在获取接口服务的背后，我们建议结合领域驱动设计的理念，对项目代码进行这样的层级划分：

  • Api接口层：用于接收参数并响应接口的请求；
  • Domain领域层：用于处理复杂的领域业务逻辑，保证规则只出现一次；
  • Model数据源层：更广义上的Model层，提供数据来源，不限于DB；

最后，是我们客户端关心的返回格式。 默认情况下，我们都是以JSON格式返回的，但仍然可以轻松支持其他格式的返回，如JSONP、XML等。只需要简单地开发实现，然后重新注册即可。

至此，接口服务的调用完毕。

5.1.8 DI支持下的轻松扩展

当使用一个开源框架时，我们既喜欢其强大的一面，但矛盾的同时我们又害怕其中的复杂性，原因莫过于：学习成本高、出现问题时怕驾驭不了。

而在这里，在PhalApi这里，这一切都是这么简单，简单地又如此明了。

当需要进行资源服务的扩展时，我们可以：

实现自己需要的服务

实现指定资源服务在规约视角约定的接口，假设我们需要用文件来当作新的缓存存储。则需：

class MyCache_File implements PhalApi_Cache {
    public function set($key, $value, $expire = 600) {
        //...
    }

    public function get($key) {
        //...
    }

    public function delete($key) {
        //...
    }
}

在入口重新注册

当实现自己的功能后，只需要简单地在入口文件重新注册即可。如：

DI()->cache = new MyCache_File();

最后，另人兴奋的是，原来全部的调用代码都不需要改动，即可享受后期调整升级后的新功能！完全避免了曾经那种“牵一发而动全身”的痛苦。并且，定制开发出来的实现类，还可以跨越业务在其他项目中共用。

这不正是我们常常所说的代码重用吗？而如今，我们很优雅地做到了！

然而，我们在实际开发中收获到的远远不是代码重用这么简单，而是一种更好的开发实践。因为通过DI使得创建和使用分离，所以我们可以让高级的开发同学实现服务功能的开发，然后再提供给普通的开发同学使用，新手亦然，因为对他们来说：会用就行。当然，对于高级的同学，还应该遵循开发的最佳实践，坚持单元测试，以保证我们提供了可靠的接口（广义上的接口，非HTTP请求的接口）给我们的“客户端”使用。

若如此，我们的开发合作岂不是很更合理、更明朗、更愉快？哈哈，我想是的。

作为一个框架，我们应当以发散的方式去设计；但为了能为应用提供可用的功能，我们又应当以收敛的方式去实现。
如果我们提供的功能不足以满足大部分主流的业务场景，那么我们至少需要提供可扩展的空间。

正是出于这样的考虑，我们虔诚地引入了DI。

上一章 文档首页 下一章#[5.2]-杂谈：扯一些PhalApi的前世和今生

上一章 文档首页 下一章

此篇章与框架技术无关，为课外读物，感兴趣的可以略读一下。

5.2.1 写在前面的话

记得曾经有一位大主教在自己的墓志铭上写道：

“小时候，我发誓要改变整个世界；
大些了，我努力改变我的祖国；
后来，我想我能改变我居住的这个城市；
直到临死之前才发现，我只能改变自己，从而影响他人，乃至影响整个世界”。

当然，我没有想过要去影响整个世界，我只想尽我自己的所学，为自己，为他人做一些有用的事情。

正如，最近在thephp.cc站点上面看到的这样一张PPT：

我不一定能记住这位国际出名的前辈的名字，但我记住了他的这句话：

Driven by his passion to help developers build better software.

5.2.2 然而，要布道，先要践道。

所以，当我希望把PhalApi框架开源出去时，我这个框架应该是初步完善并可以应用于实际项目开发的，至少我已经切身用过它来进行后台接口项目开发并不断优化重构之。

5.2.3 最初框架源于毕业论文的需要

当年，在进行毕业论文的项目开发时，因为需要用到后台接口。于是乎，便到图书馆和网上查阅整理了很多资料，知识理论包括RESTFull、phprpc、SOAP协议、Aamzon接口服务、WSDL、Web Service等等。

然而，在实际中我却发现很多人在用PHP开发接口是很简单的，或者说是很粗爆的，如直接使用fopen()函数进行获取接口结果。并且，当我需要去寻找一个可以进行后台接口开发的框架时，居然没找到很好框架支持。基于此，最初的接口框架就在这样的背景和研究下出来了。

附一下当前毕业论文的相关资料：

  • PhalApi前世： 毕业论文答辩汇报 - 精简版.ppt
  • PhalApiu前世：毕业论文 - 暗夜在火星 - 修改版.pdf
  • PhalApi前世文档：zenWS3完全开发手册 - 初稿 - 20130317.pdf

可以在 附件 下载上面的资料。

5.2.4 最初框架的首次商用和实站

在毕业后就职的第一家公司中，由于也有接口开发的需要，正好是管理后台 内部系统调用，所以在征得领导同意后，我将我毕业论文所研发的接口框架进行了部署和开发。首次投入了商业用途。^_^

并同时整理出了初步的接口框架，也就是PhalApi的前世。

5.2.5 PhalApi的前世

最初的框架命名是：zenphpWS3。
原因是zen表示开源、php表示用PHP开发、WS表示Web Service、3表示支持SOAP/HTTP/RPC三种协议以及JSON/XML/ARRAY等多种格式的返回。
更多可以访问我之前的博客： zenphpWS3：文档&示例&设计&源码

5.2.6 又一年的全职接口开发和开源框架学习

曾经有一年，我负责某个大型游戏项目的后台接口开发，同时提供接口供安卓版、iOS版和PC使用，对后台接口的开发有了更多的实战经验。

并且，在对很多国外优秀的开源框架的学习和研究后，有了很多的领会，如我个人最为推荐的Phalcon。Phalcon不仅仅在框架方面做得非常优秀，在文档编写方面也是不遗余力的，即使我们不用这个框架，也能从这个框架的文档上吸引很多有用的思想。

才发现最初提供的接口框架还有很多不成熟的地方。。。

5.2.7 PhalApi框架开源的前奏

到后来，在越来越多项目开发的需要下，出于仍然没有很好的接口框架支持（这里补充说明一下：一搜索“php接口开发框架”没有好的结果；二所接触到的框架虽然有支持接口开发，但只是其中一部分）。更为重要的是，项目往往又是如此重要，很担忧所选的框架进行接口开发后遇到问题不知如果在底层修正。

所以，还是用回了自己的接口开发框架。

当然，在做项目接口开发的同时，我也有意识地在对接口框架进行完善和重构，并融入框架所需要的特性、原则和模式。如可重用、IoC、5条设计原则、组件等。

至此，PhalApi接口框架也就慢慢地明朗了。接下来就是：

5.2.8 PhalApi正式走向开源

如今，很多大的企业都提供了开放平台，如腾讯开放平台、新浪微博开放平台、优酷开放平台等。与此相比，虽然小应用轻量级App项目的后台接口没有这么大的规模，但也是很有量级上的需求的，正如下面这张图所展示的。

在此大背景下，显然会有更多的应用需要后台接口的支撑，需要进行后台接口的开发，乃至对海量数据和访问的支持、快速扩容升级、一键发布、多端支持等。即使这样，PHP开发人员在接口开发时，仍然会显得有点“随心所欲”，缺少规范和统一，更为重要的是仍然没有发现好的PHP接口开发框架。

古云此日足可惜，吾辈更应惜秒阴。

所以，此框架通过我多个实际项目应用后，在不断整理重构、精益迭代下走向了开源。^_^

希望对你们有所帮助，谢谢。

我们通过这个框架，希望可以（正如文档里面所说的）：

一来，支撑轻量级项目后台接口的快速开发；
二来，阐明如何进行接口开发、设计和维护，以很好支持海量访问、大数据、向前向后兼容等；
三来，顺便分享一些好的思想、技巧和有用的工具、最佳实践。

其中，第一点是直接的也是最表面的，即你通过这个接口框架就可以快速开发了；第二点是更通用的接口开发中的一些好的或者必要的技术支撑和策略；第三点是面向框架级别的，即使你不用这个接口框架，也可以设计研发自己的开发框架，同时也是突破接口开发、超越PHP语言的。

书不尽言，欢迎多交流，再次谢谢！

上一章 文档首页 下一章#[5.3]-框架总结：术语表和PHP开发建议

上一章 文档首页 下一章

5.3.1 术语表

我们并不想“制造”一些新的术语来增加大家的学习成本，但为了更高效地进行专业交流，我们将PhalApi框架中所用到的一些概念进行提炼并罗列如下。

(1)接口服务

通常，我们把远程第三方提供的接口称为API。但秉承Web Service的概念，我们更愿称接口为服务。为了同时保留这两者的意思，我们在这里统一将API称为 接口服务 。也就是我们对应的?service=XXX.XXX 。

(2)资源服务

在使用DI依赖注入进行注册的组件，我们也倾向称之为服务，同时也是一些服务端上可用的资源，如数据库、缓存、加解密等。因此统称为 资源服务 。

5.3.2 PHP开发建议

在实际项目开发过程中，通过观察不同开发人员编写的PHP代码，会很趣。因为你会发现每个人的编程风格都不尽相同，但我们更提倡约定编程、规范的代码和编写人容易理解的代码。所以，下面就发现的问题进行说明。

(1)滥用的静态方法

很多框架和很多项目都说自己在使用面向对象编程，但其实很多时候是在类中全部使用静态方法的伪面向对象。这里可能会引发争议，因为有些同学会认为静态类方法比成员函数更快速，而且也确实有相关的数据表明是快了一点点（严格上来讲，很微小）。但作为代价，我们失去的更多。如我们没能使用动态，也不方便在单元测试时使用桩、短件、模拟等技巧。更为重要的是，失去了高层的概念提炼和规约层的约定，不利于接口和实现地分离。

所以，请只有在需要的时候才使用静态static方法，如工具类或实用操作。

(2)非真正意义上的单元测试

很多时候，我看到很多框架和项目中没有单元测试的代码，就算有也不是真正意义上的单元测试。可测试的代码是美的，因为可测试的代码表明职责单一明了，有低耦合度，可以进行快速模拟和替换。关于单元测试，前面已有文档详细说明。

所以，请尽量尝试和坚持PHPUnit单元测试，体验测试驱动开发的乐趣，体验浮现式设计的激动。

(3)无处不在的单例模式

很多同学在学习了设计模式后，都很想试用一把，所以往往在很多时候是为了“用设计模式”而用设计模式，而不考虑是否合适，是否真的需要。尤其对于单例模式，这种情况更为普遍。

使用单例模式的时机包括有：提高系统性能、全局只能有且只有一个实例否则会导致问题发生、提供一个全局的公共访问点等。

然而其他很多情况则不需要。例如很多情况，实例为某容器所持有，则只需要在容器内做数量控制即可，不需要被持有的实例再作单例控制。

所以，请只有确切需要使用单例时才使用。

5.3.3 PhalApi框架的不足

在我们不断维护、演进PhalApi框架的同时，我们也在使用这个框架进行了很多项目的开发，与此同时也在阅读各方面的书籍以获得更深层次的理解。
在这样实践、思考、再设计的不断反馈迭代后，我们看到了PhalApi确实在某方面表现得出色。

但一个负责任的框架，应该也明确指出它的不足。
这里，我们将PhalApi开发中的不足罗列如下，希望为你进行框架设计或者对PhalApi的使用有更好的理解。

(1)接口结果中msg应该改名为error

我们推荐的接口返回格式为：

{
    "ret": 200,
    "data": {
        "code": 0,   //对操作码进行说明
        ....         //更多结果的说明
        "msg": ""
    },
    "msg": ""
}

显然，上面两个msg字段，会给开发团队带来困惑或混淆。
更好是应该把最外层的msg改成error更为贴切，因为只有错误时此字段才有效。

但基于前期的大量文档说明，此外层的ret、data、msg三个字段已约定。所以，只能从应用层的msg进行重命名，如tips。

(2)对NotORM中limit操作的错误优化

前期，由于没有深刻留意MySql中OFFSET关键字的作用，导致了做了一些不精确的优化。

可注意以下的微妙区别：

limit 5 OFFSET 10   #从第10个位置开始，查询前5个

limit 5, 10         #从第5个位置开始，查询前10个

但重点考虑到如果修复这个之前犯下的错误，会对项目升级后有很大的冲动。
可预料的故障有：”升级后，首页列表无任何数据显示“和“升级后，列表数据过多导致App加载崩溃”。

最后，出于对已在开发或已上线项目的保护和承诺向前兼容的原则，我们不得不保留了这个污点。
所以，当对底层进行改动时，须确保已透彻理解各操作的微妙区别。

(3)对数据库操作封装的欠缺

一直以来，数据库支持这块都是比较欠缺的。所以我们使用了NotORM。

但我们为了能更好把NotORM与PhalApi整合，将它调整成更适合我们的使用方式，我们又为NotORM提供了一个封装层，类似代理。

然而，这会为新手入门这个框架造成一定的迷惑。
因为，这有两套操作数据库的区别。但他们一开始不能很好地理解这样的区别，以及这样设计的初衷。

坦白来说，PhalApi对于数据库这块不是好的设计，但好在它可以使用并正常地工作。

上一章 文档首页 下一章 #[5.4]-许可

上一章 文档首页 下一章

5.4.1 协议许可的权利 - GPL协议

PhalApi官方网站为 http://www.phalapi.net，依法独立拥有 PhalApi 及官网发布的PhalApi衍生产品著作权。PhalApi及其衍生产品著作权受到法律和国际公约保护。使用者需仔细阅读本协议，在理解、同意、并遵守本协议的全部条款后，方可开始使用 PhalApi及其衍生 软件。

本授权协议适用于 http://www.phalapi.net 发布的所有应用程序，PhalApi官方网站拥有对本授权协议的最终解释权。

5.4.2 协议规定的约束和限制

PhalApi软件及其PhalApi官网发布的PhalApi衍生产品在没有相关说明的情况下默认是免费的，可用于商业用途或者非商业用途。但是无论用途如何，是否经过修改，修改程度如何，未经过许可， 页面上的产品名称以及指向官网的连接还有源代码中关于PhalApi的版权信息及作者信息必须保留，不许修改或删除，除非您获得PhalApi官网的许可。

如果您未能遵守本协议的条款，您的授权将被终止，所被许可的权利将被收回，并承担相应法律责任。

5.4.3 有限担保和免责声明

本软件及所附带的文件是作为不提供任何明确的或隐含的赔偿或担保的形式提供的。 用户出于自愿而使用本软件，您必须了解使用本软件的风险，在尚未购买产品技术服务之前，我们不承诺提供任何形式的技术支持、使用担保，也不承担任何因使用本软件而产生问题的相关责任。 PhalApi官网不对使用本软件及官网发布的衍生产品构建的软件中的文章或信息承担责任。

有关 PhalApi 最终用户授权协议、商业授权与技术服务的详细内容，均由 PhalApi 官方网站独家提供。PhalApi官网拥有在不事先通知的情况下，修改授权协议和服务价目表的权力，修改后的协议或价目表对自改变之日起的新授权用户生效。 电子文本形式的授权协议如同双方书面签署的协议一样，具有完全的和等同的法律效力。您一旦开始安装 PhalApi以及官网发布的衍生产品，即被视为完全理解并接受本协议的各项条款，在享有上述条款授予的权力的同时，受到相关的约束和限制。 协议许可范围以外的行为，将直接违反本授权协议并构成侵权，我们有权随时终止授权，责令停止损害，并保留追究相关责任的权力。

上一章 文档首页 下一章 #[5.5]-联系和加入我们

上一章 文档首页 下一章

5.5.1 官网

http://www.phalapi.net

5.5.2 交流与咨询

(1)国际渠道

邮箱：chanzonghuang@gmail.com

(2)国内通道

QQ：376741929
QQ交流群：421032344

(3)博客与文档

博客：http://my.oschina.net/dogstar/blog?catalog=3264711
WIKI文档：PhalApi Wiki

(4)问题反馈

Issues：http://git.oschina.net/dogstar/PhalApi/issues

5.5.2 感谢以下贡献者

dogstar
火柴
George
zz.guo
文振熙
黄苗笋
爱编程的小逗比
Aevit
Summer
冰霜

期待您的加入，您也可以通过其他的途径来支持我们，谢谢！

上一章 文档首页 下一章 #[5.6]-更新日记

上一章 文档首页 下一章

PhalApi从1.2版本进入多人协助开发和项目使用后，也开始遵守严格的版本控制。我们平均每周会修复已发现的BUG，每2-4周发布一个小版本。

分支说明

  • release 发布分支，稳定的版本，可直接用于团队的项目开发
  • master 主线分支，随时待发布的版本，测试验收后会合并到release进行发布
  • master-X.X.X 主线的历史分支，以保留或查看历史版本
  • dev 开发分支，当前用于协同开发的分支，以存放共享未完成的代码

更新日记

2017-03-16

1、官网www.phalapi.net切换到https

2017-03-12

1、添加文件上传接口到七牛扩展，并补充完善其他扩展接口服务注释

2017-03-09

1、安装模块及语言模块更新，感谢@高一平 提供
2、微信支付接口修复，感谢@秋寒 提供

2017-03-02

1、listAllApis.php添加左侧菜单，便于多接口时文档分类显示，感谢 @初学者 提供
2、调试模式下，输出显示错误信息
3、添加英文文档：http://www.phalapi.net/wikis/en/，进度5%
4、把框架核心代码的注释翻译成英文，进度60%

2017-02-28

1、listAllApis.php支持显示扩展类库的接口

2017-02-20

1、添加Python版本的SDK包，感谢@高一平 提供

2017-02-18

1、把原来master-x.x.x分支，重命名为archive-x.x.x
2、添加繁体翻译文件，感谢@高一平 提供

2017-02-13

1、补充部分缺失的翻译
2、开启英文版本的文档编写与代码版本注释
3、修复FastRoute扩展缺少POST/GET参数的BUG

2017-02-08

1、微信支付接口增加服务商支持 感谢@442484396/秋寒 提供

2017-02-05

1、phalapi-buildapp脚本中sed命令的修正MacOS support 与GNU SED兼容，感谢@x2x4 反馈提供
2、添加@exception异常注释支持，并补充文档：[1.32]-在线接口文档：注释规范
3、添加CLI扩展类库，用于开发命令行项目

2017-01-01 - v1.3.5幺鸡贺岁小版本

1、C# SDK包代码优化，改用Json库，感谢@X反馈提供
2、phalapi-buildapp及phalapi-buildcode两个命令脚本的格式转换及注释修正

2016-10-15

1、将PhalApi_Model_NotORM::getTableName($id)抽象方法改为返回默认表名的实现，以防实现此方法而引起的报错。
2、提供PhalApi SDK for Ruby，项目地址：https://github.com/phalapi/phalapi-sdk-ruby

2016-10-15

1、错误日记追加，按现有的日记格式补充到已有的日记载体，感谢@442484396反馈

2016-10-13

1、调试模式下，数据库连接失败时显示详细的错误信息，方便定位排查问题

2016-9-3

1、C# SDK包编码从Default调整为UTF8，避免接口接收数据为乱码，感谢@shero反馈

2016-8-7

1、优化logs机制,对脚本程序进行优化,感谢@石头上的露珠 反馈

2016-07-27 - v1.3.4 暑假炫酷版

1、在线接口文档样式美化，感谢@转身.满目疮痍提供
2、#53 断开数据库链接优化，进一步对数据库断开进行深度精除
3、#56 postgresql无法获取新增数据的主键ID优化，感谢@clov4r-连友提供解决方案
4、NotORM_Result::query()由protected提升为public级别，放开限制 感谢@scott反馈

2016-07-25

1、优化NotROM进行Count时的效率

2016-06-18

1、修复生成文档时对返回参数匹配中含有空格显示问题BUG,感谢@clov4r-连友提供

2016-05-21

1、对请求类进行更新,新增了getHeader方法获取头信息
2、修复notrom-insert_multi键名是关键字会出错BUG

2016-05-14

1、添加生成代码的脚本命令：phalapi-buildcode

2016-04-30

1、Task扩展添加MC的MQ支持

2016-04-22 - v1.3.3

1、优化三个命令脚本的使用说明，使其更友好、更专业
2、# 48 接口文档注释中有空格问题优化
3、官方视频教程系列录制与发布
4、V1.3.3小版本再次更新及发布公告

2016-04-6

1、维护install优化目录权限访问验证

2016-04-2

1、修复sting类型长度验证是中文字符占用三个字符问题,使用format下标可用于定义字符编码的类型，如utf8、gbk,gb2312
2、install优化目录权限访问验证

2016-03-26

1、V1.3.3小版本发布

2016-03-18

1、对Redis缓存进行扩展完善，感谢 @風 提供

2016-03-12

1、添加PhalApi客户端SDK包（Go版），感谢@prettyyjnic 提供
2、添加PhalApi客户端SDK包（React-Native版），感谢@ダSimVlove辉 提供
3、修复NotORM在调试时输出日记的warning

2016-02-13

1、PhalApi_Api第54行!isset($this->name)判断bugfixed

2016-01-18

1、框架中文名起用：π框架，感谢@猫哥 @Lambda! 两位的建议！
2、将JSON返回格式中的header调整为：application/json

2016-01-13

1、添加兼容iOS下mcrypt加密的扩展类库，感谢@大蝉 提供
2、默认Demo示例下，数据库表字段从from改名为note，以防误导
3、紧急修复数据库isKeepPrimaryKeyIndex写反的问题，并紧急发布

2016-01-02 - v1.3.2

1、修改notorm使用sql语句使用:和?替换执行sql时,对于int,bool,null进行单独处理
2、优化了PhalApi_Request_Formatter_File,支持了对文件上传类型验证增加了ext后缀名验证
3、提供CHM格式的wiki文档
4、初始化文件init.php简化，并默认装载扩展类库Library目录
5、修正install可视化安装中对listAllApis.php 和 checkApiParams.php 文件中的项目名称
6、其他一些轻量bugfixed

2015-12-31（非100%完美兼容）

1、listAllApis.php在线接口列表，追加对“更多说明”的使用支持，即使用注释@desc
2、添加PhalApi基础教程到官方wiki，感谢 @喵了个咪 提供
3、添加Object-C的SDK包XSHttpTool，感谢 @garden！ 提供
4、默认采用以数组的形式返回结果集，1.3.1及以下版本需要保持原来主键为下标的返回时，需要在init.php文件手动切换：DI()->notorm->keepPrimaryKeyIndex();

2015-11-27 - v1.3.1

1、重构并设计格式化类：PhalApi_Request_Var，使之支持项目扩展定制化；并添加callable回调类型、为数组和日期补充最值检测
2、PhalApi_DB_NotORM上添加对数据库维度的事务操作支持 感谢@研究研究 反馈
3、重构PhalApi_DB_NotORM，以便支持定制MySQL外的数据库操作
4、bugfixed NotORM_Result::fetchPairs()的错误: Argument 1 passed to iterator_to_array() must implement interface Traversable, array given 感谢@scott 反馈
5、提供了Redis扩展类型，由 @喵了个咪 提供
6、添加支持Yaconf扩展读取配置
7、版本更新到v1.3.1 (感恩版)

2015-11-07

1、添加Ruby版SDK包
2、完善listAllApis.php，可按注释进行字典排列，支持多级目录扫描
3、添加了DB_Cluster扩展类库，由 @喵了个咪 提供
4、添加了Medoo扩展类库，由 @转身.满目疮痍 提供
5、DI()->notorm::queryAll()与DI()->notorm::queryRows()第二个参数$parmas改成缺省默认为array()

2015-10-25 - v1.3.0

1、用户扩展类库的完善，添加创建会话的入口，与文档完善
2、初始化文件init.php简化
3、提供可视化安装向导
4、首次线下活动尝试：官方非正式交流会第一期(广州站) --分享你在PHP接口开发中的心得与疑问
5、wiki文档二次系统整理
6、利用MDwiki部署全新的在线文档：http://wiki.phalapi.net/
7、官网首页版本迭代更新
8、添加示例目录结构图解 由@黄苗笋提供
9、listAllApis.php支持自动化生成 由@转身.满目疮痍 提供
10、落点应用框架：freeApi服务于互联网开发者的免费接口供应平台 由@转身.满目疮痍 提供，并正在致力开发完善中，请见：http://git.oschina.net/xiaoxunzhao/freeApi
11、提供基于接口查询语言的SDK包，并有JAVA版、PHP版、C#版、Objective-c版（由@Aevit 提供）、JS版（由@喵了个咪 提供）
12、PhalApi基础教程的编写 由@喵了个咪 提供，请见：http://my.oschina.net/wenzhenxi/blog/513004
13、添加了默认接口签名过滤器：PhalApi_Filter_SimpleMD5
14、加强对DI()->filter的类型检测，避免出现non-object的错误，增加容错能力
15、版本更新到v1.3.0 (1024狂欢版)

2015-09-07

1、添加扩展类库：基于FastRoute的快速路由
2、一些轻量bugfixed

2015-08-23

1、为接口参数规则扩展desc说明
2、补充了：[4.2] 开发实战2：模拟优酷开放平台接口项目开发
3、提供Auth 权限扩展 (由@黄苗笋提供，在这里表示再次感谢，^_^)
4、一些轻量bugfixed
5、版本更新到v1.2.1

2015-07-26

1、修正不能获取文件类型参数
2、修正在线接口参数查询工具中对默认值输出的展示
3、修正User扩展类库中登录态检测穿透，原因在于不该使用 isset(DI->cache) 进行判断

2015-07-11

1、计划任务Task扩展类库追加异常时的结果返回，以明确出错的原因
2、添加数据库配置注释

2015-06-27

1、自动生成SQL语句的脚本，追加对配置数据库字符集的读取，并无表时读取默认的路由配置以便可以生成SQL
2、User扩展类库的Bugfixed
3、docs文档更新

2015-06-06

1、接口返回参数，根据代码return注释，自动生成在线文档
2、接口参数规则中追加对文件参数的验证支持
3、将Task扩展类库转移并内置于PhalApi框架中
4、统一使用GPL协议
5、重构在线接口参数查看，使用OO的规范并提取成类放置在辅助类包中，以便接口更好地使用
6、提供统一的测试辅助类：PhalApi_Helper_TestRunner; 若需保留原来的写法，可追加： class PhalApiTestRunner extends PhalApi_Helper_TestRunner{}
7、一些bugfixed

2015-05-23

1、添加redis缓存
2、提供计划任务扩展类库及对应的文档
3、提供项目实战示例和对应的文档
4、补充更多的文档
5、自动生成SQL语句的脚本改从./Data/目录下获取对应的建表文件，原来为./Config/sql/
6、提供一个接口开发实战示例（投票活动）
7、补充数据库的port和charset配置，port默认为3306，charset默认为UTF8
8、完成计划任务续篇，提供统一的crontab调度

2015-05-07

1、PhalApi_Cache_Memcached更正类名为PhalApi_Cache_Memcached，注意此类名的修正所带来的影响，可以在init.php文件检测项目是否有使用MC
2、追加更低版本的Memcahce支持
3、开启1.2版本，进入多人协助开发时代

2015-05-01

1、五一小更新，提供一致性的DB操作接口
2、添加swoole扩展类库，以支持长链接和异步任务
3、版本更新到1.1.4

2015-04-24

1、修正数据库连接失败时显露帐号信息

2015-04-17

1、官网完善，添加备案信息、下载、关于、许可等页面
2、对框架添加COOKIE支持、SL快速函数、CURL重试次数、字符串正则匹配验证等
3、开源用户扩展类库
4、创建PhalApi框架交流群

2015-04-11

1、添加在线接口列表模板，及生成脚本同步更新
2、文档移至官网维护
3、官网页面开发

2015-04-04

1、WIKI文档排版，与更多开篇的编写
2、源代码注释的对齐与再整理
3、生成项目脚本与创建项目的一些工作准备
4、提供单元测试Runner及phpunit.xml配置文件

2015-03-27

1、代码注释完善与在线类参考手册生成：http://www.phalapi.net/docs/

2015-03-21

1、一些bugfixed：规则下标开头大小写问题、文件缓存个数问题预防等
2、扩展类库：七牛云存储接口调用
3、文档整理和编写新的文档

2015-03-15

1、增加对RSA加密的支持，以及超长字符串的解决方案
2、文件缓存目录拆分，以支持海量的文件缓存
3、官网再上线：www.phalapi.net

2015-02-26

1、对之前的模型代码、查询类、多级缓存等补充完善单元测试
2、WIKI文档补充UML图示
3、Release 1.1.2 发布

2015-02-24

1、文件缓存、空缓存及多级缓存的引入
2、表数据入口的Model基类，以供NotORM系列子类使用，关键点为主键映射和分表情况
3、结合多级缓存和广义Model，对高成本的数据获取的应对方案
4、WIKI文档补充

2015-02-15

1、年前的更新：三篇文档的编写，到此基础入门只差多级缓存未完成；

2015-02-13

1、代码小酌，重构代码、整理注释，让代码更明了，更统一，更达意；
2、添加工具类，其中有：IP地址获取、随机字符串生成；
3、添加扩展类库：微信开发，并编写相关使用文档；
4、添加扩展类库：phprpc协议支持及WIKI编写；

2015-02-11

1、官网发布上线：http://112.74.107.125/，域名已申请，待绑定：www.phalapi.net；

2015-02-09

1、将原来限制于JSON格式的返回调整成更灵活的组件形式，以便支持JSON、JSONP和测试环境下的格式返回，和扩展项目开发所需要的格式；
2、接口基类的初始化函数PhalApi_Api::initialize()精简名字为init()；
3、全部核心的代码注释中的author追加个人邮箱地址；
4、单元测试完善补充和文档整理；

2015-02-07

1、完善接口调试下SQL的输出、示例和单元测试，以及WIKI文档的编写；
2、日记接口文档的编写；
3、合并master到release，并将版本号更新到1.1.1；

2015-02-04

1、根据质量分析后Sonar提供的报告，整理代码，包括统一的注释、对齐、代码风格、命名规则等；
2、默认服务的注册，有：DI()->request、DI()->response；

2015-02-02 版本1.1.0 一个全新的开始！

此版本在原来的基础上进行了大量的重构和更多的规范统一，主要有：
1、分离框架代码和项目代码，便于以后框架升级；
2、统一全部的入口文件，以便不同的版本、终端、入口和测试环境使用，并隐藏PHP语言实现；
3、框架代码统一从原来的Core_改名为PhalApi_，并且把PhalApi_DI::one()统一为快速函数的写法：DI()；
4、重新界定应用项目的代码目录结构，以包的形式快速开发；
5、全部文档相应更新；
//注意！此版本不兼容旧的写法，如有问题，请与我联系。

2015-02-01

1、正常时，ret返回调整为：200，原来为0；
2、异常统一简化为两大类：客户端非法请求400、服务端运行错误500；
3、日记文件写入重构，并将权限更改为777，以便不同场合下日记写入时的permission denied；
4、单元测试整理；

2015-01-31

1、参数规则的解析，移除不必要的固定类型，以及addslashes转换，单元测试整理；
2、参数规则文档编写：http://my.oschina.net/u/256338/blog/372947

2015-01-29

1、examples代码重新整理，及入门文档同步更新；
2、入口文件的调整；

2015-01-28

1、补充入门开发示例的文档，及相关的测试代码和产品代码，主要是examples；
2、提供接口参数在线查询工具；

2015-01-24

1、PhalApi开源；

上一章 文档首页 下一章

PhalApi (π框架)

PHP开源框架，专注于接口开发，支持HTTP/SOAP/RPC协议，可用于快速搭建微服务、RESTful接口或Web Services。
友情链接： PhalApi | 开源中国 | 艾翻译 | Phalcon | PHPUnit | ThoughtWorks


wiki.phalapi.net | PhalApi(π框架) - PHP轻量级开源接口框架 - 助你创造价值！

server
{
listen 80;
server_name wiki.phalapi.net;

index index.html index.php;
root /path/to/wiki.phalapi.net;

location ~ .*\.(php|php5)?$
{
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    include fastcgi.conf;
}

#rewrite ^/wikis/ http://wiki.phalapi.net/#!index.md permanent;
#rewrite ^/wikis/$ /index.html#!index.md last;

if (!-e $request_filename) {
    #rewrite ^/wikis/(.*).html$ /#!$1.md last;
    rewrite ^/wikis/(.*).html$ http://wiki.phalapi.net/#!$1.md permanent;
}

if (!-e $request_filename) {
    #rewrite ^/(.*)$ /#!$1.md last;
    rewrite ^/(.*)$ http://wiki.phalapi.net/#!$1.md permanent;
}

#rewrite ^/#!wikis/.md$ http://wiki.phalapi.net/#!index.md permanent;
#rewrite ^/wikis/.md$ http://wiki.phalapi.net/#!index.md permanent;

error_log /home/apps/logs/nginx/wiki.phalapi.net.error.log;

}

还有疑问？欢迎到社区提问！

PhalApi (π框架)

一个轻量级PHP开源接口框架，专注于接口服务开发，支持HTTP/SOAP/RPC协议，拥有自动生成的在线文档、多种开发语言的客户端SDK包以及可重用的扩展类库，可用于快速搭建微服务、RESTful接口或Web Services。

友情链接: PhalApi 开源中国 艾翻译 Phalcon PHPUnit ThoughtWorks 友链交换

联系我们

©2015-2017 PhalApi All Rights Reserved. 粤ICP备15028808号


PhalApi - PHP轻量级后台接口开发框架

  • 中文版
  • English
  • 感谢Simba赞助了服务器！
  • 下载
  • 文档
  • 类参考手册
  • 博客
  • 在线体验
  • Git@OSC
  • 关于我们

人类一思考，上帝就偷笑了。 -- 《下一个倒下的会不会是华为》

对PhalApi框架的抉择

能使用框架来进行项目开发，和知道为何使用此框架进行项目开发明显不同。

对框架的选择，名义上是架构师的职责，但对于充满好奇心和有着更强求知欲的开发人员来说，同样应该给予关注。

之所以选择一个框架进行项目开发，明显可以减少很多不必要的重复编码，但更深层次，则是可以减少开发周期、统一开发规范、降低项目风险，不致于项目失控。

所以，在决定使用PhalApi框架前，对于此框架是否适用于即将启动的项目进行一番思索，是大有裨益的。通过 推定框架 ，有助于避免因采用不当的框架而对项目造成不必要的阻力。

1、适用的场景和项目

海量数据和移动App

此框架特别适用于现在各种 移动App项目的后台接口开发，以及服务器间的后台接口开发，同时具有应对 海量数据 的能力。并且还可以挂靠多个项目，也可以很好地支持多个终端、开放不同的入口。

同时提供了可以很好应对海量数据的解决方案：如没有提供图片上传的代码或工具而希望开发人员将图片上传到CDN，支持大数据的存储，以及后台计划任务。

HTTP协议和JSON格式

基于此，我们采用了主流设计，即将框架设计成使用HTTP协议并JSON格式返回结果的接口请求，因为这能为大众熟悉并潜意识接受。 通过规范的接口调用和返回，有利于客户端和后台开发人员的关系融洽。

2、敏捷开发和快速交付

虽然框架和工程实践间没有必然的联系，但这其中有一些微妙的关系。

一种代码编写的方式会形成一种开发的风格；一种开发风格会奠定一个团队的合作氛围；一个团队的合作氛围会决定项目交付的质量。

而PhalApi所提倡和希望做到的正是通过让后台接口开发更简单以让后台开发人员心情更愉悦，从而为客户端提供高质量稳定的接口以支撑更多优质App的开发。并且，结合重构、测试驱动开发和持续集成等，可以让你的项目如虎添翼，在快速交付的同时，体验编码开发的乐趣。

3、约束和关注

架构约束程序。

约束有时会让开发者处处受阻。但好的约束能够统一规范而不致于项目代码凌乱不堪，也不会轻易地允许低级开发新手犯下一些本可避免的BUG。正如，我们都讨厌等待红绿灯，但我们必须肯定它对交通和生命安全保障的作用。

4、复杂领域业务的应对和解决方案

正如前面说到的，我们关注在海量数据下为移动App提供稳定的接口，我们提倡敏捷开发下的快速交付。所以我们去掉与接口开发无关的功能，没有提供视图渲染和模板解析的操作。

但只是这样而已吗？

不，我们还关注对复杂领域业务的应对和解决方案。

如果我们PhalApi框架所关注的，也是你们项目所关注的，那么我们有理由相信PhalApi能为你的项目带来很多友好的约束和贴心的帮助。

5、框架的性能

很多框架都要强调其能提供的性能，然后PHP本身就是动态的脚本语言，要想提高项目的运行速度，就是要进行减法，即减少不必要的PHP代码。但是前面强调性能的框架则做了与其承诺矛盾的做法：为框架增添了很多项目可能不需要用到的功能。明显地就是一系列既定的执行流程和侦听事件、回调、调度等等。

对于这一块，我们则提供了极大的空间。因为，入口的文件，由你指定。除了一些必要的加载外，很多都可以支持自定义和定制化。

6、PhalApi的成熟度、部署与学习成本

成就度

不可否认，PhalApi还是太年轻了。

但是我们在努力完善中并致力于快速开发后台接口。我们“减少不必要的创新”，我们坚持恒星一般的接口并很好地支持框架升级， 我们尽量提供优秀的文档和及时的沟通帮助。

更为重要的是，这是一个不只为框架而框架的PhalApi框架。

如果在开发过程中对所遇到的问题而框架不能很好解决和支持时，可以考虑改写框架，将某一目标或属性提升至架构，团队成员则无须再写代码去实现它。因为，你会发现，在PhalApi下，你可以轻松做到这一点： 小步快跑下的浮现式设计 。

部署

在Nginx下，你可以快速部署此框架。

以下是为初学者参考的Nginx配置：

server {
    listen       80;
    server_name  demo.phalapi.com;

    root   /home/apps/projects/demo.phalapi.com/Public;

    charset utf-8;

    access_log  logs/demo.phalapi.com.access.log;
    error_log  logs/demo.phalapi.com.error.log;

    location / {
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include        fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

除此之前，我们已经对框架的结构目录进行了很好地划分。包括配置文件、日记文件、框架代码、多个项目挂靠、不同的入口开放以及多App的支持等等。

在结合Phing进行一键发布时，你会发现，项目的部署发布同样方便快捷。

学习成本

这块学习成本是不可避免的，毕竟PhalApi是一个新的框架，很多开发同学之前都没有接触过。

但我们尽量站在后台开发同学的角度，用心地设计每一个接口，编写每一行代码。在源代码里面倾注了必要的注释和使用示例，并同步提供了详细的文档说明和实战代码，以便快速入门上手。

何时可以采用PhalApi？

我们致力于提供可以快速开发后台接口的框架，并希望能对你的项目有实际的帮助。所以，我们不会鼓吹PhalApi框架，因为没有可以适用于任何领域、任何实际项目的框架可用，起码你可能需要作一些调整。

而且，我们也不会赞同你不加考虑就采用PhalApi框架，因为如果此框架不利于你项目的开发，甚至有反作用，这显然 有悖于我们的实心：为你的项目提供实质的帮助。因此，在采用PhalApi框架前，你应该稍微查看一下我们的文档，和相关的源代码。以下是一些初步的建议。

 1、架构无关：如果你的只是小项目，无需要过多考虑涉及大规模决策、各模块及元素、属性间的微妙关系，即架构无关时。
PhalApi和其他 框架一样，也能满足小项目的开发需求。

 2、专注架构：如果你需要专注框架，则更需要理解你项目将面临的约束、风险和需要满足的质量属性。
然后评估PhalApi是否能统一这此约束、降低风险并满足这些质量属性。若能，则用之；若不能，则另取之。

 3、提升架构：最后，如果你发现PhalApi拥有不断演进的能力并有助于浮现式设计（我们也正是如此设计和研发的），并且愿意
将PhalApi框架提升到你们产品线的后台接口框架，那么你将能为你的团队、项目乃至公司提供一个更优质的框架，并会为之感到兴奋和骄傲。
当然，权力越大，责任越大。但我们作为程序员，不正是希望能以已之所学，服务于人吗？

何时不应该使用PhalApi？

  • 需要开发网站项目，即有界面展示和视图渲染；
  • 对数据严谨性要求高，如金融行业的相关项目，因为PHP是弱类型语言；

PhalApi是一个PHP轻量级后台接口开发框架。
我们致力于将PhalApi维护成像恒星一样：不断更新，保持生气；为接口负责，为开源负责！让后台接口开发更简单！

© PhalApi 开发团队 All Rights Reserved.

在这里，特别感谢

  • 开源中国
  • Phalcon
  • PHPUnit
  • ThoughtWorks
支付宝 捐赠：chanzonghuang@gmail.com


01-create-your-project | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

You can create a project in 3 ways:

  • create follow visual installation tutorial
  • create by cmd phalapi-buildapp
  • create manually

1.2 Create follow visual installation tutorial

Visit /path/to/PhalApi/Public/install on your browser after unpack PhalApi, for example:

http://dev.phalapi.com/install/

Then following the installation step by step:

Step 0: agree to install

Step 1: environment check

Step 2: system configure

Step 3: test api request

Step 4: finally

1.3 Create by cmd phalapi-buildapp

On linux, we can create a projects in serval seconds.

Firstly, we need to

$ chmod +x /path/to/PhalApi/PhalApi/phalapi-buildapp

Secondly,

$ cd /path/to/PhalApi
$ ./PhalApi/phalapi-buildapp MyApp

OK!

Then you can see a folder named MyApp at /path/to/PhalApi, it looks like:

$ cd MyApp/
$ tree
.
├── Api
│   └── Default.php
├── Common
├── Domain
├── Model
└── Tests
    ├── Api
    │   └── Api_Default_Test.php
    ├── Common
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php

9 directories, 4 files

Meanwhile, here is an entrance under ./Public :

└── myapp
    ├── checkApiParams.php
    └── index.php

Finally, you can also visit it by:

http://dev.phalapi.com/myapp/

1.4 Create manually

On windowns, we can also create a project manually. It's easy to do this.

Assume our web root is: F:\xampp\htdocs\PhalApi

(1) Create an new project folder

For example, we create an MyApp project:
QQ截图20150404004219

Then add folder Api, Domain, Model, Common inside MyApp,
QQ截图20150404004458

(2) Prepare a API

All the API classes should be located at ./MyApp/Api , you can try to develop an simple API, or copy ./Demo/Api/Default.php to ./MyApp/Api/Default.php.

(3) Create entrance

In order to visit the API service, we need to add an entrance for MyApp,
QQ截图20150404004653

Please remmember to alter the app name in those tow file:

$vim ./Public/myapp/index.php
$vim ./Public/myapp/checkApiParams.php

(4) Visit it!

Open the follow link on browser,

http://localhost/phalapi/public/myapp/

And you will see:
QQ截图20150404004959

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


1.1-create-your-project | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

1.1.1 Create your project

You can create a project in 3 ways:

  • create follow visual installation tutorial
  • create by cmd phalapi-buildapp
  • create manually

1.1.2 Create follow visual installation tutorial

Visit /path/to/PhalApi/Public/install on your browser after unpack PhalApi, for example:

http://dev.phalapi.com/install/

Then following the installation step by step:

Step 0: agree to install

Step 1: environment check

Step 2: system configure

Step 3: test api request

Step 4: finally

1.1.3 Create by cmd phalapi-buildapp

On linux, we can create a projects in serval seconds.

Firstly, we need to

$ chmod +x /path/to/PhalApi/PhalApi/phalapi-buildapp

Secondly,

$ cd /path/to/PhalApi
$ ./PhalApi/phalapi-buildapp MyApp

OK!

Then you can see a folder named MyApp at /path/to/PhalApi, it looks like:

$ cd MyApp/
$ tree
.
├── Api
│   └── Default.php
├── Common
├── Domain
├── Model
└── Tests
    ├── Api
    │   └── Api_Default_Test.php
    ├── Common
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php

9 directories, 4 files

Meanwhile, here is an entrance under ./Public :

└── myapp
    ├── checkApiParams.php
    └── index.php

Finally, you can also visit it by:

http://dev.phalapi.com/myapp/

1.1.4 Create manually

On windowns, we can also create a project manually. It's easy to do this.

Assume our web root is: F:\xampp\htdocs\PhalApi

(1) Create an new project folder

For example, we create an MyApp project:
QQ截图20150404004219

Then add folder Api, Domain, Model, Common inside MyApp,
QQ截图20150404004458

(2) Prepare a API

All the API classes should be located at ./MyApp/Api , you can try to develop an simple API, or copy ./Demo/Api/Default.php to ./MyApp/Api/Default.php.

(3) Create entrance

In order to visit the API service, we need to add an entrance for MyApp,
QQ截图20150404004653

Please remmember to alter the app name in those tow file:

$vim ./Public/myapp/index.php
$vim ./Public/myapp/checkApiParams.php

(4) Visit it!

Open the follow link on browser,

http://localhost/phalapi/public/myapp/

And you will see:
QQ截图20150404004959

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


1.2-tutorial | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

1.2.1 Tutorial

In this chapter, we wil explore how to enjoy developing an API in the shortest time.

Here we go!

1.2.2 Write an API

Let's start from "Hello World!". Put the follow code inside file ./Demo/Api/Welcome.php.

<?php

class Api_Welcome extends PhalApi_Api {

    public function say() {
        $rs = array();
        $rs['title'] = 'Hello World!';
        return $rs;
    }
}

1.2.3 Visit the API

The API service link looks like:

http:// + host + entrance + ?service=XXX.XXX

Yeah out demo link is:

http://dev.phalapi.com/demo/?service=Welcome.Say

1.2.4 Get the result

PhalApi will response with JSON, so here it is:

{"ret":200,"data":{"title":"Hello World!"},"msg":""}

To make the result more clear,

{
    "ret": 200,
    "data": {
        "title": "Hello World!"
    },
    "msg": ""
}

1.2.5 Screenshoot

1.2.5 Congraduations!

You have been smark at develop APIs with PhalApi!
Enjoy it and more detail at PhalApi.

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


1.3-use-phaApi-or-not | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

1.3.1 Use PhalApi or not

It's obviously different between good at how to develop with a framework and kowning why we develop with the framework.

As we know, a framework can help to reduce repeat code. In the other side, it indicates that everything will be under our control, such as code style, project risk, project management, etc.

Here is to say, it's very helpfull to spend some time to think about it whether we can use PhalApi to develop or not.

1.3.2 What PhalApi major in

PhalApi focus on providing various stable API services for mobile apps, big data and other API systems.

Consequently, we combine HTTP protocol with JSON to provide API service.
Yet if you need other style, you can custom it very easily. We will talk about it later.

1.3.3 Agile development and fast delivery

We believe in that:
a good code style help to form a good development style, a good development style help to build an excelent teamwork, a excelent teamwork help to deliver prodcution fastly.

1.3.4 Constrains and focus

Use contrains as guide rails.

A good framework not only tells us how to do, but also what we can't do. It's very important.
For examplye, we hate to wait trafic lights, but we konw they protected us from accident.

//TODO

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


1.4-API-Request | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

1.4 API Request

1.4.1 Specify service: ?service=XXX.XXX

Client can request an API by specifing service, usually with service parameter by GET method. The form of URI to request is something like this.

API HOST + ENTRANCE + ?service=XXX.XXX

e.g:
http://dev.phalapi.com   +   /demo/  +  ?service=User.GetBaseInfo

After we request with GET method in a browser, we can see some nginx logs which looks like:

127.0.0.1 - - [07/Feb/2015:22:46:46 -0800] "GET /demo/?service=User.GetBaseInfo&sign=&user_id=1 HTTP/1.1" 200 107 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:11.0) Gecko/20100101 Firefox/11.0"

If we request by POST instead, we will see:

127.0.0.1 - - [07/Feb/2015:19:32:05 -0800] "POST /demo/?service=User.GetBaseInfo&sign= HTTP/1.1" 200 135 "-" "-"

The first letter of service parameter is case-insensitive, but we encourage it should be a capital letter. The API we request in above is related to Api_User::getBaseInfo.

class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }
}

If there is no service parameter, it will be Default.Index in default.

1.4.2 When to use GET params?

When developing a project, we will run into some common API params, such as client agent, the version ofclient app, app key, etc. These params should be post by GET method, because we can statistics, analyse and locate where the bugs are by access.log in nginx in the future. That is one of best practices.

1.4.3 When to use POST params?

On the otherhand, we suggest to use POST params while we in the situations as beblow.

    1. sensitive data, such as password.
    1. some special characters or big data package.

1.4.4 How to get header information?

There are some headers in each request, and we can get header information as below.

Assume the request header is:

Host        demo.phalapi.net
User-Agent  Mozilla/5.0

If we want to just get one header, e.g. host in this case, we can:

$host = DI()->request->getHeader("Host"); // we will get "demo.phalapi.net"

If we want to get all the headers, we can:

$headers = DI()->request->getAllHeaders(); // which will return an array

1.4.5 Switch to other param source

As we can see before, we could request an API by GET method, or POST method, or both of them, which will retrieve client params from $_GET, $_POST, and $_REQUEST. But in some situations, we need to use other param source. In this case, we can register DI()->request with specified data source in the file index.php.

For example, when force to use POST data, these code should be added into index.php.

//vim ./index.php

$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : "{}";
DI()->request = new PhalApi_Request(array_merge($_GET,json_decode($HTTP_RAW_POST_DATA, true)));

While running in unit tests, we need to mock data for API to be tested.

// mock data
$data = array(...);

DI()->request = new PhalApi_Request($data);

We can also mock data with PhalApi_Helper_TestRunner class.

    public function testIndexByRunner()
    {
        //Step 1. build a URL
        $url = 'service=Default.Index&username=dogstar';
        $params = array();

        //Step 2. excute request
        $rs = PhalApi_Helper_TestRunner::go($url, $params);
    }

1.4.6 Encrypt requst params

It's a good idea to encrypt some sensitive client params for security in our project. In that way, we need to override PhalApi_Request::genData().

Assume our project want to post all the client params in $_POST['data'] after base64 encoding.

Firstly, create an new request class and implement how to decode client data.

<?php

class Common_Request extends PhalApi_Request {

    public function genData($data) {
        if (!isset($data) || !is_array($data)) {
            $data = $_POST; // only recieve POST data
        }

        return isset($data['data']) ? base64_decode($data['data']) : array();
    }
}

Secondly, register DI()->request in file index.php.

DI()->request = 'Common_Request';

So it is! Try it out, and how to encrypt client data is up to you.

1.4.7 API signature with filter

In PhalApi, we treat many resource as back-end service. The filter servcie, which is DI()->filter, can do some extral operations before excute a request. Usually, we can check API signature in the filter.

There is a default filter service, i.e. PhalApi_Filter_SimpleMD5 class. You can open it by uncommenting the code below in file init.php.

// filter service
DI()->filter = 'PhalApi_Filter_SimpleMD5';

Then client have to generate a sign as below.

    1. exclude sign param(default to be sign)
    1. sort all the rest of params
    1. concatenate all the value of params as string after sort
    1. md5

For example, we will request such a URI: ?service=Default.Index&username=dogstar.

  1. exclude sign param(default to be sign) ?service=Default.Index&username=dogstar

  2. sort all the rest of params
    service=Default.Index
    username=dogstar

  3. concatenate all the value of params as string after sort
    "Default.Indexdogstar" = "Default.Index" + "dogstar"

  4. md5
    sign = 35321cc43cfc1e4008bf6f1bf9b7e3b8 = md5("Default.Indexdogstar")

At last, client need to add sign into GET params. Finally, the URI should be: ?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8.

Check it out!

Firstly, let's request without sign. Of course, it'll fail.

http://localhost/phalapi/public/demo/?service=Default.Index&username=dogstar

Reponse:
{
    "ret": 406,
    "data": [],
    "msg": "illegal request: wrong sign"
}

When fail to check the sign, we can get the right sign in logs.

2015-10-23 23:16:16|DEBUG|Wrong Sign|{"needSign":"35321cc43cfc1e4008bf6f1bf9b7e3b8"}

Secondly, let's request with a right sign, we generate in above.

http://localhost/phalapi/public/demo/?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8

And it works!

If we want to rename the name of sign parameter, we can set the new name when registering DI()->filter. For example, we use s for short instead.

DI()->filter = new PhalApi_Filter_SimpleMD5('s');

How to cancel signature verification in some APIs?

We can cancel signature verification by overriding PhalApi_Api::filterCheck() function in our implementation API sub-class. And do nothing inside filterCheck().

//vim ./Demo/Api/Default.php
class Api_Default extends PhalApi_Api
{
    //....

    protected function filterCheck()
    {
    }
}

1.4.8 COOKIE

DI()->cookie is not a default backend service, which means we need to register it in file init.php when we want to deal with COOKIEs.

It's very simple to register DI()->cookie as other backend services.

// COOKIE
DI()->cookie = 'PhalApi_Cookie';

There are three main operations in PhalApi_Cookie, and they are set, get, and delete.

// set a COOKIE
DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

// get a COOKIE
echo DI()->cookie->get('name');  //输出 phalapi

// delete a COOKIE
DI()->cookie->delete('name');

Smart COOKIE in PhalApi

In fact, it's more complicated to use COOKIE in the real world. According defferent situations, we might need to encrypt COOKIE, or get the cookie after we set in the same request. All these situations can be under control with out smart COOKIE service, i.e. PhalApi_Cookie_Multi class.

Again, if we want a smart COOKIE service, we need to register it with some config.

$config = array('crypt' => $crypt, 'key' => 'a secrect');
DI()->cookie = new PhalApi_Cookie_Multi($config);

We config how to encrypt COOKIE here. crypt will encrypt the COOKIE, and default is DI()->crypt, which need to implement interface PhalApi_Crypt. key is the encryption key.

In order to show how to encrypt our COOKIE with PhalApi_Cookie_Multi, let's take an example step by step.

First of all, create an encryption class, which implement interface PhalApi_Crypt. It is really simple, just doing base64_encode and base64_decode.

class Cookie_Crypt_Mock implements PhalApi_Crypt {

    public function encrypt($data, $key) {
        return base64_encode($data);
    }

    public function decrypt($data, $key) {
        return base64_decode($data);
    }
}

Then register DI()->cookie with Cookie_Crypt_Mock in init.php.

$config = array('crypt' => new Cookie_Crypt_Mock(), 'key' => 'a secrect');
DI()->cookie = new PhalApi_Cookie_Multi($config);

Now we can test whether it works correctly. Let's create an new API service as below.

    public function cookieTest() {
        $rs = array();

        $rs['aEKey'] = DI()->cookie->get('name');
        DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

        return $rs;

    }

After request the API service, we can check the cookies in response.

AHA~! It works!

Some advise as other framwork will provide too

    1. DO NOT save sensitive data into COOKIE, protect your data
    1. DO NOT save too much data into COOKIE, just keep client fit

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


1.5-Parameter-Rule | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

1.5 Parameter Rule

This part will be a little long, but it will be very helpful.

1.5.1 Parse client params

Parameter is important for backend APIs but also for client request.

That's why we hope to simplify backend APIs development in getting client params, checking params, verifying params and writting API docs. On the other hand, client can post params with more freedom.

So in PhalApi, we introuce the concept of parsing client params, i.e. auto getting and verifying client params by rule configs.

What developers need to do is just writting rule config, and PhalApi will do the rest automatically. Let's explore more detail.

1.5.2 Rule configs

Developers who are from Yii should by familiar with rule configs. Those who have not used Yii before can handle thess configs in a short time. With time moving on, you can find these configs follow the format as below.

array(
    'backend param name' => array('name' => 'API param name', 'type' => 'param type', 'default' => 'param default value', ...),
    ... ...
)

1.5.3 Example

(1) A simple example

Let's take login API as example. Assume that we need to provide a login API service, which will allow user to login by providing his/her username and password.

<?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'login' => array(
                'username' => array('name' => 'username'),
                'password' => array('name' => 'password'),
            ),
        );
    }

    public function login()
    {
        return array('username' => $this->username, 'password' => $this->password);
    }
}

Then we can request it like this:

/?service=User.Login&username=test&password=123456

and it will reponse:

{"ret":0,"data":{"username":"test","password":"123456"},"msg":""}

All rule configs should located at getRules() method. In this case, there are tow params for login service. Then develop can get the client param by class member property, such as $this->username, $this->password.

(2)A full example

We just only define tow simple params. However, we need to do more other things before we get the param from client, such as verifying whether required or not, max length, min/max value limitiation etc.

Let's continue to complete the rule configs above. The params username and password should be required, and the min length of password should be greater than 6.

'login' => array(
   'username' => array('name' => 'username', 'require' => true),
   'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),

Try to request again without any params. When we request /?service=User.Login, we will get:

{"ret":400,"data":[],"msg":"Illegal Param: wrong param: username"}

When we request with wrong length of password like /?service=User.Login&username=test&password=123, we will get:

{"ret":400,"data":[],"msg":"Illegal Param: password.len should >= 6, but now password.len = 3"}

Look! PhalApi do many things automatically, what we need to do is just a little config and then enjoy it!

4.5.4 Three kinds of params

(1)System params

Currently, there is only one system param, i.e. service. The param service is a string, which looks like XXX.XXX, determining which API service client will call.

Here are some examples.

# Recommend example
/?service=User.GetBaseInfo

# Right example(start with lower case letter)
/?service=user.getBaseInfo(method name is all lower case letter. NOTE: class name just can start with lower case letter, the rest should keep the same with class file name, or lead to a NOT FOUND exception)
/?service=user.getbaseinfo

# Illegal service(missing method name)
/?service=User
# Illegal service(missing dot between class name and method name)
/?service=UserGetBaseInfo
# Illegal service(only support split with dot now)
/?service=User|GetBaseInfo

(2)Application params

Application params are common client params in one API project. All thess application params should be located at apiCommonRules in the config file ./Config/app.php. For example, if we need a required sign param, and a NOT required version param for all APIS, we can edit the apiCommonRules and add the code as below.

//$vim ./Config/app.php
<?php

return array(
    /**
     * API common param rules
     */
    'apiCommonRules' => array(
        // a required ```sign``` param
        'sign' => array(
            'name' => 'sign', 'require' => true,
        ),
        // a NOT required ```version``` param
        'version' => array(
            'name' => 'version', 'default' => '',
        ),
    ),

    ... ...

(3)API params

At last, we can define sepecified client params for each API service. In addition, in order to reuse rule configs in the same API class, we can use * rules, which are common rules only in one API class. For example, we add a reqiured code param, which's length is 4, for all User APIs.

    public function getRules()
    {
        return array(
            '*' => array(
                'code' => array('name' => 'code', 'require' => true, 'min' => 4, 'max' => 4),
                ),
            'login' => array(
                'username' => array('name' => 'username', 'require' => true),
                'password' => array('name' => 'password', 'require' => true, 'min' => 6),
            ),
        );
    }

After we configure application common params, API common params and sepcecified API params, client can request the login API service as below.

/?service=User.login&sign=77f81c17d512302383e5f26b99dae4d9&username=test&password=123456&version=1.3.6&code=abcd

NOTE: The indexes of rule configs in API class are case-insensitive.

In summary, there are tow kinds of params in API class, i.e. API common params and sepcecified API params. The former is represent with *, while the latter is represent with the name of method.

(4) Params priority

When one rule config for the same param repeat in application params, API common params and sepcecified API params, the latter rule will cover the former one. That's to say, sepcecified API params rule will take replace of common params rule to ensure developers can custom their rules by need.

In short, the priority of parameter rule is as below(as you expect):

    1. HIGHEST, sepcecified API parameter rule
    1. common API parameter rule
    1. application parameter rule
    1. LOWEST, system parameter rule(only one param, i.e. service)

4.5.5 Online API detail documents(auto generated)

For the sake of convenience to check all the client params fast, PhalApi provides online API detail documents. They are auto generated by the parameter rules above. Vist the follow URI in the bowser:

/demo/checkApiParams.php?service=Default.Index

And you can see:

Thess API docs can be also provided to client developers during project development.

Self-description data

It is worth to mention that the parameter rules we define actually are self-description data. It means the code of rules reflect related properties of params.

4.5.6 All kinds of rule types

Param Type(type) | IS Required(require) | Defaul Value(default) | Min&Max Value(min&max) | MORE
string | true/false, default is false | should be a string | Optional | regex index defines the regex need to be matched, and format index defines charaset, such as utf8, gbk, gb2312 etc.
int | true/false, default is false | should be a integer | Optional | ---
float | true/false, default is false | should be a float number | Optional | ---
boolean | true/false, default is false | true/false | --- | It will treat ok, true, success, on, yes, 1 as TRUE
date | true/false, default is false | translate with the format | Optional, only need to check while the format is timestamp|It will turns into timestamp while the format is timestamp
array | true/false, default is false | should be an array, or try to trun into an array if NOT | Optional, check array count | It will call explode() with format separator if format is explode, and will call json_decode() if format is json
enum | true/false, default is false | should inside the range|---|Required，use range range to sepecify enum range
file | true/false, default is false | should be an array | min nad max are represent the size of upload file|range index is represent for the types of upload file allowing to post, while ext will filter the external name
callable | true/false, default is false | --- | --- | callback can set callback function with params as the third param, the first is param value, the second is current rule config

NOTE: You can add desc index for all of the rule types, which will be displayed in the section Desc in online API detail docs.

Let's study each rule type with examples one by one.

(1)Rule type: string

When a rule config is defined without type, it will be string in default. A full config for string type is:

array('name' => 'username', 'type' => 'string', 'require' => true, 'default' => 'nobody', 'min' => 1, 'max' => 10)

We use 'type' => 'string' to sepecify username is a string.

Then if anyone post a too much long username, such as &username=alonglonglonglongname, he will get an exception as returned.

{"ret":400,"data":[],"msg":"Illegal Param: username.len should <= 10, but now username.len = 21"}

It there will be some special(other charset) in username, we can add format index to sepecify which charset we use.

array('name' => 'username', 'type' => 'string','format' => 'utf8', 'min' => 1, 'max' => 10)

We can add more powerful verification with regex index. Take email for example.

'email' => array(
    'name' => 'email',
    'require' => true,
    'min' => '1',
    'regex' => "/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i",
    'desc' => 'You should post a correct email address',
),

(2)Rule type: int

Usually, we can configure id param for database tables as below.

array('name' => 'id', 'type' => 'int', 'require' => true, 'min' => 1 )

NOTE: id should be in [min, max).

If client post an id beyond the range, such as &id=0, it will fail.

{"ret":400,"data":[],"msg":"Illegal Param: id should >= 1, but now id = 0"}

(3)Rule type: float

See int in above.

(4)Rule type: boolean

PhalApi will treat strings like ok, true, success, on, yes as boolean TRUE, any other strings PHP will parse as TRUE will also turn into TRUE. For example, we can add one more param for login service.

array('name' => 'isRememberMe', 'type' => 'boolean', 'default' => true)

(4)Rule type: date

We can post a date as we want.

array('name' => 'registerData', 'type' => 'date')

When client post &risterData=2015-01-31 10:00:00, we will get 2015-01-31 10:00:00 in API.

If we want a timestamp instead of a string, we can configure as below:

array('name' => 'registerData', 'type' => 'date', 'format' => 'timestamp')

At last, we will get 1422669600, which is equals to strtotime('2015-01-31 10:00:00').

(5)Rule type: array

Usually, client need to post serveral items in one request. That's why we need array type. For example:

array('name' => 'uids', 'type' => 'array', 'format' => 'explode', 'separator' => ',')

Then params &uids=1,2,3 will be turned into:

array ( 0 => '1', 1 => '2', 2 => '3', )

If client want to post params in JSON, we need to set format index with json.

array('name' => 'params', 'type' => 'array', 'format' => 'json')

Then params &params={"username":"test","password":"123456"} will be turned into:

array ( 'username' => 'test', 'password' => '123456', )

Please note that, if we use array type but without format, it will be turned into an array, which contains one element. For example, &name=test will become array(0 => 'test').

(7)Rule type: enum

You can use this enum type when you need to limit the range of parameters. Take sex for example.

array('name' => 'sex', 'type' => 'enum', 'range' => array('female', 'male'))

We only allow female/male for sex param. Therefore, any other strings will be treated as illegal param. Let's try to post &sex=unknow. No doubt that we will get something like this.

{"ret":400,"data":[],"msg":"Illegal Param: sex should be in female\/male, but now sex = unknow"}

Please keep in mind, it's better to use string, NOT int/float, for range.
Because in generally, what we get from GET/POST method are strings, not int/float, otherwise will lead to unexpected result. Here is an example show how wrong range occur a bug.

// Assume client post &type=N

// rule config as below
array('name' => 'type', 'type' => 'enum', 'range' => array(0, 1, 2))

// wrong result
var_dump(in_array('N', array(0, 1, 2))); // it prints true, since 'N' == 0 in PHP

To avoid this kind bug, we should configure as below.

// rule config (use STRING, not integer)
array('name' => '&type', 'type' => 'enum', 'range' => array(`0`, `1`, `2`))

(8)Rule type: file

When we want to receive files from client, we can use type file. Let's take an exmaple.

array(
    'name' => 'upfile',
    'type' => 'file',
    'min' => 0,
    'max' => 1024 * 1024,
    'range' => array('image/jpeg', 'image/png') ,
    'ext' => array('jpeg', 'png')
)

These config means that, the size of file should be greater than 0B, and less than 1MB, client only can post jpeg/png files in form filed upfile, and the external file name should be jpeg/png. If client upload successfully, we will get $_FILES["upfile"].

array(
     'name' => '',
     'type' => '',
     'size' => '',
     'tmp_name' => '',
)

More information about $_FILE, please visit $_FILE.

There are some more examples about ext.

// single external name in array
'ext' => array('jpg')

// single external name in string
'ext' => 'jpg'

// multi external names in array
'ext' => array('jpg', 'jpeg', 'png', 'bmp')

// multi external names in string, split with comma
'ext' => 'jpg,jpeg,png,bmp'

(9)Rule type: callable

If we want to verify and parse the client params by ourself, we can use type callable. Let's try to check the version param with Common_MyVersion::formatVersion().

array('name' => 'version', 'type' => 'callable', 'callback' => array('Common_MyVersion', 'formatVersion'))

Then PhalApi will call Common_MyVersion::formatVersion() client post a version. And we can do anything in Common_MyVersion::formatVersion() as we want.

// create an new class

class Common_MyVersion {

    public static function formatVersion($value, $rule) {
        if (count(explode('.', $value)) < 3) {
            throw new PhalApi_Exception_BadRequest('Illegal version');
        }
    }
}

NOTE: the first param $value is the origin value from client param, the second param $rule is our rule config as above, the third param(OPTIONAL) equals to the params index in $rule.

4.5.7 The principle to design parameter rule

(1) Free to map client param name with backend member name

In some case, backend would like to name variable by Camel-Case, while client would prefer name parameters with underline. That's why we need tow name in rule config, one is for backend, anther one is for client.

array(
    'isRememberMe' => array('name' => 'is_remember_me', 'type' => 'boolean', 'default' => true),
)

Here, client post with &is_remember_me=1, and backend get with $this->isRememberMe. Perfect cooporation between backend and frontend? AhA~ I think so.

We can also make a shorter name for client to save network traffic.

array(
    'isRememberMe' => array('name' => 're', 'type' => 'boolean', 'default' => true),
)

Then client can post with &re=1 instead.

(2)Returning exception

PhalApi will return an exception, which's code is usually 4xx, while client post illegal params. This can remind client they post wrong params.

4.5.8 Extend your rule type

When PhalApi provides parameter rules that do not meet your need, you can extend the PhalApi_Request_Formatter interface to customize your own type, in addition to using the callable type.

There are tow more step before you use your own type.

    1. Extend and implement the PhalApi_Request_Formatter interface.
    1. Register the new type in DI.

Let's take email type for example.

Firstly, we need to implement interface PhalApi_Request_Formatter.

<?php

class Common_MyFormatter_Email implements PhalApi_Request_Formatter {

    public function parse($value, $rule) {
        if (!preg_match('/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/', $value)) {
            throw new PhalApi_Exception_BadRequest('Illegal email');
        }

        return $value;
    }
}

Secondly, register it into DI.

DI()->_formatterEmail = 'Common_MyFormatter_Email';

NOTE: The name of service should follow DI()->_formatter + type name(first letter is upper case, the rest is lower case).

At last, we can use our new type email now as below.

array('name' => 'user_email', 'type' => 'email')

There are all the formatting service by now.

  • _formatterArray
  • _formatterBoolean
  • _formatterCallable
  • _formatterDate
  • _formatterEnum
  • _formatterFile
  • _formatterFloat
  • _formatterInt
  • _formatterString

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


1.6-API-Response | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

1.6 API Response

1.6.1 JSON Response

Obviously, we response result to clients with JSON at default.

1.6.2 Uniform Response Structure

As we can see above, default API Default.Index return something like this.

{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "Hi PHPer, welcome to use PhalApi!",
        "version": "1.3.6",
        "time": 1489910945
    },
    "msg": ""
}

The response is mainly consisted of three parts. ret is returning code, which can tell what status is, data is anything you want to transfer to clients, while msg is represent for error message when fail to request. We will explain them one by one in detail.

(1)Returning Code: ret

Reference to HTTP Status Code, we make a convention.

  • 200: API response successfully and return data in data field as expected.
  • 4XX: Illegal request from clients, the reason why fail to request is displayed in msg field.
  • 5XX: API service internal error, and no data can be provided.
ret = 200 means response successfully

When the ret is equals to 200, it means API response successfully. Then clients can retrieve more business data in data field that API service provide.

ret = 4XX means illegal request

If clients request incorrectly, such as requesting a non-exists API serivce, missing required API params or failling to pass verification, they will get a returning code range from 400 to 499. When this happen, client developers should adjust how to request the API service and ensure they can request correctly. And the reason why fail to request can be found in msg field.

At backend, we can use PhalApi_Exception_BadRequest to specify returning code and error message. Take signature verification as an example. When the signature from client is wrong, we should throw an exception to interupt the request and warn the clients.

throw new PhalApi_Exception_BadRequest('wrong sign', 1);

Finally, clients get a response where ret is 401 and msg is Illegal request: wrong sign. Please note that, PhalApi will add the exception code with 400 automatically, i.e. 401 = 1 + 400. That is why we get ret = 401, not ret = 1. Therefore, the exception code passed to PhalApi_Exception_BadRequest should between 1 and 99.

ret = 5XX means internal error

This case should not happen generally. When client developers get this, they usually can do nothing but warn backend developers what's going on. As backend developers, we should fix it as soon as possible according the error message in msg field. We need to check whether we break the rule of PhalApi, or do something wrong such as trying to get an non-exsist field.

In this case, PhalApi will occur a PhalApi_Exception_InternalServerError exception.

1.6.3 Business data: data

The data field is the bridge between API service and cleint APP. It can be any type, and it's up to backend. But it's a rule of thumb to return data as an array for extendibility and compatibility.

When developing an API, we can add annotation to specify clearly what we will return to clients. Then client developers can visit the online document and check them out.

Let's look into Default.Index again.

    /**
     * Default API service
     * @return string title title
     * @return string content content
     * @return string version versio with format: X.X.X
     * @return int time current timestamp
     */
    public function index() {
        return array(
            'title' => 'Hello World!',
            'content' => T('Hi {name}, welcome to use PhalApi!', array('name' => $this->username)),
            'version' => PHALAPI_VERSION,
            'time' => $_SERVER['REQUEST_TIME'],
        );
    }

Then open one browser and visit the online API detail document as beblow.
http://demo.phalapi.net/checkApiParams.php?service=Default.Index

The document looks like:

Annotation Format

Reference to return annotation in PHP Documentor 2, the annotation format is similar to PHPDocs.

@return  [Type] [The path of field name, split with dot] [Description of the field]

In above, the type should be one of these.

keyword | Description
string | String, such as "PhalApi".
int | Integer, such as 2017.
float | Float number, such as 3.17.
boolean | Boolean, i.e. true or false.
date | Date and timestamp, such as "2017-03-19 17:40:50" or 1489916450.
array | Array, such as [1, 2, 3]
fixed | Fixed value
enum | Enumeration type
object | Object, such as {'name' : 'PhalApi'}

NOTE: The difference between array and object is that there are no string but only naturnal number indexes in array and each item has the same type, while object is stand for a structure, something like a map.

Here are some examples of an array.

["dogstar", "Aevit", "uwin"]  // all item are string

[28, 27, 23]  // all item are integer

// all item are structure with same fields name and age
[
    {
        "name": "dogstar",
        "age": 28
    },
    {
        "name": "Aevit",
        "age": 27
    },
    {
        "name": "uwin",
        "age": 23
    }
]

Here is an example of a object.

{
    "name": "dogstar",
    "age": 28
}

In order to tell client developers clearly what kind of type we will return to them, especially from array or object, we recommend add square brackets behind the field name.

     * @return array list user info list
     * @return int list[].id user ID
     * @return string list[].name username
     * @return string list[].note user note

Here, we make it clear that we will return list as an array to clients. Besides, these square brackets indicate that id, name, and note come from an array, not a object, which also indicates it's possible there are multiple user in the list.

NOTE: You can add more description for API service with @desc annotation.

1.6.4 Error Message: msg

As all know, msg is short for message, i.e. error message. When the returning code is not equals to 200, the msg should not be empty. If there is something wrong, probably illegal client request or server interal error, PhalApi will automatically transfer exception message as msg.

NOTE: Only exception class which extends from PhalApi_Exception can reponse with ret, data and msg. Throwing any other exception classes which not extend from PhalApi_Exception will response nothing to clients, no ret, no data, neither msg.

If we throw a PhalApi_Exception_BadRequest exception as below.

throw new PhalApi_Exception_BadRequest(T('wrong sign'), 1);

The msg will be "Bad Request: wrong sign", and ret will be 401(401 = 1 + 400).

If we throw a PhalApi_Exception_InternalServerError exception as below.

throw new PhalApi_Exception_InternalServerError(T('system is busy now'), 2);

Then the msg will be "Interal Server Error: system is busy now", and ret will be 502(502 = 2 + 500).

1.6.5 Header Response

When response via HTTP/HTTPS, we can use PhalApi_Response::addHeaders() to add more header information as we want. Assume that we allow all other domain visiting our API services, we can add Access-Control-Allow-Origin to the headers int file init.php as below.

DI()->response->addHeaders('Access-Control-Allow-Origin', '*');

In addition, PhalApi add Content-Type for JSON response.

Content-Type:"application/json;charset=utf-8"

Please note that the same header will cover the former ones, as .

1.6.6 Why PhalApi DO NOT catch Exception

As we talk about it before, PhalApi DO NOT catch any exceptions which not extends from PhalApi_Exception for the following considerations.

  • Help backend developers to figure out what's the problem fastly with original PHP trace infomation in development environment.
  • Besides, allow server to monitor and to record PHP errors.

1.6.7 Response with other format, such as JSONP

When we develop H5 light app, we may need to response with JSONP, allowing JavaScript do more things. If so, we have to register DI()->response with PhalApi_Response_JsonP in entrace file.

if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

When in unit tests, we do not need a real response as in the browser, but a mock response without no affects. Then we can use PhalApi_Response_Explorer, just displaying result on the console.

DI()->response = 'PhalApi_Response_Explorer';

1.6.8 Extend Your Response

If you want to response with other format, such as XML, you should implement your own response firstly, which should extend from PhalApi_Response.

class MyResponse_XML extends PhalApi_Response {

    protected function formatResult($result) {
        //TODO: tranfer $result from array into xml ......
    }
}

Secondly, register DI()->response again.

DI()->response = new MyResponse_XML();

1.6.9 Better Advice

In most time, client need to handle different sutiations according to the operation status from server in business scene. But there is only one returning code for client developers, which is not for final users. Consequently, we suggest to add one more status in the field data, which is used for business logic because field ret has been used for technology need.

For example, adding a code into data, i.e. the full path is data.code. When code is equals to zero, it means to be successful, otherwise if fails.

Take User.Login as an example.

  • data.code = 0, login successfully
  • data.code = 1, user not exist
  • data.code = 2, password wrong
  • data.code = 3, user in the blacklist
  • ... ...

1.6.10 Domain-Specific Designs and Fiat Standard

There are four kinds of standard in RESTful Web APIs. And they are Fiat Standard, Personal Standard, Company Standard, and Open Standard.

Obviously, the format JSON + ret-data-msg we recommend here, is not belong to Personal Standard, not to Company Standard either. And it's far away from Open Standard. I think it belong to Fiat Standard in some degree.

Anyway, we hope we could have a good common sense at API service development.

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


02-tutorial | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

2.1 Tutorial

In this chapter, we wil explore how to enjoy developing an API in the shortest time.

Here we go!

2.2 Write an API

Let's start from "Hello World!". Put the follow code inside file ./Demo/Api/Welcome.php.

<?php

class Api_Welcome extends PhalApi_Api {

    public function say() {
        $rs = array();
        $rs['title'] = 'Hello World!';
        return $rs;
    }
}

2.3 Visit the API

The API service link looks like:

http:// + host + entrance + ?service=XXX.XXX

Yeah out demo link is:

http://dev.phalapi.com/demo/?service=Welcome.Say

2.4 Get the result

PhalApi will response with JSON, so here it is:

{"ret":200,"data":{"title":"Hello World!"},"msg":""}

To make the result more clear,

{
    "ret": 200,
    "data": {
        "title": "Hello World!"
    },
    "msg": ""
}

2.5 Screenshoot

2.5 Congraduations!

You have been smark at develop APIs with PhalApi!
Enjoy it and more detail at PhalApi.

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


03-use-phaApi-or-not | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

3.1 Use PhalApi or not

It's obviously different between good at how to develop with a framework and kowning why we develop with the framework.

As we know, a framework can help to reduce repeat code. In the other side, it indicates that everything will be under our control, such as code style, project risk, project management, etc.

Here is to say, it's very helpfull to spend some time to think about it whether we can use PhalApi to develop or not.

3.2 What PhalApi major in

PhalApi focus on providing various stable API services for mobile apps, big data and other API systems.

Consequently, we combine HTTP protocol with JSON to provide API service.
Yet if you need other style, you can custom it very easily. We will talk about it later.

3.3 Agile development and fast delivery

We believe in that:
a good code style help to form a good development style, a good development style help to build an excelent teamwork, a excelent teamwork help to deliver prodcution fastly.

3.4 Constrains and focus

Use contrains as guide rails.

A good framework not only tells us how to do, but also what we can't do. It's very important.
For examplye, we hate to wait trafic lights, but we konw they protected us from accident.

//TODO

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


04-API-Request | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

1.4 API Request

4.1 Specify service: ?service=XXX.XXX

Client can request an API by specifing service, usually with service parameter by GET method. The form of URI to request is something like this.

API HOST + ENTRANCE + ?service=XXX.XXX

e.g:
http://dev.phalapi.com   +   /demo/  +  ?service=User.GetBaseInfo

After we request with GET method in a browser, we can see some nginx logs which looks like:

127.0.0.1 - - [07/Feb/2015:22:46:46 -0800] "GET /demo/?service=User.GetBaseInfo&sign=&user_id=1 HTTP/1.1" 200 107 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:11.0) Gecko/20100101 Firefox/11.0"

If we request by POST instead, we will see:

127.0.0.1 - - [07/Feb/2015:19:32:05 -0800] "POST /demo/?service=User.GetBaseInfo&sign= HTTP/1.1" 200 135 "-" "-"

The first letter of service parameter is case-insensitive, but we encourage it should be a capital letter. The API we request in above is related to Api_User::getBaseInfo.

class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }
}

If there is no service parameter, it will be Default.Index in default.

4.2 When to use GET params?

When developing a project, we will run into some common API params, such as client agent, the version ofclient app, app key, etc. These params should be post by GET method, because we can statistics, analyse and locate where the bugs are by access.log in nginx in the future. That is one of best practices.

4.3 When to use POST params?

On the otherhand, we suggest to use POST params while we in the situations as beblow.

    1. sensitive data, such as password.
    1. some special characters or big data package.

4.4 How to get header information?

There are some headers in each request, and we can get header information as below.

Assume the request header is:

Host        demo.phalapi.net
User-Agent  Mozilla/5.0

If we want to just get one header, e.g. host in this case, we can:

$host = DI()->request->getHeader("Host"); // we will get "demo.phalapi.net"

If we want to get all the headers, we can:

$headers = DI()->request->getAllHeaders(); // which will return an array

4.5 Switch to other param source

As we can see before, we could request an API by GET method, or POST method, or both of them, which will retrieve client params from $_GET, $_POST, and $_REQUEST. But in some situations, we need to use other param source. In this case, we can register DI()->request with specified data source in the file index.php.

For example, when force to use POST data, these code should be added into index.php.

//vim ./index.php

$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : "{}";
DI()->request = new PhalApi_Request(array_merge($_GET,json_decode($HTTP_RAW_POST_DATA, true)));

While running in unit tests, we need to mock data for API to be tested.

// mock data
$data = array(...);

DI()->request = new PhalApi_Request($data);

We can also mock data with PhalApi_Helper_TestRunner class.

    public function testIndexByRunner()
    {
        //Step 1. build a URL
        $url = 'service=Default.Index&username=dogstar';
        $params = array();

        //Step 2. excute request
        $rs = PhalApi_Helper_TestRunner::go($url, $params);
    }

4.6 Encrypt requst params

It's a good idea to encrypt some sensitive client params for security in our project. In that way, we need to override PhalApi_Request::genData().

Assume our project want to post all the client params in $_POST['data'] after base64 encoding.

Firstly, create an new request class and implement how to decode client data.

<?php

class Common_Request extends PhalApi_Request {

    public function genData($data) {
        if (!isset($data) || !is_array($data)) {
            $data = $_POST; // only recieve POST data
        }

        return isset($data['data']) ? base64_decode($data['data']) : array();
    }
}

Secondly, register DI()->request in file index.php.

DI()->request = 'Common_Request';

So it is! Try it out, and how to encrypt client data is up to you.

4.7 API signature with filter

In PhalApi, we treat many resource as back-end service. The filter servcie, which is DI()->filter, can do some extral operations before excute a request. Usually, we can check API signature in the filter.

There is a default filter service, i.e. PhalApi_Filter_SimpleMD5 class. You can open it by uncommenting the code below in file init.php.

// filter service
DI()->filter = 'PhalApi_Filter_SimpleMD5';

Then client have to generate a sign as below.

    1. exclude sign param(default to be sign)
    1. sort all the rest of params
    1. concatenate all the value of params as string after sort
    1. md5

For example, we will request such a URI: ?service=Default.Index&username=dogstar.

  1. exclude sign param(default to be sign) ?service=Default.Index&username=dogstar

  2. sort all the rest of params
    service=Default.Index
    username=dogstar

  3. concatenate all the value of params as string after sort
    "Default.Indexdogstar" = "Default.Index" + "dogstar"

  4. md5
    sign = 35321cc43cfc1e4008bf6f1bf9b7e3b8 = md5("Default.Indexdogstar")

At last, client need to add sign into GET params. Finally, the URI should be: ?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8.

Check it out!

Firstly, let's request without sign. Of course, it'll fail.

http://localhost/phalapi/public/demo/?service=Default.Index&username=dogstar

Reponse:
{
    "ret": 406,
    "data": [],
    "msg": "illegal request: wrong sign"
}

When fail to check the sign, we can get the right sign in logs.

2015-10-23 23:16:16|DEBUG|Wrong Sign|{"needSign":"35321cc43cfc1e4008bf6f1bf9b7e3b8"}

Secondly, let's request with a right sign, we generate in above.

http://localhost/phalapi/public/demo/?service=Default.Index&username=dogstar&sign=35321cc43cfc1e4008bf6f1bf9b7e3b8

And it works!

If we want to rename the name of sign parameter, we can set the new name when registering DI()->filter. For example, we use s for short instead.

DI()->filter = new PhalApi_Filter_SimpleMD5('s');

How to cancel signature verification in some APIs?

We can cancel signature verification by overriding PhalApi_Api::filterCheck() function in our implementation API sub-class. And do nothing inside filterCheck().

//vim ./Demo/Api/Default.php
class Api_Default extends PhalApi_Api
{
    //....

    protected function filterCheck()
    {
    }
}

4.8 COOKIE

DI()->cookie is not a default backend service, which means we need to register it in file init.php when we want to deal with COOKIEs.

It's very simple to register DI()->cookie as other backend services.

// COOKIE
DI()->cookie = 'PhalApi_Cookie';

There are three main operations in PhalApi_Cookie, and they are set, get, and delete.

// set a COOKIE
DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

// get a COOKIE
echo DI()->cookie->get('name');  //输出 phalapi

// delete a COOKIE
DI()->cookie->delete('name');

Smart COOKIE in PhalApi

In fact, it's more complicated to use COOKIE in the real world. According defferent situations, we might need to encrypt COOKIE, or get the cookie after we set in the same request. All these situations can be under control with out smart COOKIE service, i.e. PhalApi_Cookie_Multi class.

Again, if we want a smart COOKIE service, we need to register it with some config.

$config = array('crypt' => $crypt, 'key' => 'a secrect');
DI()->cookie = new PhalApi_Cookie_Multi($config);

We config how to encrypt COOKIE here. crypt will encrypt the COOKIE, and default is DI()->crypt, which need to implement interface PhalApi_Crypt. key is the encryption key.

In order to show how to encrypt our COOKIE with PhalApi_Cookie_Multi, let's take an example step by step.

First of all, create an encryption class, which implement interface PhalApi_Crypt. It is really simple, just doing base64_encode and base64_decode.

class Cookie_Crypt_Mock implements PhalApi_Crypt {

    public function encrypt($data, $key) {
        return base64_encode($data);
    }

    public function decrypt($data, $key) {
        return base64_decode($data);
    }
}

Then register DI()->cookie with Cookie_Crypt_Mock in init.php.

$config = array('crypt' => new Cookie_Crypt_Mock(), 'key' => 'a secrect');
DI()->cookie = new PhalApi_Cookie_Multi($config);

Now we can test whether it works correctly. Let's create an new API service as below.

    public function cookieTest() {
        $rs = array();

        $rs['aEKey'] = DI()->cookie->get('name');
        DI()->cookie->set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

        return $rs;

    }

After request the API service, we can check the cookies in response.

AHA~! It works!

Some advise as other framwork will provide too

    1. DO NOT save sensitive data into COOKIE, protect your data
    1. DO NOT save too much data into COOKIE, just keep client fit

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


05-Parameter-Rule | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

This part will be a little long, but it will be very helpful.

5.1 Parse client params

Parameter is important for backend APIs but also for client request.

That's why we hope to simplify backend APIs development in getting client params, checking params, verifying params and writting API docs. On the other hand, client can post params with more freedom.

So in PhalApi, we introuce the concept of parsing client params, i.e. auto getting and verifying client params by rule configs.

What developers need to do is just writting rule config, and PhalApi will do the rest automatically. Let's explore more detail.

5.2 Rule configs

Developers who are from Yii should by familiar with rule configs. Those who have not used Yii before can handle thess configs in a short time. With time moving on, you can find these configs follow the format as below.

array(
    'backend param name' => array('name' => 'API param name', 'type' => 'param type', 'default' => 'param default value', ...),
    ... ...
)

5.3 Example

(1) A simple example

Let's take login API as example. Assume that we need to provide a login API service, which will allow user to login by providing his/her username and password.

<?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'login' => array(
                'username' => array('name' => 'username'),
                'password' => array('name' => 'password'),
            ),
        );
    }

    public function login()
    {
        return array('username' => $this->username, 'password' => $this->password);
    }
}

Then we can request it like this:

/?service=User.Login&username=test&password=123456

and it will reponse:

{"ret":0,"data":{"username":"test","password":"123456"},"msg":""}

All rule configs should located at getRules() method. In this case, there are tow params for login service. Then develop can get the client param by class member property, such as $this->username, $this->password.

(2)A full example

We just only define tow simple params. However, we need to do more other things before we get the param from client, such as verifying whether required or not, max length, min/max value limitiation etc.

Let's continue to complete the rule configs above. The params username and password should be required, and the min length of password should be greater than 6.

'login' => array(
   'username' => array('name' => 'username', 'require' => true),
   'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),

Try to request again without any params. When we request /?service=User.Login, we will get:

{"ret":400,"data":[],"msg":"Illegal Param: wrong param: username"}

When we request with wrong length of password like /?service=User.Login&username=test&password=123, we will get:

{"ret":400,"data":[],"msg":"Illegal Param: password.len should >= 6, but now password.len = 3"}

Look! PhalApi do many things automatically, what we need to do is just a little config and then enjoy it!

5.4 Three kinds of params

(1)System params

Currently, there is only one system param, i.e. service. The param service is a string, which looks like XXX.XXX, determining which API service client will call.

Here are some examples.

# Recommend example
/?service=User.GetBaseInfo

# Right example(start with lower case letter)
/?service=user.getBaseInfo(method name is all lower case letter. NOTE: class name just can start with lower case letter, the rest should keep the same with class file name, or lead to a NOT FOUND exception)
/?service=user.getbaseinfo

# Illegal service(missing method name)
/?service=User
# Illegal service(missing dot between class name and method name)
/?service=UserGetBaseInfo
# Illegal service(only support split with dot now)
/?service=User|GetBaseInfo

(2)Application params

Application params are common client params in one API project. All thess application params should be located at apiCommonRules in the config file ./Config/app.php. For example, if we need a required sign param, and a NOT required version param for all APIS, we can edit the apiCommonRules and add the code as below.

//$vim ./Config/app.php
<?php

return array(
    /**
     * API common param rules
     */
    'apiCommonRules' => array(
        // a required ```sign``` param
        'sign' => array(
            'name' => 'sign', 'require' => true,
        ),
        // a NOT required ```version``` param
        'version' => array(
            'name' => 'version', 'default' => '',
        ),
    ),

    ... ...

(3)API params

At last, we can define sepecified client params for each API service. In addition, in order to reuse rule configs in the same API class, we can use * rules, which are common rules only in one API class. For example, we add a reqiured code param, which's length is 4, for all User APIs.

    public function getRules()
    {
        return array(
            '*' => array(
                'code' => array('name' => 'code', 'require' => true, 'min' => 4, 'max' => 4),
                ),
            'login' => array(
                'username' => array('name' => 'username', 'require' => true),
                'password' => array('name' => 'password', 'require' => true, 'min' => 6),
            ),
        );
    }

After we configure application common params, API common params and sepcecified API params, client can request the login API service as below.

/?service=User.login&sign=77f81c17d512302383e5f26b99dae4d9&username=test&password=123456&version=1.3.6&code=abcd

NOTE: The indexes of rule configs in API class are case-insensitive.

In summary, there are tow kinds of params in API class, i.e. API common params and sepcecified API params. The former is represent with *, while the latter is represent with the name of method.

(4) Params priority

When one rule config for the same param repeat in application params, API common params and sepcecified API params, the latter rule will cover the former one. That's to say, sepcecified API params rule will take replace of common params rule to ensure developers can custom their rules by need.

In short, the priority of parameter rule is as below(as you expect):

    1. HIGHEST, sepcecified API parameter rule
    1. common API parameter rule
    1. application parameter rule
    1. LOWEST, system parameter rule(only one param, i.e. service)

5.5 Online API detail documents(auto generated)

For the sake of convenience to check all the client params fast, PhalApi provides online API detail documents. They are auto generated by the parameter rules above. Vist the follow URI in the bowser:

/demo/checkApiParams.php?service=Default.Index

And you can see:

Thess API docs can be also provided to client developers during project development.

Self-description data

It is worth to mention that the parameter rules we define actually are self-description data. It means the code of rules reflect related properties of params.

5.6 All kinds of rule types

Param Type(type) | IS Required(require) | Defaul Value(default) | Min&Max Value(min&max) | MORE
string | true/false, default is false | should be a string | Optional | regex index defines the regex need to be matched, and format index defines charaset, such as utf8, gbk, gb2312 etc.
int | true/false, default is false | should be a integer | Optional | ---
float | true/false, default is false | should be a float number | Optional | ---
boolean | true/false, default is false | true/false | --- | It will treat ok, true, success, on, yes, 1 as TRUE
date | true/false, default is false | translate with the format | Optional, only need to check while the format is timestamp|It will turns into timestamp while the format is timestamp
array | true/false, default is false | should be an array, or try to trun into an array if NOT | Optional, check array count | It will call explode() with format separator if format is explode, and will call json_decode() if format is json
enum | true/false, default is false | should inside the range|---|Required，use range range to sepecify enum range
file | true/false, default is false | should be an array | min nad max are represent the size of upload file|range index is represent for the types of upload file allowing to post, while ext will filter the external name
callable | true/false, default is false | --- | --- | callback can set callback function with params as the third param, the first is param value, the second is current rule config

NOTE: You can add desc index for all of the rule types, which will be displayed in the section Desc in online API detail docs.

Let's study each rule type with examples one by one.

(1)Rule type: string

When a rule config is defined without type, it will be string in default. A full config for string type is:

array('name' => 'username', 'type' => 'string', 'require' => true, 'default' => 'nobody', 'min' => 1, 'max' => 10)

We use 'type' => 'string' to sepecify username is a string.

Then if anyone post a too much long username, such as &username=alonglonglonglongname, he will get an exception as returned.

{"ret":400,"data":[],"msg":"Illegal Param: username.len should <= 10, but now username.len = 21"}

It there will be some special(other charset) in username, we can add format index to sepecify which charset we use.

array('name' => 'username', 'type' => 'string','format' => 'utf8', 'min' => 1, 'max' => 10)

We can add more powerful verification with regex index. Take email for example.

'email' => array(
    'name' => 'email',
    'require' => true,
    'min' => '1',
    'regex' => "/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i",
    'desc' => 'You should post a correct email address',
),

(2)Rule type: int

Usually, we can configure id param for database tables as below.

array('name' => 'id', 'type' => 'int', 'require' => true, 'min' => 1 )

NOTE: id should be in [min, max).

If client post an id beyond the range, such as &id=0, it will fail.

{"ret":400,"data":[],"msg":"Illegal Param: id should >= 1, but now id = 0"}

(3)Rule type: float

See int in above.

(4)Rule type: boolean

PhalApi will treat strings like ok, true, success, on, yes as boolean TRUE, any other strings PHP will parse as TRUE will also turn into TRUE. For example, we can add one more param for login service.

array('name' => 'isRememberMe', 'type' => 'boolean', 'default' => true)

(4)Rule type: date

We can post a date as we want.

array('name' => 'registerData', 'type' => 'date')

When client post &risterData=2015-01-31 10:00:00, we will get 2015-01-31 10:00:00 in API.

If we want a timestamp instead of a string, we can configure as below:

array('name' => 'registerData', 'type' => 'date', 'format' => 'timestamp')

At last, we will get 1422669600, which is equals to strtotime('2015-01-31 10:00:00').

(5)Rule type: array

Usually, client need to post serveral items in one request. That's why we need array type. For example:

array('name' => 'uids', 'type' => 'array', 'format' => 'explode', 'separator' => ',')

Then params &uids=1,2,3 will be turned into:

array ( 0 => '1', 1 => '2', 2 => '3', )

If client want to post params in JSON, we need to set format index with json.

array('name' => 'params', 'type' => 'array', 'format' => 'json')

Then params &params={"username":"test","password":"123456"} will be turned into:

array ( 'username' => 'test', 'password' => '123456', )

Please note that, if we use array type but without format, it will be turned into an array, which contains one element. For example, &name=test will become array(0 => 'test').

(7)Rule type: enum

You can use this enum type when you need to limit the range of parameters. Take sex for example.

array('name' => 'sex', 'type' => 'enum', 'range' => array('female', 'male'))

We only allow female/male for sex param. Therefore, any other strings will be treated as illegal param. Let's try to post &sex=unknow. No doubt that we will get something like this.

{"ret":400,"data":[],"msg":"Illegal Param: sex should be in female\/male, but now sex = unknow"}

Please keep in mind, it's better to use string, NOT int/float, for range.
Because in generally, what we get from GET/POST method are strings, not int/float, otherwise will lead to unexpected result. Here is an example show how wrong range occur a bug.

// Assume client post &type=N

// rule config as below
array('name' => 'type', 'type' => 'enum', 'range' => array(0, 1, 2))

// wrong result
var_dump(in_array('N', array(0, 1, 2))); // it prints true, since 'N' == 0 in PHP

To avoid this kind bug, we should configure as below.

// rule config (use STRING, not integer)
array('name' => '&type', 'type' => 'enum', 'range' => array(`0`, `1`, `2`))

(8)Rule type: file

When we want to receive files from client, we can use type file. Let's take an exmaple.

array(
    'name' => 'upfile',
    'type' => 'file',
    'min' => 0,
    'max' => 1024 * 1024,
    'range' => array('image/jpeg', 'image/png') ,
    'ext' => array('jpeg', 'png')
)

These config means that, the size of file should be greater than 0B, and less than 1MB, client only can post jpeg/png files in form filed upfile, and the external file name should be jpeg/png. If client upload successfully, we will get $_FILES["upfile"].

array(
     'name' => '',
     'type' => '',
     'size' => '',
     'tmp_name' => '',
)

More information about $_FILE, please visit $_FILE.

There are some more examples about ext.

// single external name in array
'ext' => array('jpg')

// single external name in string
'ext' => 'jpg'

// multi external names in array
'ext' => array('jpg', 'jpeg', 'png', 'bmp')

// multi external names in string, split with comma
'ext' => 'jpg,jpeg,png,bmp'

(9)Rule type: callable

If we want to verify and parse the client params by ourself, we can use type callable. Let's try to check the version param with Common_MyVersion::formatVersion().

array('name' => 'version', 'type' => 'callable', 'callback' => array('Common_MyVersion', 'formatVersion'))

Then PhalApi will call Common_MyVersion::formatVersion() client post a version. And we can do anything in Common_MyVersion::formatVersion() as we want.

// create an new class

class Common_MyVersion {

    public static function formatVersion($value, $rule) {
        if (count(explode('.', $value)) < 3) {
            throw new PhalApi_Exception_BadRequest('Illegal version');
        }
    }
}

NOTE: the first param $value is the origin value from client param, the second param $rule is our rule config as above, the third param(OPTIONAL) equals to the params index in $rule.

5.7 The principle to design parameter rule

(1) Free to map client param name with backend member name

In some case, backend would like to name variable by Camel-Case, while client would prefer name parameters with underline. That's why we need tow name in rule config, one is for backend, anther one is for client.

array(
    'isRememberMe' => array('name' => 'is_remember_me', 'type' => 'boolean', 'default' => true),
)

Here, client post with &is_remember_me=1, and backend get with $this->isRememberMe. Perfect cooporation between backend and frontend? AhA~ I think so.

We can also make a shorter name for client to save network traffic.

array(
    'isRememberMe' => array('name' => 're', 'type' => 'boolean', 'default' => true),
)

Then client can post with &re=1 instead.

(2)Returning exception

PhalApi will return an exception, which's code is usually 4xx, while client post illegal params. This can remind client they post wrong params.

5.8 Extend your rule type

When PhalApi provides parameter rules that do not meet your need, you can extend the PhalApi_Request_Formatter interface to customize your own type, in addition to using the callable type.

There are tow more step before you use your own type.

    1. Extend and implement the PhalApi_Request_Formatter interface.
    1. Register the new type in DI.

Let's take email type for example.

Firstly, we need to implement interface PhalApi_Request_Formatter.

<?php

class Common_MyFormatter_Email implements PhalApi_Request_Formatter {

    public function parse($value, $rule) {
        if (!preg_match('/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/', $value)) {
            throw new PhalApi_Exception_BadRequest('Illegal email');
        }

        return $value;
    }
}

Secondly, register it into DI.

DI()->_formatterEmail = 'Common_MyFormatter_Email';

NOTE: The name of service should follow DI()->_formatter + type name(first letter is upper case, the rest is lower case).

At last, we can use our new type email now as below.

array('name' => 'user_email', 'type' => 'email')

There are all the formatting service by now.

  • _formatterArray
  • _formatterBoolean
  • _formatterCallable
  • _formatterDate
  • _formatterEnum
  • _formatterFile
  • _formatterFloat
  • _formatterInt
  • _formatterString

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


06-API-Response | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

6.1 JSON Response

Obviously, we response result to clients with JSON at default.

6.2 Uniform Response Structure

As we can see above, default API Default.Index return something like this.

{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "Hi PHPer, welcome to use PhalApi!",
        "version": "1.3.6",
        "time": 1489910945
    },
    "msg": ""
}

The response is mainly consisted of three parts. ret is returning code, which can tell what status is, data is anything you want to transfer to clients, while msg is represent for error message when fail to request. We will explain them one by one in detail.

(1)Returning Code: ret

Reference to HTTP Status Code, we make a convention.

  • 200: API response successfully and return data in data field as expected.
  • 4XX: Illegal request from clients, the reason why fail to request is displayed in msg field.
  • 5XX: API service internal error, and no data can be provided.

ret = 200 means response successfully

When the ret is equals to 200, it means API response successfully. Then clients can retrieve more business data in data field that API service provide.

ret = 4XX means illegal request

If clients request incorrectly, such as requesting a non-exists API serivce, missing required API params or failling to pass verification, they will get a returning code range from 400 to 499. When this happen, client developers should adjust how to request the API service and ensure they can request correctly. And the reason why fail to request can be found in msg field.

At backend, we can use PhalApi_Exception_BadRequest to specify returning code and error message. Take signature verification as an example. When the signature from client is wrong, we should throw an exception to interupt the request and warn the clients.

throw new PhalApi_Exception_BadRequest('wrong sign', 1);

Finally, clients get a response where ret is 401 and msg is Illegal request: wrong sign. Please note that, PhalApi will add the exception code with 400 automatically, i.e. 401 = 1 + 400. That is why we get ret = 401, not ret = 1. Therefore, the exception code passed to PhalApi_Exception_BadRequest should between 1 and 99.

ret = 5XX means internal error

This case should not happen generally. When client developers get this, they usually can do nothing but warn backend developers what's going on. As backend developers, we should fix it as soon as possible according the error message in msg field. We need to check whether we break the rule of PhalApi, or do something wrong such as trying to get an non-exsist field.

In this case, PhalApi will occur a PhalApi_Exception_InternalServerError exception.

6.3 Business data: data

The data field is the bridge between API service and cleint APP. It can be any type, and it's up to backend. But it's a rule of thumb to return data as an array for extendibility and compatibility.

When developing an API, we can add annotation to specify clearly what we will return to clients. Then client developers can visit the online document and check them out.

Let's look into Default.Index again.

    /**
     * Default API service
     * @return string title title
     * @return string content content
     * @return string version versio with format: X.X.X
     * @return int time current timestamp
     */
    public function index() {
        return array(
            'title' => 'Hello World!',
            'content' => T('Hi {name}, welcome to use PhalApi!', array('name' => $this->username)),
            'version' => PHALAPI_VERSION,
            'time' => $_SERVER['REQUEST_TIME'],
        );
    }

Then open one browser and visit the online API detail document as beblow.
http://demo.phalapi.net/checkApiParams.php?service=Default.Index

The document looks like:

Annotation Format

Reference to return annotation in PHP Documentor 2, the annotation format is similar to PHPDocs.

@return  [Type] [The path of field name, split with dot] [Description of the field]

In above, the type should be one of these.

keyword | Description
string | String, such as "PhalApi".
int | Integer, such as 2017.
float | Float number, such as 3.17.
boolean | Boolean, i.e. true or false.
date | Date and timestamp, such as "2017-03-19 17:40:50" or 1489916450.
array | Array, such as [1, 2, 3]
fixed | Fixed value
enum | Enumeration type
object | Object, such as {'name' : 'PhalApi'}

NOTE: The difference between array and object is that there are no string but only naturnal number indexes in array and each item has the same type, while object is stand for a structure, something like a map.

Here are some examples of an array.

["dogstar", "Aevit", "uwin"]  // all item are string

[28, 27, 23]  // all item are integer

// all item are structure with same fields name and age
[
    {
        "name": "dogstar",
        "age": 28
    },
    {
        "name": "Aevit",
        "age": 27
    },
    {
        "name": "uwin",
        "age": 23
    }
]

Here is an example of a object.

{
    "name": "dogstar",
    "age": 28
}

In order to tell client developers clearly what kind of type we will return to them, especially from array or object, we recommend add square brackets behind the field name.

     * @return array list user info list
     * @return int list[].id user ID
     * @return string list[].name username
     * @return string list[].note user note

Here, we make it clear that we will return list as an array to clients. Besides, these square brackets indicate that id, name, and note come from an array, not a object, which also indicates it's possible there are multiple user in the list.

NOTE: You can add more description for API service with @desc annotation.

6.4 Error Message: msg

As all know, msg is short for message, i.e. error message. When the returning code is not equals to 200, the msg should not be empty. If there is something wrong, probably illegal client request or server interal error, PhalApi will automatically transfer exception message as msg.

NOTE: Only exception class which extends from PhalApi_Exception can reponse with ret, data and msg. Throwing any other exception classes which not extend from PhalApi_Exception will response nothing to clients, no ret, no data, neither msg.

If we throw a PhalApi_Exception_BadRequest exception as below.

throw new PhalApi_Exception_BadRequest(T('wrong sign'), 1);

The msg will be "Bad Request: wrong sign", and ret will be 401(401 = 1 + 400).

If we throw a PhalApi_Exception_InternalServerError exception as below.

throw new PhalApi_Exception_InternalServerError(T('system is busy now'), 2);

Then the msg will be "Interal Server Error: system is busy now", and ret will be 502(502 = 2 + 500).

6.5 Header Response

When response via HTTP/HTTPS, we can use PhalApi_Response::addHeaders() to add more header information as we want. Assume that we allow all other domain visiting our API services, we can add Access-Control-Allow-Origin to the headers int file init.php as below.

DI()->response->addHeaders('Access-Control-Allow-Origin', '*');

In addition, PhalApi add Content-Type for JSON response.

Content-Type:"application/json;charset=utf-8"

Please note that the same header will cover the former ones, as .

6.6 Why PhalApi DO NOT catch Exception

As we talk about it before, PhalApi DO NOT catch any exceptions which not extends from PhalApi_Exception for the following considerations.

  • Help backend developers to figure out what's the problem fastly with original PHP trace infomation in development environment.
  • Besides, allow server to monitor and to record PHP errors.

6.7 Response with other format, such as JSONP

When we develop H5 light app, we may need to response with JSONP, allowing JavaScript do more things. If so, we have to register DI()->response with PhalApi_Response_JsonP in entrace file.

if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

When in unit tests, we do not need a real response as in the browser, but a mock response without no affects. Then we can use PhalApi_Response_Explorer, just displaying result on the console.

DI()->response = 'PhalApi_Response_Explorer';

6.8 Extend Your Response

If you want to response with other format, such as XML, you should implement your own response firstly, which should extend from PhalApi_Response.

class MyResponse_XML extends PhalApi_Response {

    protected function formatResult($result) {
        //TODO: tranfer $result from array into xml ......
    }
}

Secondly, register DI()->response again.

DI()->response = new MyResponse_XML();

6.9 Better Advice

In most time, client need to handle different sutiations according to the operation status from server in business scene. But there is only one returning code for client developers, which is not for final users. Consequently, we suggest to add one more status in the field data, which is used for business logic because field ret has been used for technology need.

For example, adding a code into data, i.e. the full path is data.code. When code is equals to zero, it means to be successful, otherwise if fails.

Take User.Login as an example.

  • data.code = 0, login successfully
  • data.code = 1, user not exist
  • data.code = 2, password wrong
  • data.code = 3, user in the blacklist
  • ... ...

6.10 Domain-Specific Designs and Fiat Standard

There are four kinds of standard in RESTful Web APIs. And they are Fiat Standard, Personal Standard, Company Standard, and Open Standard.

Obviously, the format JSON + ret-data-msg we recommend here, is not belong to Personal Standard, not to Company Standard either. And it's far away from Open Standard. I think it belong to Fiat Standard in some degree.

Anyway, we hope we could have a good common sense at API service development.

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


07-Database-Based-NotORM | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

7.1 Database MySQL Config

All the database config should be located at config file dbs.php.

It looks like:

<?php
return array(
    /**
     * Database Servers
     */
    'servers' => array(
        'db_demo' => array(                         // server ID
            'host'      => 'localhost',             // database host
            'name'      => 'phalapi',               // database name
            'user'      => 'root',                  // database username
            'password'  => '',                      // database password
            'port'      => '3306',                  // database port
            'charset'   => 'UTF8',                  // database charset
        ),
    ),
    /**
     * Customing Table Routes
     */
    'tables' => array(
        // Common Defatult Routes
        '__default__' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
            ),
        ),
    ),
);

In above, all our database tables should start with prefix tbl_, and primary key id. Futhermore, all data store into database db_demo, which is MySQL with host localhost, name phalapi, user root, etc.

7.2 NotORM

We use NotORM to operate database in PhalApi. Whether you are familiar with NotORM or not, it's easy to learn.

Related: NotORM - PHP library for simple working with data in the database

(1) Register NotROM

It's simple to register NotORM into DI.

DI()->notorm = new PhalApi_DB_NotORM(DI()->config->get('dbs'), !empty($_GET['__sql__']));

We use the config above, and start database debug mode if $_GET['__sql__'] is not empty.

(2) Database Debug Mode

Show SQL

As we talk before, if $_GET['__sql__'] is not empty, it will print all the sql sentences that excuted in the request.

For example, if we request ?service=User.getBaseInfo with &__sql__=1.

http://demo.phalapi.net/?service=User.getBaseInfo&user_id=1&__sql__=1

It will show:

[1 - 0.00035s]SELECT * FROM tbl_user WHERE (id = ?); -- 1<br>
{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}

Show Exception

Usually, when database fail to connect, we will get:

{
    "ret": 500,
    "data": [],
    "msg": "can not connect to database: db_demo"
}

It is safe to simplify exception message in production environment, but not helpful to find out what really going on in development environment. Instead we need to enable debug mode.

There are tow way to enable debug mode.

    1. Add parameter &__debug__=1 in per request.
    1. Set debug => true, in file ./Config/sys.php.

Then when exception again, we will see more details.

{
    "ret": 500,
    "data": [],
    "msg": "can not connect to database: db_demo, code: ：1045, cause: SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost' (using password: NO)"
}

(3) How To Get NotORM

By now, we have already know how to configure database and register NotORM. What's next is to get NotORM so that we can do something interesting on database.

We can get a notorm instance in tow ways.

  • Get NotORM by DI anywhere, but NOT recommend

    $user = DI()->notorm->user;
  • Get NotORM inside model, and STRONG recommend
    <?php
class Model_User extends PhalApi_Model_NotORM {
public function doSth() {
    $user = $this->getORM(); // inside model
}
}

NOTE: The subclass of PhalApi_Model_NotORM should be Model_{table_name_to_lower} at default. If you need a different table name, you can override method getTableName($id) and return custom table name.

<?php
class Model_User extends PhalApi_Model_NotORM {
protected function getTableName($id) {
return 'my_user';
}
}

We can get the instance for table tbl_user by $user = DI()->notorm->user; or $user = $this->getORM();.

Table tbl_user is just a simple table as beblow.

CREATE TABLE `tbl_user` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  `age` int(3) DEFAULT NULL,
  `note` varchar(45) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Keep NotORM Query Criteria Or Not

Instances of NotORM are mutable, so they can keep all the query criteria in the same instance. For example:

$user = DI()->notorm->user;
$user->where('age > ?', 18);  // age > 18
$user->where('name LIKE ?', '%dog%');  // age > 18 AND name LIKE '%dog%'

If we do not want to keep the state, we should create an new instance.

$user = DI()->notorm->user;
$user->where('age > ?', 18);

$user = DI()->notorm->user;
$user->where('name LIKE ?', '%dog%');  // name LIKE '%dog%'

Inside the model, $this->getORM(); will return an new instance each time.

7.3 Databse CURD

By far, we move forward a futher more step. We have an instance for table tbl_user now! Let's explore what we can do with it.

(0) Base CURD With Model Using Primary Key

$model = new Model_User();

// Retrieve
$row = $model->get(1);
$row = $model->get(1, 'id, name');
$row = $model->get(1, array('id', 'name'));

// Upgrade
$data = array('name' => 'test', 'update_time' => time());
$model->update(1, $data);

// Insert
$data = array('name' => 'phalapi');
$id = $model->insert($data);
//$id = $model->insert($data, 5);

// Delete
$model->delete(1);

(1) Database Creation

Insert Data

// INSERT INTO tbl_user (id, name, age, note) VALUES (4, 'PhalApi', 1, 'framework')
$data = array('id' => 4, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$user->insert($data);
$id = $user->insert_id(); // must be the same instance
var_dump($id); // new ID

// inside Model
$model = new Model_User();
var_dump($model->insert($data)); // new ID

Insert Multi Data

// INSERT INTO tbl_user (name, age, note) VALUES ('A君', 12, 'AA'), ('B君', 14, 'BB'), ('C君', 16, 'CC')
$rows = array(
    array('name' => 'A君', 'age' => 12, 'note' => 'AA'),
    array('name' => 'B君', 'age' => 14, 'note' => 'BB'),
    array('name' => 'C君', 'age' => 16, 'note' => 'CC'),
);
$rs = $user->insert_multi($rows);
var_dump($rs);

// output
int(3) // the number of new entry

Insert/Upgrade

// INSERT INTO tbl_user (id, name, age, note) VALUES (8, 'PhalApi', 1, 'framework') ON DUPLICATE KEY UPDATE age = 2
$unique = array('id' => 8);
$insert = array('id' => 8, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$update = array('age' => 2);
$rs = $user->insert_update($unique, $insert, $update);
var_dump($rs); // affect rows

(2) Database Upgrade

Upgrade

// UPDATE tbl_user SET age = 2 WHERE (name = 'PhalApi');
$data = array('age' => 2);
$rs = $user->where('name', 'PhalApi')->update($data);
var_dump($rs);

// output
int(1) // affect rows if success
int(0) // nothing affect if nothing change
boolean(false) // exception

Increment

// UPDATE tbl_user SET age = age + 1 WHERE (name = 'PhalApi')
$rs = $user->where('name', 'PhalApi')->update(array('age' => new NotORM_Literal("age + 1")));
var_dump($rs); // affect rows

(3) Database Retrieve

SELECT

Single table field:

// SELECT id FROM `tbl_user`
$user->select('id')

Multi table fields:

// SELECT id, name, age FROM `tbl_user`
$user->select('id, name, age')

Field alias:

// SELECT id, name, MAX(age) AS max_age FROM `tbl_user`
$user->select('id, name, MAX(age) AS max_age')

All fileds:

// SELECT * FROM `tbl_user`
$user->select('*')

WHERE

Single parameter:

// WHERE id = 1
$user->where('id', 1)
$user->where('id = ?', 1)
$user->where(array('id', 1))

Multi parameters:

// WHERE id > 1 AND age > 18
$user->where('id > ?', 1)->where('age > ?', 18)
$user->and('id > ?', 1)->and('age > ?', 18)
$user->where('id > ? AND age > ?', 1, 18)
$user->where(array('id > ?' => 1, 'age > ?' => 10))

// WHERE name = 'dogstar' AND age = 18
$user->where(array('name' => 'dogstar', 'age' => 18))

// WHERE name = 'dogstar' OR age = 18
$user->or('name', 'dogstar')->or('age', 18)

// WHERE ((name = ? OR id = ?)) AND (note = ?) -- 'dogstar', '1', 'xxx'
# use AND
$user->where('(name = ? OR id = ?)', 'dogstar', '1')->and('note = ?', 'xxx')
# or use WHERE without array
$user->where('(name = ? OR id = ?) AND note = ?', 'dogstar', '1', 'xxx')
# or use WHERE with array
$user->where('(name = ? OR id = ?) AND note = ?', array('dogstar', '1', 'xxx'))
# or use WHERE with array
$user->where('(name = :name OR id = :id) AND note = :note', array(':name' => 'dogstar', ':id' => '1', ':note' => 'xxx'))

IN query:

// WHERE id IN (1, 2, 3)
$user->where('id', array(1, 2, 3))

// WHERE id NOT IN (1, 2, 3)
$user->where('NOT id', array(1, 2, 3))

// WHERE (id, age) IN ((1, 18), (2, 20))
$user->where('(id, age)', array(array(1, 18), array(2, 20)))

Like query:

// WHERE name LIKE '%dog%'
$user->where('name LIKE ?', '%dog%')

NULL query:

// WHERE (name IS NULL)
$user->where('name', null)

NOT NULL query:

// WHERE (name IS NOT ?) LIMIT 1; -- NULL
$user->where('name IS NOT ?', null)

ORDER BY

Sort by single field:

// ORDER BY age
$user->order('age')

// ORDER BY age DESC
$user->order('age DESC')

Sort by multi fileds:

// ORDER BY id, age DESC
$user->order('id')->order('age DESC')
$user->order('id, age DESC')

LIMIT

Limit amount:

// LIMIT 10
$user->limit(10)

Limit amount and offset:

// LIMIT 2,10
$user->limit(2, 10)

NOTE: offset first, then amount.

GROUP BY, HAVING

No HAVING:

// GROUP BY note
$user->group('note')

With HAVING:

// GROUP BY note HAVING age > 10
$user->group('note', 'age > 10')

(4) Database Deletion

Delete Some Records

// DELETE FROM tbl_user WHERE (id = 404);
$user->where('id', 404)->delete();

Delete All Records

// Exception: sorry, you can not delete the whole table
$user->delete();

NOTE: It's not allowed to delete all the records in table to proected our data.

(5) Database Transaction

Transaction in NotORM way, but NOT recommend:

// Step 1. get an instance
$user = DI()->notorm->user;

// Step 2. start transaction
DI()->notorm->transaction = 'BEGIN';

// Step 3. do something
$user->insert(array('name' => 'test1',));
$user->insert(array('name' => 'test2',));

// Step 4. commit/rollback
DI()->notorm->transaction = 'COMMIT';
//DI()->notorm->transaction = 'ROLLBACK';

Transaction in PhalApi way:

    public function testTransactionCommit()
    {
        // Step 1. start transaction
        DI()->notorm->beginTransaction('db_demo');

        // Step 2. do something
        DI()->notorm->user>insert(array('name' => 'test1'));
        DI()->notorm->user>insert(array('name' => 'test2'));

        // Step 3. commit/rollback
        DI()->notorm->commit('db_demo');
        //DI()->notorm->rollback('db_demo');
    }

7.4 Multi Tables/Database

We can use several different databases in the project. We even can use different databases for different tables. All what we need to do is just configure out database config, and PhalApi will handle it with magic.

For example, there are tow database servers: db_A, db_B, i.e:

<?php
return array(
    /**
     * Database Servers
     */
    'servers' => array(
        'db_A' => array(                              // db_A
            'host'      => '192.168.0.1',             // database host
            // ... ...
        ),
        'db_B' => array(                              // db_B
            'host'      => '192.168.0.2',             // database host
            // ... ...
        ),
    ),

    //... ...

And tables a_table_user and a_table_friends are stored intto db_A, while tables b_table_article and b_table_comments are stored intto db_B, i.e:

<?php
return array(

    //... ...

    /**
     * Customing Table Routes
     */
    'tables' => array(
        // Common Defatult Routes
        '__default__' => array(
            'prefix' => 'a_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_A'),  // use db_A default
            ),
        ),

        'table_article' => array(                                     // b_table_article
            'prefix' => 'b_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_B'),                                // b_table_article use db_B
            ),
        ),

        'table_comments' => array(                                    // b_table_article
            'prefix' => 'b_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_B'),                                // b_table_comments use db_B
            ),
        ),
    ),

7.6 Build SQL

Table structure without primary key should locate at folder ./Data.

Assume we have save the follow SQL into ./Data/demo.sql:

      `name` VARCHAR(45) NULL,

If we configure multi tables for user in config file:

    'tables' => array(
        '__default__' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
            ),
        ),
        'demo' => array(
            'prefix' => 'tbl_',
            'key' => 'id',
            'map' => array(
                array('db' => 'db_demo'),
                array('start' => 0, 'end' => 2, 'db' => 'db_demo'),
            ),
        ),
    ),

Then we can run:

$ php ./PhalApi/phalapi-buildsqls ./Config/dbs.php user_session

It will print:

/**
 * DB: localhost  db_demo
 */

CREATE TABLE `demo` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/**
 * DB: localhost  db_demo
 */

CREATE TABLE `tpl_demo_0` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_1` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_2` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

7.7 Extends Your DB With Other Database

We can also connect to Oracle, postgreSQL, MS SQL, SQLite, etc.

Firstly, we need to override method PhalApi_DB_NotORM::createPDOBy() in our subclass.

<?php
class Common_MyDB extends PhalApi_DB_NotORM {

    protected function createPDOBy($dbCfg) {
        /* Connect to an ODBC database using driver invocation */
    $dsn = 'uri:file:///usr/local/dbconnect';
    return new PDO($dsn, $dbCfg['user'], $dbCfg['password']);
    }
}

Secondly, re-register DI()->notorm in file ./Public/init.php.

DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new Common_MyDB(DI()->config->get('dbs'), $debug);
};

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


07-Database-Operations | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


08-Config | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

8.1 Simple Config Retrieving

Generally, there are three config files in PhalApi.

$ tree ./PhalAPi/Config
.
├── app.php
├── dbs.php
└── sys.php

The file app.php contains project configs, dbs.php includes database configs, and sys.php kepp system configs for different environments.

We can specify where are these config files in entrance file.

//$ vim ./Public/init.php

// Configurations
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

Assume there are some configs as below in file app.php.

return array(

    'version' => '1.1.1',

    'email' => array(
        'address' => 'chanzonghuang@gmail.com',
    );
);

Then we can retrieve configs simply.

// all configs in app.php
DI()->config->get('app');  // array( ... )

// one config in app.php
DI()->config->get('app.version');  // 1.1.1

// multi level config in app.php
DI()->config->get('app.email.address');  // 'chanzonghuang@gmail.com'

We can also add new config files as we want.

8.2 Config Management Under Continuous Intergration

Actually, there are serveral enviroments during project development, such as development enviroment, testing enviroment, production enviroment etc. Consequently, we need different config files for different enviroment.

Usually, we maintain different config files with different suffixes. Take production configs as an example. The Config folder looks like as below after we add new prod files dbs.php.prod and sys.php.prod:

$ tree ./PhalApi/Config
.
├── app.php
├── dbs.php
├── dbs.php.prod
├── sys.php
└── sys.php.prod

After that, we can switch to related config files automatically in deployment. Take PHing for example.

We could replace these config files with production config files in build.xml.

//$ vim ./build.xml

        <copy
            file="./Config/dbs.php.prod"
            tofile="./Config/dbs.php"
            overwrite="true" />
        <copy
            file="./Config/sys.php.prod"
            tofile="./Config/sys.php"
            overwrite="true" />

Once we excute phing to deploy, we will see:

     [copy] Copying 1 file to /home/phapapi/Config
     [copy] Copying 1 file to /home/phapapi/Config

8.3 Using Yaconf Under PHP7

(1) PHP7 Installation

If you have PHP7 installed, you can ignore this part, or you need to install PHP7 first of all.

(2) Installing Yaconf library

Please visit laruence/yaconf.

(3) Register DI()->config with PhalApi_Config_Yaconf

At entrance file, let's register DI()->config with PhalApi_Config_Yaconf.

DI()->config = new PhalApi_Config_Yaconf();

(4) Usage

Assume we have a test.ini as below in ./Config folder.

//$ vim ./Config/test.ini

name="PhalApi"
version="1.3.1"

Then we can retrieve the configs as usual.

var_dump(DI()->config->get('foo')); // same as var_dump(Yaconf::get("foo"));

var_dump(DI()->config->has('foo')); // same as var_dump(Yaconf::has("foo"));

8.4 Extend Your Own Config

PhalApi save config into files at default. Of course, we can store our configs at anywhere, like database, cache, even remote servers wherever we want. All we need to do is just implementing the interface PhalApi_Config.

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


09-Logger | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

9.1 PHP Uniform Logger Interface

A great logger interface has been describeted at Logger Interface.

9.2 PhalApi Simpler Logger Interface

After many project developments in real world, we found that actually in most time we only use some of these interfaces, not all of them. For simplicity, PhalApi only provide three kinds of interfaces in PhalApi_Logger.

  • ERROR level
  • INFO level

  • DEBUG level

    We will explain them in more details.

(1) ERROR Level

When system exception happens, which means something happening unexpectly, we can log the context as ERROR level. For example, user fail to pay or program fail to insert new data into database.

We can use PhalApi_Logger::error() to log error information as below. It's very simple as you can see.

// only record decription
DI()->logger->error('fail to insert DB');

// record decription and more message
DI()->logger->error('fail to insert DB', 'try to register user dogstar');

// record decription and context data
$data = array('name' => 'dogstar', 'password' => '123456');
DI()->logger->error('fail to insert DB', $data);

These loggers will be saved into log files at default.

$ tailf ./PhalAPi/Runtime/log/201502/20150207.log
2015-02-07 20:37:55|ERROR|fail to insert DB
2015-02-07 20:37:55|ERROR|fail to insert DB|try to register user dogstar
2015-02-07 20:37:55|ERROR|fail to insert DB|{"name":"dogstar","password":"123456"}

(2) INFO Level

It's necessary to record some business opertaions history at key point in project. These INFO loggs can help us to locate and fix bugs in the future. We can also collect, analyze , dig into these loggs, and do some statistics. In summary, they are useful, because they contain important business information from ou clients.

We can use PhalApi_Logger::info() to log business information as PhalApi_Logger::error() above.

// record infomation
DI()->logger->info('add user exp', array('name' => 'dogstar', 'before' => 10, 'addExp' => 2, 'after' => 12, 'reason' => 'help one more phper'));

// log in file
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}

(3) DEBUG Level

ERROR and INFO log are usually used in production environment, while DEBUG log is only used in development environment.

Here are some examples of PhalApi_Logger::debug().

DI()->logger->debug('just for test');

DI()->logger->debug('just for test', 'some description ...');

DI()->logger->debug('just for test', array('name' => 'dogstar', 'password' => '******'));

9.3 More Powfull Log Interface

Besides ERROR, INFO, and DEBUG level log, we can use PhalApi_Logger::log() to log anything as we want.

DI()->logger->log('demo', 'add user exp', array('name' => 'dogstar', 'after' => 12));
DI()->logger->log('test', 'add user exp', array('name' => 'dogstar', 'after' => 12));

// related logs in file
2015-02-07 21:13:27|DEMO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}
2015-02-07 21:15:39|TEST|add user exp|{"name":"dogstar","after":12}

Please NOTE that, the first parameter passed to the method will be tranfered into upper case, which is represented for the type of log.

9.4 DON NOT Ferget to Set Log Levels

As we talk above, there are three kinds of log levels.

  • ERROR log: PhalApi_Logger::LOG_LEVEL_ERROR
  • INFO log: PhalApi_Logger::LOG_LEVEL_INFO
  • DEBUG log: PhalApi_Logger::LOG_LEVEL_DEBUG

We can choose to record which kinds of logs by specifying log levels in the entrance file.

DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

9.5 Extend Your Own Logge

In order to save logs into other storage medium except file, we can extend our own logger interface.

Take store loggs into database for example.

Firstly, implement an new logger.

//$ vim ./Apps/Common/Logger/DB.php

class Common_Logger_DB extends PhalApi_Logger {

    public function log($type, $msg, $data) {
        //TODO save loggs into database ...
    }

Secondly, register DI()->logger with Common_Logger_DB above.

DI()->logger = new Common_Logger_DB($dbConfig,
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

So it is! Check it out by yourself!

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


10-Autoload | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

10.1 The Principle of Autoload in PhalApi

First of all, the autoload mechanism in PhalApi is very simple. Secondly, PhalApi can work very well with any other autoload mechanism, which means PhalApi do not limit you to only use one way to load file.

In summery, the principle is: simple, uniform, normative.

10.2 PEAR Package Naming Specification

The relationship between class path and class name in PEAR package is very simple.

And it's much more simpler in PhalApi. We do not consider namespace yet, so let's ignore namespace. Assume we have these classes as below.

Api_User
Domain_User
Model_User

We should have these related class files.

.
|-- Api
|   `-- User.php
|-- Domain
|   `-- User.php
|-- Model
|   `-- User.php

Take another little more complicated class as an example. Class Api_Game_User_Equitment should be located at file ./Api/Game/User/Equitment.php.

NOTE: Class name and file path are case-sensitive.

10.3 Autoload By Mounting

How could we make PhalApi autoload these classes that we just create?

At default, PhalApi do not scan all the folders for performance reasons. It just scan framework and project root folder instead. When there are some new source ocde folders, we need to mount them into PhalApi. That means, we need to tell PhalApi where to find our new project members.

We can finish that easily by using PhalApi_Loader::addDirs().

DI()->loader->addDirs('Demo');

We just add folder Demo into PhalApi, where our project codes are. If there are serveral folders, we can pass an array instead of a string.

DI()->loader->addDirs(array('Demo', 'Demo1', 'Demo2'));

(1) Add Relative Folder Paths

Please keep in mind that we can only add relative folder paths that inside PhalApi. That is to say, any folders to be added should under path API_ROOT. On Linux, these three ways are equivalent.

// path: API_ROOT/Demo
DI()->loader->addDirs('Demo');

// path: API_ROOT/./Demo
DI()->loader->addDirs('./Demo');

// path: API_ROOT/Demo
DI()->loader->addDirs('/Demo');

If the folder is not under folder PhalApi, we can solve this problem by using command ls on Linux.

(2) Add Single Absolute File Path

We can add folder paths but also can add single file path by using PhalApi_Loader::loadFile(). It's different between passing relative file path and absolute file path when load single file.

// path: API_ROOT/Demo/Tool.php
DI()->loader->loadFile('Demo/Tool.php');

// path: /path/to/Demo/Tool.php
DI()->loader->loadFile('/path/to/Demo/Tool.php');

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


11-Cache | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

11.1 The Importance of Cache

As we know, cache is important. And it' more important to know when and how to use cache properly in our application. Let's explore cache in deeop.

11.2 Simple Cache

Simple cache here means file cache in PhalApi. The advantage of file cache is simple and common, and its shortcoming is obvious, including heavy I/O, unabled to be used in distributed machines.

After register PhalApi_Cache_File into DI, we can use cache as we want.

//$ vim ./Public/init.php
DI()->cache = new PhalApi_Cache_File(array('path' => API_ROOT . '/Runtime', 'prefix' => 'demo'));

// set a cache
DI()->cache->set('thisYear', 2015, 600);

// get the cache
echo DI()->cache->get('thisYear');

// delete the cache
DI()->cache->delete('thisYear');

11.3 NoSQL Cache

NoSQl is more and more popular in these years, such as Memcache/Memcached, Redis. They are free with high-performance, and stored in distributed memory.

Here is an example of Memcached.

DI()->cache = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211, 'prefix' => 'demo_'));

// set a cache
DI()->cache->set('thisYear', 2015, 600);

// get the cache
echo DI()->cache->get('thisYear');

// delete the cache
DI()->cache->delete('thisYear');

Please note that we use prefix demo_ in cace of cache key conflict in the same memcached instance.

11.4 Multiple Cache

In more complicate business situation, we need more powerful cache strategy. That's to say, we need multiple cache, not just only single cache.

An interesting problem arises here: how can we organize multiple cache in an elegant way?

For example, how can we use the file cache and memcached cache above together, without changing any client usage?

Let's introduce PhalApi_Cache_Multi. With the help of PhalApi_Cache_Multi, we can re-register DI()->cache as below.

$cache = PhalApi_Cache_Multi();

$mcCache = new PhalApi_Cache_Memcached(array('host' => '127.0.0.1', 'port' => 11211, 'prefix' => 'demo_'));
$cache->addCache($mcCache);

$fileCache = new PhalApi_Cache_File(array('path' => API_ROOT . '/Runtime', 'prefix' => 'demo'));
$cache->addCache($fileCache);

DI()->cache = $cache;

Then the clients who use DI()->cache do not change anything to keep everything working well.

// set a cache
DI()->cache->set('thisYear', 2015, 600);

// get the cache
echo DI()->cache->get('thisYear');

// delete the cache
DI()->cache->delete('thisYear');

11.5 UML Static Structure

We use some design patterns here. The main pattern is Composite Pattern.

The inheritance structure is very clear. On the left side is multiple cache, in the middle is special cache i.e. None-Object pattern, while on the right side is implements cache.

11.6 Extend Your Cache

When need to extend an new cache, you just have to implement interface PhalApi_Cache.

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


12-Cache | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


12-Gateway | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

12.1 What Is Gateway in PhalApi ?

In general, we wrap our API services and only provide some entrances, which connect internal APIs and the outside world. According to different strategy, clients can visit the APIs by RESTfule, PHPRPC, HTTP, or SOAP. What's more, you can custom the format of response, specify current language, publish accessable APIs, etc. Such entrances are what we call gateway in PhalApi.

Some frameworks provide a solid initialization process, which limits developers too much to custom anything else. Instead PhalApi prefer to provide a open initialization process where developers have more freedom to satisfy their business need.

When clients request an API, they will go through open entrance and closed initialization.

12.2 Open Entrance

As an API server system, it need to provide different services for different client terminals and even different user groups usually.

For example,

  • According to different terminals: iOS, Android, PC, Web, Watch
  • According to different user groups: internal admin system, open service platform, A/B Test
  • According to different version: 1.1.1, 1.1.2, ..., 1.1.N
  • According to different projects: maybe there are several projects in the same system

Therefore, is's really a good idea to have multiple access protal.

The most important thing is that, these entrances should keep KISS, and reuse uniform closed initialization as much as possible. Here is such an simple entrance.

<?php
// $vim ./Public/demo/index.php

/**
 * Demo entrance
 */

require_once dirname(__FILE__) . '/../init.php';

// load your API folder
DI()->loader->addDirs('Demo');

/** ---------------- deal with API request ---------------- **/

$api = new PhalApi();
$rs = $api->response();
$rs->output();

What happen in the above entrance is mainly three things as below.

  • Firstly, require uniform initialization file init.php.
  • Secondly, add API project folder Demo.
  • At last, create an instance of PhalApi and reponse to client.

Let's keep going and see what exactly happen in closed initialization.

12.3 Closed Initialization

As we know, even though we provide different entrance for different demension, it's necessary to maintance an uniform initialzation process.

(1) Regular Initialization

The uniform initialization file init.php looks like as before.

/** ---------------- define API project root folder, and register autoload ---------------- **/

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT, 'Library');

/** ---------------- register & initialize base service components ---------------- **/

// autoload
DI()->loader = $loader;

// configuration
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

// debug mode, rename $_GET['__debug__'] as you want
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

if (DI()->debug) {
    error_reporting(E_ALL);
    ini_set('display_errors', 'On');
}

// logs
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime', PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

// database operations based on NotORM, rename $_GET['__sql__']as you want
DI()->notorm = new PhalApi_DB_NotORM(DI()->config->get('dbs'), !empty($_GET['__sql__']));

// setting language, default is English
SL('en');

The codes above are a little long, as they finish most of the initialization work.

At the beginning of the initialization, we set default timezone with PHP original function. We also define a const variable API_ROOT, which is represent the root path of our project.

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

Then we introduce PhalApi by requiring file PhalApi.php and creating an instance of PhalApi_Loader. Please note that we load folder `Library at first. Library is very usefull and we talk about later.

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT, 'Library');

After that, everything begins to be very interesting and amazing. We have switched from original PHP development to fresh PhalApi development. But if we want to go futher more, we need to prepare some necessary weapons at first.

Autoload Service

// autoload
DI()->loader = $loader;

More detail is avaliable on Autoload.

Config Service

// configuration
DI()->config = new PhalApi_Config_File(API_ROOT . '/Config');

More detail is avaliable on Config.

Debug Mode

// debug mode, rename $_GET['__debug__'] as you want
DI()->debug = !empty($_GET['__debug__']) ? true : DI()->config->get('sys.debug');

If you want to enable debug mode in the whole project, you can set sys.debug to be true. Otherwise, if you just want to enable debug mode in one request, you can set $_GET['__debug__'] to be true.

Logger Service

// logs
DI()->logger = new PhalApi_Logger_File(API_ROOT . '/Runtime', PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

More detail is avaliable on Logger.

Databse Service

// database operations based on NotORM, rename $_GET['__sql__']as you want
DI()->notorm = new PhalApi_DB_NotORM(DI()->config->get('dbs'), !empty($_GET['__sql__']));

More detail is avaliable on Logger.

Besides, we can set current language as below.

// setting language, default is English
SL('en');

More detail is avaliable on i18n.

(2) Custom Initialization

There is some comment custom service following regular initialization. This part is optional, not necessary.

/** ---------------- custom more optional service components ---------------- **/

/**
// signature verification servcie
DI()->filter = 'PhalApi_Filter_SimpleMD5';
 */

/**
// cache" Memcache/Memcached
DI()->cache = function () {
    return new PhalApi_Cache_Memcache(DI()->config->get('sys.mc'));
};
 */

/**
// support with JsonP reponse
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}
 */

If you want verfify whether the request is legal, you can use DI()->filer service.

// signature verification servcie
DI()->filter = 'PhalApi_Filter_SimpleMD5';

More detail is avaliable on 4.7 API signature with filter.

If you need a cache service, you can uncomment these lines.

// cache" Memcache/Memcached
DI()->cache = function () {
    return new PhalApi_Cache_Memcache(DI()->config->get('sys.mc'));
};

More detail is avaliable on Cache.

If you want to response with JSONP, you should uncomment these lines.

// support with JsonP reponse
if (!empty($_GET['callback'])) {
    DI()->response = new PhalApi_Response_JsonP($_GET['callback']);
}

More detail is avaliable on 6.7 Response with other format, such as JSONP.

12.4 The Architecture

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


13-i18n | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

13.1 Language Setting

When we need to switch to other language for different clients, we can use function SL() to set special language.

SL('zh_cn');

We switch to Simple Chinese above, which is the folder name as bebow:

.
|-- en
|   `-- common.php
`-- zh_cn
    `-- common.php

Of course, we can set the language according to the parameter from clients. Take $_GET['lan'] for example.

SL($_GET['lan']);

13.2 Add Language Translation Message

After set the language, we need to add the related translation file into folder ./Language. That means, if we set language to en, we need translation file ./Language/en/common.php; if zh_cn, then ./Language/zh_cn/common.php.

The translation file is very simple. It is nothing but a mapping array. The keys are original content, while the value is translation.

Here is an example of Simple Chinese.

return array(
    'Hi {name}, welcome to use PhalApi!' => '{name}您好，欢迎使用PhalApi！',
    'user not exists' => '用户不存在',
);

13.3 How to Translate?

After set the language and prepare the translation file, we can translate content with fast function T() in the project.

(1) Simple Translation Without Any Variable

echo T('user not exists');

It will show:

用户不存在

(2) Translation With A Variable

Sometimes we have to display some dynamic data for different sutiation. Dynemic variable should be inside between with bracketing without any blank.

echo T('hello {name}', array('name' => 'dogstar'));

While the translation is:

'hello {name}' => '您好，{name}',

We will see:

您好，dogstar

(3) Translation With Multi Variables

If there are many variables, we can also use numeric index for convenience.

echo T('{0} I love you because {1}', array('PhalApi', 'no reasons'));

While the translation is:

'{0} I love you because {1}' => '{0} 我爱你因为{1}',

It will print:

PhalApi 我爱你因为no reasons

13.4 When Happen If No Translation?

It is straightforward that when there is no translation, it will output the original content.

13.5 Extend Your Translation

In addtion, if translation files are not under folder ./Language but other fodler else, we can add the new folder by PhalApi_Translator::addMessage().

Take library User for example,

PhalApi_Translator::addMessage(API_ROOT . '/Library/User');

And under folder /path/to/Library/User, it should be like this:

./Language/
└── zh_cn
    └── common.php

1 directory, 1 file

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


14-Gateway | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


_Footer | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

Feel free to help us to correct this page. Welcome to post any advises on Issues.

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


Home | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

Contents

  • 1 Create your project
  • 2 Tutorial
  • 3 Use PhalApi or not
  • 4 API Request
  • 5 Parameter Rule
  • 6 API Response
  • 7 Database Based NotORM
  • 8 Config
  • 9 Logger
  • 10 Autoload
  • 11 Cache
  • 12 Gateway
  • 13 i18n

中文版文档，请访问：官方文档

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


index | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

Contents

  • 1 Create your project
  • 2 Tutorial
  • 3 Use PhalApi or not
  • 4 API Request
  • 5 Parameter Rule
  • 6 API Response
  • 7 Database Based NotORM
  • 8 Config
  • 9 Logger
  • 10 Autoload
  • 11 Cache
  • 12 Gateway
  • 13 i18n

中文版文档，请访问：官方文档

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More


README | PhalApi - A light-weight PHP framework for API

 Fork me on GitHub
  • Chinese
  • English
  • Welcome Here!
  • Community
  • Download
  • Wikis
  • Classes docs
  • Demo
  • About us

PhalApi - A light-weight PHP framework for API - V1.3.2

PhalApi

Welcome to use PhalApi!

PhalApi is a light-weight framework focus on how to develop API faster and simple. If you have any questions, please feel free to contact us with email(chanzonghuang@gmail.com), or create an issue (https://github.com/phalapi/phalapi/issues)

You can use PhalApi to develop APIs very fastly and easily for Android/iOS/Windowns/Website/H5, but also build microservice/web services/RESTful for big data. Besides, you will explore Agile development, TDD, auto-generation etc. on the way to emergent design.

There is more than I can say. Just rock it!

PhalApi is created from China since 2015, and we will try out best to make it clear how to use it smootly to everyone.

Install & Usage

  • download lastest stable version from release branch
  • PhalApi need PHP >= 5.3.3

After unpack PhalApi on your server, you can visit:

http://localhost/path/to/PhalApi/Public/demo/

// output
{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1422779027
    },
    "msg": ""
}

Demo online

//default api
http://demo.phalapi.net/

//api with params
http://demo.phalapi.net/?service=Default.Index&username=oschina

//illegal api
http://demo.phalapi.net/?service=Demo.None
{
    "ret": 400,
    "data": [],
    "msg": "非法请求：服务Demo.None不存在"
}

[COOL!] Auto-generated API documents online

Here is a demo,

http://demo.phalapi.net/demo/checkApiParams.php

[TOP!] SDK base on DSL

We can request api like:

PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withService("Default.Index")          //service
       .withParams("username", "dogstar")     //params
       .withTimeout(3000)                     //timeout
       .request();

Curently, we provide:

  • JAVA SDK
  • Objective-c SDK
  • PHP SDK
  • C# SDK
  • JS SDK

Structure

.
│
├── PhalApi         // PhalApi framework, you can upgrade it in the furture
│
│
├── Public          // public visit
│   └── demo        // Demo entrance
│
│
├── Config          // common configs include app.php/sys.php/dbs.php, ect
├── Data            // common data
├── Language        // common translation
├── Runtime         // runtime log
│
│
└── Demo            // Demo
    ├── Api             // Api
    ├── Domain          // Domain
    ├── Model           // Model
    └── Tests           // PHPUnit tests

Join us!

Welcome to join us to do something cool!

Changelog

changelog

PhalApi is a light-weight PHP framework for API.
We try to keep PhalApi as stuip simple as possible!

©2015-2016 PhalApi All Rights Reserved. 粤ICP备15028808号

Thanks!

  • Open Source China
  • Phalcon
  • PHPUnit
  • ThoughtWorks
Alipay Donate: chanzonghuang@gmail.com

Share: Weibo More