Middle: питання на співбесіді з теми «Форми й валідація»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Об'єкт форми - окремий клас, куди виносять поля, правила валідації й логіку збереження. Компонент стає коротшим, а форму можна використати в кількох компонентах.
php artisan livewire:form PostForm # app/Livewire/Forms/PostForm.php
namespace App\Livewire\Forms;
use App\Models\Post;
use Illuminate\Validation\Rule;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post = null;
public string $title = '';
public string $content = '';
protected function rules(): array
{
return [
'title' => ['required', 'max:255', Rule::unique('posts')->ignore($this->post)],
'content' => ['required', 'min:10'],
];
}
public function setPost(Post $post): void
{
$this->post = $post;
$this->fill($post->only(['title', 'content']));
}
public function save(): Post
{
$this->validate();
$post = $this->post ?? new Post(['author_id' => auth()->id()]);
$post->fill($this->only(['title', 'content']))->save();
return $post;
}
}
Компоненти створення й редагування:
// створення
public PostForm $form;
public function save(): void
{
$post = $this->form->save();
$this->redirectRoute('posts.show', $post);
}
// редагування
public function mount(Post $post): void
{
$this->authorize('update', $post);
$this->form->setPost($post);
}
У шаблоні поля адресуються через назву властивості з формою:
<input wire:model="form.title">
@error('form.title') <p>{{ $message }}</p> @enderror
Корисні методи форми: $this->form->all(), only([...]), reset() (повертає значення за замовчуванням), pull() (отримати значення й одразу скинути), validate().
Що варто знати:
- ключі помилок мають префікс назви властивості:
form.title, а неtitle- і в@error, і в тестах (assertHasErrors('form.title')); - хуки
updated...працюють і всередині об'єкта форми; - авторизація лишається в компоненті чи в методі форми - об'єкт форми не робить дії безпечними сам по собі;
reset()повертає типізовані властивості без значення за замовчуванням у неініціалізований стан - полям варто задавати значення за замовчуванням.
Аналог у звичайному Laravel - Form Request, але об'єкт форми ще й тримає стан між запитами компонента.
Валідація в реальному часі - перевірка поля під час заповнення форми, а не лише при відправці.
Що для цього потрібно:
- правила на властивості через
#[Validate]- Livewire перевіряє властивість при кожному оновленні з браузера; - оновлення має доходити до сервера -
wire:model.liveчиwire:model.live.blur.
#[Validate('required|email|unique:users,email')]
public string $email = '';
<input type="email" wire:model.live.blur="email">
@error('email') <p>{{ $message }}</p> @enderror
Користувач заповнив поле й перейшов до наступного - запит, перевірка, помилка з'являється одразу.
Чому не просто .live: перевірка на кожне натискання клавіші показує «Некоректна адреса», поки людина ще друкує, і створює запит на кожну паузу. Перевірка при виході з поля (.blur) - зручніша і дешевша. Виняток - поля, де миттєвий відгук корисний (сила пароля, доступність імені користувача).
Чому rules() не спрацьовує при введенні. Метод rules() застосовується лише при виклику $this->validate(). При оновленні властивості Livewire перевіряє тільки ті, що мають атрибут #[Validate]. Щоб і правила з rules() (наприклад, з об'єктами Rule::unique(), яких атрибут не дозволяє) перевірялися при введенні, додайте порожній атрибут:
#[Validate]
public string $email = '';
protected function rules(): array
{
return ['email' => ['required', 'email', Rule::unique('users')->ignore(auth()->id())]];
}
Вимкнути автоматичну перевірку для поля з атрибутом - #[Validate('required', onUpdate: false)].
Перевірити одне поле вручну: $this->validateOnly('email') у хуку updated.
Помилки в JavaScript - магічна властивість $errors:
<p wire:show="$errors.has('email')" wire:text="$errors.first('email')"></p>
Підсумкова перевірка обов'язкова: валідація в реальному часі - зручність. Перед збереженням $this->validate() все одно викликається, бо частину полів користувач міг не чіпати, а запит можна відправити й в обхід інтерфейсу.
Запит Livewire - не звичайний запит сторінки, а AJAX. HTTP-редирект у відповідь на нього браузер не виконає як перехід. Тому Livewire передає «команду перейти» в JavaScript, а той змінює сторінку.
Методи компонента:
public function save(): void
{
$post = $this->form->save();
session()->flash('status', 'Пост опубліковано');
$this->redirectRoute('posts.show', ['post' => $post]);
}
$this->redirect('/posts')- за адресою;$this->redirectRoute('posts.show', [...])- за назвою маршруту;$this->redirectAction([PostController::class, 'index'])- на дію контролера;$this->redirectIntended('/dashboard')- туди, куди користувач ішов до входу.
Повернути redirect()->route(...) з методу теж можна - Livewire розпізнає звичайну відповідь-редирект Laravel.
SPA-перехід замість повного завантаження:
$this->redirect('/posts', navigate: true);
$this->redirectRoute('posts.index', navigate: true);
Сторінка підвантажується як при wire:navigate: без перезавантаження скриптів і стилів, швидше.
Флеш-повідомлення працюють, як у звичайному Laravel, - session()->flash(), а на сторінці призначення:
@if (session('status'))
<div class="alert">{{ session('status') }}</div>
@endif
Повідомлення без редиректу. Якщо після збереження користувач лишається на тій самій сторінці, флеш-повідомлення з'явиться лише при наступному запиті - не одразу. Тут краще:
- подія в браузер, яку слухає компонент сповіщень (
$this->dispatch('notify', message: 'Збережено')); - властивість компонента з текстом повідомлення.
Пастки:
- код після редиректу виконується:
$this->redirect()лише записує намір, а не зупиняє метод. Якщо після нього є ще логіка, вона спрацює - за потреби додайтеreturn; - редирект на зовнішній домен з
navigate: trueне працює як SPA-перехід - для зовнішніх адрес звичайнийredirect(); - не редиректити за адресою з введення користувача без перевірки - відкритий редирект використовують для фішингу.
Після збереження форма, на якій користувач лишається (коментар, повідомлення в чаті, додавання рядка в список), має очиститися.
reset() повертає властивості до значень, оголошених у класі:
public string $body = '';
public ?int $rating = null;
public function addComment(): void
{
$this->validate();
$this->post->comments()->create($this->only(['body', 'rating']));
$this->reset('body', 'rating'); // лише ці поля
// $this->reset(); // усі публічні властивості
}
Обережно з reset() без аргументів: він скидає всі публічні властивості компонента, включно з тими, що встановлено в mount() (наприклад, $post), - і компонент «губить» свій контекст. Безпечніше явно перелічувати поля або тримати поля форми в об'єкті форми й скидати лише його: $this->form->reset().
pull() - отримати значення і одразу скинути:
$this->post->comments()->create($this->pull(['body', 'rating']));
resetExcept([...]) - скинути все, крім указаного.
Помилки валідації зберігаються окремо від значень:
$this->resetValidation(); // усі помилки
$this->resetValidation('body'); // помилку одного поля
$this->resetErrorBag(); // те саме, що resetValidation()
Типовий випадок - кнопка «Скасувати» в модальному вікні: скинути і поля, і помилки, щоб при наступному відкритті форма була чистою.
Що варто знати:
- типізована властивість без значення за замовчуванням (
public string $title;) післяreset()стає неініціалізованою, і звернення до неї в шаблоні кине помилку. Задавайте значення за замовчуванням; reset()не викликає хуківupdated- це зміна на сервері, а не з браузера;- поля з
wire:modelбез.liveочищаються в інтерфейсі, бо після відповіді сервера Livewire морфить DOM з новими значеннями; - для полів, якими керує сторонній JavaScript (редактор тексту, вибір дати під
wire:ignore), скидання властивості не очистить сам віджет - йому треба надіслати подію.