Laravel: Глубокое погружение в FormRequest
FormRequest
Чтобы создать FormRequest
для страницы входа в систему, выполним:
php artisan make:request LoginRequest
Далее обновляем сгенерированный класс следующим образом:
class LoginRequest extends FormRequest
{
public function rules()
{
return [
'email' => 'required|string',
'password' => 'required|string',
];
}
}
Для использования достаточно добавить его к соответствующему методу в контроллере:
class LoginController extends Controller
{
public function store(LoginRequest $request)
{
// Ваш код
}
}
Все просто и понятно!
Далее мы рассмотрим некоторые его дополнительные возможности.
Метод: validated
Если вы хотите получить доступ ко всем ключам и значениям, заданным в методе rules
, есть простой способ сделать это.
Вместо того чтобы извлекать их по отдельности, можно поступить следующим образом:
Post::create([
'title' => $request->title,
'body' => $request->body,
'published_at' => $request->published_at,
]);
Можно просто использовать метод validated()
:
Post::create($request->validated());
Метод: afterValidation
Класс FormRequest
позволяет вносить изменения на лету
, что помогает поддерживать чистоту других классов. Рассмотрим работу с массивом тегов:
В правилах:
public function rules()
{
return [
'title' => 'required|string',
'body' => 'required|string',
'published_at' => 'datetime',
];
}
Синхронизация тегов с нашим постом традиционно выглядит следующим образом:
public function store(StorePostRequest $request) {
$post = Post::create($request->only('title', 'body','published_at'));
if($request->has('tags')) {
$post->tags()->attach(explode(',', $request->tags));
}
}
Обратите внимание, как мы разделили теги, чтобы прикрепить каждый из них к сообщению? Мы можем сделать это непосредственно в FormRequest
с помощью метода afterValidation
:
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|string',
'body' => 'required|string',
'published_at' => 'datetime',
];
}
protected function passedValidation() {
$this->merge([
'tags' => $this->tags ? explode(',', $this->tags) : []
]);
}
}
При такой настройке после валидации теги уже будут иметь формат массива. Это упрощает код контроллера:
public function store(StorePostRequest $request) {
$post = Post::create($request->only('title', 'body','published_at'));
$post->tags()->attach($request->tags);
}
Метод: prepareForValidation
Используя метод prepareForValidation
, мы можем корректировать вводимые данные непосредственно перед тем, как FormRequest
начнёт процесс проверки. Для наших тегов мы хотим, чтобы они были целыми числами.
Сначала настроим ввод тегов:
protected function prepareForValidation() {
$this->merge([
'tags' => $this->tags ? explode(',', $this->tags) : []
]);
}
Теперь обновлённый класс FormRequest
должен иметь вид:
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|string',
'body' => 'required|string',
'published_at' => 'datetime',
'tags' => 'array',
'tags.*' => 'integer',
];
}
protected function prepareForValidation() {
$this->merge([
'tags' => $this->tags ? explode(',', $this->tags) : []
]);
}
}
Контроллер, несмотря на эти изменения, по-прежнему будет рассматривать теги как массив:
public function store(StorePostRequest $request) {
$post = Post::create($request->only('title', 'body','published_at'));
$post->tags()->attach($request->tags);
}
Метод: user
Из класса запроса можно напрямую обратиться к аутентифицированному пользователю:
public function store(StorePostRequest $request) {
$post = $request->user()->posts()->create(
$request->only('title', 'body','published_at')
);
}
Этот метод получает данные о текущем аутентифицированном пользователе, что облегчает привязку новых сообщений к нему.
Понимание валидации
Когда вы используете FormRequest
в сигнатуре метода, Laravel автоматически обрабатывает валидацию за вас. Это означает, что вам не нужно вручную вызывать $request->validate()
в контроллере. Давайте разберёмся, как происходит эта магия.
Для анализа компонента валидации хорошей отправной точкой является поиск случаев использования метода rules
из нашего класса StorePostRequest
.
Вы можете быть удивлены, обнаружив, что функция PHPStorm "find usages" не обнаруживает использования этого метода непосредственно в нашем пользовательском классе FormRequest
. Но если поискать его в родительском классе FormRequest
, то можно обнаружить, что он используется в методе createDefaultValidator
.
protected function createDefaultValidator(ValidationFactory $factory)
{
$rules = method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : [];
$validator = $factory->make(
$this->validationData(), $rules,
$this->messages(), $this->attributes()
)->stopOnFirstFailure($this->stopOnFirstFailure);
if ($this->isPrecognitive()) {
$validator->setRules(
$this->filterPrecognitiveRules($validator->getRulesWithoutPlaceholders())
);
}
return $validator;
}
Класс FormRequest использует сервис-контейнер Laravel при вызове метода rules
. Такой подход выгоден тем, что позволяет выполнять внедрение зависимостей непосредственно в метод rules
, то есть любой необходимый сервис или класс может быть автоматически разрешён и внедрён.
Теперь посмотрим, как используется createDefaultValidator
protected function getValidatorInstance()
{
if ($this->validator) {
return $this->validator;
}
$factory = $this->container->make(ValidationFactory::class);
if (method_exists($this, 'validator')) {
$validator = $this->container->call([$this, 'validator'], compact('factory'));
} else {
$validator = $this->createDefaultValidator($factory);
}
if (method_exists($this, 'withValidator')) {
$this->withValidator($validator);
}
if (method_exists($this, 'after')) {
$validator->after($this->container->call(
$this->after(...),
['validator' => $validator]
));
}
$this->setValidator($validator);
return $this->validator;
}
При валидации с помощью FormRequest
Laravel выполняет ряд шагов. Сначала Laravel проверяет, есть ли в FormRequest
собственный класс Validator
; если нет, то используется createDefaultValidator
. Затем Laravel проверяет, есть ли какие-либо последующие хуки, которые необходимо запустить.
Продолжая исследование, мы видим, что вызывается getValidatorInstance
.
Далее расположен трейт ValidatesWhenResolvedTrait
. Этот трейт применяется ко всем FormRequest
и содержит методы для проверки входных данных при определении класса FormRequest
.
public function validateResolved()
{
$this->prepareForValidation();
if (! $this->passesAuthorization()) {
$this->failedAuthorization();
}
$instance = $this->getValidatorInstance();
if ($this->isPrecognitive()) {
$instance->after(Precognition::afterValidationHook($this));
}
if ($instance->fails()) {
$this->failedValidation($instance);
}
$this->passedValidation();
}
Здесь мы сталкиваемся с методами prepareForValidation
и passedValidation
, о которых мы говорили ранее.
Перейдём к рассмотрению того, как используется метод validateResolved()
.
namespace Illuminate\Foundation\Providers;
class FormRequestServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
$resolved->validateResolved();
});
$this->app->resolving(FormRequest::class, function ($request, $app) {
$request = FormRequest::createFrom($app['request'], $request);
$request->setContainer($app)->setRedirector($app->make(Redirector::class));
});
}
}
Действительно, в FormRequestServiceProvider
Laravel устанавливает хук afterResolving
, запускающий трейт ValidatesWhenResolved
для вызова метода validateResolved
, который эффективно выполняет валидацию.
Это и есть основной процесс выполнения валидации FormRequest
при включении его в метод контроллера.