本篇概要:
1. Http Server;
1.1 控制器;
- 包含:RequestMapping 注释、请求与响应、返回 JSON 格式、路由 path 参数、GET \ POST 参数获取
- PhpStorm 安装插件
- 新建
App/Http/Controller/ProductController.php
<?php
namespace App\Http\Controller;
use App\lib\MyRequest;
use function foo\func;
use Swoft\Context\Context;
use Swoft\Http\Message\ContentType;
use Swoft\Http\Message\Response;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;
use Swoft\Http\Server\Annotation\Mapping\Middleware;
use App\Http\Middleware\ControllerMiddleware;
/**
* 商品模块
* // @Controller()
* @Controller(prefix="/product")
* 使用一个中间件
* @Middleware(ControllerMiddleware::class)
*/
class ProductController{
/**
* 1.1.1 RequestMapping 注释
*
* @RequestMapping(route="/product", method={RequestMethod::GET})
*/
public function prod_list(){
// 1.1.2 请求与响应
// 参考:https://www.swoft.org/documents/v2/core-components/http-server/#http-1
// 上下文对象:Contexts
// 获取请求对象
$req = Context::get()->getRequest();
// 获取响应对象
$res = Context::get()->getResponse();
// 返回内容
// return $res->withContent("abc");
// 1.1.3 返回 JSON 格式
// return $res->withContentType("application/json")->withContent("abc");
// 1.2 调用全局函数
// return $res->withContentType("application/json")
// ->withContent(json_encode([NewProduct(101, "测试商品"), NewProduct(102, "测试商品2")]));
return $res->withContentType("application/json")
->withData([NewProduct(101, "测试商品"), NewProduct(102, "测试商品2")]);
}
/**
* //@RequestMapping(route="/product/{pid}", method={RequestMethod::GET})
* 路由 path 参数前缀,正则控制路由参数
* 注入参数 Response,可能或造成参数过多
* 1.1.4 路由 path 参数
* @RequestMapping(route="{pid}", params={"pid"="\d+"}, method={RequestMethod::GET, RequestMethod::POST})
*/
public function prod_detail(int $pid, Response $response){
$p = NewProduct($pid, "测试商品");
//return \response()->withContentType("application/json")->withData([$p]);
// return $response->withContentType(ContentType::JSON)->withData([$p]);
// 1.3 此处返回值会调用中间件
// return [$p];
// 1.1.5 GET \ POST 参数获取
// echo request()->get("type", "default type");
// if(request()->getMethod() == RequestMethod::GET){
// *1.5 JSON 参数转实体对象
/** @var $product ProductEntity */
// $product = jsonForObject(ProductEntity::class);
// var_dump($product);
if(isGet()){
return $p;
}else if(isPost()) {
$p->pname = "修改产品" . request()->post('title', 'none');
return $p;
}
// *1.4 链式调用
// $my = new MyRequest();
// $my->if(isGet())
// ->then(function() use ($p){
// return $p;
// })
// ->if(isPost())
// ->then(function() use ($p){
// $p->pname = "修改产品" . request()->post('title', 'none');
// return $p;
// })
// ->getResult();
}
}
- 实例操作
# 终端进入项目目录
cd /data/test/php/swoft/
# 启动 Swoft
php ./bin/swoft http:start
# 返回
SERVER INFORMATION(v2.0.8)
********************************************************************************
* HTTP | Listen: 0.0.0.0:18306, Mode: Process, Worker: 6, Task worker: 12
********************************************************************************
HTTP Server Start Success!
# 浏览器访问:http://localhost:18306/product
1.2 全局函数;
- 修改
App/Helper/Functions.php
<?php
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;
require_once (__DIR__ . "/WebFunctions.php");
require_once (__DIR__ . "/OrderFunctions.php");
/**
* This file is part of Swoft.
*
* @link https://swoft.org
* @document https://swoft.org/docs
* @contact [email protected]
* @license https://github.com/swoft-cloud/swoft/blob/master/LICENSE
*/
function user_func(): string
{
return 'hello';
}
function NewProduct(int $pid, string $pname){
$p = new stdClass();
$p->pid = $pid;
$p->pname = $pname . $p->pid;
return $p;
}
function response($contentType=false){
if($contentType){
return Swoft\Context\Context::get()->getResponse()->withContentType($contentType);
}
return Swoft\Context\Context::get()->getResponse();
}
function request(){
return Swoft\Context\Context::get()->getRequest();
}
function isGet(){
return request()->getMethod() == RequestMethod::GET;
}
function isPost(){
return request()->getMethod() == RequestMethod::POST;
}
function ip(){
$req = request();
if($req->server('http_x_forwarded_for')){
return $req->server('http_x_forwarded_for');
}else if($req->server('http_client_ip')){
return $req->server('http_client_ip');
}else{
return $req->server('remote_addr');
}
}
// 事务封装(也可以用 AOP)
function tx(callable $func, &$result=null){
\Swoft\Db\DB::beginTransaction();
try{
$result = $func();
\Swoft\Db\DB::commit();
}catch(Exception $exception){
$result = OrderCreateFail($exception->getMessage());
\Swoft\Db\DB::rollBack();
}
}
1.3 中间件;
- 参考:https://www.swoft.org/documents/v2/core-components/http-server/#heading13
- 新建
App/Http/Middleware/ControllerMiddleware.php
<?php
namespace App\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Http\Message\Request;
use Swoft\Http\Message\Response;
use Swoft\Http\Server\Contract\MiddlewareInterface;
use Swoft\Http\Message\ContentType;
/**
* @Bean()
*/
class ControllerMiddleware implements MiddlewareInterface
{
/**
* Process an incoming server request.
*
* @param ServerRequestInterface|Request $request
* @param RequestHandlerInterface|Response $handler
* 放到控制器里面进行对应,也可以设置全局的中间件
*
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var $ret \Swoft\Http\Message\Response */
// response 对象
$ret = $handler->handle($request);
$data = $ret->getData();
//$p2 = NewProduct(10000, "中间件测试商品");
//$data[] = $p2;
if(is_object($data)){
return \response(ContentType::JSON)->withContent(json_encode($data));
}
return \response(ContentType::JSON)->withData($data);
}
}
*1.4 链式调用;
- 新建
App\lib\MyRequest.php
<?php
namespace App\lib;
class MyRequest{
private $result;
private $do = false;
function if($bool){
$this->do = $bool;
return clone $this;
}
function then(callable $func){
if($this->do){
$this->result = $func();
$this->do = !$this->do;
}
return clone $this;
}
function getResult(){
return $this->result;
}
}
*1.5 JSON 参数转实体对象;
- 新建
App\lib\ProductEntity.php
<?php
namespace App\lib;
class ProductEntity{
private $prod_id;
private $prod_name;
private $prod_price;
/**
* @return mixed
*/
public function getProdId(){
return $this->prod_id;
}
/**
* @param mixed $prod_id
*/
public function setProdId($prod_id): void{
$this->prod_id = $prod_id;
}
/**
* @return mixed
*/
public function getProdName(){
return $this->prod_name;
}
/**
* @param mixed $prod_name
*/
public function setProdName($prod_name): void{
$this->prod_name = $prod_name;
}
/**
* @return mixed
*/
public function getProdPrice(){
return $this->prod_price;
}
/**
* @param mixed $prod_price
*/
public function setProdPrice($prod_price): void{
$this->prod_price = $prod_price;
}
}
- 新建
App/Helper/WebFunction.php
<?php
/**
* @param $class
* $class 不是实例化对象,而是 function 名称
* 需要用到反射
*/
function jsonForObject($class=""){
$req = request();
try {
$contentType = $req->getHeader('content-type');
if (!$contentType || false === stripos($contentType[0], \Swoft\Http\Message\ContentType::JSON)) {
return false;
}
// 获取原始的 body 内容
$raw = $req->getBody()->getContents();
// 把得到的对象和 $class 做比对
$map = json_decode($raw, true); // kv 数组
if($class == "") return $map; // 实现 2.0.2 版本前的 request()->json() 方法
$class_obj = new ReflectionClass($class); // 反射对象
$class_instance = $class_obj->newInstance(); // 根据反射对象创建实例
// 获取所有方法(公共方法)
$methods = $class_obj->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method){
// 正则获取 set 方法
if(preg_match("/set(\w+)/", $method->getName(), $matches)){
// echo $matches[1] . PHP_EOL; // 得到方法名 ProdId,ProdName,ProdPrice
invokeSetterMethod($matches[1], $class_obj, $map,$class_instance);
}
}
return $class_instance;
}catch (Exception $exception){
return false;
}
}
// 把数组映射成实体(一维数组)
function mapToModel(array $map, $class){
try {
$class_obj = new ReflectionClass($class);
$class_instance = $class_obj->newInstance(); // 根据反射对象创建实例
// 获取所有方法(公共方法)
$methods = $class_obj->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method){
// 正则获取 set 方法
if(preg_match("/set(\w+)/", $method->getName(), $matches)){
// echo $matches[1] . PHP_EOL; // 得到方法名 ProdId,ProdName,ProdPrice
invokeSetterMethod($matches[1], $class_obj, $map,$class_instance);
}
}
return $class_instance;
} catch (Exception $exception){
return null;
}
}
// 二维数组
// $fill 填充新字段
// $toarray=false 返回实体数组
function mapToModelsArray(array $maps, $class, $fill=[], $toarray=false){
$ret = [];
foreach ($maps as $map){
$getObject = mapToModel($map, $class);
if($getObject){
if($fill && count($fill) > 0){
$getObject->fill($fill);
}
if($toarray){
// 数组
$ret[] = $getObject->getModelAttributes();
}else{
// 实体对象
$ret[] = $getObject;
}
}
}
return $ret;
}
function invokeSetterMethod($name, ReflectionClass $class_obj, $jsonMap, &$class_instance){
// 把 ProdId 转化成 Prod_Id
$filter_name = strtolower(preg_replace("/(?<=[a-z])([A-Z])/", "_$1", $name));
// 兼容 swoft
// 把 ProdId 转化成 prodId
$filter_name_ForSwoft = lcfirst($name);
$props = $class_obj->getProperties(ReflectionProperty::IS_PRIVATE);
foreach ($props as $prop) {
// 存在对应的私有属性
if(strtolower($prop->getName()) == $filter_name || $prop->getName() == $filter_name_ForSwoft){
$method = $class_obj->getMethod("set" . $name);
$args = $method->getParameters(); // 取出参数
if(count($args) == 1 && isset($jsonMap[$filter_name])){
// 对实例对象进行赋值(通过引用)
$method->invoke($class_instance, $jsonMap[$filter_name]);
}
}
}
}
- 关于反射类和实例化类的区别:new class() 是一个类对象封装后的展现,你不需要知道类的私有成员和方法,以及内部作用机制,便可以直接通过类开放的成员方法和属性来使用它
- 而 new ReflectionClass() 反射类则是一个类对象开封后的展现,它将类的内部属性、包括公开或私有的属性和方法,是否是静态,接口、继承、命名空间信息,甚至注释等全部公开,都可以通过反射 API 进行访问
- 由此可见反射类的强大之处。但通常使用反射在于编写业务更为复杂的底层逻辑。而对外的功能开发还是使用实例化类封装,也更安全便捷
- 反射机制本身就是解除封闭的手段,其用途大多在于底层或不对外业务开发上,比如接口文档自动生成、实现钩子。既然反射后类如此开放了,自然是可以任意修改的
2. 数据库;
2.1 基本配置;
- 参考:https://www.swoft.org/documents/v2/mysql/config/
- 修改文件:
App\bean.php
<?php
// 设置配置文件如下
db' => [
'class' => Database::class,
'dsn' => 'mysql:dbname=test;host=127.0.0.1',
'username' => 'root',
'password' => 'asdf',
'charset' => 'utf8mb4',
'config' => [
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
'timezone' => '+8:00',
'modes' => 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES',
'fetchMode' => PDO::FETCH_ASSOC
]
],
# 连接池主要作用:保护数据库
# 有内置连接池需要自己调控
'db.pool' => [
'class' => Pool::class,
'database' => bean('db'),
'minActive' => 10,
'maxActive' => 20,
'minWait' => 0,
'maxWaitTime' => 0
],
# 实际开发应该使用中间件,而非框架给的方法
# 切换数据源需要创建新的 db 和 db.pool
2.2 原生操作、查询构造器;
- 参考:https://www.swoft.org/documents/v2/mysql/origin/
- 新建
App/Http/Controller/Product2Controller.php
<?php
namespace App\Http\Controller;
use App\lib\ProductEntity;
use App\Model\Product;
use Swoft\Db\DB;
use Swoft\Http\Message\ContentType;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;
use Swoft\Validator\Annotation\Mapping\Validate;
/**
* @Controller(prefix="/product2")
*/
class Product2Controller{
/**
* 2.3 验证器:https://www.swoft.org/documents/v2/core-components/validator/
* 注解使用
* // @Validate(validator="product") // type="get"
* @RequestMapping(route="{pid}", params={"pid"="\d+"}, method={RequestMethod::GET, RequestMethod::POST})
*/
public function prod_detail(int $pid){
// 2.2.1 原生查询
// $product = DB::selectOne("SELECT * FROM product WHERE id = ?", [$pid]);
// 切换数据库
// $product = DB::db('test')->selectOne("SELECT * FROM product WHERE id = :pid", ["pid" => $pid]);
// 切换数据源
// $product = DB::query("db.pool")->getConnection()->selectOne("SELECT * FROM product WHERE id = :pid", ["pid" => $pid]);
// 2.2.2 查询构造器使用、关联表:https://www.swoft.org/documents/v2/mysql/query/
// 获取所有
// $product = DB::table("product")->get();
// 获取一条
// $product = DB::table("product")->where("id", "=", $pid)->select("id")->first();
// 关联表
// $product = DB::table("product")
// ->join("product_class", "product.id", "product_class.pid")
// ->select("product.*", "product_class.pid")
// ->where("product.id", $pid)
// ->first();
// 2.4 模型的使用
$product = Product::find($pid);
// 不影响界面展示,代码加入协程,加快取值过程
// 参考:https://www.swoft.org/documents/v2/basic-components/public-function/#heading1
if($product){
// 代码块 作用域
{
sgo(function () use ($product){
\Swoole\Coroutine::sleep(3);
// 指定字段递增1
$product->increment("prod_click");
});
}
{
sgo(function () use ($pid){
\Swoole\Coroutine::sleep(5);
// 每次商品被访问,都会增加商品访问日志
// 方法 1 : 模型方式插入
$product_view = ProductView::new();
{
$product_view->setProdId($pid);
$product_view->setViewIp(ip());
$product_view->setViewNum(1);
$product_view->save();
}
// 方法 2 : 数组方式插入
$pviewData = [
"prod_id" => $pid,
"view_ip" => ip(),
"view_num" => 1
];
$product_view = ProductView::new($pviewData)->save();
// 如果一个 ip 在当天访问过每个商品,只需要 view_num +1 就行
// ProductView::updateOrCreate() // 更新和创建过程中需要取数据,使用这个方法
// 仅仅执行新增或者插入 用以下方法,每次 view_num +1
ProductView::updateOrInsert(
[
"prod_id" => $pid,
"view_ip" => ip()
],
[
"view_num" => DB::raw("view_num+1")
]
);
});
}
}
/** //@var $product ProductEntity */
//$product = jsonForObject(ProductEntity::class);
//var_dump($product);
if(isGet()){
return response(ContentType::JSON)->withData($product);
}else if(isPost()) {
// 非注解使用:https://www.swoft.org/documents/v2/core-components/validator/#heading15
\validate(jsonForObject(),"product");
$product['prod_name'] = "修改产品" . request()->post('prod_name', 'none');
$product['prod_price'] = "修改价格" . request()->post('prod_price', 0);
return response(ContentType::JSON)->withData($product);
}
}
}
2.3 验证器;
- 注解使用
- 新建
App\Http\MyValidator\ProductValidator.php
<?php
namespace App\Http\MyValidator;
use Swoft\Validator\Annotation\Mapping\IsFloat;
use Swoft\Validator\Annotation\Mapping\IsString;
use Swoft\Validator\Annotation\Mapping\Length;
use Swoft\Validator\Annotation\Mapping\Min;
use Swoft\Validator\Annotation\Mapping\Max;
use Swoft\Validator\Annotation\Mapping\Validator;
/**
* 验证:https://www.swoft.org/documents/v2/core-components/validator/#heading4
* *** 遗留问题:空值判断
* @Validator(name="product")
*/
class ProductValidator{
/**
* @IsString(message="商品名称不能为空")
* @Length(min=5,max=20,message="商品名称长度为5-20")
* @var string
*/
protected $prod_name;
/**
* @IsString(message="商品短名称不能为空")
* @Length(min=2,max=20,message="商品名称长度为2-20")
* @var string
*/
protected $prod_sname;
/**
* @IsFloat(message="商品价格不能为空")
* @Min(value=20,message="价格最低20")
* @Max(value=1000,message="价格最高1000")
* @var float
*/
protected $prod_price;
}
2.4 模型的使用;
php ./bin/swoft entity:create --table=product,product_class --pool=db.pool --path=@app/Model
# 后期新加字段:定义私有变量后, PhpStorm 用 Alt + Ins 新生成 set 和 get 方法
2.5 场景练习(数据验证,主子订单入库,事务控制);
- 生成实体:
# 主订单表
php ./bin/swoft entity:create --table=orders_main --pool=db.pool --path=@app/Model
# 子订单表
php ./bin/swoft entity:create --table=orders_detail --pool=db.pool --path=@app/Model
- 修改
App\Model\OrdersMain.php
// Alt + ins 生成 set 和 get 方法
// 自己定义的属性不要打注解
// 这个属性在数据库里是没有的,仅仅是映射成对象
private $orderItems;
/**
* @return mixed
*/
public function getOrderItems()
{
return $this->orderItems;
}
/**
* @param mixed $orderItems
*/
public function setOrderItems($orderItems): void
{
$this->orderItems = $orderItems;
}
- 主表验证器:新建
App\Http\MyValidator\OrderValidator.php
<?php
namespace App\Http\MyValidator;
use App\Http\MyValidator\MyRules\OrderDetail;
use Swoft\Validator\Annotation\Mapping\IsArray;
use Swoft\Validator\Annotation\Mapping\IsFloat;
use Swoft\Validator\Annotation\Mapping\IsInt;
use Swoft\Validator\Annotation\Mapping\Min;
use Swoft\Validator\Annotation\Mapping\Max;
use Swoft\Validator\Annotation\Mapping\Validator;
/**
* @Validator(name="order")
*/
class OrderValidator{
// /**
// * @IsString(message="订单号不能为空")
// * @var string
// */
// protected $order_no;
/**
* @IsInt(message="用户ID不能为空")
* @Min(value=1,message="用户id不正确")
* @var int
*/
protected $user_id;
/**
* @IsInt(message="订单状态不能为空")
* @Min(value=0,message="状态不正确min")
* @Max(value=5,message="状态不正确max")
* @var int
*/
protected $order_status;
/**
* @IsFloat(message="订单金额不能为空")
* @Min(value=1,message="订单金额不正确")
* @var int
*/
protected $order_money;
// 订单明细数据,是一个数组,包换若干子订单实体
// 自定义验证器规则:https://www.swoft.org/documents/v2/core-components/validator/#heading7
/**
* @IsArray(message="订单明细不能为空")
* @OrderDetail(message="订单明细不正确")
* @var array
*/
protected $order_items;
}
- 副表验证器:新建 1
App\Http\MyValidator\OrderDetailValidator.php
<?php
namespace App\Http\MyValidator;
use Swoft\Validator\Annotation\Mapping\IsFloat;
use Swoft\Validator\Annotation\Mapping\IsInt;
use Swoft\Validator\Annotation\Mapping\IsString;
use Swoft\Validator\Annotation\Mapping\Min;
use Swoft\Validator\Annotation\Mapping\Max;
use Swoft\Validator\Annotation\Mapping\Validator;
/**
* @Validator(name="order_detail")
*/
class OrderDetailValidator{
/**
* @IsInt(message="商品ID不能为空")
* @Min(value=1,message="商品id不正确")
* @var int
*/
protected $prod_id;
/**
* @IsString(message="商品名称不能为空")
* @var string
*/
protected $prod_name;
/**
* @IsFloat(message="商品价格不能为空")
* @Min(value=0,message="商品价格不正确")
* @var int
*/
protected $prod_price;
/**
* @IsInt(message="折扣不能为空")
* @Min(value=1,message="折扣不正确min")
* @Max(value=10,message="折扣不正确max")
* @var int
*/
protected $discount;
/**
* @IsInt(message="商品数量不能为空")
* @Min(value=1,message="商品数量不正确")
* @var int
*/
protected $prod_num;
}
- 自定义验证器,新建 2
App\Http\MyValidator\MyRules\OrderDetail.php
<?php
namespace App\Http\MyValidator\MyRules;
/**
* @Annotation
* @Attributes({
* @Attribute("message",type="string")
* })
*/
class OrderDetail{
/**
* @var string
*/
private $message = '';
/**
* @var string
*/
private $name = '';
/**
* StringType constructor.
*
* @param array $values
*/
public function __construct(array $values)
{
if (isset($values['value'])) {
$this->message = $values['value'];
}
if (isset($values['message'])) {
$this->message = $values['message'];
}
if (isset($values['name'])) {
$this->name = $values['name'];
}
}
/**
* @return string
*/
public function getMessage(): string
{
return $this->message;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
}
- 自定义验证器,新建 3
App\Http\MyValidator\MyRules\OrderDetailParser.php
<?php
namespace App\Http\MyValidator\MyRules;
use Swoft\Annotation\Annotation\Mapping\AnnotationParser;
use Swoft\Annotation\Annotation\Parser\Parser;
use App\Http\MyValidator\MyRules\OrderDetail;
use Swoft\Validator\ValidatorRegister;
/**
* Class OrderDetailParser
* @AnnotationParser(annotation=OrderDetail::class)
*/
class OrderDetailParser extends Parser{
/**
* Parse object
*
* @param int $type Class or Method or Property
* @param object $annotationObject Annotation object
*
* @return array
* Return empty array is nothing to do!
* When class type return [$beanName, $className, $scope, $alias] is to inject bean
* When property type return [$propertyValue, $isRef] is to reference value
*/
public function parse(int $type, $annotationObject): array
{
if ($type != self::TYPE_PROPERTY) {
return [];
}
//向验证器注册一个验证规则
ValidatorRegister::registerValidatorItem($this->className, $this->propertyName, $annotationObject);
return [];
}
}
- 自定义验证器,新建 4
App\Http\MyValidator\MyRules\OrderDetailRule.php
<?php
namespace App\Http\MyValidator\MyRules;
use App\Http\MyValidator\MyRules\UserPass;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Validator\Contract\RuleInterface;
use Swoft\Validator\Exception\ValidatorException;
/**
* Class OrderDetailRule
* @Bean(OrderDetail::class)
*/
class OrderDetailRule implements RuleInterface {
/**
* @param array $data
* @param string $propertyName
* @param object $item
* @param mixed $default
*
* @return array
*/
public function validate(array $data, string $propertyName, $item, $default = null, $strict = false): array
{
$getData = $data[$propertyName];
if(!$getData || !is_array($getData) || count($getData) == 0){
throw new ValidatorException($item->getMessage());
}
foreach ($getData as $data) {
validate($data, "order_detail");
}
return $data;
}
}
- 新建
App/Http/Controller/OrderController.php
<?php
namespace App\Http\Controller;
use App\Exception\ApiException;
use App\Model\OrdersDetail;
use App\Model\OrdersMain;
use Swoft\Db\DB;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;
use Swoft\Task\Task;
use Swoft\Validator\Annotation\Mapping\Validate;
/**
* @Controller(prefix="/order")
*/
class OrderController{
/**
* 创建订单
* @Validate(validator="order")
* @RequestMapping(route="new",method={RequestMethod::POST})
*/
public function createOrder(){
// 获取 POST 数据做基本验证
/** @var OrdersMain $orderPost */
// 包含了主订单和子订单数据
$orderPost = jsonForObject(OrdersMain::class);
// 也可以使用菊花算法等
$orderNo = date("YmdHis") . substr(implode(NULL, array_map('ord',str_split(substr(uniqid(), 7, 13), 1))),0,8);
// ApiExceptionHandler 拦截
// throw new ApiException("api exception");
// 获取子订单数据,是一个数组
// 需要通过ORM方式插入,数组是不行的
// 需要一个函数把它映射成 ORM 实体
// 子订单数据([],[]),模型实体的数组
// 最后一个 true 参数代表返回一个数组,字段和数据库一致
$orderDetail_array = mapToModelsArray($orderPost->getOrderItems(), OrdersDetail::class, ["order_no" => $orderNo], true);
// var_dump($orderDetail_array);
// $orderPost->getModelAttributes() // 实体对象转数组
// 订单数据双双入库
if($orderPost){
// $orderPost->setOrderNo($orderNo);
// $orderPost->setCreateTime(date("Y-m-d H:i:s"));
$orderPost->fill(["order_no" => $orderNo, "create_time" => date("Y-m-d H:i:s")]);
// 加入事务: https://www.swoft.org/documents/v2/mysql/transaction/
// DB::beginTransaction();
// if($orderPost->save() && OrdersDetail::insert($orderDetail_array) ){
// DB::commit();
// return OrderCreateSuccess($orderNo);
// }else{
// DB::rollBack();
// }
tx(function () use ($orderPost, $orderDetail_array, $orderNo){
if($orderPost->save() && OrdersDetail::insert($orderDetail_array) ){
return OrderCreateSuccess($orderNo);
}
throw new \Exception("创建订单失败");
}, $result);
// 3.2.1 异步任务投递
// Task::async("orders", "putorderno", [$orderNo]);
// 3.2.2 放入延迟队列
Task::async("orders", "putorderno_zset", [$orderNo, 5]);
return $result;
}
return OrderCreateFail();
}
}
- 抛异常相关修改:
# 文件 App\Exception\Handler\HttpExceptionHandler.php
# 修改以下部分,通用异常在此抛出
if (!APP_DEBUG) {
//return $response->withStatus(500)->withContent($e->getMessage());
return $response->withStatus(500)->withData(["message" => $e->getMessage()]);
}
# 文件 App\Exception\Handler\ApiExceptionHandler.php
# 代码 throw new ApiException("api exception"); 进行以下输出
public function handle(Throwable $except, Response $response): Response
{
// $data = [
// 'code' => $except->getCode(),
// 'error' => sprintf('(%s) %s', get_class($except), $except->getMessage()),
// 'file' => sprintf('At %s line %d', $except->getFile(), $except->getLine()),
// 'trace' => $except->getTraceAsString(),
// ];
//
// return $response->withData($data);
return $response->withStatus(500)->withData([
"apimessage" => $except->getMessage()
]);
}
- 添加:
App/Helper/OrderFunctions.php
<?php
function OrderCreateSuccess($orderNo){
return ["status" => "success", "orderNo" => $orderNo];
}
function OrderCreateFail($msg="error"){
return ["status" => $msg, "orderNo" => ""];
}
function getInWhere($array) {
$where = "";
foreach ($array as $item){
if($where == ""){
$where .= "'" . $item . "'";
}else{
$where .= ",'" . $item . "'";
}
}
return $where;
}
3. Redis 相关;
3.1 Redis 配置使用;
- 基础配置:https://www.swoft.org/documents/v2/redis/config/
- 修改:
App\bean.php
'redis' => [
'class' => RedisDb::class,
'host' => '127.0.0.1',
'port' => 6379,
'password' => 'asdf',
'database' => 0,
'option' => [
//'serializer' => Redis::SERIALIZER_PHP
'serializer' => 0
]
],
'redis.pool' => [
'class' => Swoft\Redis\Pool::class,
'redisDb' => bean('redis'),
'minActive' => 2,
'maxActive' => 5,
'maxWait' => 0,
'maxWaitTime' => 0,
'maxIdleTime' => 60,
],
- 新建
App/Http/Controller/testController.php
<?php
namespace App\Http\Controller;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;
use Swoft\Redis\Redis;
/**
* Class testController
* @Controller(prefix="/test")
*/
class TestController{
/**
* @RequestMapping(route="/test",method={RequestMethod::GET})
* 另外:连接池注入:https://www.swoft.org/documents/v2/redis/usage/#heading1
*/
public function test(){
$redis = Redis::connection("redis.pool");
$v = $redis->get("name");
return $v;
}
}
3.2 场景练习(订单过期);
3.2.1 异步任务;
- 接上,主子订单入库是必须成功的。如果还有其它步骤,比如订单号插入 redis,这个部分是不需要让客户等待的。只要主子订单插入成功,就立马返回给客户端一个响应,插入 redis 可以使用协程或者异步任务来完成
- 这里使用异步任务:https://www.swoft.org/documents/v2/core-components/task/
- 新建
App/Task/OrderTask.php
<?php
namespace App\Tasks;
use Swoft\Redis\Redis;
use Swoft\Task\Annotation\Mapping\Task;
use Swoft\Task\Annotation\Mapping\TaskMapping;
/**
* Class OrderTask
* @Task(name="orders")
*/
class OrderTask{
/**
* @TaskMapping(name="putorderno")
*/
public function putOrderNoToRedis($orderNo){
// 过期时间 5 秒
Redis::setex("order" . $orderNo, 5, 1);
}
}
- key 过期触发事件监听:在压力不是特别大的时候可以用此方法,压力很大的情况下使用队列完成
- Redis 的 Keyspace 通知:https://redis.io/topics/notifications ,触发某些事件后可以向指定的频道发送通知
- 需要设置相关的事件,一旦 key 过期,就会往指定的频道发送一个 key 名,得到这个 key 之后,就可以做相应的处理
# 操作 1:
# 修改 Redis 配置文件 redis.conf,把 notify-keyspace-events Ex 前面的 “#” 去掉,保存退出,重启 redis
# 操作 2:使用 Redis 订阅发布功能
subscribe __keyevent@0__:expired
# 返回:
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
# 生成订单 10 秒以后 redis 过期,往频道里发送一个事件触发,控制台返回
1) "message"
2) "__keyevent@0__:expired"
3) "order2020022614311652575653"
3.2.2 用户进程;
- 代码实现,需要用到用户进程:https://www.swoft.org/documents/v2/core-components/process/#heading2
- 如果进程带的事情特别的复杂,特别耗机器性能,这个进程就要单独去做,不要直接嵌入在 Swoft 里面。如果进程比较简单(做订单监控),那可以嵌入在 HTTP Server 里面(在 Swoft 启动的时候,同时启动一个进程,就可以在进程做一些监听等相关事宜)
- 新建:
App/MyProcess/OrderProcess.php
<?php
namespace App\MyProcess;
use App\Model\OrdersMain;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Process\Process;
use Swoft\Process\UserProcess;
use Swoft\Redis\Redis;
/**
* Class OrderProcess
* @Bean()
*/
class OrderProcess extends UserProcess {
/**
* Run
*
* @param Process $process
*/
public function run(Process $process): void
{
// 官方说一定要加,其实不加也是可以的
// 如果不加,输出 abc 后,进程结束,然后再起进程输出 abc
// while(true){
// echo "abc\n";
// \Swoole\Coroutine::sleep(5);
// }
// 以下代码类似于阻塞,不需要 while
// 订单 10 秒过期,设置订单状态为过期
Redis::subscribe(["__keyevent@0__:expired"], function ($redis, $chan, $msg){
// echo "Channel: $chan\n";
// echo "Payload: $msg\n";
$orderNo = str_replace("order", "", $msg);
OrdersMain::where("order_no", $orderNo)
->update(["order_status" => -1]);
echo "订单号为" . $orderNo . "的订单过期" . PHP_EOL;
});
}
}
- 修改:
App\bean.php
// httpServer 里添加如下内容
'process' => [
// 3.2.2
// 'orderp' => bean(\App\MyProcess\OrderProcess::class)
// 3.2.3
'orderp' => bean(\App\MyProcess\OrderProcessBySet::class)
]
3.2.3 Redis 延迟队列;
- 之前使用 Redis 的订阅发布功能。在实际开发的时候,在订单量比较大的情况下,如果使用自动触发,会影响性能,所以就会使用到延迟队列方式来完成,并且使用客户端进程来进行轮询
- 使用 Redis 的有序集合的方式进行轮询。订单下单成功,则
zadd key score value
,其中 key 是固定值(比如 orderset),score 是时间戳值(time+5),value 是订单号 - 在进程里面就可以循环读取,一旦超过时间就把他删除
- 修改
App/Task/OrderTask.php
// 插入如下代码
/**
* @TaskMapping(name="putorderno_zset")
*/
public function putOrderNoToRedisZset($orderNo, $delay){
Redis::zAdd("orderset", [$orderNo => time() + $delay] );
echo "插入 redis_zset 成功" . PHP_EOL;
}
- Redis 相关操作
# 查看有序队列
zrange orderset 0 -1 withscores
zrangebyscore orderset -inf +inf withscores
- 新建:
App/MyProcess/OrderProcessBySet.php
<?php
namespace App\MyProcess;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Db\DB;
use Swoft\Process\Process;
use Swoft\Process\UserProcess;
use Swoft\Redis\Redis;
/**
* Class OrderProcessBySet
* @Bean()
*/
class OrderProcessBySet extends UserProcess {
/**
* Run
*
* @param Process $process
*/
public function run(Process $process): void
{
while (true){
$orders = Redis::zRangeByScore("orderset", '-inf', strval(time()), ["limit" => [0, 3]]);
if($orders && count($orders) > 0) {
DB::beginTransaction();
try {
// 批量执行
$rows = DB::update("UPDATE orders_main SET order_status = -1 WHERE order_no in (" . getInWhere($orders) . ")");
// 延展符:"..."
// 此处逻辑有问题,一旦触发 MySQL 回滚,Redis 没有回滚,数据不一致
if(Redis::zRem("orderset", ...$orders) === $rows){
throw new \Exception("orderset remove error");
}
DB::commit();
}catch (\Exception $exception){
echo $exception->getMessage();
DB::rollBack();
}
usleep(1000*500); // 500 毫秒
}
}
}
}