
FormRequest: валідація форм в Laravel
05.07.2018 17:33 | Laravel
Якщо форма проста, і містить усього пару полів, то можна робити перевірку введення і в контролерах, але навіть в таких випадках особисто я надаю перевагу іншому підходу - FormRequest
, тобто використанню класів, в яких описана логіка валідації.
Чому не в контролерах?
По-перше, якщо якийсь метод контролера виконує якесь конкретне завдання - нехай ним і займається, решта - не його проблема. Простіше кажучи, якщо метод призначений для збереження даних, нехай зберігає дані, а перевірку делегує комусь іншому. По-друге, подивимося на приклад валідації в методі store()
, а щоб було наочніше, припустимо, що ми хочемо вивести якесь кастомне повідомлення про помилку:
public function store(Request $request)
{
$request->validate([
'title' => 'required'
], [
'title.required' => 'Hey! You have to fill in the :attribute field.'
]);
// ИЛИ
$this->validate($request, [
'title' => 'required'
], [
'title.required' => 'Hey! You have to fill in the :attribute field.'
]);
}
Можна уявити, наскільки збільшиться код методу, якщо поле буде не одне, а набагато більше, що не рідкість. Як варіант - винести перевірку в окремий метод. Але зазвичай там, де є збереження, є і оновлення, і часто відповідні перевірки даних дещо відрізняються, тобто отримаємо не один метод, а два і більше. Звідси запитання - навіщо засмічувати контролер зайвим кодом, коли під рукою є вже готове рішення? Подивимося на нього.
FormRequest та його методи
Кілька слів щодо структури каталогів. За замовчуванням Laravel буде розміщувати всі створювані підкласи FormRequest
у теці app/Http/Requests
. Але не дуже комфортно, коли все скинуто в одну купу. Файли можна впорядковувати, наприклад, так:
|-Requests
|-Category
| |- StoreCategory.php
| |- UpdateCategory.php
|
|-Post
| |-StorePost.php
| |-UpdatePost.php
...
Відповідно до вищесказаного створимо клас для перевірки даних форми нової категорії. Як завжди, скористаємося консоллю:
php artisan make:request Category/StoreCategory
В результаті буде створено файл app/Http/Requests/Category/StoreCategory.php
з однойменним класом-нащадком FormRequest
і двома методами - authorize()
і rules()
.
Метод authorize()
Призначений для того, щоб перевірити, чи має користувач право відправляти форму. На час забудемо про категорії і обговоримо метод. Якщо мова йде про відправку форми зворотного зв'язку з фронту, яку можуть відправляти неавторизовані користувачі - просто міняємо false
на true
, надаючи це право всім бажаючим. Якщо ми говоримо про додавання коментарів, тобто юзер повинен бути залогіненим, метод може виглядати так:
public function authorize()
{
return auth()->check();
}
Ну а якщо перевіряємо будь-яку форму в адмін-панелі, то ... вибачте, тупо ставимо true
. Справа в тому, що якщо хтось зміг дістатися до цієї форми, то ми вже точно перевіряли його якимось admin middleware у роутах або контролері, а дублювати одне й те ж зовсім ні до чого.
Метод rules()
Тут як би "і так все зрозуміло", але є деякі цікаві моменти: як зробити валідацію масиву? А якщо кожен елемент масиву теж масив? Наприклад, перевіряємо користувача, і є не просто кілька телефонів, а й тип - мобільний, робочий, домашній. Оскільки ці питання стосуються більше правил, ніж безпосередньо методу, повернемося до них пізніше..
А поки подивимося, що можна зробити в такій ситуації: потрібно, щоб назва категорії була унікальною. При збереженні нової категорії з цим проблем немає. Просто в правилах пропишемо:
public function rules()
{
return [
'title' => 'required|unique:categories',
...
];
}
При оновленні даних ми так зробити вже не можемо, оскільки будуть перевірятися всі записи, у тому числі і той, що оновлюється. Тобто треба дати команду перевірити title
на унікальність у всіх записах, крім поточного, що буде виглядати приблизно так:
...
use Illuminate\Validation\Rule;
class UpdateCategory extends FormRequest
{
...
public function rules()
{
return [
'title' => [
'required',
Rule::unique('categories')->ignore($this->route('id'))
],
...
];
}
}
І ніби все добре, крім одного "але" - всі інші правила крім title дублюються. Вихід з ситуації - успадкувати UpdateCategory
від StoreCategory
. В останньому пропишемо всі правила, а в першому тільки ті, які відрізняються. Потім злитиємо масиви з методів rules()
функцією array_merge()
. Нагадаю: "якщо вхідні масиви мають однакові рядкові ключі, тоді кожне наступне значення буде замінювати попереднє". Тобто в UpdateCategory
отримаємо прописану нами нову перевірку унікальності назви, доповнену усіма іншими правилами з StoreCategory
. Ось повний код класу:
<?php
namespace App\Http\Requests\Category;
use App\Http\Requests\Category\StoreCategory;
class UpdateCategory extends StoreCategory
{
public function authorize()
{
return true;
}
public function rules()
{
return array_merge(parent::rules(), [
'title' => [
'required',
Rule::unique('categories')->ignore($this->route('id'))
]
]);
}
}
Метод messages()
Цього методу немає в створеному класі, але він є в батьківському FormRequest
, тобто можемо його перевизначити, і саме в ньому прописати кастомне повідомлення про помилки, наприклад:
public function messages()
{
return [
'title.required' => 'Hey! You have to fill in the :attribute field.'
];
}
Метод attributes()
Також перевизначимо метод з класу-батька. Навіщо це може бути потрібно? У повідомленнях про помилки замість :attribute
підставляється назва поля. Тобто якщо взяти повідомлення з методу вище, то буде виведено:
Hey! You have to fill in the title field.
Але що робити, якщо замість title ми хочемо використовувати будь-яке інше слово або фразу? І знову приклад використання:
public function attributes()
{
return [
'title' => 'category title'
];
}
Тепер в разі помилки побачимо:
Hey! You have to fill in the category title field.
Про правила та локалізацію
Повернемося до правил. Опишу пару ситуацій, які мені здалися цікавими.
Про першу згадував вище: у користувача кілька телефонів, причому під словом "телефон" я маю на увазі не просто номер, а сутність зі своїми властивостями - номер телефону, тип (мобільний, домашній), прив'язаний чи телефон до вайберу. Аналогічний випадок - робимо багатомовний сайт і є, наприклад, Post
із загальними властивостями, як то id автора, id категорії, чи опублікований пост, дата створення. І до посту прив'язані переклади (PostTranslation
) з властивостями: назва, текст поста, мета опис. І в тому, і в іншому випадку будемо мати масиви даних, які треба перевірити. І відразу подумаємо ось про що: в першому випадку ми хочемо, щоб користувач вказав хоча б один номер телефону, у другому - щоб були заповнені поля перекладу як мінімум на одну мову (інакше який сенс в пості?). Все не так вже й складно, дивимося код (приклад для телефонів):
public function rules()
{
return [
'phones.*.phone_number' => [
'nullable',
'distinct',
'regex:/^\d{2}\s\(\d{3}\)\s\d{3}-\d{4}$/'
],
'phones.*.type' => [
'nullable',
Rule::in(['mobile', 'home'])
],
'phones.0.phone_number' => [
'required',
'regex:/^\d{2}\s\(\d{3}\)\s\d{3}-\d{4}$/'
],
'phones.0.type' => [
'required',
Rule::in(['mobile', 'home'])
],
];
}
Тобто вказуємо, що всі поля phone_number
можуть бути порожніми, але якщо вони заповнені, то повинні бути різними і відповідати певному формату, значення "тип" також може бути порожнім, і знову-таки, якщо не порожнє, то повинно бути рівним або mobile, або home. Після чого говоримо, що перший елемент масиву phones
, а точніше вкладені значення phone_number
та type
- обов'язкові. Причому саме в такому порядку, інакше nullable
перекриє required
.
Потрібно розуміти, що якщо маємо справу з масивами, то при виведенні помилок замість :attribute
буде підставлятися щось на зразок phones.0.type
- не дуже то красиво. Ми знаємо, що є метод attributes()
, який може допомогти. Але є ще один спосіб, і якщо сайт багатомовний, цей спосіб досить зручний. Маю на увазі про файли локалізації. Нам доведеться робити переклади, а значить в нас буде файл app/resources/lang/uk/validation.php
, в кінці якого можна прописати масив з атрибутами:
'attributes' => [
'phones.*.phone_number' => 'номер телефону',
'phones.*.type' => 'тип'
],
Кастомні правила валідації
Ще одна цікава ситуація - створення кастомних правил. У Laravel багато правил, які покривають майже все, що завгодно. Ключове слово - майже. Якось треба було зробити наступну перевірку: поле X обов'язкове, якщо поле Y не є порожнім, тобто містить будь-яке значення. Здивувало те, що подібного правила "в коробці" не знайшлося - required_if
, required_with
працюють трохи інакше. Тому довелося трохи поламати голову над своїм required_if_not_empty:field
.
У документації пропонуються три варіанти створення користувацьких правил - використання об'єктів Rule, використання замикань, і використання розширень. Перших два відпали, оскільки неможливо "дістати" значення поля Y (крім атрибута і значення поля Х, потрібні також параметри, в даному випадку параметр field
). Я не став, як описано в доках, закидати розширення в AppServiceProvider
, а створив окремого постачальника послуг - ValidationServiceProvider
. Ось код:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;
class ValidationServiceProvider extends ServiceProvider
{
public function boot()
{
Validator::extend('required_if_not_empty', function($attribute, $value, $parameters, $validator) {
$other = array_get($validator->getData(), $parameters[0], null);
if (!empty($other)) {
return $validator->validateRequired($attribute, $value);
}
return true;
});
}
public function register()
{
//
}
}
Тобто отримуємо всі дані валідатора, і за допомогою хелперу array_get()
дістаємо значення першого параметра, яким є зазначений у правилі field
. На той випадок, якщо такий параметр не знайдений, говоримо функції повернути null
. І далі, якщо параметр (поле Y) не порожній, перевіряємо чи заповнене поле Х. Якщо ж Y не заповнений - повертаємо true
- іншими словами поле Х пройшло перевірку.
Не забудемо додати нового провайдера в масив $providers
у файлі config/app.php
. Тепер можна використовувати правило де завгодно.
Висновок
Перевірка будь-яких даних в Laravel не проблема. Навіть якщо відсутнє необхідне правило, можна написати своє, вибравши найбільш відповідний для конкретного випадку підхід. Що стосується валідації в загальному - перевірка даних в контролерах можлива, але все ж FormRequest - рішення більш гнучке, що дозволяє зберігати код чистим і читабельним.