Руководство по валидации в Laravel

Источник: «The ultimate guide to Laravel Validation»
Валидация — важная часть любого веб-приложения. Она помогает предотвратить уязвимости в системе безопасности, повреждение данных и множество других проблем, которые могут возникнуть при работе с пользовательским вводом.

Рассмотрим, что такое валидация и почему она важна. Затем сравним валидацию на стороне клиента с валидацией на стороне сервера и рассмотрим, почему никогда не следует полагаться только на валидацию на стороне клиента в приложениях.

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

Что такое валидация

Валидация — процесс проверки достоверности данных перед попыткой их использования. Это может быть что угодно: от проверки таких простых вещей, как наличие обязательного поля в запросе, до более сложных проверок, таких как соответствие поля определённому шаблону или его уникальность в базе данных.

Как правило, при валидации данных в веб-приложении, если данные недействительны, необходимо вернуть пользователю сообщение об ошибке.

Это позволяет предотвратить уязвимости в системе безопасности, повреждение данных и повысить их достоверность. Поэтому продолжаем обрабатывать запрос только в том случае, если данные являются валидными.

Помните, что никаким данным, полученным от пользователя, нельзя доверять (по крайней мере, пока они не проверены)!

Почему валидация важна

Валидация важна по ряду причин, в том числе:

Повышение безопасности

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

Предотвращение сохранения некорректных данных

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

В качестве другого примера представьте, что создаёте веб-приложение, позволяющее пользователям голосовать в опросах. Голосовать в опросах можно только между временем открытия (opens_at) и временем закрытия (closes_at), которые указаны в модели App\Models\Poll. Что произойдёт, если кто-то, настраивая опрос, случайно установит время close_at раньше времени open_at? В зависимости от того, как обрабатывать это в приложении, это может вызвать различные проблемы.

Проверяя данные перед их сохранением в модели, можно повысить корректность данных в приложении и снизить вероятность сохранения некорректных данных.

Обеспечение корректного ввода команд Artisan

Помимо проверки данных, передаваемых в HTTP-запросах, также можно проверять команды Artisan. Это поможет предотвратить случайный ввод разработчиком недопустимого значения и возникновение проблем в приложении.

Валидация на стороне клиента vs Валидация на стороне сервера

Существует два типа валидации, которые можно использовать в приложениях: валидация на стороне клиента и валидация на стороне сервера.

Валидация на стороне клиента

Валидация на стороне клиента — это проверка, выполняемая в браузере перед отправкой данных на сервер. Она может быть реализована с помощью JavaScript или даже с помощью HTML атрибутов.

Например, можно добавить простую валидацию в поле типа number, чтобы убедиться, что пользователь вводит число от 1 до 10:

<input type="number" name="quantity" min="1" max="10" required>

В этом поле input есть четыре отдельные части, используемые для валидации на стороне клиента:

В большинстве браузеров, если пользователь попытается отправить форму с недопустимым значением (или вообще без значения), браузер предотвратит отправку формы и покажет пользователю сообщение об ошибке или подсказку.

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

Если кто-то откроет инструменты разработчика в браузере, то сможет с лёгкостью удалить и обойти валидацию на стороне клиента, которую вы установили.

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

Валидация на стороне сервера

Валидация на стороне сервера — это валидация, выполняемая в бэкенде приложения на сервере. В контексте приложений Laravel это, как правило, проверка, которая выполняется в контроллерах или классах запросов форм/form request.

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

Поэтому важно, чтобы в приложениях всегда присутствовала валидация на стороне сервера. В идеальном мире каждое поле, которое можно считать из запроса, должно быть проверено, прежде чем пытаться использовать его для выполнения бизнес-логики.

Как Laravel обрабатывает валидацию

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

Если вы давно работаете с Laravel, то наверняка знаете, что в Laravel встроена потрясающая система валидации. Поэтому начать работу с валидацией в приложении очень просто.

В Laravel существует несколько способов валидации данных, но мы рассмотрим два наиболее распространённых:

Ручная валидация данных

Для ручной валидации данных (например, в методе контроллера) можно использовать фасад Illuminate\Support\Facades\Validator и вызвать метод make.

Методу make можно передать два параметра:

Примечание: метод make также принимает два необязательных параметра: messages и attributes. Их можно использовать для настройки сообщений об ошибках, возвращаемых пользователю, но в статье их рассматривать не будем.

Рассмотрим пример того, как можно проверить два поля:

use Illuminate\Support\Facades\Validator;

$validator = Validator::make(
data: [
'title' => 'Blog Post',
'description' => 'Blog post description',
],
rules: [
'title' => ['required', 'string', 'max:100'],
'description' => ['required', 'string', 'max:250'],
]
);

В примере видно, что проверяются два поля: title и body. Для наглядности примера значения этих двух полей закодированы, но в реальном проекте их обычно получают из запроса. Проверяем, что поле title установлено, является строкой и имеет максимальную длину 100 символов. Также проверяем, что поле description установлено, является строкой и имеет максимальную длину 250 символов.

После создания валидатора можно вызывать методы возвращаемого экземпляра Illuminate\Validation\Validator. Например, чтобы проверить, не прошла ли валидация, можно вызвать метод fails:

$validator = Validator::make(
data: [
'title' => 'Blog Post',
'description' => 'Blog post description',
],
rules: [
'title' => ['required', 'string', 'max:100'],
'description' => ['required', 'string', 'max:250'],
]
);

if ($validator->fails()) {
// Одно или несколько полей не прошли валидацию.
// Обработаем их здесь...
}

Аналогичным образом можно вызвать метод validate на экземпляре валидатора:

Validator::make(
data: [
'title' => 'Blog Post',
'description' => 'Blog post description',
],
rules: [
'title' => ['required', 'string', 'max:100'],
'description' => ['required', 'string', 'max:250'],
]
)->validate();

Метод validate вызовет Illuminate\Validation\ValidationException, если валидация не прошла. Laravel автоматически обработает это исключение в зависимости от типа выполняемого запроса (если не изменена стандартная обработка исключений в приложении). Если запрос является веб-запросом, Laravel перенаправит пользователя на предыдущую страницу с ошибками в сессии для отображения. Если запрос является API-запросом, Laravel вернёт ответ 422 Unprocessable Entity с JSON-представлением ошибок валидации, как показано ниже:

{
"message": "The title field is required. (and 1 more error)",
"errors": {
"title": [
"The title field is required."
],
"description": [
"The description field is required."
]
}
}

Валидация данных с помощью классов запросов формы

Другой способ валидации данных в приложениях Laravel — это использование классов запросов форм. Классы запросов форм — это классы, расширяющие Illuminate\Foundation\Http\FormRequest и используемые для проверки авторизации и валидации входящих запросов.

Считаю, это отличный способ сохранить чистоту методов контроллера, потому что Laravel автоматически выполняет валидацию данных, переданных в запросе, до того, как будет запущен код метода контроллера. Таким образом, не нужно помнить о необходимости запускать какие-либо методы на экземпляре валидатора.

Рассмотрим простой пример. Представьте, что есть базовый контроллер App\Http\Controllers\UserController с методом store, позволяющим создать нового пользователя:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\Users\StoreUserRequest;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Hash;

final class UserController extends Controller
{
public function store(StoreUserRequest $request): RedirectResponse
{
User::create([
'name' => $request->validated('name'),
'email' => $request->validated('email'),
'password' => Hash::make($request->validated('password')),
]);

return redirect()
->route('users.index')
->with('success', 'User created successfully.');
}
}

В методе контроллера видим, что в качестве параметра метода принимаем класс запроса формы App\Http\Requests\Users\StoreUserRequest (который рассмотрим далее). Это укажет Laravel, что мы хотим, чтобы валидация в этом классе запроса автоматически выполнялась при вызове этого метода через HTTP-запрос.

Затем используем метод validated на экземпляре запроса в методе контроллера, чтобы получить проверенные данные из запроса. Это означает, что он вернёт только те данные, которые были проверены. Например, если попытаемся сохранить новое поле profile_picture в контроллере, оно также должно быть добавлено в класс запроса формы. В противном случае оно не будет возвращено методом validated, и $request->validated('profile_picture') вернёт null.

Теперь рассмотрим класс запроса формы App\Http\Requests\Users\StoreUserRequest:

declare(strict_types=1);

namespace App\Http\Requests\Users;

use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;

final class StoreUserRequest extends FormRequest
{
/**
* Определяем, авторизован ли пользователь для выполнения этого запроса.
*/

public function authorize(): bool
{
return $this->user()->can('create', User::class);
}

/**
* Получаем правила валидации, применяемые к данному запросу.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/

public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', Rule::unique('users')],
'password' => [Password::defaults()],
];
}
}

Мы видим, что класс request содержит два метода:

В методе rules указываем, что поле name должно быть задано, должно быть строкой и иметь максимальную длину 100 символов. Также указывается, что поле email должно быть задано, должно быть адресом электронной почты и должно быть уникальным в таблице users (в столбце email). Наконец, указываем, что поле password должно быть задано и должно проходить правила валидации пароля по умолчанию, которые были установлены нами (рассмотрим валидацию пароля позже).

Как видите, это отличный способ отделить логику валидации от логики контроллера, и считаю, что так код легче читать и поддерживать.

Полезные правила валидации в Laravel

Как уже говорилось, система валидации Laravel действительно мощная и позволяет легко добавлять валидацию в приложения.

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

Если хотите ознакомиться со всеми правилами, доступными в Laravel, можете найти их в документации по Laravel: https://laravel.com/docs/11.x/validation.

Валидация массивов

Чаще всего приходится выполнять проверку массивов. Это может быть что угодно: от проверки того, что массив переданных идентификаторов является валидным, до проверки того, что массив объектов, переданных в запросе, содержит определённые поля.

Рассмотрим пример валидации массива, а затем обсудим, что при этом происходит:

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

Validator::make(
data: [
'users' => [
[
'name' => 'Eric Barnes',
'email' => 'eric@example.com',
],
[
'name' => 'Paul Redmond',
'email' => 'paul@example.com',
],
[
'name' => 'Ash Allen',
'email' => 'ash@example.com',
],
],
],
rules: [
'users' => ['required', 'array'],
'users.*' => ['required', 'array:name,email'],
'users.*.name' => ['required', 'string', 'max:100'],
'users.*.email' => ['required', 'email', 'unique:users,email'],
]
);

В приведённом примере передаётся массив объектов, каждый из которых содержит поле name и email.

Для валидации сначала определяем, что поле users задано и является массивом. Затем указываем, что каждый элемент массива (указываем с помощью users.*) — это массив, содержащий поля name и email.

Затем указываем, что поле name (указанное с помощью users.*.name) должно быть установлено, должно быть строкой и не может быть длиннее 100 символов. Также указывается, что поле email (указанное с помощью users.*.email) должно быть задано, должно быть адресом электронной почты и должно быть уникальным в таблице users в столбце email.

Благодаря возможности использовать подстановочный знак * в правилах валидации, можно легко проверять массивы данных в приложениях.

Валидация дат

Laravel предоставляет несколько удобных правил валидации даты, которые можно использовать. Во-первых, для проверки того, что поле является валидной датой, можно использовать правило date:

$validator = Validator::make(
data: [
'opens_at' => '2024-04-25',
],
rules: [
'opens_at' => ['required', 'date'],
]
);

Если хотите проверить, что дата имеет определённый формат, можно использовать правило date_format:

$validator = Validator::make(
data: [
'opens_at' => '2024-04-25',
],
rules: [
'opens_at' => ['required', 'date_format:Y-m-d'],
]
);

Вполне вероятно, что потребуется проверить, что дата наступает до или после другой даты. Например, есть поля opens_at и closes_at в запросе, и необходимо убедиться, что closes_at наступает после opens_at и что opens_at наступает после или равна сегодняшнему дню. Можно использовать правило after следующим образом:

$validator = Validator::make(
data: [
'opens_at' => '2024-04-25',
'closes_at' => '2024-04-26',
],
rules: [
'opens_at' => ['required', 'date', 'after:today'],
'closes_at' => ['required', 'date', 'after_or_equal:opens_at'],
]
);

В приведённом примере видно, что аргументом правила after для поля opens_at был передан today. Laravel попытается преобразовать эту строку в правильный объект DateTime с помощью функции strtotime и сравнить с ним.

Для поля closes_at передаём opens_at в качестве аргумента в правиле after_or_equal. Laravel автоматически определит, что это ещё одно проверяемое поле, и сравнит их между собой.

Аналогично, Laravel также предоставляет правила before и before_or_equal, которые можно использовать для проверки того, что дата наступает раньше другой даты:

$validator = Validator::make(
data: [
'opens_at' => '2024-04-25',
'closes_at' => '2024-04-26',
],
rules: [
'opens_at' => ['required', 'date', 'before:closes_at'],
'closes_at' => ['required', 'date', 'before_or_equal:2024-04-27'],
]
);

Валидация паролей

Разработчики веб-сайтов должны стараться помочь своим пользователям оставаться в безопасности в Интернете. Один из способов сделать это — внедрить в приложения хорошие практики использования паролей, например, требовать, чтобы пароль был определённой длины, содержал определённые символы и т. д.

Laravel облегчает эту задачу, предоставляя класс Illuminate\Validation\Rules\Password, который можно использовать для валидации паролей.

В нём есть несколько методов, которые можно объединить в цепочку, создав нужные правила валидации паролей. Например, нужно, чтобы пароли пользователей соответствовали следующим критериям:

Валидация может выглядеть так:

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;

$validator = Validator::make(
data: [
'password' => 'my-password-here'
'password_confirmation' => 'my-password-here',
],
rules: [
'password' => [
'required',
'confirmed',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(),
],
],
);

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

Для облегчения этой задачи Laravel позволяет определить стандартный набор правил валидации паролей, который можно использовать во всём приложении. Это можно сделать, определив набор правил по умолчанию в методе boot провайдера App\Providers\AppServiceProvider, например, с помощью метода Password::defaults():

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;

class AppServiceProvider extends ServiceProvider
{
// ...

/**
* Bootstrap any application services.
*/

public function boot(): void
{
Password::defaults(static function (): Password {
return Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised();
});
}
}

После этого можно вызывать Password::defaults() в правилах валидации, и будут использоваться те правила, которые были указаны в AppServiceProvider:

'password' => ['required', 'confirmed', Password::defaults()],

Валидация цветов

Почти в каждом проекте, над которым мне доводилось работать, в той или иной форме присутствовала функция выбора цвета. Будь то выбор цвета пользователем для своего профиля, цвета фона для раздела страницы или что-то ещё — это то, что часто встречается.

Раньше для проверки правильности цвета в шестнадцатеричном формате (например, #FF00FF) приходилось использовать регулярные выражения (в которых я, не очень разбирался). Однако в Laravel теперь есть удобная функция hex_color, которую можно использовать вместо этого:

use Illuminate\Support\Facades\Validator;

Validator::make(
data: [
'color' => '#FF00FF',
],
rules: [
'color' => ['required', 'hex_color'],
]
);

Валидация файлов

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

Допустим, требуется разрешить пользователю загрузить файл PDF (.pdf) или Microsoft Word (.docx). Валидация может выглядеть следующим образом:

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\File;

Validator::validate($request->all(), [
'document' => [
'required',
File::types(['pdf', 'docx'])
->min('1kb')
->max('10mb'),
],
]);

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

Методы min и max также могут принимать строку с другими суффиксами, указывающими на единицы измерения размера файла. Например, также можно использовать:

Кроме того, у есть возможность убедиться, что файл является изображением, используя метод image в классе Illuminate\Validation\Rules\File:

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\File;

Validator::validate($input, [
'photo' => [
'required',
File::image()
->min('1kb')
->max('10mb')
->dimensions(Rule::dimensions()->maxWidth(500)->maxHeight(500)),
],
]);

В данном примере проверяется, что файл является изображением, устанавливаются ограничения на минимальный и максимальный размер файла, а также задаются максимальные размеры изображения (500 x 500px).

Возможно, захотите использовать другой подход к загрузке файлов в своём приложении. Например, можно загружать файлы прямо из браузера пользователя в облачное хранилище (такие, как S3). Если предпочитаете делать именно так, ознакомьтесь со статьёй Laravel: Загрузка файлов с помощью FilePond, в которой рассказывается, как это сделать, какой подход к проверке можно применить и как это протестировать.

Валидация существования поля в базе данных

Ещё одна распространённая проверка, которая может понадобиться, — убедиться, что значение существует в базе данных.

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

Для этого можно использовать правило exists и передать имя таблицы, в которой нужно проверить существование значения:

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

Validator::make(
data: [
'users_ids' => [
111,
222,
333,
],
],
rules: [
'user_ids' => ['required', 'array'],
'user_ids.*' => ['required', 'exists:users,id'],
]
);

В рассмотренном примере проверяется, что каждый из идентификаторов, переданных в массиве user_ids, существует в таблице users в столбце id.

Это отличный способ убедиться, что данные валидны и существуют в базе данных, прежде чем попытаться их использовать.

Если хотите сделать ещё один шаг вперёд, можете применить выражение where к правилу exists, чтобы дополнительно отфильтровать выполняемый запрос:

use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

Validator::make(
data: [
'users_ids' => [
111,
222,
333,
],
],
rules: [
'user_ids' => ['required', 'array'],
'user_ids.*' => ['required', Rule::exists('users')->where(static function (Builder $query): void {
$query->where('is_verified', true);
})],
]
);

В приведённом выше примере проверяется, что каждый из идентификаторов, переданных в массиве user_ids, существует в таблице users по столбцу id и что столбец users is_verified имеет значение true. Поэтому если передать id не верифицированного пользователя, проверка завершится неудачей.

Валидация уникальности поля в базе данных

Подобно правилу exists, Laravel также предоставляет правило unique, которое можно использовать для проверки уникальности значения в базе данных.

Допустим, есть таблица users, и нужно, чтобы поле email было уникальным. Можно использовать правило unique следующим образом:

use Illuminate\Support\Facades\Validator;

Validator::make(
data: [
'email' => 'mail@ashallendesign.co.uk',
],
rules: [
'email' => ['required', 'email', 'unique:users,email'],
]
);

В приведённом выше примере проверяется, что поле email установлено, является адресом электронной почты и уникально в таблице users в столбце email.

Но что произойдёт, если попытаться использовать эту проверку на странице профиля, где пользователь может обновить свой адрес электронной почты? Проверка завершится неудачей, потому что в таблице users уже существует строка с адресом электронной почты, который пользователь пытается обновить. В этом случае можно использовать метод ignore, чтобы игнорировать ID пользователя при проверке на уникальность:

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

Validator::make(
data: [
'email' => 'mail@ashallendesign.co.uk',
],
rules: [
'email' => ['required', 'email', Rule::unique('users')->ignore($user->id)],
]
);

Если решите использовать метод ignore, обязательно ознакомьтесь с предупреждением из документации Laravel:

Вы никогда не должны передавать в метод ignore запрос, контролируемый пользователем. Вместо этого следует передавать только сгенерированный системой уникальный ID, например автоинкрементный ID или UUID из экземпляра модели Eloquent. В противном случае ваше приложение будет уязвимо к атакам SQL-инъекции.

Бывают случаи, когда к правилу unique необходимо добавить дополнительные условия where. Это может понадобиться, чтобы убедиться, что адрес электронной почты уникален для определённой команды (то есть у другого пользователя в другой команде может быть такой же адрес). Это можно сделать, передав замыкание в метод where:

use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

Validator::make(
data: [
'email' => 'mail@ashallendesign.co.uk',
],
rules: [
'email' => [
'required',
'email',
Rule::unique('users')
->ignore($user->id)
->where(fn (Builder $query) => $query->where('team_id', $teamId));
)],
],
);

Создание собственного правила валидации

Несмотря на то, что Laravel поставляется с огромным количеством встроенных правил валидации, скорее всего, возникнет необходимость создать собственное правило валидации для конкретного случая использования.

К счастью, это легко сделать в Laravel!

Рассмотрим, как создать собственное правило валидации, как его использовать и как написать для него тесты.

Для целей статьи не слишком важно, что именно мы проверяем. Нужно просто рассмотреть общую структуру создания пользовательского правила валидации и способы его тестирования. Итак, создадим простое правило, проверяющее, является ли строка палиндромом.

Если не знаете, палиндром — это слово, фраза, число или другая последовательность символов, которая читается одинаково как в прямом, так и в обратном направлении. Например, racecar — это палиндром, потому что если перевернуть строку в обратном направлении, то получится racecar. В то время как слово laravel не является палиндромом, потому что если перевернуть строку, то получится levaral.

Для начала создадим новое правило валидации, выполнив следующую команду в проекте:

php artisan make:rule Palindrome

Она должна создать новый файл App/Rules/Palindrome.php:

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Palindrome implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/

public function validate(string $attribute, mixed $value, Closure $fail): void
{
//
}
}

Laravel будет автоматически вызывать метод validate при запуске правила. Метод принимает три параметра:

Поэтому можно добавить логику валидации внутри метода validate следующим образом:

declare(strict_types=1);

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;

final readonly class Palindrome implements ValidationRule
{
/**
* Run the validation rule.
*
* @param Closure(string): PotentiallyTranslatedString $fail
*/

public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($value !== strrev($value)) {
$fail('The :attribute must be a palindrome.');
}
}
}

В приведённом выше правиле просто проверяем, совпадает ли переданное в правило значение с перевёрнутым. Если нет, вызываем замыкание $fail с сообщением об ошибке. Это приведёт к тому, что валидация поля будет провалена. Если проверка успешно пройдена, то правило ничего не делает, и можно продолжать работу с приложением.

Теперь, когда правило создано, его можно использовать в приложении следующим образом:

use App\Rules\Palindrome;
use Illuminate\Support\Facades\Validator;

$validator = Validator::make(
data: [
'word' => 'racecar',
],
rules: [
'word' => [new Palindrome()],
]
);

Хотя это простое правило, которое было создано нами в демонстрационных целях, надеюсь, оно даёт представление о том, как можно создавать более сложные правила для приложений.

Тестирование собственного правила валидации

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

Чтобы понять, как это сделать, давайте посмотрим, как можно проверить правило палиндрома, которое было создано в предыдущем разделе.

Для этого конкретного правила есть два сценария, которые необходимо проверить:

В более сложных правилах может быть больше сценариев, но для целей статьи будем придерживаться простого подхода.

Мы создадим новый файл теста в каталоге tests/Unit/Rules с названием PalindromeTest.php.

Давайте посмотрим на файл теста, а затем обсудим, что он делает:

declare(strict_types=1);

namespace Tests\Unit\Rules;

use App\Rules\PalindromeNew;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class PalindromeTest extends TestCase
{
#[Test]
#[DataProvider('validValues')]
public function rule_passes_with_a_valid_value(string $word): void
{
(new PalindromeNew())->validate(
attribute: 'word',
value: $word,
fail: fn () => $this->fail('The rule should pass.'),
);

// Мы дошли до этого момента без каких-либо исключений, так что правило выполнено.
$this->assertTrue(true);
}

#[Test]
#[DataProvider('invalidValues')]
public function rule_fails_with_an_invalid_value(string $word): void
{
(new PalindromeNew())->validate(
attribute: 'word',
value: $word,
fail: fn () => $this->assertTrue(true),
);
}

public static function validValues(): array
{
return [
['racecar'],
['radar'],
['level'],
['kayak'],
];
}

public static function invalidValues(): array
{
return [
['laravel'],
['eric'],
['paul'],
['ash'],
];
}
}

В файле теста выше были определены два теста: rule_passes_with_a_valid_value и rule_fails_with_an_invalid_value.

Как следует из названий тестов, первый тест гарантирует, что правило выполняется, если значение является палиндромом, а второй тест гарантирует, что правило не выполняется, если значение не является палиндромом.

Используем атрибут PHPUnit\Framework\Attributes\DataProvider, чтобы предоставить тесту список валидных и невалидных значений для тестирования. Это отличный способ сохранить чистоту тестов, и иметь возможность проверять несколько значений в одном тесте. Например, если кто-то добавит новое валидное значение в метод validValues, тест будет автоматически работать с этим значением.

В тесте rule_passes_with_a_valid_value вызывается метод validate для правила с валидным значением. В параметр fail передаётся замыкание (это параметр, который вызывается, если валидация не проходит внутри правила). Указываем, что если замыкание будет выполнено (т. е. валидация не пройдёт), то тест должен быть провален. Если мы дошли до конца теста без выполнения замыкания, то будем уверены, что правило выполнено, и можем добавить простое утверждение assertTrue(true), для завершения теста.

В тесте rule_fails_with_an_invalid_value делаем то же самое, что и в первом тесте, но на этот раз передаём в правило невалидное значение. Мы указали, что если замыкание будет выполнено (т. е. проверка не пройдёт), то тест должен пройти, поскольку ожидается, что замыкание будет вызвано. Если дойдём до конца теста без выполнения замыкания, то никаких утверждений выполнено не будет, и PHPUnit должен выдать предупреждение. Однако если необходимо быть более явным и убедиться в том, что тест не пройдёт, а не просто выдаст ошибку, можно использовать другой подход к написанию теста.

Заключение

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

Также рассмотрели несколько полезных правил валидации, которые мне нравится использовать в своих приложениях Laravel. Наконец, рассмотрели, как можно создать собственное правило валидации и протестировать его, чтобы убедиться, что оно работает так, как ожидается.

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

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

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

PHP 8.4: Новая функция grapheme_str_split

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

Генераторы статических сайтов (и какой из них выбрать)