加了强类型如何将请求参数转为整型

最近公司项目,加了强类型,踩了不少坑,这次分享一下关于强制转换请求参数格式的问题。

背景

我们有很多接口,需要前端传$limit这个参数来限制请求数据条数,在Controller中获取参数

$params = $request->validate([
    'limit' => 'required|integer'
]);
$this->service->foo($params['limit']);

在Service中定义具体实现方法

public function foo(int $limit): array
{
    return [];
}

这样子看起来没啥问题,但实际运行,就会报错,Argument 1 passed to foo() must be of the type integer, string given。虽然我们在validate中限定了integer,但是实际上传过来的参数还是字符串,这样一旦加了强类型,方法就会报错了,那遇到这种情况该如何处理呢,总不能每次调用方法的时候都加一个(int)强制类型转换吧,下面我会介绍一下处理的几种方式。

中间件

我们接口有自定义了一些中间件,最开始的想法就只直接在中间件中强制转换一下$limit的类型

$params = $request->all();
foreach ($params as $key => $param) {
    if ('limit' != $key) {
        continue;
    }

    $request->request->set($key, intval($param));
}

这种做法,虽然解决了$limit的类型问题,但是有很多局限性,比如$quantity等其他参数也需要转换呢?而且这种写法也损耗性能。

重写ValidatesRequests

我们在基类Controller当中是有引用Illuminate\Foundation\Validation\ValidatesRequests这个trait的,既然我们的参数都要经过validate,那么我们就直接重写这个文件,在验证的时候,就进行类型转换

<?php

declare(strict_types=1);

namespace App\Http\Validation;

use Illuminate\Foundation\Validation\ValidatesRequests as BaseValidatesRequests;
use Illuminate\Http\Request;

trait ValidatesRequests
{
    use BaseValidatesRequests;

    /**
     * {@inheritdoc}
     */
    public function validate(Request $request, array $rules, array $messages = [], array $customAttributes = []): array
    {
        if (array_get($rules, 'limit')) {
            $request->request->set('limit', intval($request->limit));
        }

        return $this->getValidationFactory()->make(
            $request->all(), $rules, $messages, $customAttributes
        )->validate();
    }
}

这种做法也局限了参数,不同的参数需要写成一个数组,使用in_array()去判断,并不是一种好的实现方式,不过不需要每个请求都判断一次了,有validate才判断,性能损耗比上一种方式好一些。

为了更好的匹配不同参数,又进行了优化下,将验证后的数据取出,根据判断验证规则中是否包含integer规则来处理类型转换

$validated = $this->getValidationFactory()->make(
    $request->all(), $rules, $messages, $customAttributes
)->validate();
foreach ($rules as $key => $rule) {
    if (str_contains($rule, 'integer')) {
        $validated[$key] = intval($validated[$key]);
    }
}
return $validated

这样子,就不需要特殊定义需要转换的参数了。

重写ValidatorFactory

第三种实现方式其实与第二种类似,区别就是第一种方式是引用的trait,所以我们控制器中验证参数的时候就必须使用,$this->validate($request, []),而我们的代码中很多是使用$request->validate([]),这种方式的,为了不大量修改代码,所以换了一种实现方式。我们重写了ValidationServiceProvider,这个文件只是复制继承一下原文件,然后在配置文件app.php中使用我们自己定义的ValidationServiceProvider,最后我们需要重写Illuminate\Validation\Factory这个文件,去实现我们的需求。

public function make(array $data, array $rules, array $messages = [], array $customAttributes = []): \Illuminate\Validation\Validator
{
    $validator = parent::make($data, $rules, $messages, $customAttributes);

    $validator->after(function ($v): void {
        if (! $v->getMessageBag()->isEmpty()) {
            return;
        }

        $data = $v->getData();

        foreach (array_keys($v->getRules()) as $attribute) {
            if (! $v->hasRule($attribute, 'Integer')) {
                continue;
            }

            $value = array_get($data, $attribute, null);
            //只有当用户有传值进来时, 才转换
            if (null === $value) {
                continue;
            }

            $data[$attribute] =  $value;
        }

        $v->setData($data);
    });

    return $validator;
}

可以看出,三种不同的方式,一种种在改善,正如我们老大所说的,没有最完美的实现方式,我们能做的,就是不断的去完善自己的代码,不断的去code review。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 5

赞!

4年前 评论
zdg1992 (楼主) 4年前

如果我没记错的话 laravel 是都会帮你转成string 类型的 你应该去找找转换的地方……(我也不知道在哪 哈哈哈哈)

4年前 评论
zdg1992 (楼主) 4年前

PHP 弱类型,用强类型方式去写,意义何在?解决了什么问题?

对于 >= PHP7 引入的强类型,个人认为是在放弃优势。在为了强类型而强类型,或者其他强类型人的习惯。对语言本身开发项目没什么帮助(然并卵的功能)

4年前 评论
zdg1992 (楼主) 4年前

Illuminate\Foundation\Http\Middleware\TransformsRequest 中间件有一个 transform 方法可以转换请求参数

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Foundation\Http\Middleware\TransformsRequest as Transforms;

class TransformsRequest extends Transforms
{
    /**
     * Handle an incoming request.
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (!$request->hasHeader('Authorization') && $request->has('_token')) {
            $request->headers->set('Authorization', "Bearer $request->_token", true);
        }

        return parent::handle($request, $next);
    }

    protected function transform($key, $value)
    {
        if ($key == 'per_page') {
            return min($value, 20);
        }

        return $value;
    }
}
4年前 评论
zdg1992 (楼主) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!