TDD测试驱动开发笔记

教程

learnku社区 Laravel TDD

TDD 构建 Laravel 论坛笔记

Testing Laravel 单元测试入门笔记

总结

在使用 TDD 进行开发时,请牢记以下三项法则:

  1. 在编写失败的测试之前,不要编写任何业务代码;
  2. 只要有一个单元测试失败了,就不要再写测试代码。无法通过编译也是一种失败情况;
  3. 业务代码恰好能够让当前失败的测试成功通过即可,不要多写;

要掌握PHPUnit的用法,如何测试方法,如何测试类,等等…

根据需求结果逆向分析开发的方式。先假设已经实现了需求的方法,然后一步步倒退,缺少什么就创建什么。

需求结果 => 结果方法 => 按需(缺少创建)代码的元素

根据需求准备好测试用例数据

非常适合复杂逻辑需求的推理分析。对于简单的一下就能想出方案的需求,处于节省时间的考虑,可以不用。

测试覆盖率不一定要100%,个人想法,测试要首先覆盖核心复杂迭代频率较高主要业务,简单的业务可以跳过。

和普通开发方式一样,首先要对项目的整体需求和完整逻辑有详细了解,要有基本的解决方案思路

使用渐进式的编码思路,在mvc框架做业务开发时,可以先在控制器方法写出完整的逻辑,即使代码非常多,然后再根据需求迭代使用设计模式或面向对象设计方法,按需要拆分封装成类或方法,完成由面向过程=>面向对象的演变

项目案例说明

要实现一个产品型号搜索候选项功能

需要给前端提供一个http api ,并且要满足各种情景的搜索条件,如图

1.假设这个接口已经存在了,然后创建一个商品接口测试类,写好接口测试方法和各种情境下的测试用例数据。

namespace Tests\Feature;
class GoodsPcApiTest extends TestCase
{ 
/**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_goods_search_option()
    {
        //只有分隔符
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'-',
        ]);
       //使用dump打印结果是为了方便调试,调试完毕可以注释掉
        dump('只有分隔符', $response->json());
        $response->assertStatus(200);

        //只有产品型号
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'AC013',
        ]);
        dump('只有产品型号', $response->json());
        $response->assertStatus(200);

        //产品型号+1分隔符
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'AC013-',
        ]);
        dump('产品型号+1分隔符', $response->json());
        $response->assertStatus(200);

        //产品型号+1分隔符和参数
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'AC013-d',
        ]);
        dump('产品型号+1分隔符和1参数', $response->json());
        $response->assertStatus(200);
       ...
    }
...
}

然后运行测试,此时测试必然报错,就需要去创建这个接口的控制器和方法以及路由

//路由文件routes/api.php
Route::prefix('goods')->group(function () {
...
    Route::post('search-option', [GoodsController::class, 'searchOption']); //搜索候选项
...
});
//控制器
namespace App\Http\Controllers\Api;
class GoodsController extends Controller
{
  ...
  public function searchOption(Request $request)
    {   
        //验证参数
        $request->validate([
            'keywords'=>'bail|required|string',
        ]);
        //需要根据分隔符-拆分数据
        $keywords = $request->input('keywords');
        $attribute = array_filter(explode($sep, $keywords));
        //要从模型查询,直接调用,如果测试时报错模型不存在就去创建模型
        //如果运行测试报错数据表不存在,就创建数据迁移
        GoodsParamsSku::select()->where()->get();
        //需要一个方法或类来实现某个功能,先调用,然后运行测试,报错functionName方法不存在
        $this->functionName()
 
    }
  ...
}

根据报错创建上述functionName的单元测试

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class xxxxFunctionTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function test_function_name()
    {
        $this->assertTrue(true);
    }
}

在mvc框架中如何封装业务和功能代码

在laravel中如何使用repository模式

单一的CRUD使用ORM,没有封装的必要,比如banner的增删改查

将单一的功能封装成类,比如验证码的发送和验证,数学公式计算,

具有复杂逻辑的同一类需求,购物车整套逻辑, 商品SKU筛选,订单价格计算器(优惠,抵扣,积分等),这里需求可能需要查询多个模型,或者需要复杂的处理数据

一般在框架中封装成单个类放到services目录中

//验证码肯定具有两个方法,发送和验证
//这种固定方法的类可以使用静态方法方便调用
class VerificationCode
{
    const KEY_TEMPLATE = 'verify_code_of_%s';

    /**
     * 创建并存储验证码
     *
     * @param string $phone
     * @return int
     */
    public static function create($phone)
    {
        ...
    }

    /**
     * 检查手机号与验证码是否匹配.
     *
     * @param string $phone
     * @param int    $code
     *
     * @return bool
     */
    public static function validate($phone, $code)
    {
        ...
    }
}

第三方接口调用的封装

只有几个接口,封装成单个类,放到services目录中

需要调用很多接口,有场景和功能的划分,新建SDK目录,按接口功能类型或需求,使用设计模式(也可以不用)封装成简易SDK

一个项目示例

需要快速调用第三方接口完成业务需求,但是又没有顺手的sdk,或者过于复杂文档不全,学习成本过高,可以使用trait快速完成业务分割解耦,实现功能同时又易于维护。

使用http客户端guzzle,主文件sdkclient,实现签名认证参数组合 ,发送请求等方法。

<?php
/**
 * 契约锁SDK
 * https://open.qiyuesuo.com/document?id=2279605053767548928
 */

namespace App\Services\QiYueSuo;

use App\Exceptions\QiYueSuoException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Storage;

class SDKClient
{
   //使用trait 引用template业务相关接口
    use Template,Helps;

    public $config;

    public function __construct()
    {
        $this->config = config('services.qiyuesuo');
    }

    /**
     * 发送请求
     * 使用laravel封装的http客户端,DOC:https://learnku.com/docs/laravel/7.x/http-client/7487#introduction
     *
     * @param string $method 请求方法 'get', 'post', 'put', 'patch', 'delete'
     * @param string $subUrl 接口url,不包含域名
     * @param array $params 请求参数
     * @param string $content_type 请求类型 'asJson', 'asForm', 'asMultipart'
     * @param array $files 文件上传 $files['name']上传字段名称 $files['contents']文件内容,$files['filename']文件名,可选
     * @param bool $debug 是否开启debug日志,true是 false否
     * @return mixed
     */
    public function sendRequest(string $method, string $subUrl, array $params, string $content_type = '', array $files = [], bool $debug = false)
    {
        if (! in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) {
            throw new QiYueSuoException('Error Request Method');
        }

        if ($content_type && ! in_array($content_type, ['asJson', 'asForm', 'asMultipart'])) {
            throw new QiYueSuoException('Error Method');
        }

        $url = $this->config['api_host'].$subUrl;
        $headers = $this->playloadHeader();

        $http = Http::withHeaders($headers)->timeout($this->config['timeout']);

        //上传文件附件
        if ($files) {
            $http = $http->attach($files['name'], $files['contents'], $files['filename'] ?? null);
        }

        //请求类型
        if ($content_type) {
            $http = $http->$content_type();
        }

        //请求方法
        $response = $http->$method($url, $params);
        $headers = $response->headers();

        //记录debug日志
        if ($debug) {
            Log::channel('qiyueuso')
            ->info($url.'---->',
             [
                 'params' => $params,
                 'response_json'=>$response->json(),
                 'response_status' => $response->status(),
             ]);
        }

        //响应json
        if (isset($headers['Content-Type'][0]) && strpos($headers['Content-Type'][0], 'application/json') !== false) {
            $result_arr = $response->json();
        } else {
            abort(422, '契约锁,接口响应数据类型不是json');
        }

        //dd(json_encode($params), $result_arr);
        if (! $response->successful()) {
            //转换成422http异常
            abort(422, $result_arr['message']);
        } else {
            //状态码不是成功的响应错误信息
            if (isset($result_arr['code']) && $result_arr['code'] != 0) {
                abort(422, $result_arr['message']);
            } else {
                return $result_arr['result'] ?? [];
            }
        }
    }

    /**
     * 下载文件
     *
     * @param string $method 请求方法 get
     * @param string $subUrl 接口路径
     * @param array $params 请求参数
     * @param mixed $save_path 保存路径
     * @param string $disk
     * @return void
     */
    public function downloadFile(string $subUrl, array $params, string $save_path, string $disk = 'public')
    {
        $url = $this->config['api_host'].$subUrl;
        $headers = $this->playloadHeader();
        $response = Http::withHeaders($headers)
            ->timeout($this->config['timeout'])
            ->send(
                'get',
                $url,
                [
                    'query' => $params,
                    'save_to' => $save_path,
                ]
            );

        if (isset($headers['Content-Type'][0]) && strpos($headers['Content-Type'][0], 'application/json') !== false) {
            $result_arr = $response->json();

            if (isset($result_arr['code']) && $result_arr['code'] != 0) {
                //响应错误信息
                abort(422, $result_arr['message']);
            }
        }

        if ($response->successful()) {
            if ($save_path) {
                Storage::disk($disk)->put($save_path, $response->body());

                return $save_path;
            } else {
                return $response->body();
            }
        }

        return false;
    }

    protected function playloadHeader()
    {
        $headers['x-qys-open-timestamp'] = $this->timestamp();
        $headers['x-qys-open-accesstoken'] = $this->config['app_token'];
        $headers['x-qys-open-nonce'] = $this->nonce();
        $headers['x-qys-open-signature'] = $this->signature(
            $this->config['app_token'],
            $this->config['app_secret'],
            $headers['x-qys-open-timestamp'],
            $headers['x-qys-open-nonce']
        );

        return $headers;
    }

    private function timestamp()
    {
        $timestamp = time() * 1000;

        return $timestamp;
    }

    private function nonce()
    {
        return (string) Str::uuid();
    }

    private function signature($app_token, $app_secret, $timestamp, $nonce)
    {
        return md5($app_token.$app_secret.$timestamp.$nonce);
    }
}

使用trait解耦业务方法,模板管理相关业务,可以在方法中进行相关业务处理

<?php
/**
 * 模板管理
 */

namespace App\Services\QiYueSuo;

trait Template
{
    /**
     * 创建word模板
     * DOC:https://open.qiyuesuo.com/document?id=2784729992477544720
     *
     * @param array $params 请求参数
     * @param array $files 合同文件
     * @return array
     */
    public function v3TemplateCreatebyword(array $params, array $files)
    {
        return $this->sendRequest('post', '/v3/template/createbyword', $params, 'asMultipart', $files);
    }

    /**
     * 模板列表
     * DOC:https://open.qiyuesuo.com/document?id=2657160660600767485
     *
     * @param int $page 	  查询起始位置,默认为0 第几页
     * @param int $per_page   查询列表大小,默认1000
     * @param string $tenant_name  	查询条件:子公司名称,若传递了则查询子公司下的模板
     * @param string $modify_time_start  数据变更(创建、变更)时间的起始时间,格式为yyyy-MM-dd HH:mm:ss,默认无限制
     * @param string $modify_time_end  数据变更(创建、更新)时间的结束时间,格式为yyyy-MM-dd HH:mm:ss,默认无限制
     * @return array
     */
    public function v2TemplateList(int $page = 1, int $per_page = 15, string $tenant_name = '', string $modify_time_start = '', string $modify_time_end = '')
    {
        return $this->sendRequest('get', '/v2/template/list', [
            'selectOffset' => $page,
            'selectLimit' => $per_page,
            'tenantName' => $tenant_name,
            'modify_time_start' => $modify_time_start,
            'modify_time_end' => $modify_time_end,
        ]);
    }

    /**
     * 模板详情接口
     * DOC:https://open.qiyuesuo.com/document?id=2657160735708155950
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateDetail(int $templateId)
    {
        return $this->sendRequest('get', '/v2/template/detail', ['templateId'=> $templateId]);
    }

    /**
     * 编辑模板
     * DOC:https://open.qiyuesuo.com/document?id=2784730224355451710
     *
     * @param int $templateId 模板id
     * @param array $params
     *
     * @return array
     */
    public function v2TemplateEdit(int $templateId, array $params)
    {
        return $this->sendRequest('post', '/v2/template/edit',
            array_merge(['templateId' => $templateId], $params),
        );
    }

    /**
     * 下载模板
     * DOC:https://open.qiyuesuo.com/document?id=2784730224355451710
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateDownload(int $templateId)
    {
        return $this->downloadFile('/v2/template/download', ['templateId' => $templateId], false);
    }

    /**
     * 删除模板
     * DOC:https://open.qiyuesuo.com/document?id=2786490624780538307
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateRemove(int $templateId)
    {
        return $this->sendRequest('get', '/v2/template/remove', ['templateId' => $templateId]);
    }
}

以单一模型为主逻辑或功能封装到model中模型中封装方法过多时,可以用trait进行拆分

商品扣库存封装到商品模型中,

添加实体有很多关联表必须依赖主表,将添加主表数据和关联数据的方法封装到主表对应的模型中,举例:添加商品时,要添加商品规格,商品属性等等,将添加完整数据的方法,封装到商品模型中

更新商品的销量统计,需要查询其他关联表,在商品模型封装一个刷新统计的方法

Laravel中使用Repository的相关文章和总结

参考文章

在 Laravel 5.8 中正确地应用 Repository 设计模式

Laravel repository

Laravel 设计模式:Repository + Service 实战

关于 Repository 的设计模式

Laravel 中的存储库模式(Repository)

laravel5-repository 怎么拼接搜索条件

推荐:好用的 Laravel Repository 包

关于项目中 Repository 层的思考

为什么要在 Laravel 中使用存储库模式(Repository)?

在 Laravel 5 中使用仓库模式

使用该模式的项目

大商创电商

应用场景

抽象通用的查询,很多页面或接口,要展示相同内容,每次都要select筛选一堆字段和with一堆关联,还有加相同的查询条件,在需要的控制器中直接调用repository就能复用数据

todo,写个试用demo

正则表达式笔记

极客时间教程 php正则文档 转义序列(反斜线)符号表例如\n等

PHP正则语法

/foo bar/mode

分隔符可以是任意非字母数字、非反斜线、非空白字符。 静默忽略合法分隔符之前的空白字符。一般用/分割

方括号外的元字符

元字符描述
\一般用于转义字符
^断言目标的开始位置(或在多行模式下是行首)
$断言目标的结束位置(或在多行模式下是行尾)
.匹配除换行符外的任何字符(默认)
[开始字符类定义
]结束字符类定义
|开始一个可选分支
(子组的开始标记
)子组的结束标记
?作为量词,表示 0 次或 1 次匹配。位于量词后面用于改变量词的贪婪特性。 (查阅量词)
*量词,0 次或多次匹配
+量词,1 次或多次匹配
{自定义量词开始标记
}自定义量词结束标记
方括号外

方括号内的元字符(字符类

元字符描述
\转义字符
^仅在作为第一个字符(方括号内)时,表明字符类取反
标记字符范围
方括号内

模式修复符号

 模式修饰符中的空格,换行符会被忽略,其他字符会导致错误。

i (PCRE_CASELESS) 字母会进行大小写不敏感匹配。

m (PCRE_MULTILINE) 多行匹配,如果目标字符串 中没有 “\n” 字符,或者模式中没有出现 ^ 或 $,设置这个修饰符不产生任何影响。

s (PCRE_DOTALL)如果设置了这个修饰符,模式中的点号元字符匹配所有字符,包含换行符。如果没有这个 修饰符,点号不匹配换行符。这个修饰符等同于 perl 中的/s修饰符。 一个取反字符类比如 [^a] 总是匹配换行符,而不依赖于这个修饰符的设置。

x (PCRE_EXTENDED)如果设置了这个修饰符,模式中的没有经过转义的或不在字符类中的空白数据字符总会被忽略, 并且位于一个未转义的字符类外部的#字符和下一个换行符之间的字符也被忽略。 这个修饰符 等同于 perl 中的 /x 修饰符,使被编译模式中可以包含注释。 注意:这仅用于数据字符。 空白字符 还是不能在模式的特殊字符序列中出现,比如序列 (?( 引入了一个条件子组(译注: 这种语法定义的 特殊字符序列中如果出现空白字符会导致编译错误。 比如(?(就会导致错误)。

A (PCRE_ANCHORED)如果设置了这个修饰符,模式被强制为”锚定”模式,也就是说约束匹配使其仅从 目标字符串的开始位置搜索。这个效果同样可以使用适当的模式构造出来,并且 这也是 perl 种实现这种模式的唯一途径。

D (PCRE_DOLLAR_ENDONLY)如果这个修饰符被设置,模式中的元字符美元符号仅仅匹配目标字符串的末尾。如果这个修饰符 没有设置,当字符串以一个换行符结尾时, 美元符号还会匹配该换行符(但不会匹配之前的任何换行符)。 如果设置了修饰符m,这个修饰符被忽略.

S当一个模式需要多次使用的时候,为了得到匹配速度的提升,值得花费一些时间 对其进行一些额外的分析。如果设置了这个修饰符,这个额外的分析就会执行。当前, 这种对一个模式的分析仅仅适用于非锚定模式的匹配(即没有单独的固定开始字符)。

U (PCRE_UNGREEDY)这个修饰符逆转了量词的”贪婪”模式。 使量词默认为非贪婪的,通过量词后紧跟? 的方式可以使其成为贪婪的。这和 perl 是不兼容的。 它同样可以使用 模式内修饰符设置 (?U)进行设置, 或者在量词后以问号标记其非贪婪(比如.*?)。

注意:

在非贪婪模式,通常不能匹配超过 pcre.backtrack_limit 的字符。

X (PCRE_EXTRA)这个修饰符打开了 PCRE 与 perl 不兼容的附件功能。模式中的任意反斜线后就 ingen 一个 没有特殊含义的字符都会导致一个错误,以此保留这些字符以保证向后兼容性。 默认情况下,在 perl 中,反斜线紧跟一个没有特殊含义的字符被认为是该字符的原文。 当前没有其他特性由这个修饰符控制。

J (PCRE_INFO_JCHANGED)内部选项设置(?J)修改本地的PCRE_DUPNAMES选项。允许子组重名, (译注:只能通过内部选项设置,外部的 /J 设置会产生错误。) 自 PHP 7.2.0 起,也能支持 J 修饰符。

u (PCRE_UTF8)此修正符打开一个与 Perl 不兼容的附加功能。 模式和目标字符串都被认为是 UTF-8 的。 无效的目标字符串会导致 preg_* 函数什么都匹配不到; 无效的模式字符串会导致 E_WARNING 级别的错误。 5 字节和 6 字节的 UTF-8 字符序列以无效字符序列对待。

正则表达式中的常用术语

1) grep

最初是 ED 编辑器中的一条命令,用来显示文件中特定的内容。后来成为一个独立的工具 grep。

2) egrep

grep 虽然不断地更新升级,但仍然无法跟上技术的脚步。为此,贝尔实验室写出了 egrep,意为“扩展的 grep”。这大大增强了正则表达式的能力。

3) POSIX(Portable Operating System Interface of UNIX)

可移植操作系统接口。在 grep 发展的同时,其他一些开发人员也根据自己的喜好开发出了具有独特风格的版本。但问题也随之而来,有的程序支持某个元字符,而有的程序则不支持。因此,就有了POSIX。POSIX 是一系列标准,确保了操作系统之间的移植性。不过 POSIX 和 SQL 一样,没有成为最终的标准而只能作为一个参考。

4) Perl(Practical Extraction and Reporting Language)

实际抽取与汇报语言。1987 年,Larry Wall 发布了 Perl。在随后的 7 年时间里,从 Perl1 到现在的 Perl5,最终成为了 POSIX 之后的另一个标准。

5) PCRE

Perl 的成功,让其他的开发人员在某种程度上要兼容”Perl”,包括 C/C++、Java、Python 等都有自己的正则表达式。1997 年,Philip Hazel 开发了 PCRE 库,这是兼容 Perl 正则表达式的一套正则引擎,其他开发人员可以将 PCRE 整合到自己的语言中,为用户提供丰富的正则功能。许多软件都使用 PCRE,PHP 正是其中的一员。

PHP 有两套函数库支持的正则表达式处理操作:

  • 一套是由 PCRE(Perl Compatible Regular Expression)库提供、与 Perl 语言兼容的正则表达式函数,以preg_xxx为前缀函数
  • 另一套是 POSIX(Portable Operating System Interface)扩展语法正则表达式函数,以ereg_xxxx(mb扩展)为前缀的函数

Javascript中正则表达式是对象 JS正则表达式指南

区别

js正则:var patrn=/^[0-9]{1,20}$/;

php正则:$pattern='/(\d)(\d)/';

java正则:String pattern = “(\\D*)(\\d+)(.*)”;

java没有分解符,java中的转义字符是\\

JS和PHP和JAVA的正则表达式的区别规则大致一样,注意细微处的差别

常用正则表达式

php判断一个字符是否是汉字

参考

PHP正则表达式,看这一篇就够了

JS和PHP和JAVA的正则表达式的区别

数据迁移使用规范

目的,为了使命名更具有语义化和可读性,方便维护

以laravel框架为例,按实际操作行为划分

  • 添加 删除 修改(主要参考迁移功能,提供的api方法) 对应名称create drop modify
  • 2种以上的操作行为,都算修改
  • 给单一操作增加后缀,字段 column,索引 index,视图view, 存储过程 ,函数
  • 命名重复情况增加版本号 v1.0.0 …

操作表

表字段,标注注释,表索引

创建表

create_xxxx_table.php

删除表

drop_xxxx_table.php

修改表

包含添加删除字段或索引或其他 ,2种以上行为

modify_xxxx_table.php

操作字段

字段名称,字段数据类型,字段注释

添加字段

create_xxxx_table_column.php

删除字段

drop_xxxx_table_column.php

修改字段

包含添加和删除和修改字段,2种以上行为

modify_xxxx_table_column.php

操作索引

操作普通索引,全文索引,空间索引等;

索引优化是业务开发中,修改频率很高的行为,所以需要单独列出来,有很多场景需要,单独的变更索引,而不修改字段

添加索引

create_xxxx_table_index.php

删除索引

drop_xxxx_table_index.php

修改索引

包含添加 删除 修改 和自定义 2种以上行为时

modify_xxxx_table_index.php

单一操作行为的规则,参考上文以此类推。

多次重复行为时增加语义化版本后缀

需要改进

比如第一次业务变更 增加了字段 create_users_table_column.php

第二次业务变更,又要对users表增加字段,则命名为 create_users_table_column_v1.0.0.php

laravel8.x以上 可以使用压缩迁移

// 转储当前数据库架构并删除所有现有迁移。。。
php artisan schema:dump --prune

Guzzle使用经验总结

中文文档 英文文档

调用接口

主要功能,很多sdk都是使用该类库开发

写爬虫抓取页面

Laravel 下使用 Guzzle 编写多线程爬虫实战

项目中应用案例

java的古籍PC网站,该项目无人维护,无法提供书籍数据的接口。分析页面结构和接口使用guzzle库爬取书籍数据,完成数据对接。

在所用请求中共享cookie功能 文档

//创建客户端
$this->client = new Client([
    'base_uri' => $this->config['base_uri'],
    'timeout'  => 20.0,
    'cookies' => true, //共享cookie会话
);
//登录
protected function login()
{
    $response = $this->client->request('POST', 'XXX', [
            'headers' => [
                'Accept' => 'application/json'
            ],
            'form_params' => [
                'loginName' => $this->config['login_name'],
                'loginPassword' => $this->config['login_password']
            ]
        ]);

        $json = json_decode($response->getBody(), true);

        if (isset($json['operateMsg']) && $json['operateMsg'] !== '登录成功!') {
            throw new GujiException('原古籍系统账号故障');
        }
}

//请求接口数据
protected function request(string $pathUrl, array $param)
{
        $this->login(); //首先登录获取Cookies
        $response = $this->client->request('POST', $pathUrl, [
            'headers' => [
                'Accept' => 'application/json'
            ],
            'form_params' => $param
        ]);

        $contents = $response->getBody()->getContents();
        $json = json_decode($contents, true);

        if (json_last_error() === JSON_ERROR_NONE) {
            return $json;
        } elseif (json_last_error() == 10) {
            //解决json_decode错误Single unpaired UTF-16 surrogate in unicode escape
            $contents = \preg_replace('/(?<!\\\)\\\u[a-f0-9]{4}/iu', '', $contents);
            $json = \json_decode($contents, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                $json = $this->customJsonDecode($contents);
            }

            return $json;
        }
        {
            throw new GujiException("请求古籍系统接口失败");
        }
}

//抓取页面数据
protected function capture(string $pathUrl, array $param = [])
{
        $this->login(); //首先登录获取Cookies
        $response = $this->client->request('GET', $pathUrl, $param);

        if ($response->getStatusCode() == 200) {
            //获取页面内容
            return $response->getBody()->getContents();
        } else {
            throw new GujiException("古籍系统故障");
        }
}

跟随重定向

https://docs.guzzlephp.org/en/stable/faq.html#how-can-i-track-redirected-requests

https://docs.guzzlephp.org/en/stable/request-options.html#allow-redirects

调用非知名第三方支付系统,前后端分离架构,前端重定向到接口,接口调用第三方支付接口,成功后跟随响应到成功页面

use Illuminate\Support\Facades\Http;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

//Http::withOptions laravel对guzzle的封装,详情看文档
$response = Http::withOptions([
            'allow_redirects' => [
                'max'             => 1,
                'on_redirect'     => function (
                    RequestInterface $request,
                    ResponseInterface $response,
                    UriInterface $uri
                ) use ($data) {

                    //自动跟随重定向响应
                    header('Location:' . $uri);
                },
            ]
        ])->asForm()->post($this->config['base_uri'] . '/multipay/h5.do', $data);