timacdonald / has-parameters
一个特质,允许你以更PHP风格的方式向Laravel中间件传递参数。
Requires
- php: ^8.1
- illuminate/http: ^10.0 || ^11.0
- illuminate/support: ^10.0 || ^11.0
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0
- phpunit/phpunit: ^10.0 || ^11.0
README
参数化
Laravel中间件的特质,允许你以更PHP风格的方式传递参数,包括作为键 => 值对的命名参数,以及作为列表的变长参数。改进了静态分析/IDE支持,允许你通过引用参数名称来指定参数,可以跳过可选参数(将回退到其默认值),并添加了一些验证,以防止你意外忘记任何必需的参数。
更多关于原因的信息,请参阅我的博客文章 重新思考Laravel的中间件参数API
安装
你可以使用 composer 来安装
composer require timacdonald/has-parameters
基本用法
为了开始使用示例,我将使用Laravel的ThrottleRequests
的一个简化版本。首先,将HasParameters
特质添加到你的中间件中。
class ThrottleRequests { use HasParameters; public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '') { // } }
现在,你可以使用静态的with()
方法,使用参数名称作为键来向这个中间件传递参数。
Route::stuff() ->middleware([ ThrottleRequests::with([ 'maxAttempts' => 120, ]), ]);
一开始你可能觉得这有点冗长,但我觉得在阅读这些文档并亲自尝试之后,你会喜欢这个完整的特性集。
Middleware::with()
静态的with()
方法允许你在声明中间件时轻松地看到哪些值代表什么,而不是仅仅声明一个逗号分隔的值列表。键的顺序并不重要。特质会将键与handle()
方法中的参数名称配对。
// before... Route::stuff() ->middleware([ 'throttle:10,1' // what does 10 or 1 stand for here? ]); // after... Route::stuff() ->middleware([ ThrottleRequests::with([ 'decayMinutes' => 1, 'maxAttempts' => 10, ]), ]);
跳过参数
如果handle
方法中的任何参数有默认值,你不需要传递它们,除非你想改变它们的值。例如,如果你想只为ThrottleRequests
中间件指定一个前缀,但保留$decayMinutes
和$maxAttempts
的默认值,你可以这样做...
Route::stuff() ->middleware([ ThrottleRequests::with([ 'prefix' => 'admins', ]), ]);
正如我们在之前的handle
方法中看到的,$decayMinutes
的默认值是1
,而$maxAttempts
的默认值是60
。中间件将接收到这些参数的值,但现在将接收到"admins"
作为$prefix
。
数组用于变长参数
当你的中间件以变长参数结尾时,你可以传递一个包含值数组的变长参数键。看看下面的handle()
方法。
public function handle(Request $request, Closure $next, string $ability, string ...$models)
这是我们将如何向变长的$models
参数传递值列表的方法...
Route::stuff() ->middleware([ Authorize::with([ 'ability' => PostVideoPolicy::UPDATE, 'models' => [Post::class, Video::class], ]), ]);
参数别名
某些中间件将根据传递给特定参数的值的类型有不同的行为。例如,Laravel的ThrottleRequests
中间件允许你将速率限制器的名称传递给$maxAttempts
参数,而不是一个数值,以便在端点上利用该命名限制器。
// a named rate limiter... RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); }); // using the rate limiter WITHOUT an alias... Route::stuff() ->middleware([ ThrottleRequests::with([ 'maxAttempts' => 'api', ]), ]);
在这种场景下,能够将$maxAttempts
参数名称别名为更易读的名称会很好。
Route::stuff() ->middleware([ ThrottleRequests::with([ 'limiter' => 'api', ]), ]);
为了实现这一点,你可以在你的中间件中设置一个参数别名映射...
class ThrottleRequests { use HasParameters; public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '') { // } protected static function parameterAliasMap(): array { return [ 'limiter' => 'maxAttempts', // 'alias' => 'parameter', ]; } }
验证
这些验证发生在加载或编译路由文件时,而不仅仅是当你访问包含声明的路由时。
意外的参数
确保你不在 handle()
方法中声明任何不存在的参数变量键。这有助于确保你不会误输参数名。
必需参数
确保所有必需参数(没有默认值的参数)都已提供。
别名
- 确保所有指定的别名都引用了一个现有的参数。
- 提供的别名不引用相同的参数。
- 原始参数键和别名没有同时提供。
Middleware::in()
静态的 in()
方法在很大程度上反映了现有的连接 API,并且以相同的方式工作。它接受一个值列表,即非关联数组。如果你的 handle()
方法是一个单个可变参数,即期望一个值列表,你应该使用此方法,如下面的中间件处理方法所示...
public function handle(Request $request, Closure $next, string ...$states) { // }
你可以像这样将一个“状态”列表传递给中间件...
Route::stuff() ->middleware([ EnsurePostState::in([PostState::DRAFT, PostState::UNDER_REVIEW]), ]);
验证
必需参数
就像 with()
方法一样,in()
方法将验证你是否已传递足够多的值来覆盖所有必需的参数。因为可变参数不需要传递任何值,所以你只有在应该使用 with()
方法时才会真正遇到这个问题。
值转换
你应该记住,所有内容仍然会被转换成字符串。虽然你传递的是整数,但中间件本身始终会接收一个字符串。这是 Laravel 为了实现路由缓存而在底层将所有内容转换为字符串的方式。
需要注意的是,false
实际上会被转换为字符串 "0"
,以保持与将 true
转换为字符串 "1"
的一致性。
类型值
你可以通过在你的中间件类上使用文档块来提供更强的类型信息。以下是如何创建一个强类型中间件的示例
/** * @method static string with(array{ * maxAttempts?: int, * decayMinutes?: float|int, * prefix?: string, * }|'admin' $arguments) */ class ThrottleMiddleware { use HasParameters; // ... }
然后你将接收到来自你的语言服务器的自动完成和诊断
ThrottleMiddleware::with('admin'); // ✅ ThrottleMiddleware::with(['decayMinutes' => 10]); // ✅ ThrottleMiddleware::with('foo'); // ❌ fails because 'foo' is not in the allowed string values ThrottleMiddleware::with(['maxAttempts' => 'ten']); // ❌ fails because `maxAttempts` must be an int
请查看 PHPStan playground 中的示例。
鸣谢
特别感谢 Caneco 为其标志提供支持(vegi)✨
致谢
你可以自由使用这个包,但我要求你联系某人(不是我自己),他们以前或目前正在维护或为你在项目中使用的开源库做出贡献,并感谢他们的工作。请考虑你的整个技术栈:包、框架、语言、数据库、操作系统、前端、后端等。