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;
}
}
Вот несколько способов валидации данных. Всё верно, и всё можно протестировать. Каким способом вы предпочитаете обрабатывать свою валидацию? У вас есть способ, не упомянутый здесь или в документации? Напишите о нём.