目标。 1 用migration 建表。
2 实现 /v1/apply 接口,机构入驻。 打开自动审核配置后,直接通过。(建机构,建用户数据)
备注: 文档内容为真实开发日志,很多个第一次。(migrate,查表,读写redis,读配置,事务 hyperf watch ,使用枚举,验频)等, 比较折腾,耐心看。
1 表格设计。
表名 edu_user \n
字段 id, tname,pwd,status,created_at,updated_at,deleted_at,last_login_at
这里简化处理,真实生产项目 ,建议加salt 字段,再加login_log 表
2 官网文档阅读 。
3 migration
3.1 创建迁移文件 。
docker exec -it hyperf bash
cd api
php bin/hyperf.php gen:migration create_users_table --create=users
3.2 编写migration 文件 。
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string("tname", 32);
$table->string("pwd", 32);
$table->tinyInteger("status")->default(1);
$table->integer("last_login_at");
$table->integer("created_at");
$table->integer("updated_at");
$table->integer("deleted_at")->nullable(true);
$table->datetimes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
}
3.3 跑migration 代码 。
php bin/hyperf.php migrate
SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'created_at' (SQL: create table `edu_users` (`id` bigint unsigned not null auto_increm
ent primary key, `tname` varchar(32) not null, `pwd` varchar(32) not null, `status` tinyint not null default '1', `last_login_at` int not null, `created_
at` int not null, `updated_at` int not null, `deleted_at` int null, `created_at` datetime null, `updated_at` datetime null) default character set utf8mb4
collate 'utf8mb4_unicode_ci')
[ERROR] PDOException: SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'created_at' in /data/project/api.xuxing.tech/vendor/hyperf/data
base/src/Connection.php:347
Column already exists: 1060 Duplicate column name 'created_at' 去掉代码的 $table->datetimes(); 再跑一次。
Migrating: 2023_06_23_113756_create_users_table
App\Listener\DbQueryExecutedListener listener.
Migrated: 2023_06_23_113756_create_users_table
用工具看,表已创建 。
3.4 再练手一下修改表.
加个mobile 字段,用于手机号登录,修改tname 长度,限20
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class AlterUsersTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string("mobile", "32");
$table->string("tname", 20)->change();
$table->index("mobile");
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
//
});
}
}
跑migration 时报错。
php bin/hyperf.php migrate
Changing columns for table "users" requires Doctrine DBAL; install "doctrine/dbal".
[ERROR] RuntimeException: Changing columns for table "users" requires Doctrine DBAL; install "doctrine/dbal". in /data/project/api.xuxing.tech/vendor/hyp
erf/database/src/Schema/Grammars/ChangeColumn.php:34
排下坑,装一下
composer require "doctrine/dbal:^3.0"
composer require "doctrine/dbal:^3.0"
Info from https://repo.packagist.org: #StandWithUkraine
./composer.json has been updated
Running composer update doctrine/dbal
Loading composer repositories with package information
Updating dependencies
Lock file operations: 3 installs, 0 updates, 0 removals
- Locking doctrine/cache (2.2.0)
- Locking doctrine/dbal (3.6.3)
- Locking doctrine/event-manager (1.2.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 3 installs, 0 updates, 0 removals
- Downloading doctrine/event-manager (1.2.0)
- Downloading doctrine/cache (2.2.0)
- Downloading doctrine/dbal (3.6.3)
- Installing doctrine/event-manager (1.2.0): Extracting archive
- Installing doctrine/cache (2.2.0): Extracting archive
- Installing doctrine/dbal (3.6.3): Extracting archive
Generating optimized autoload files
再跑一次迁移。
php bin/hyperf.php migrate
四 重新设计。
前面练手试了下 hyperf 的migrate . 现在重新设计一下接口。
4.1 . 表格
edu_org 机构表 edu_apply 申请记录 edu_users 添加 cur_org_id 当前机构字段。
4.2. 接口清单 。
- v1/apply 申请机构. 当前阶段,apply 即生效,后期再做管理后台
curl -H 'Content-Type: application/json' -X POST https://api.xxx.com/v1/apply -d '{
"mobile": "138666688878",
"org_name": "测试机构1",
"contact": "王先生",
"remark" : ""
}'
- v1/login 使用jwt + redis 实现单点登录.
curl -H 'Content-Type: application/json' -X POST https://api.xxx.com/v1/login -d '{
"mobile": "138666688878",
"pwd": "123456",
}'
- v1/logout 使用jwt + redis 实现单点登录.
curl -X GET https://api.xxx.com/v1/logout?token=xxx
五 开始实施。
5.1 建表
1 apply 表
docker-compose>docker exec -it hyperf bash
cd api
php bin/hyperf.php gen:migration create_apply_table --create=apply
编辑migrate 文件
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class CreateOrgTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('org', function (Blueprint $table) {
$table->bigIncrements('id');
$table->datetimes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('org');
}
}
保存文件后
php bin/hyperf.php migrate
2 org 表。 (越简单越好,别一开始过度设计,符合场景和真实业务有必要再补字段)
php bin/hyperf.php gen:migration create_org_table --create=org
当前,如果是生产项目 ,不要如此草率,值多进一步考虑,在一阶段要实现哪些,后期明确有和大概率有的提交准备上。
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class CreateOrgTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('org', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string("tname", 32);
$table->string("remark", 255);
$table->tinyInteger("status")->default(0);
$table->integer("created_at");
$table->integer("updated_at");
$table->integer("deleted_at")->nullable(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('org');
}
}
保存文件后
php bin/hyperf.php migrate
3 用户表,增加与机构的关系 (员工可在多个机构为不同的员工) 当前机构 ,当前员工id . 这里简要设计 cur_staff_id 为 0 时为超管。 cur_org_id + cur_staff_id 唯一。 cur_org_id ,cur_staff_id。 这里先点到为止,后续rbac ,以及添加机构员工时,再补关系表。
创migrate 文件
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class AlertUserTable2 extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('user', function (Blueprint $table) {
$table->integer("cur_org_id" );
$table->integer("cur_staff_id");
$table->unique(["cur_org_id", "cur_staff_id"]);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('user', function (Blueprint $table) {
//
});
}
}
php bin/hyperf.php migrate
这里发现,表名叫 edu_users 不好,更换为 edu_user
php bin/hyperf.php gen:migration rename_user
migration 文件
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class RenameUser extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::rename("users", "user");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
}
保存文件后
php bin/hyperf.php migrate
建表完成。如图。
5.2 实现apply 接口。
翻下文档 ,解决几个问题。
- 如何创建控制器。
- 接 postJson 数据。
- 入mysql 数据,期望实现 insert(data) , update(data, $where) 等方法。 (说直白点,将数组转sql 。
- mysql 事务。
- 如何定义接口返回。
- 设置与读取 .env
5.2.1 如何创建控制器
翻了下文档 。 个人选择 通过配置文件定义路由 hyperf.wiki/3.0/#/zh-cn…
这样出来的代码,入口归总后,找起来方便。避免后续乱添加文件,不知道哪里是哪里。(很久以前用Yii ,ci 时就极不爽这个)
// 1 在 config/route.php 文件中,加上
Router::post('/post', 'App\Controller\IndexController::post');
// 2 copy paste IndexController 至 ApplyController ,然后照着改即可。
5.2.2 获取json 数据。
翻文档得
// 存在则返回,不存在则返回 null
$name = $request->input('user.name');
// 存在则返回,不存在则返回默认值 Hyperf
$name = $request->input('user.name', 'Hyperf');
// 以数组形式返回所有 Json 数据
$name = $request->all();
5.2.3 数据库相关用法示例。
Db::table('users')->insert( ['email' => 'john@example.com', 'votes' => 0] );
$id = Db::table('users')->insertGetId( ['email' => 'john@example.com', 'votes' => 0] );
Db::table('users')->where('id', 1)->update(['votes' => 1]);
说明一下,个人较为反感orm ,sql 本来是件简单事,又不是存在啥智力缺陷写不出来sql , 或者你要今天mysql 明天oracle 等装x 需求,就没必要去研究什么 where,or_where,union 等破事了。 而且,真到性能优化时, 恶心的orm 套一层后,你会想哭。 所以,原生sql 加几个助手方法就ok 了。
5.2.4 mysql 事务。
use Hyperf\DbConnection\Db;
Db::beginTransaction();
try{
// Do something...
Db::commit();
} catch(\Throwable $ex){
Db::rollBack();
}
5.2.5 定义返回 。
抄 IndexController 的例子,直接return 数组即可。
return [
'method' => $method,
'message' => "Hello {$user}.",
];
}
通常响应数据 类似
{
"code": 0,
"msg": "SUCCESS",
"data1": "1111",
"data2": "1111",
}
然后code 使用枚举类来实现 。 hyperf.wiki/3.0/#/zh-cn…
php bin/hyperf.php gen:constant ErrorCode
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class ErrorCode extends AbstractConstants
{
/**
* @Message("Server Error!")
*/
const SERVER_ERROR = 500;
/**
* @Message("系统参数错误")
*/
const SYSTEM_INVALID = 700;
}
用户可以使用 ErrorCode::getMessage(ErrorCode::SERVER_ERROR) 来获取对应错误信息。
5.2.6 折腾下配置。
hyperf.wiki/3.0/#/zh-cn… .env 中配置。 config 中设置,默认值。 原则, 先取 .env 然后取 config 的默认值。 使用注解来获取配置。
use Hyperf\Config\Annotation\Value;
class IndexController
{
#[Value("config.key")]
private $configValue;
public function index()
{
return $this->configValue;
}
}
这玩意,越来越像 java,dotnet 了,怪怪的。还好我都会点皮毛,见怪不怪了。
再次说明,就一般问题而言,查下官网文档是效率最高的,没有之一。 不要碰到事就百度,google ,严重浪费时间
5.3 准备就绪,开整代码。
5.3.1 定义apply 错误Constant
php bin/hyperf.php gen:constant ErrorCode
app\Constants\ErrorCode.php
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class ErrorCode extends AbstractConstants
{
/**
* @Message("Server Error!")
*/
public const SERVER_ERROR = 500;
/**
* @Message("参数不足")
*/
public const APPLY_LOCK_PARAMS= 701;
/**
* @Message("参数不足")
*/
public const APPLY_REPEAT= 702;
}
一开始,就不往复杂的整了,我就做个小项目,就不折腾国际化, 响应不同的http 状态码这些破事了, 直接响应json ,简单直接,前端(也是自己整)也喜欢
再定义一个 Enum 用于存状态,类别等业务枚举数据
5.3.2 配置 .env
# org 申请业务
APPLY_AUTO_AUDIT=1
后期有转为后台审核时改掉即可。
5.3.3 route 添加配置
\config\routes.php
//添加
Router::post('/v1/apply', 'App\Controller\ApplyController::apply');
5.3.4 处理代码热更新。
突然发现一件事情,改了代码后,hyperf 未生效。需要ctrl +c 再重启服务 . 这个在开发场景下体验极差. 翻了下文档,有个watcher hyperf.wiki/3.0/#/zh-cn…
//安装
composer require hyperf/watcher --dev
//发布
php bin/hyperf.php vendor:publish hyperf/watcher
//启动。
php bin/hyperf.php server:watch
提示端口被占, 于是改Dockerfile
FROM swr.cn-south-1.myhuaweicloud.com/docker-study/hyperf:1.0
WORKDIR /data/project
ENTRYPOINT ["php", "/data/project/api.xuxing.tech/bin/hyperf.php", "server:watch"]
EXPOSE 9501
docker-compose down docker-compose up -d
调试一下,改下indexController 的返回,看是否生效。
很遗憾,没生效。 开启开启排坑流程
docker inspect hyperf |grep -C2 hyperf
"Path": "php",
"Args": [
"/data/project/api/bin/hyperf.php",
"start"
],
发现还是start
将build 出来的镜像剁了
docker-compose down
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-compose_hyperf latest e680c0352182 12 hours ago 122MB
hyperf/hyperf 8.0-alpine-v3.15-swoole 2139f47d3995 6 days ago 122MB
swr.cn-south-1.myhuaweicloud.com/docker-study/hyperf 1.0 2139f47d3995 6 days ago 122MB
redis latest 8e69fcb59ff4 8 days ago 130MB
docker rmi docker-compose_hyperf:latest
docker-compose up -d
再看 docker inspect hyperf |grep -C2 hyperf
"Path": "php",
"Args": [
"/data/project/api/bin/hyperf.php",
"server:watch"
],
再测,修改代码后重启curl . 搞定。 curl 127.0.0.1 {"method":"GET","message":"Hello23332sss2 Hyperf.","debug":2}
最后,文档还提到有些不足。
- 删除文件和修改
.env需要手动重启才能生效。
5.3.5
终于到写控制器了。 千万别急 走一些前续流程。
- 前序处理。 1 写注释代码用来干啥 , 2 列出代码步骤 , 3 打开postman 搭好测试环境 两个原因。
1 代码是给人来看的。
2 理清思路,做事不慌,记忆力是件不靠谱的事,列清楚了,更有效率。
3 tdd. 快速可测是效率的根基.
代码示例。
<?php
declare(strict_types=1);
/**
*
* @Author xuxing
* @description 机构申请接口. (apply_auto_audit 打开后,自动生效)
*/
namespace App\Controller;
class ApplyController extends AbstractController
{
public function apply()
{
return [
'test' => '收到请求'
];
//请求示例。
/*
curl -H 'Content-Type: application/json' -X POST https://api.xxx.com/v1/apply -d '{
"mobile": "138666688878",
"org_name": "测试机构1",
"contact": "王先生",
"remark" : ""
}'
*/
//一 校验
//1 cc 用redis ,限一下请求频率。
//2 接收请求数据,校验参数。
//3 是否重复apply (状态为pending)
//二 数据入库。
//2 开启事务。
//3 组装apply 数据,入库
//4 判断 apply_auto_audit 是否打开
//5 打开后,org 数据入库,user 数据入库,默认密码 123456
//三 发通知后台审。
// later 处理。
}
}
- postman 测试看route 是否生效。
- 先实现功能。(这个时间,简单实现,别想太多封装技巧啥的)
3.1 走通读取配置。 又tmd 遇坑
//参照文档 ,结果失效。
#[Value("config.app_name")]
private $configValue;
//太晚,我对框架实现细节无兴趣,不想排坑,换个方法实现
return [
'test' => '收到请求',
"apply_auto_audit" => config("apply_auto_audit")
];
返回处理包装一下 ,请求数据验频这些通用处理 app/AbstractController.php
<?php
...
abstract class AbstractController
{
...
* @param $code
* @param array $data
* @return array|string[]
*/
protected function code($code, $data = [])
{
return !empty($data) ? array_merge([
'code' => $code,
'msg' => ErrorCode::getMessage($code)
], $data) : [
'code' => $code,
'msg' => ErrorCode::getMessage($code)];
}
/**
* 请求数据验率
* @param $data
* @return array|string[]
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
protected function validCc($data)
{
$redis = $this->container->get(\Redis::class);
$redisKey = md5(json_encode($data));
if ($redis->get($redisKey) != null) {
return $this->code(ErrorCode::TOO_FREQUENTLY);
}
$redis->setex($redisKey,3,1);
}
}
最后,实现代码如下。
<?php
declare(strict_types=1);
/**
*
* @Author xuxing
* @description 机构申请接口. (apply_auto_audit 打开后,自动生效)
*/
namespace App\Controller;
use App\Constants\Enum;
use App\Constants\ErrorCode;
use Hyperf\Config\Annotation\Value;
use Hyperf\DbConnection\Db;
use Hyperf\Redis\Redis;
class ApplyController extends AbstractController
{
/**
* 机构申请.
* @return array|string[]
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function apply()
{
$postData = $this->request->all();
$this->validCc($postData);
//获取数据,并校验参数。
$mobile = $this->request->input('mobile');
if($mobile == null) {
return $this->code(ErrorCode::APPLY_LOCK_PARAMS);
}
$org_name = $this->request->input('org_name');
if($org_name == null) {
return $this->code(ErrorCode::APPLY_LOCK_PARAMS);
}
$contact = $this->request->input('contact');
if($contact == null) {
return $this->code(ErrorCode::APPLY_LOCK_PARAMS);
}
$remark = $this->request->input('remark');
//判断是否申请过
$has_apply = Db::table('apply')->where(
[
['status', '=', Enum::APPLY_STATUS_PENDING],
['mobile', '=', $mobile],
]
)->exists();
if($has_apply) {
return $this->code(ErrorCode::APPLY_REPEAT);
}
$has_user = Db::table('user')->where(
[
['mobile', '=', $mobile],
]
)->exists();
if($has_user) {
return $this->code(ErrorCode::APPLY_USER_EXIST);
}
Db::beginTransaction();
try{
//组装 apply 数据。
$applyData = [
'mobile' => $mobile,
'org_name' => $org_name,
"contact" => $contact,
"remark" => $remark,
"status" => Enum::APPLY_STATUS_PENDING,
"created_at" => time(),
"updated_at" => time(),
];
$apply_id = Db::table('apply')->insertGetId( $applyData );
//开启自动审核后
if (config("apply_auto_audit")) {
$org_data = [
"tname" => $org_name,
"remark" => $remark,
"status" => Enum::ORG_STATUS_OK,
"created_at" => time(),
"updated_at" => time(),
];
$org_id = Db::table('org')->insertGetId( $org_data );
$userData =[
'tname' => $contact,
"pwd" => md5('123456'),
"status" => Enum::USER_STATUS_OK,
"mobile" => $mobile,
"cur_org_id" => $org_id,
"cur_staff_id" => 0,
"last_login_at" => time(),
"created_at" => time(),
"updated_at" => time(),
];
Db::table('user')->insertGetId( $userData );
Db::table('apply')->where('id', $apply_id)->update(['status' => Enum::APPLY_STATUS_OK]);
}
Db::commit();
} catch(\Throwable $ex){
return $this->code(ErrorCode::ERROR, ["ex" => $ex->getMessage()]);
Db::rollBack();
}
return ['code' => ErrorCode::SUCCESS, "msg" => 'apply success'];
}
}
测试ok,查看数据库,符合预期 。
总结。
第一次用hyperf 写接口。大坑没有,小坑有一些。(config 读取,watch )等 。
边开发,连写文档的经验不足 ,效率被拉址得很低。 最后写controller 时,实在不愿意每步写文档了, 还有一些待处理的事宜。 例如,写日志。
最后,git 提交,睡觉 .明天再来。
遇坑,重启容器后报错。
watcher 失效。
带着报错调试,Listener 失效,取数据还是对象。 修改 DbQueryExecutedListener.php
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\Collection\Arr;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Database\Events\StatementPrepared;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use PDO;
#[Listener]
class DbQueryExecutedListener implements ListenerInterface
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('sql');
}
public function listen(): array
{
return [
QueryExecuted::class,
StatementPrepared::class,
];
}
/**
* @param QueryExecuted $event
*/
public function process(object $event): void
{
if ($event instanceof StatementPrepared) {
$event->statement->setFetchMode(PDO::FETCH_ASSOC);
}
if ($event instanceof QueryExecuted) {
$sql = $event->sql;
if (! Arr::isAssoc($event->bindings)) {
$position = 0;
foreach ($event->bindings as $value) {
$position = strpos($sql, '?', $position);
if ($position === false) {
break;
}
$value = "'{$value}'";
$sql = substr_replace($sql, $value, $position, 1);
$position += strlen($value);
}
}
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}
}
还有一些不好的代码习惯,引发的bug
例如,未使用use ,就直接Inject
use Hyperf\Di\Annotation\Inject;
然后还碰到了 ErrorCode::getMessage($code) 内容突然为空。 重启hyperf 后解决。
总结。 当新增文件后,watcher 可能会失效, 后续有大改动时, docker restart hyperf