Swoft 2.x 基础(HTTP、数据库、Redis)

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 中间件;

<?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 基本配置;

<?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 原生操作、查询构造器;

<?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 配置使用;

 '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 毫秒
            }
        }
    }
}
发布了125 篇原创文章 · 获赞 13 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/hualaoshuan/article/details/104306759
今日推荐