Laravel Form Request 表单请求技巧分享
首先,如果您不了解 laravel 的表单请求,或者还没有试过水,请您先做好准备。 超级容易理解。
从本质上讲,我们使用 laravel 表单验证来检查向您的控制器传入的请求,但要从您的控制器中抽象出来,这比在控制器的方法中验证请求更为整洁。 这样就可以在抽象化验证规则后重用它们。Laravel 为您提供了 artisan 命令来创建表单请求验证:
php artisan make:request UpdatePostFormRequest
好了,让我们逐个分解此命令的输出的类来了解详细内容。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostFormRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => sprintf(
'required|string|unique:posts,title,%s', $this->post->title
),
'description' => 'required|min:8|max:255|string',
];
}
}
- authorize 方法用于确定用户是否已对此请求进行授权。 因此,它必须返回布尔值。
- Rules方法用于确定验证规则,稍后我们将使用该验证规则来验证传入的请求。
我们可以像这样使用:
<?php
namespace App\Http\Controllers;
use App\Post;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdatePostFormRequest;
class PostController extends Controller
{
public function edit(Post $post)
{
return view('posts.edit')->withPost($post);
}
/**
* @param UpdatePostFormRequest $request
*/
public function update(Post $post, UpdatePostFormRequest $request)
{
$post->update($request->all());
}
}
让我们在这里花几分钟来了解这里做了什么操作:
- 我们使用了 edit 方法来显示该表单,该表单将包含几个输入,以便用户更新该帖子。
- 我们使用 update 方法来更新文章
了解了这里的操作之后,让我们来具体了解一下 update 方法,因为它使用了表单请求验证。
很简单,如果验证出现问题,update 方法的主体不会执行,因为验证会抛出错误消息并重定向会上一个页面(在API处理的情况下,我们可能不需要这样做,因为重定向会导致您出现404错误。我们稍后会对这种情况进行了解)。
注意: 不要使用 $request->all()
,因为这消除了表单验证的好处,因为一旦通过验证阶段,您将使用所有提交的表单数据并将其插入数据库,并且这些输入可能包含一个隐藏的或不必要的输入,有可能会导致安全问题.
例如,假设用户在绕过客户端验证后使用了检查元素拦截了请求,并注入了一个 id 字段,如果您未在模型中定义可填充的字段,很可能会导致数据库异常。
相反,您可以使用 $request->validated()
,它仅获取验证通过的数据,只需将其插入数据库即可。
- Tip: 验证数据的工作原理是,通过表单请求中的 rules 方法返回的数组键名检索请求中的数据
/**
* Get the attributes and values that were validated.
*
* @return array
*
* @throws \Illuminate\Validation\ValidationException
*/
public function validated()
{
if ($this->invalid()) {
throw new ValidationException($this);
}
$results = [];
$missingValue = Str::random(10);
foreach (array_keys($this->getRules()) as $key) {
$value = data_get($this->getData(), $key, $missingValue);
if ($value !== $missingValue) {
Arr::set($results, $key, $value);
}
}
return $results;
}
好吧,我想您已经知道如何使用表单请求,让我们继续。
那有哪些高级用法呢?
- 怎样自定义提示消息?
- 动态处理授权
- API 验证的情况下,如何处理失败的验证和控制重定向。
- 授权失败怎么处理?
- 如何注入必填的数据,但是您不希望用户提交。
- 验证通过后如何自定义传递的值。
1. 自定义错误提示
在编码的某个时刻,您可能需要向用户显示一条自定义消息,使其更具可读性。 幸运的是,laravel通过 messages 方法允许您自定义。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostFormRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => sprintf(
'required|string|unique:posts,title,%s', $this->post->title
),
'description' => 'required|min:8|max:255|string',
];
}
public function messages()
{
return [
'title.unique' => 'title must be a unique.',
'description.min' => 'description minimum length bla bla bla'
];
}
}
您可以分解如上所述创建的每一个验证规则,并在需要时编写自定义消息。 此外,您可以使用本地化来显示消息,具体取决于用户使用的语言。
2. 动态授权
您可能已经注意到,我们在 authorize 方法中返回了一个硬编码的布尔值,该值目前没有实际意义。 那么,我们如何动态地控制它呢? 您可以使用通过身份验证的用户来做任何您想做的事情,并深入研究您的角色关系,并且在做完所有事情的最后,您必须返回一个布尔值。 或者,您可以只使用策略和policy,幸运的是,laravel是原生提供的。
您是否曾经考虑过在 app/Http/Kernel.php 文件 的 $routeMiddleware 数组上绑定中间件?
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's route middleware.
*
* These middlewares may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
我们在那里指定可以在 路由或控制器上使用的中间件。 如果您想扩展 Authenticatable 类,Laravel 提供了能够通过我们的用户模型使用中间件的功能。
<?php
namespace App\User;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
}
如果您有阅读 Illuminate\Foundation\Auth\User
的源码:
<?php
namespace Illuminate\Foundation\Auth;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword, MustVerifyEmail;
}
然后看看 Authorizable trait 的源码,你会看到:
<?php
namespace Illuminate\Foundation\Auth\Access;
use Illuminate\Contracts\Auth\Access\Gate;
trait Authorizable
{
/**
* Determine if the entity has a given ability.
*
* @param string $ability
* @param array|mixed $arguments
* @return bool
*/
public function can($ability, $arguments = [])
{
return app(Gate::class)->forUser($this)->check($ability, $arguments);
}
/**
* Determine if the entity does not have a given ability.
*
* @param string $ability
* @param array|mixed $arguments
* @return bool
*/
public function cant($ability, $arguments = [])
{
return ! $this->can($ability, $arguments);
}
/**
* Determine if the entity does not have a given ability.
*
* @param string $ability
* @param array|mixed $arguments
* @return bool
*/
public function cannot($ability, $arguments = [])
{
return $this->cant($ability, $arguments);
}
}
我们来看看 can 方法,它使用服务容器来解决我们之前讨论的 gate。 同样,它也需要返回布尔值。 这使得您可以轻松地做到这一点。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostFormRequest extends FormRequest
{
public function authorize()
{
return auth()->user()->can('update-post', $this->post);
}
}
- 注意: 为了能够正常获取当前通过身份验证的用户,请确保此路由受 auth中间件保护,以避免产生错误。否则您会对 null 使用 can 方法;
为了实现上述方法,您必须注册一个策略(policy)并定义一个名为 create-post
的网关(gate),并且它将调用包含您的逻辑的策略(policy)。
- Laravel 还拥有 artisan 命令,可为您创建策略(policy)。
php artisan make:policy PostPolicy
- 命名策略的约定是由策略关联的 {ModelName}。
<?php
namespace App\Policies;
use App\{User, Post};
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*
* @param \App\User $user
* @param \App\Post $post
* @return bool
*/
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
Laravel保留任何策略方法的第一个参数,并为您检索用户。 如果您需要任何其他模型,则可以将其作为其他参数传递给方法(在我们的情况下为 Post 模型)。
注意: 在一个真实的示例中,我热衷于使用声明式编程风格,这时可以像这样遵循。
public function update(User $user, Post $post)
{
return $user->is($post->user);
// or with extra check if we have roles the declarative way.
// return $user->is($post->user) && $user->roles->contains($roleModel);
}
这是更具声明性的方式,因为我们在这里没有放置任何冗余代码且无法重用的逻辑,因此应尽量避免这种情况。
- 注意:如果您不是
is
方法,则该方法用于确定两个模型是否具有相同的ID并属于同一表。
好的,我们根据自己的逻辑更新了 update
方法,那我们应该如何把它加入到策略中呢 ?
<?php
namespace App\Providers;
use App\Post;
use App\Policies\PostPolicy;
use Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];
/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', 'App\Policies\PostPolicy@update');
//
}
}
恭喜,现在我们可以实现之前所做的事情了,因为我们通过使用 can
方法将根据自己的逻辑编写的 update-post
方法绑定到了策略中。
3. API 请求时处理失败的验证和控制重定向.
默认情况下,只要未通过我们创建的任何验证规则,Laravel都会调用failValidation
方法。 如果我们研究一下 laravel 的表单请求提供的此方法,我们将理解为什么会重定向。
<?php
namespace Illuminate\Foundation\Http;
class FormRequest extends Request implements ValidatesWhenResolved
{
/**
* Handle a failed validation attempt.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function failedValidation(Validator $validator)
{
throw (new ValidationException($validator))
->errorBag($this->errorBag)
->redirectTo($this->getRedirectUrl());
}
}
嗯,我们在那里抛出了一个异常,并且进行了重定向,对于API请求来说,这非常烦人。 该如何解决呢?
让我们创建自己的表单请求,该请求继承了 laravel 的表单请求来重写此方法。
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
abstract class APIRequest extends FormRequest
{
/**
* Determine if user authorized to make this request
* @return bool
*/
public function authorize()
{
return true;
}
/**
* If validator fails return the exception in json form
* @param Validator $validator
* @return array
*/
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json(['errors' => $validator->errors()], 422));
}
abstract public function rules();
}
然后,在用于 API 的任何表单请求中,我们都可以使用此类代替 laravel 提供的表单请求。
4. 处理授权失败.
默认情况下,每当授权失败时 laravel 都会抛出异常,您可以在 app/Exceptions/Handler.php
的 render
方法处进行处理。
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* @param Exception $execption
* @return void
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if ($exception instanceof AuthorizationException) {
return response()->json([
'message' => $exception->getMessage(),
], 401);
}
return parent::render($request, $exception);
}
}
如果您不知道这里发生了什么,每当抛出异常时,laravel 就会通过 render 方法传递,然后呈现此异常,我们可以检查即将到来的异常并拦截该异常,并返回我们自己的错误提示或任何您想要的处理。 也可以 laravel 自己渲染错误提示页面。
那我们怎么知道抛出的异常是授权异常? 如果我们检查laravel的表单请求的源码,可以发现 failedAuthorization
将抛出此异常。
在还不清楚授权失败的原因时, 我们可以再次重写此方法,并自定义此异常,而不是将错误消息直接呈现给最终用户。
<?php
use Illuminate\Auth\Access\AuthorizationException;
class UpdatePostFormRequest extends FormRequest
{
public function failedAuthorization()
{
throw new AuthorizationException("You don't have the authority to update this post");
}
}
它不仅可以通过自定义异常错误提示,还可以随意处理所需的任何逻辑。
5. 如何注入必须经过验证但是又不希望用户提交的数据.
我记得很久以前我曾需要这样做,但是我现在不记得是什么场景了。 无论如何,我将向您展示如何实现这一目标,并且您可能会在特定情况下使用它。
Laravel 接收请求体和 query 的数据,并在其上应用验证规则,但它并没有直接这样做,而是通过 validationData
的方法验证。 在这里,我们可以覆盖这个方法并注入我们想要的任何数据,然后该方法将被调用,并且将 validationData
返回的内容应用于验证。
我们看看 laravel 的表单请求基类做了什么。
/**
* Get data to be validated from the request.
*
* @return array
*/
public function validationData()
{
return $this->all();
}
因为继承了具有 all 方法的 laravel 请求类,所以它可以使用 all 方法返回所有的请求数据。
我们可以通过重写此方法来利用它。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostFormRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => sprintf(
'required|string|unique:posts,title,%s', $this->post->title
),
'description' => 'required|min:8|max:255|string',
'user_id' => ''
];
}
protected function validationData()
{
return array_merge($this->all(), [
'user_id' => $this->user()->id
]);
}
}
我们将 user_id 添加到了验证规则中,该用户肯定不会通过验证,即使用户传递 user_id 通过验证,我们也将使用当前已验证的用户ID覆盖它。
- 提示: 验证规则为空字符串值意味着没有对该键进行验证, 但是它可以存在,我们可以在使用只获取通过验证的数据的方法时使用它(即:
$request->validated()
, 这个方法只获取我们 rules 方法中定义的数据键名对应的值)
6. 验证通过后如何自定义传递的请求值。
我估计您可能已经猜到了,因为我们有通过验证的方法,可以重写它并将通过验证的原始数据与我们想要的内容合并。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostFormRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => sprintf(
'required|string|unique:posts,title,%s', $this->post->title
),
'description' => 'required|min:8|max:255|string',
];
}
public function validated()
{
return array_merge(parent::validated(), [
'user_id' => $this->user()->id
]);
}
}
此时,用户没有传递 user_id,当我们使用 validated 方法时,会调用此处创建的validated方法,该方法将覆盖 laravel 的默认方法。 我们获取验证通过的值并将其与 user_id 合并,这样我们便自动注入了 user_id ,我们可以使用它直接在数据库中更新/创建记录,而无需准备/关联/附加。
好了,就是表单请求了。 我希望你喜欢它 ✌️
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
这个方法学习到了 :+1: