Laravel: Валидация данных приложения

Источник: «Valid validators validating data»
Валидация обязательна для любого современного проекта, и в Laravel очень просто начать. Внутри методов контроллера вы можете вызвать метод, передать запрос и массив правил, на соответствие которыми хотите проверить данные.

Это правильный подход? Или это не правильный подход? В этом подходе нет ничего плохого; он работает и тестируется. Важно помнить, что хотя его можно улучшить, но возможно ему не требуется улучшение.

В этом руководстве я проведу вас через своё путешествие по валидации Laravel, расскажу какие изменения я внёс и почему. Давайте начнём с самого начала.

Когда я начал изучать Laravel, я делал то, что мне говорила документация, просто и понятно. Я бы расширил app/Http/Controller и вызвал $this->validate в этом месте. Мои контроллеры были объёмными. Мой типичный метод store был похож на следующий, модернизированный для текущего синтаксиса:

namespace App\Http\Controllers\Api;

class PostController extends Controller
{
public function store(Request $request): JsonResponse
{
$this->validate($request, [
'title' => 'required|string|min:2|max:255',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
]);

$post = Post::query()->create(
attributes: [
...$request->validated(),
'user_id' => auth()->id(),
],
);

return new JsonResponse(
data: new PostResource(
resource: $post,
),
status: Http::CREATED->value,
);
}
}

Помимо логики создания нет ничего плохого в том, как работает эта валидация. Я могу её протестировать и управлять ею. И я знаю, что она проверяет, так как мне это нужно. Так что, если ваша валидация выглядит так, хорошая работа!

Затем я перешёл к invokable контроллерам, так как предпочёл чтобы всё было проще — на тот момент это выглядело также, просто с методом __invoke вместо store.

namespace App\Http\Controllers\Api\Posts;

class StoreController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$this->validate($request, [
'title' => 'required|string|min:2|max:255',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
]);

$post = Post::query()->create(
attributes: [
...$request->validated(),
'user_id' => auth()->id(),
],
);

return new JsonResponse(
data: new PostResource(
resource: $post,
),
status: Http::CREATED->value,
);
}
}

Затем я обнаружил, насколько полезными были Form Request, и как мне помогло инкапсулирование моей валидации в эти классы. После этого мой контроллер снова изменился. На этот раз он выглядел так:

namespace App\Http\Controllers\Api\Posts;

class StoreController
{
public function __invoke(StoreRequest $request): JsonResponse
{
$post = Post::query()->create(
attributes: [
...$request->validated(),
'user_id' => auth()->id(),
],
);

return new JsonResponse(
data: new PostResource(
resource: $post,
),
status: Http::CREATED->value,
);
}
}

Мне больше не нужно было расширять базовый контроллер, так как мне не нужен метод валидации. Я мог бы легко ввести запрос формы в свой метод invoke контроллера, и все данные будут предварительно проверяться. Это сделало мои контроллеры очень маленькими и лёгкими, так как я перенёс валидацию в отдельный класс. Мой Form Request будет выглядеть примерно так:

namespace App\Http\Requests\Api\Posts;

class StoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

public function rules(): array
{
return [
'title' => ['required', 'string', 'min:2', 'max:255',]
'content' => ['required', 'string'],
'category_id' => ['required', 'exists:categories,id'],
];
}
}

Какое-то время я придерживался этого стиля валидации, опять, в этом нет ничего плохого. Если ваша валидация выглядит так же — хороша работа! Опять, этот код масштабируемый, тестируемый и воспроизводимый. Вы можете внедрять его везде, где используете HTTP-запросы и нужна валидация данных.

Но куда двигаться дальше? Как можно его улучшить? Это вопрос, который я задал себе и застрял на довольно долгое время. Позвольте объяснить сценарий заставивший меня усомниться в этом подходе.

Представьте, что у вас есть проект позволяющий создавать сообщения через API, веб интерфейс, и возможно командную строку. API и веб интерфейсы могут совместно использовать запрос формы, так как оба могут быть внедрены в контроллеры. Как насчёт командной строки? Нужно ли повторять валидацию для неё? Кто-то может возразить, что не нужно проверять командную строку в той же степени, но всё равно нужна некая валидация данных.

Некоторое время я обдумывал идею валидаторов. В этом нет ничего нового поэтому я понятия не имею, почему понадобилось столько времени, чтобы понять это! Валидаторы, по крайней мере для меня, были классами содержащими правила и информацию необходимые для проверки любого запроса — HTTP или иного. Позвольте показать как это может выглядеть:

namespace App\Validators\Posts;

class StoreValidator implements ValidatorContract
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:2', 'max:255',]
'content' => ['required', 'string'],
'category_id' => ['required', 'exists:categories,id'],
];
}
}

Всё начинается просто, я хотел централизовано хранить правила валидации. Оттуда я мог расширять их по мере необходимости.

namespace App\Validators\Posts;

class StoreValidator implements ValidatorContract
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:2', 'max:255',]
'content' => ['required', 'string'],
'category_id' => ['required', 'exists:categories,id'],
];
}

public function messages(): array
{
return [
'category_id.exists' => 'This category does not exist, you Doughnut',
];
}
}

Я добавил сообщения, когда хотел настроить сообщения валидации. Я мог бы добавить больше методов, инкапсулировать больше логики проверки. Но как это выглядит на практике? Вернёмся к примеру Store Controller. Контроллер будет выглядеть так же, как когда мы перенесли валидацию, поэтому давайте посмотрим на запрос формы:

namespace App\Http\Requests\Api\Posts;

class StoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

public function rules(): array
{
return (new StoreValidator())->rules();
}
}

Вот так просто я могу переключить массив правил валидации застрявший в классе, и заменить его классом, специфичным для того как мы хотим хранить и проверять эту информацию.

Я видел и другой подход, который считаю и хорошим, и плохим. Позвольте рассказать о нём. Я видел, как некоторые разработчики хранят правила валидации в моделях Eloquent. Теперь я на 100% уверен в том, что мы немного смешиваем цели — однако, это также гениально. Поскольку то, что вы хотите сделать, — это сохранить правила, касающиеся того, как эта Модель создаётся внутри самой модели. Она знает свои собственные правила. Это будет выглядеть примерно так:

namespace App\Models;

class Post extends Model
{
public static array $rules = [
'title' => ['required', 'string', 'min:2', 'max:255',]
'content' => ['required', 'string'],
'category_id' => ['required', 'exists:categories,id'],
];

// Оставшаяся часть модели здесь.

Правила можно легко использовать в запросе формы и они остаются в вашей модели, так что вы можете управлять ими из одной центральной точки в классе, который содержит их.

namespace App\Http\Requests\Api\Posts;

class StoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}

public function rules(): array
{
return Post::$rules;
}
}

Вот несколько способов валидации данных. Всё верно, и всё можно протестировать. Каким способом вы предпочитаете обрабатывать свою валидацию? У вас есть способ, не упомянутый здесь или в документации? Напишите о нём.

Дополнительные материалы

Предыдущая Статья

Laravel: Что такое Pipeline / Пайплайн

Следующая Статья

XSS: Использование уязвимостей