PhalApi(π框架)是一个 PHP 轻量级开源接口框架,专注于接口服务开发。支持 HTTP/SOAP/RPC 协议,采用 ADM(Api-Domain-Model)分层架构,自动生成在线接口文档。
| 层级 | 职责 | 不应该做 |
|---|---|---|
| ------ | ------ | ---------- |
| Api层 | 接收请求、参数验证、调度 Domain 层、返回结果 | 直接操作数据库、业务规则处理 |
| Domain层 | 业务规则处理、数据逻辑、调用 Model 层 | 直接数据库操作、实现缓存 |
| Model层 | 数据库操作、缓存实现 | 业务规则处理 |
Api层 → Domain层 → Model层
严格禁止:Api 直接调用 Model、Domain 调用 Api、Model 调用 Domain
composer create-project phalapi/phalapi
./phalapi
├── public/ # 网站根目录
│ ├── index.php # 入口
│ ├── init.php # 初始化
│ └── docs.php # 离线文档生成
├── src/app/ # 源代码
│ ├── Api/ # 接口层
│ ├── Domain/ # 领域层
│ ├── Model/ # 模型层
│ └── Common/ # 公共类
├── config/ # 配置
│ ├── app.php # 应用配置
│ ├── dbs.php # 数据库配置
│ ├── di.php # DI服务
│ └── sys.php # 系统配置
└── runtime/ # 运行目录(需写权限)
http://dev.phalapi.net/?s=App.Site.Index
文件:./src/app/Api/User.php
<?php
namespace App\Api;
use PhalApi\Api;
class User extends Api {
public function getRules() {
return array(
'login' => array(
'username' => array('name' => 'username', 'require' => true),
'password' => array('name' => 'password', 'require' => true, 'min' => 6),
),
);
}
public function login() {
$name = $this->username;
$pass = $this->password;
return array('username' => $name);
}
}
{
"ret": 200,
"data": { "key": "value" },
"msg": ""
}
throw new \PhalApi\Exception\BadRequestException('签名失败', 401);
{
"ret": 401,
"data": {},
"msg": "Bad Request: 签名失败"
}
// ./src/app/Common/AppException.php
namespace App\Common;
use PhalApi\Exception;
class AppException extends Exception {}
// 抛出
throw new AppException('提示消息', 1000);
// config/di.php
$di->response = new \PhalApi\Response\JsonpResponse($_GET['callback']);
$di->response = new \PhalApi\Response\XmlResponse();
config/app.php 的 apiCommonRules 配置)| 类型 | 配置 | 说明 |
|---|---|---|
| ------ | ------ | ------ |
| string | type => 'string' | 字符串,默认 |
| int | type => 'int' | 整数 |
| float | type => 'float' | 浮点数 |
| boolean | type => 'boolean' | 布尔值 |
| date | type => 'date', 'format' => 'timestamp' | 日期/时间戳 |
| array | type => 'array', 'format' => 'explode' | 数组 |
| enum | type => 'enum', 'range' => ['a', 'b'] | 枚举 |
| file | type => 'file' | 文件上传 |
| callable | type => 'callable', 'callback' => 'Class::method' | 回调验证 |
array(
'name' => 'username', // 参数名
'type' => 'string', // 类型
'require' => true, // 是否必须
'default' => 'PhalApi', // 默认值
'min' => 1, // 最小值/长度
'max' => 50, // 最大值/长度
'regex' => '/^[a-z]+$/', // 正则验证
'format' => 'utf8', // 格式(utf8/gbk)
'source' => 'post', // 数据来源
'desc' => '用户名', // 描述
'message' => '自定义错误提示', // 错误提示
)
文件:./src/app/Domain/User.php
<?php
namespace App\Domain;
use PhalApi\Domain_Base;
class User extends Domain_Base {
public function login($name, $pass) {
$model = new \App\Model\User();
return $model->getByName($name);
}
}
文件:./src/app/Model/User.php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class User extends NotORM {
protected function getTableName($id) {
return 'user'; // 自动映射为 tbl_user
}
public function getByName($name) {
return $this->getORM()->where('name', $name)->fetchOne();
}
}
// 获取单条
$this->getORM()->where('id', 1)->fetchOne();
// 获取多条
$this->getORM()->where('id', array(1, 2, 3))->fetchAll();
// 条件
$this->getORM()->where('name LIKE ?', '%test%')->fetchAll();
$this->getORM()->where('age > ?', 18)->order('age DESC')->fetchAll();
// 分页
$this->getORM()->page(1, 20)->fetchAll(); // 第1页,每页20条
// 原生SQL
$this->getORM()->queryAll('SELECT * FROM user WHERE id > ?', [$id]);
$data = array('name' => 'test', 'age' => 20);
$id = $this->getORM()->insert($data);
// 批量插入
$this->getORM()->insert_multi($rows);
// 插入或更新
$this->getORM()->insert_update($unique, $insert, $update);
$this->getORM()->where('id', 1)->update($data);
// 计数器
$this->getORM()->where('id', 1)->updateCounter('age', 1); // age + 1
$this->getORM()->where('id', 1)->delete();
// 禁止全表删除!
protected function getTableName($id) {
$tableName = 'log';
if ($id !== null) {
$tableName .= '_' . ($id % 100); // log_0, log_1, ...
}
return $tableName;
}
return array(
'servers' => array(
'db_master' => array(
'type' => 'mysql',
'host' => '127.0.0.1',
'name' => 'phalapi',
'user' => 'root',
'password' => '',
'port' => 3306,
'charset' => 'UTF8',
),
),
'tables' => array(
'__default__' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(array('db' => 'db_master')),
),
),
);
$user = \PhalApi\DI()->notorm->user->where('id', 1)->fetchOne();
$di->cache = new \PhalApi\Cache\FileCache(array(
'path' => API_ROOT . '/runtime',
'prefix' => 'demo',
));
\PhalApi\DI()->cache->set('key', $value, 600);
\PhalApi\DI()->cache->get('key');
\PhalApi\DI()->cache->delete('key');
$di->cache = new \PhalApi\Cache\RedisCache(array(
'host' => '127.0.0.1',
'port' => 6379,
));
$di->cache = new \PhalApi\Cache\MemcachedCache(array(
'host' => '127.0.0.1',
'port' => 11211,
));
\PhalApi\DI()->logger->error('错误描述', $context);
\PhalApi\DI()->logger->info('业务记录', $context);
\PhalApi\DI()->logger->debug('调试信息', $context);
$di->logger = new \PhalApi\Logger\FileLogger(
API_ROOT . '/runtime',
\PhalApi\Logger::LOG_LEVEL_DEBUG | \PhalApi\Logger::LOG_LEVEL_INFO | \PhalApi\Logger::LOG_LEVEL_ERROR
);
// config/di.php
$di->filter = new \PhalApi\Filter\SimpleMD5Filter();
// config/app.php
'service_whitelist' => array(
'Site.Index', // 精确匹配
'*.List', // 通配符匹配
),
// ./src/app/Common/SignFilter.php
namespace App\Common;
use PhalApi\Filter;
use PhalApi\Exception\BadRequestException;
class SignFilter implements Filter {
public function check() {
$signature = \PhalApi\DI()->request->get('signature');
// 验签逻辑
if ($signature !== $expected) {
throw new BadRequestException('wrong sign', 402);
}
}
}
// config/di.php
$di->filter = new \App\Common\SignFilter();
$di = \PhalApi\DI();
// 配置
$di->config = new \PhalApi\Config\FileConfig(API_ROOT . '/config');
// 数据库
$di->notorm = new \PhalApi\Database\NotORMDatabase($di->config->get('dbs'));
// 缓存
$di->cache = new \PhalApi\Cache\RedisCache([...]);
// 日志
$di->logger = new \PhalApi\Logger\FileLogger([...]);
// 签名
$di->filter = new \PhalApi\Filter\SimpleMD5Filter();
| 服务 | 说明 |
|---|---|
| ------ | ------ |
$di->config | 配置读取 |
$di->request | 请求参数 |
$di->response | 响应输出 |
$di->notorm | 数据库 |
$di->cache | 缓存 |
$di->logger | 日志 |
$di->filter | 过滤器 |
\PhalApi\DI()->config->get('app'); // 全部配置
\PhalApi\DI()->config->get('app.version'); // 单个配置
\PhalApi\DI()->config->get('app.not_found', 'default'); // 默认值
defined('API_MODE') || define('API_MODE', 'prod');
// dev: 加载 *_dev.php
// test: 加载 *_test.php
// prod: 加载 *.php
defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');
defined('API_MODE') || define('API_MODE', 'prod');
require_once API_ROOT . '/vendor/autoload.php';
date_default_timezone_set('Asia/Shanghai');
include API_ROOT . '/config/di.php';
\PhalApi\SL('zh_cn');
| 命名空间 | 文件路径 |
|---|---|
| ---------- | ---------- |
App\Api\User | src/app/Api/User.php |
App\Domain\User | src/app/Domain/User.php |
App\Model\User | src/app/Model/User.php |
use App\Domain\User as DomainUser;
$domain = new DomainUser();
// 或完整路径
$domain = new \App\Domain\User();
// src/app/functions.php
namespace App;
function hello() { return 'world'; }
// 使用
\App\hello();
http://dev.phalapi.net/docs.phphttp://dev.phalapi.net/docs.php?service=App.Site.Index&detail=1/**
* 用户登录
* @desc 用于用户登录验证
* @return int user_id 用户ID
* @return string token 登录令牌
* @exception 400 参数错误
* @exception 401 签名错误
*/
public function login() { }
// 隐藏接口
/**
* @ignore
*/
public function hidden() { }
php ./public/docs.php expand # 展开版
php ./public/docs.php fold # 折叠版
php ./bin/phalapi-buildtest ./src/app/Api/User.php App\\Api\\User > ./tests/app/Api/User_Test.php
class PhpUnderControl_AppApiUser_Test extends \PHPUnit_Framework_TestCase
{
public function testLogin() {
// 构造
$url = 's=User.Login';
$params = array('username' => 'test', 'password' => '123456');
// 操作
$rs = \PhalApi\Helper\TestRunner::go($url, $params);
// 检验
$this->assertEquals(0, $rs['code']);
}
}
$curl = new \PhalApi\CUrl();
// GET
$rs = $curl->get('http://api.example.com/?id=1', 3000);
// POST
$rs = $curl->post('http://api.example.com/', array('name' => 'test'), 3000);
常用扩展(通过 composer 安装):
{
"require": {
"phalapi/redis": "2.*",
"phalapi/jwt": "2.*",
"phalapi/pay": "2.*"
}
}
// 全局指定
$di->request = new \PhalApi\Request($_POST);
// 参数级别
'source' => 'get' // $_GET
'source' => 'post' // $_POST
'source' => 'header' // $_SERVER['HTTP_X']
// 方式1:全局
\PhalApi\DI()->notorm->beginTransaction('db_master');
\PhalApi\DI()->notorm->user->insert($data);
\PhalApi\DI()->notorm->commit('db_master');
// 方式2:局部
$this->getORM()->transaction('BEGIN');
$this->getORM()->transaction('COMMIT');
$this->getORM()->transaction('ROLLBACK');
// 基础查询
$this->getORM()->select('id, name')->fetchAll();
$this->getORM()->select('DISTINCT name')->fetchAll();
$this->getORM()->select('id, name, MAX(age) AS max_age')->fetchAll();
// 指定字段
$this->getORM()->select('id, name, email')->where('status', 1)->fetchAll();
// 等值条件
$this->getORM()->where('id', 1)->fetchOne();
$this->getORM()->where('id = ?', 1)->fetchOne();
// 多条件 AND(链式调用)
$this->getORM()->where('status', 1)->where('age > ?', 18)->fetchAll();
// OR 条件
$this->getORM()->where('status = ? OR type = ?', 1, 2)->fetchAll();
$this->getORM()->where('status', 1)->or('type', 2)->fetchAll();
// 嵌套条件
$this->getORM()->where('(name = ? OR id = ?)', 'test', 1)->fetchAll();
// IN 查询
$this->getORM()->where('id', array(1, 2, 3))->fetchAll();
// NOT IN
$this->getORM()->where('NOT id', array(1, 2, 3))->fetchAll();
// 模糊匹配
$this->getORM()->where('name LIKE ?', '%test%')->fetchAll();
// NULL 判断
$this->getORM()->where('name IS NULL')->fetchAll();
$this->getORM()->where('name IS NOT NULL')->fetchAll();
// 单字段排序
$this->getORM()->order('age DESC')->fetchAll();
// 多字段排序
$this->getORM()->order('id, age DESC')->fetchAll();
// 限制条数
$this->getORM()->limit(10)->fetchAll();
// 偏移量(跳过5条,取10条)
$this->getORM()->limit(10, 5)->fetchAll();
// 分页(2.8.0+),第2页每页10条
$this->getORM()->page(2, 10)->fetchAll();
// 分组统计
$this->getORM()->select('note, COUNT(*) AS count')->group('note')->fetchAll();
// 分组 + HAVING
$this->getORM()->select('note, COUNT(*) AS count')->group('note', 'age > 10')->fetchAll();
// fetch() - 循环获取单条
$row = $this->getORM()->where('status', 1)->fetch();
// fetchOne() / fetchRow() - 获取单条(推荐)
$row = $this->getORM()->where('id', 1)->fetchOne();
// fetchAll() / fetchRows() - 获取全部
$rows = $this->getORM()->where('status', 1)->fetchAll();
// fetchPairs() - 获取键值对
$pairs = $this->getORM()->fetchPairs('id', 'name');
// 结果:array(1 => '张三', 2 => '李四')
// queryAll() / queryRows() - 原生SQL查询
$rows = $this->getORM()->queryAll('SELECT * FROM user WHERE id > ?', array($id));
// 计数
$count = $this->getORM()->where('status', 1)->count();
// 最小值
$min = $this->getORM()->min('age');
// 最大值
$max = $this->getORM()->max('age');
// 求和
$sum = $this->getORM()->sum('score');
// 综合使用
$this->getORM()->select('COUNT(*) AS total, MAX(age) AS max_age, SUM(score) AS total_score')
->where('status', 1)->fetchOne();
// 单条插入
$id = $this->getORM()->insert(array(
'name' => '张三',
'email' => 'zhangsan@example.com',
'created_at' => time(),
));
// 获取自增ID
$id = $this->getORM()->insert_id();
// 批量插入
$rows = array(
array('name' => '张三', 'email' => 'zhangsan@example.com'),
array('name' => '李四', 'email' => 'lisi@example.com'),
);
$this->getORM()->insert_multi($rows);
// 插入或更新(有则更新,无则插入)
$this->getORM()->insert_update(
array('name' => '张三'), // 唯一键条件
array('name' => '张三', 'age' => 25), // 插入数据
array('age' => 26) // 更新数据(已存在时)
);
// 条件更新
$affected = $this->getORM()->where('id', 1)->update(array(
'name' => '新名字',
'updated_at' => time(),
));
// 返回值说明:
// int(1) - 更新成功
// int(0) - 数据无变化
// false - 更新失败
// 计数器更新(2.6.0+)- age + 1
$this->getORM()->where('id', 1)->updateCounter('age', 1);
// 计数器递减 - age - 1
$this->getORM()->where('id', 1)->updateCounter('age', -1);
// 多计数器同时更新(2.6.0+)
$this->getORM()->where('id', 1)->updateMultiCounters(array(
'age' => 1,
'points' => 10,
));
// 条件删除
$affected = $this->getORM()->where('id', 1)->delete();
// 禁止全表删除!以下写法会报错
// $this->getORM()->delete();
// 批量删除
$this->getORM()->where('id', array(1, 2, 3))->delete();
// 条件删除
$this->getORM()->where('status = ? AND created_at < ?', 0, time() - 86400)->delete();
// queryAll - 查询类SQL,返回结果集
$rows = $this->getORM()->queryAll('SELECT * FROM user WHERE id > ?', array($id));
// executeSql - 写入类SQL(2.6.0+),返回影响行数
$affected = $this->getORM()->executeSql('UPDATE user SET status = ? WHERE id = ?', array(0, 1));
// query - 最底层的查询方法
$result = $this->getORM()->query('SELECT * FROM user WHERE id = ?', array($id));
\PhalApi\DI()->notorm->beginTransaction('db_master');
try {
\PhalApi\DI()->notorm->user->insert(array('name' => 'test'));
\PhalApi\DI()->notorm->order->insert(array('user_id' => 1));
\PhalApi\DI()->notorm->commit('db_master');
} catch (\Exception $e) {
\PhalApi\DI()->notorm->rollback('db_master');
throw $e;
}
$this->getORM()->transaction('BEGIN');
try {
$this->getORM()->insert(array('name' => 'test'));
$this->getORM()->where('id', 1)->update(array('status' => 1));
$this->getORM()->transaction('COMMIT');
} catch (\Exception $e) {
$this->getORM()->transaction('ROLLBACK');
throw $e;
}
$this->getORM() 获取新实例'db_master')$rows = $this->getORM()
->alias('u') // 主表别名
->select('u.id, u.name, p.avatar') // 选择字段
->leftJoin('profile', 'p', 'u.id = p.user_id') // 左连接
->where('u.status', 1)
->order('u.id DESC')
->fetchAll();
// 前提:表间有外键关联
$this->getORM()->select('user.username, comment.content')->fetchAll();
在 config/dbs.php 的 tables 中配置分表策略:
'tables' => array(
'__default__' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_master'),
),
),
// 分表示例:log 表分成3个表
'log' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_master', 'start' => 0, 'end' => 2),
// 对应 tbl_log_0, tbl_log_1, tbl_log_2
),
),
),
class Log extends NotORM {
protected function getTableName($id) {
$tableName = 'log';
if ($id !== null) {
$tableName .= '_' . ($id % 100); // log_0, log_1, ..., log_99
}
return $tableName;
}
// 使用时传入 $id 获取分表实例
public function getByLogId($id) {
return $this->getORM($id)->where('id', $id)->fetchOne();
}
}
'servers' => array(
'db_master' => array( // 主数据库
'type' => 'mysql',
'host' => '192.168.1.100',
'name' => 'phalapi_main',
'user' => 'root',
'password' => '',
'port' => 3306,
'charset' => 'UTF8',
),
'db_ext' => array( // 扩展数据库
'type' => 'mysql',
'host' => '192.168.1.200',
'name' => 'phalapi_ext',
'user' => 'root',
'password' => '',
'port' => 3306,
'charset' => 'UTF8',
),
),
'tables' => array(
'__default__' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_master'),
),
),
'order' => array( // order 表使用扩展库
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_ext'),
),
),
),
php ./bin/phalapi-buildsqls
当表名保留下划线+数字后缀时:
'tables' => array(
'log' => array(
'prefix' => 'tbl_',
'key' => 'id',
'keep_suffix_if_no_map' => true, // 保留后缀
'map' => array(
array('db' => 'db_master'),
),
),
),
在 config/dbs.php 中配置多个 servers,通过 tables 的 map 路由到不同数据库。
步骤 1:创建配置文件 config/dbs_ms.php
return array(
'servers' => array(
'db_ms' => array(
'type' => 'sqlserver', // dblib_sqlserver
'host' => '192.168.1.100',
'name' => 'phalapi_ms',
'user' => 'sa',
'password' => '',
'port' => 1433,
'charset' => 'UTF8',
),
),
'tables' => array(
'__default__' => array(
'prefix' => '',
'key' => 'id',
'map' => array(
array('db' => 'db_ms'),
),
),
),
);
步骤 2:创建 Model 基类
// ./src/app/Common/MSModelBase.php
namespace App\Common;
use PhalApi\Model\NotORMModel as NotORM;
class MSModelBase extends NotORM {
protected function getNotORM() {
return \PhalApi\DI()->notorm_ms;
}
}
步骤 3:在 config/di.php 中注册
// 注册 SQL Server 的 NotORM 实例
$di->notorm_ms = new \PhalApi\Database\NotORMDatabase(
$di->config->get('dbs_ms')
);
步骤 4:Model 继承 MSModelBase
namespace App\Model;
use App\Common\MSModelBase;
class MSUser extends MSModelBase {
protected function getTableName($id) {
return 'ms_user';
}
}
| 类型 | type 配置 | 说明 |
|---|---|---|
| ------ | ---------- | ------ |
| MySQL | mysql | 默认,最常用 |
| SQL Server | sqlserver / dblib_sqlserver | 需安装 dblib 扩展 |
| PostgreSQL | pgsql | 需安装 pgsql 扩展 |
class ExtModel extends NotORM {
protected function getNotORM() {
// 重写此方法切换到其他数据库实例
return \PhalApi\DI()->notorm_ext;
}
}
| 配置项 | 位置 | 说明 |
|---|---|---|
| -------- | ------ | ------ |
sys.debug | config/sys.php | 接口调试模式 |
sys.notorm_debug | config/sys.php | NotORM 调试模式 |
sys.enable_sql_log | config/sys.php | SQL 日志记录 |
// config/sys.php
return array(
'debug' => true, // 开启接口调试
'notorm_debug' => true, // 开启 NotORM 调试
'enable_sql_log' => true, // 开启 SQL 日志
);
开启 sys.debug 后,返回结果中会包含 debug.sqls 字段,列出所有执行的 SQL 语句。
开启 sys.enable_sql_log 后,SQL 语句会写入日志文件:
# 实时查看 SQL 日志
tail -f ./runtime/log/20240101.log
// ./src/app/Common/SqlTracer.php
namespace App\Common;
use PhalApi\Helper\Tracer;
class SqlTracer extends Tracer {
public function sql($sql, $params = array()) {
// 自定义追踪逻辑
\PhalApi\DI()->logger->info('SQL: ' . $sql, $params);
}
}
// 注册
$di->tracer = new \App\Common\SqlTracer();
// config/di.php
$di->cache = new \PhalApi\Cache\FileCache(array(
'path' => API_ROOT . '/runtime',
'prefix' => 'demo',
));
// 使用
\PhalApi\DI()->cache->set('key', $value, 600); // 设置缓存(600秒=10分钟)
$value = \PhalApi\DI()->cache->get('key'); // 获取缓存
\PhalApi\DI()->cache->delete('key'); // 删除缓存
// config/di.php
$di->cache = new \PhalApi\Cache\APCUCache();
// 使用(接口同上)
\PhalApi\DI()->cache->set('key', $value, 600);
$value = \PhalApi\DI()->cache->get('key');
\PhalApi\DI()->cache->delete('key');
// config/di.php
$di->cache = new \PhalApi\Cache\MemcachedCache(array(
'host' => '127.0.0.1',
'port' => 11211,
));
// 多实例配置(用逗号分隔)
$di->cache = new \PhalApi\Cache\MemcachedCache(array(
'host' => '192.168.1.1,192.168.1.2', // 多台Memcached
'port' => '11211,11211',
));
// config/di.php
$di->cache = new \PhalApi\Cache\RedisCache(array(
'type' => 'redis',
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 300,
'prefix' => 'phalapi_',
'auth' => 'your_password',
'db' => 0,
));
// Socket 方式连接
$di->cache = new \PhalApi\Cache\RedisCache(array(
'type' => 'unix',
'socket' => '/var/run/redis/redis.sock',
));
| 驱动 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| ------ | ---------- | ------ | ------ |
| FileCache | 开发环境、小数据量 | 无需额外服务 | 性能低 |
| APCUCache | 单机、高速缓存 | 极快 | 不跨进程共享 |
| MemcachedCache | 多进程共享、中等数据量 | 分布式、成熟 | 无持久化 |
| RedisCache | 高性能、大数据量、分布式 | 持久化、丰富数据结构 | 需维护 Redis |
// Domain 层中的缓存使用模式
class User {
public function getUserInfo($userId) {
$key = 'user_info_' . $userId;
// 先查缓存
$data = \PhalApi\DI()->cache->get($key);
if ($data !== null) {
return $data;
}
// 缓存未命中,查询数据库
$model = new \App\Model\User();
$data = $model->get($userId);
if (!empty($data)) {
// 写入缓存,1小时过期
\PhalApi\DI()->cache->set($key, $data, 3600);
}
return $data;
}
}
| 方法 | 用途 | 示例场景 |
|---|---|---|
| ------ | ------ | ---------- |
error() | 系统异常 | 数据库连接失败、外部服务异常 |
info() | 业务记录 | 用户登录、订单创建 |
debug() | 开发调试 | 变量值、执行流程 |
\PhalApi\DI()->logger->error('数据库连接失败', array('host' => $host));
\PhalApi\DI()->logger->info('用户登录', array('user_id' => $userId, 'ip' => $ip));
\PhalApi\DI()->logger->debug('查询参数', array('where' => $where, 'bind' => $bind));
// 使用 log() 方法记录自定义类型
\PhalApi\DI()->logger->log('access', '接口访问', array('url' => $url, 'time' => time()));
// 方式一:直接创建
$di->logger = new \PhalApi\Logger\FileLogger(
API_ROOT . '/runtime',
\PhalApi\Logger::LOG_LEVEL_DEBUG | \PhalApi\Logger::LOG_LEVEL_INFO | \PhalApi\Logger::LOG_LEVEL_ERROR
);
// 方式二:使用 create 工厂方法(2.7.0+)
$di->logger = \PhalApi\Logger\FileLogger::create(array(
'log_path' => API_ROOT . '/runtime',
'log_level' => \PhalApi\Logger::LOG_LEVEL_DEBUG | \PhalApi\Logger::LOG_LEVEL_INFO | \PhalApi\Logger::LOG_LEVEL_ERROR,
));
// 方式三:配置文件 + 工厂方法
// config/sys.php
return array(
'log_path' => API_ROOT . '/runtime',
'log_level' => \PhalApi\Logger::LOG_LEVEL_DEBUG | \PhalApi\Logger::LOG_LEVEL_INFO | \PhalApi\Logger::LOG_LEVEL_ERROR,
);
// config/di.php
$di->logger = \PhalApi\Logger\FileLogger::create($di->config->get('sys'));
当不同业务模块需要独立的日志文件时:
// config/sys.php
return array(
'log_path' => API_ROOT . '/runtime',
'log_level' => \PhalApi\Logger::LOG_LEVEL_ALL,
'file_logger_app' => array(
'log_path' => API_ROOT . '/runtime/app',
'log_level' => \PhalApi\Logger::LOG_LEVEL_INFO,
),
'file_logger_pay' => array(
'log_path' => API_ROOT . '/runtime/pay',
'log_level' => \PhalApi\Logger::LOG_LEVEL_ALL,
),
);
// config/di.php
$di->logger_app = \PhalApi\Logger\FileLogger::create($di->config->get('sys.file_logger_app'));
$di->logger_pay = \PhalApi\Logger\FileLogger::create($di->config->get('sys.file_logger_pay'));
// 使用
\PhalApi\DI()->logger_app->info('应用日志', $data);
\PhalApi\DI()->logger_pay->error('支付错误', $errorData);
签名计算规则:
sign 参数key1=value1&key2=value2&... 格式// 启用 MD5 签名验证
// config/di.php
$di->filter = new \PhalApi\Filter\SimpleMD5Filter();
免签名验证的接口列表:
// config/app.php
'service_whitelist' => array(
'Site.Index', // 精确匹配
'User.GetBaseInfo', // 精确匹配
'*.List', // 通配符匹配(所有 List 结尾的接口)
'Site.*', // 通配符匹配(Site 下所有接口)
),
// ./src/app/Common/SignFilter.php
namespace App\Common;
use PhalApi\Filter;
use PhalApi\Exception\BadRequestException;
class SignFilter implements Filter {
public function check() {
$signature = \PhalApi\DI()->request->get('signature');
$timestamp = \PhalApi\DI()->request->get('timestamp');
// 验证时间戳(5分钟内有效)
if (abs(time() - $timestamp) > 300) {
throw new BadRequestException('请求过期', 401);
}
// 验证签名
$params = \PhalApi\DI()->request->getAll();
unset($params['signature']);
ksort($params);
$signStr = http_build_query($params) . '&key=your_secret_key';
$expectedSign = md5($signStr);
if ($signature !== $expectedSign) {
throw new BadRequestException('签名验证失败', 402);
}
}
}
// config/di.php
$di->filter = new \App\Common\SignFilter();
DataApi 提供5个通用数据操作接口,无需为每张表编写单独的接口。
| 接口名 | 功能 | 说明 |
|---|---|---|
| -------- | ------ | ------ |
CreateData | 创建数据 | 新增记录 |
DeleteDataIDs | 删除数据 | 根据ID批量删除 |
GetData | 获取数据 | 单条/多条查询 |
TableList | 表列表 | 分页查询表数据 |
UpdateData | 更新数据 | 根据ID更新 |
步骤 1:创建 Api 类继承 DataApi
// ./src/app/Api/UserData.php
namespace App\Api;
use PhalApi\DataApi;
class UserData extends DataApi {
protected function userCheck() {
// 权限验证(必须重写)
// 返回 true 或抛出异常
$token = \PhalApi\DI()->request->get('token');
if (empty($token)) {
throw new \PhalApi\Exception\BadRequestException('缺少token');
}
return true;
}
protected function getDataModel() {
// 返回对应的 Model 实例(必须重写)
return new \App\Model\UserDataModel();
}
}
步骤 2:创建 Model 继承 DataModel
// ./src/app/Model/UserDataModel.php
namespace App\Model;
use PhalApi\Model\DataModel;
class UserDataModel extends DataModel {
protected function getTableName($id) {
return 'user';
}
}
class UserData extends DataApi {
// 定制表列表查询
protected function getTableListSelect() { return 'id, name, email'; }
protected function getTableListOrder() { return 'id DESC'; }
protected function getTableListWhere() { return array('status' => 1); }
// 定制创建数据
protected function createDataRequireKeys() { return array('name', 'email'); }
protected function createDataExcludeKeys() { return array('id', 'created_at'); }
// 定制更新数据
protected function updateDataExcludeKeys() { return array('created_at'); }
}
# 获取表列表(分页)
GET /?s=App.UserData.TableList&page=1&per_page=20
# 获取单条数据
GET /?s=App.UserData.GetData&id=1
# 创建数据
POST /?s=App.UserData.CreateData
name=张三&email=zhangsan@example.com
# 更新数据
POST /?s=App.UserData.UpdateData&id=1&name=新名字
# 删除数据
POST /?s=App.UserData.DeleteDataIDs&ids=1,2,3
// 设置 Cookie
\PhalApi\Cookie\FCookie::set('key', 'value', 3600);
// 获取 Cookie
$value = \PhalApi\Cookie\FCookie::get('key');
// 删除 Cookie
\PhalApi\Cookie\FCookie::delete('key');
// config/di.php
$di->cookie = new \PhalApi\Cookie\MultiCookie(array(
'crypt' => new \PhalApi\Crypt\McryptCrypt('your_secret_key'),
'path' => '/',
'domain' => '.example.com',
));
// 使用
\PhalApi\DI()->cookie->set('user_token', $token, 86400);
$token = \PhalApi\DI()->cookie->get('user_token');
\PhalApi\DI()->cookie->delete('user_token');
use PhalApi\Crypt\McryptCrypt;
$crypt = new McryptCrypt('your_secret_key');
// 加密
$encrypted = $crypt->encrypt('敏感数据', 'secret_key');
// 解密
$decrypted = $crypt->decrypt($encrypted, 'secret_key');
use PhalApi\Crypt\MultiMcryptCrypt;
$crypt = new MultiMcryptCrypt('your_secret_key');
// 加密(自动序列化+base64)
$encrypted = $crypt->encrypt(array('user_id' => 1, 'role' => 'admin'), 'secret_key');
// 解密(自动反序列化)
$data = $crypt->decrypt($encrypted, 'secret_key');
use PhalApi\Crypt\Rsa\MultiPri2PubCrypt;
// 私钥加密,公钥解密
$rsa = new MultiPri2PubCrypt();
// 加密(使用私钥)
$encrypted = $rsa->encrypt($data, $privateKey);
// 解密(使用公钥)
$decrypted = $rsa->decrypt($encrypted, $publicKey);
所有加密类实现 PhalApi\Crypt 接口:
interface Crypt {
public function encrypt($data, $key);
public function decrypt($data, $key);
}
$curl = new \PhalApi\CUrl();
// GET 请求
$rs = $curl->get('http://api.example.com/?id=1', 3000); // 3秒超时
// POST 请求
$rs = $curl->post('http://api.example.com/', array('name' => 'test'), 3000);
// 构造参数为重试次数
$curl = new \PhalApi\CUrl(3); // 失败后最多重试3次
$rs = $curl->get('http://api.example.com/?id=1', 3000);
use PhalApi\Tool;
// 获取客户端IP
$ip = Tool::getClientIp();
// 生成随机字符串
$str = Tool::createRandStr(16); // 默认字母数字
$str = Tool::createRandStr(6, '0123456789'); // 纯数字验证码
// 数组转XML
$xml = Tool::arrayToXml($data);
// XML转数组
$data = Tool::xmlToArray($xml);
// 排除数组中的指定键
$result = Tool::arrayExcludeKeys($data, 'password,token');
// composer.json
{
"require": {
"phalapi/redis": "2.*",
"phalapi/jwt": "2.*",
"phalapi/pay": "2.*",
"phalapi/qrcode": "2.*",
"phalapi/wechat": "2.*",
"phalapi/sms": "2.*",
"phalapi/upload": "2.*"
}
}
composer update
每个扩展需要三步:安装 → 配置 → 注册
// 步骤1:安装(composer require)
// 步骤2:配置(config/app.php 中添加扩展配置)
return array(
'redis' => array(
'host' => '127.0.0.1',
'port' => 6379,
'auth' => '',
),
);
// 步骤3:注册(config/di.php)
$di->redis = new \PhalApi\Redis\Redis($di->config->get('app.redis'));
扩展目录结构:
phalapi-custom-ext/
├── composer.json # 包信息
├── src/
│ └── Ext.php # 扩展类
└── README.md
// composer.json
{
"name": "yourname/phalapi-custom-ext",
"type": "phalapi-extension",
"require": {
"phalapi/kernal": "2.*"
},
"autoload": {
"psr-4": {
"YourName\\CustomExt\\": "src/"
}
}
}
// ./src/app/Api/Task.php
namespace App\Api;
use PhalApi\Api;
class Task extends Api {
public function createTask() {
$client = new \GearmanClient();
$client->addServer('127.0.0.1', 4730);
$data = array(
'task_id' => $this->taskId,
'params' => $this->taskParams,
);
$client->doBackground('process_task', json_encode($data));
return array('task_id' => $this->taskId);
}
}
// ./bin/worker.php
$worker = new \GearmanWorker();
$worker->addServer('127.0.0.1', 4730);
$worker->addFunction('process_task', function($job) {
$data = json_decode($job->workload(), true);
// 处理任务逻辑
$domain = new \App\Domain\Task();
$domain->process($data);
});
while ($worker->work()) {}
# 启动 worker
nohup php ./bin/worker.php > /dev/null 2>&1 &
# 查看运行状态
ps aux | grep worker
# 停止
kill -9 $(pgrep -f worker.php)
// 获取完整配置
$config = \PhalApi\DI()->config->get('app');
// 获取单个配置(多级用点号分隔)
$version = \PhalApi\DI()->config->get('app.version');
// 获取配置(带默认值)
$name = \PhalApi\DI()->config->get('app.name', '默认名称');
根据 API_MODE 自动加载不同环境配置:
| API_MODE | 加载规则 | 示例 |
|---|---|---|
| ---------- | ---------- | ------ |
prod | app.php | 生产配置 |
dev | app_dev.php(优先),app.php(回退) | 开发配置 |
test | app_test.php(优先),app.php(回退) | 测试配置 |
// 定义环境模式
defined('API_MODE') || define('API_MODE', 'dev');
// 使用 Yaconf 读取配置(高性能)
$di->config = new \PhalApi\Config\YaconfConfig('phalapi.');
$di = \PhalApi\DI();
// 配置(必须手动注册)
$di->config = new \PhalApi\Config\FileConfig(API_ROOT . '/config');
// 数据库(推荐注册)
$di->notorm = new \PhalApi\Database\NotORMDatabase($di->config->get('dbs'));
// 日志(必须手动注册)
$di->logger = new \PhalApi\Logger\FileLogger(
API_ROOT . '/runtime',
\PhalApi\Logger::LOG_LEVEL_DEBUG | \PhalApi\Logger::LOG_LEVEL_INFO | \PhalApi\Logger::LOG_LEVEL_ERROR
);
// 缓存(推荐注册)
$di->cache = new \PhalApi\Cache\RedisCache(array('host' => '127.0.0.1'));
// 过滤器(推荐注册)
$di->filter = new \PhalApi\Filter\SimpleMD5Filter();
| 服务 | 说明 |
|---|---|
| ------ | ------ |
$di->request | 请求对象(自动注册) |
$di->response | 响应对象(自动注册) |
// 错误方式(未注册时报错)
if (isset($di->cache)) { ... }
// 正确方式(先获取再判断)
$cache = $di->cache;
if (isset($cache)) {
$cache->get('key');
}
| 服务 | 是否必须注册 | 说明 |
|---|---|---|
| ------ | ------------- | ------ |
$di->config | 必须 | 配置读取 |
$di->logger | 必须 | 日志记录 |
$di->notorm | 推荐 | 数据库操作 |
$di->cache | 推荐 | 缓存 |
$di->filter | 推荐 | 签名验证 |
$di->request | 自动 | 请求参数 |
$di->response | 自动 | 响应输出 |
http://dev.phalapi.net/docs.phphttp://dev.phalapi.net/docs.php?service=App.Site.Index&detail=1/**
* 用户登录
*
* @desc 用于用户登录验证,返回用户信息和Token
* @param string username 用户名(必填)
* @param string password 密码(必填,最少6位)
* @return int user_id 用户ID
* @return string token 登录令牌
* @return string username 用户名
* @exception 400 参数错误
* @exception 401 签名错误
* @exception 403 权限不足
*/
public function login() { }
/**
* 内部接口(不在文档中显示)
*
* @ignore
*/
public function internalMethod() { }
# 展开版(所有接口详情展开显示)
php ./public/docs.php expand
# 折叠版(接口列表折叠显示)
php ./public/docs.php fold
| 命名空间 | 文件路径 |
|---|---|
| ---------- | ---------- |
App\Api\User | src/app/Api/User.php |
App\Domain\User | src/app/Domain/User.php |
App\Model\User | src/app/Model/User.php |
App\Common\Utils | src/app/Common/Utils.php |
// App\Api\Weixin\User → src/app/Api/Weixin/User.php
namespace App\Api\Weixin;
class User {
// 微信用户相关接口
}
use App\Domain\User as DomainUser;
use App\Model\User as ModelUser;
$domain = new DomainUser();
$model = new ModelUser();
// 或使用完整类名
$domain = new \App\Domain\User();
{
"autoload": {
"psr-4": {
"App\\": "src/app/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Test\\": "tests/"
}
}
}
// src/app/functions.php
namespace App;
function hello() {
return 'world';
}
function now() {
return date('Y-m-d H:i:s');
}
// 使用
$result = \App\hello();
$time = \App\now();
php ./bin/phalapi-buildtest ./src/app/Api/User.php App\\Api\\User > ./tests/app/Api/User_Test.php
class PhpUnderControl_AppApiUser_Test extends \PHPUnit_Framework_TestCase
{
public function testLogin() {
// BUILD - 构造
$url = 's=User.Login';
$params = array('username' => 'test', 'password' => '123456');
// OPERATE - 操作
$rs = \PhalApi\Helper\TestRunner::go($url, $params);
// CHECK - 检验
$this->assertNotEmpty($rs);
$this->assertEquals(200, $rs['ret']);
$this->assertArrayHasKey('user_id', $rs['data']);
}
public function testGetUserInfo() {
$url = 's=User.GetUserInfo';
$params = array('id' => 1);
$rs = \PhalApi\Helper\TestRunner::go($url, $params);
$this->assertEquals(200, $rs['ret']);
$this->assertEquals(1, $rs['data']['id']);
}
}
// 模拟 GET 请求
$rs = \PhalApi\Helper\TestRunner::go('s=User.GetUserInfo&id=1');
// 模拟 POST 请求
$rs = \PhalApi\Helper\TestRunner::go('s=User.Login', array(
'username' => 'test',
'password' => '123456',
));
// 带自定义配置
$rs = \PhalApi\Helper\TestRunner::go('s=User.GetUserInfo', array('id' => 1), array(
'HTTP_X_TOKEN' => 'test_token',
));
| 问题 | 解决方案 |
|---|---|
| ------ | ---------- |
| 中文返回为空 | 检查 json_encode 编码,确保数据库 UTF8 |
| 跨层调用报错 | 确认调用关系:Api→Domain→Model |
| 缓存不生效 | 检查 runtime 目录权限 |
| NotORM 状态问题 | 每次查询获取新实例 $this->getORM() |
| 全表删除禁止 | 必须带 WHERE 条件 |
| 签名验证失败 | 确认密钥一致、参数排序规则、编码格式 |
| 数据库连接失败 | 检查 config/dbs.php、MySQL服务、防火墙 |
| 参数校验失败 | 查看 code 错误码、检查参数类型和范围 |
| 日志文件过大 | FileLogger 自动按天分割,也可手动清理 |
| 分表查询不到数据 | 确认 $id 正确传入 getORM($id) |
| 功能 | 配置/使用位置 | 关键类/方法 |
|---|---|---|
| ------ | -------------- | ------------ |
| 数据库配置 | config/dbs.php | NotORMDatabase |
| 缓存配置 | config/di.php | FileCache/RedisCache/MemcachedCache |
| 日志配置 | config/di.php | FileLogger |
| 签名配置 | config/di.php | SimpleMD5Filter |
| 白名单 | config/app.php | service_whitelist |
| 系统配置 | config/sys.php | debug/notorm_debug/enable_sql_log |
| 应用配置 | config/app.php | apiCommonRules |
| DI服务注册 | config/di.php | \PhalApi\DI() |
| 接口层 | src/app/Api/ | extends Api |
| 领域层 | src/app/Domain/ | 业务逻辑 |
| 数据层 | src/app/Model/ | extends NotORMModel |
| 通用数据 | DataApi | extends DataApi |
| 参数规则 | getRules() | 9种类型 |
| 在线文档 | /docs.php | @desc/@return/@exception |
| 单元测试 | ./bin/phalapi-buildtest | TestRunner::go() |
| 扩展安装 | composer.json | phalapi/* |
| 环境模式 | API_MODE | dev/test/prod |
| 加密 | PhalApi\Crypt | McryptCrypt/RSA |
| HTTP请求 | PhalApi\CUrl | get()/post() |
| Cookie | PhalApi\Cookie | set()/get()/delete() |
| 工具 | PhalApi\Tool | getClientIp()/createRandStr() |
共 1 个版本