Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Форми й валідація»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

wire:submit на тегу <form> перехоплює відправку форми й викликає метод компонента замість звичайного POST-запиту з перезавантаженням сторінки.

new class extends Component {
    public string $title = '';
    public string $content = '';

    public function save(): void
    {
        $validated = $this->validate([
            'title' => 'required|min:3',
            'content' => 'required',
        ]);

        auth()->user()->posts()->create($validated);

        $this->redirectRoute('posts.index');
    }
};
<form wire:submit="save">
    <input type="text" wire:model="title">
    <textarea wire:model="content"></textarea>
    <button type="submit">Опублікувати</button>
</form>

Що робить Livewire автоматично:

  • preventDefault() - модифікатор .prevent не потрібен, на відміну від інших подій;
  • блокує форму на час запиту: кнопки відправки стають неактивними, поля - доступними лише для читання. Подвійне натискання не створить два пости;
  • значення з wire:model потрапляють на сервер разом із запитом на відправку - окремі запити на кожне поле не потрібні.

Параметри методу можна передати прямо в директиві: wire:submit="save('draft')".

Чому type="submit" важливий: кнопка без типу всередині форми теж відправляє її, а кнопка «Скасувати» з wire:click усередині форми має бути type="button", інакше натискання на неї відправить форму.

Enter у полі відправляє форму, як у звичайному HTML, - це правильна поведінка для доступності, і її не варто ламати.

Порівняно з контролером: логіка така сама - валідація, авторизація, збереження, редирект, - але без окремого маршруту й з помилками валідації, що з'являються без перезавантаження сторінки. Метод save() - публічна дія, яку можна викликати з браузера будь-коли, тож перевірки прав мають бути всередині нього.

Докладніше в документації: wire:submit

Валідація в Livewire побудована на звичайній валідації Laravel - ті самі правила й повідомлення.

Спосіб 1 - $this->validate() з правилами:

public function save(): void
{
    $validated = $this->validate([
        'title' => 'required|min:3|max:255',
        'email' => 'required|email',
    ]);

    Post::create($validated);
}

Якщо дані невалідні, метод кидає виняток, виконання дії зупиняється, а помилки повертаються в браузер. validate() повертає лише перевірені поля - їх безпечно передавати в create().

Спосіб 2 - атрибут #[Validate] над властивістю:

use Livewire\Attributes\Validate;

new class extends Component {
    #[Validate('required|min:3')]
    public string $title = '';

    #[Validate('required|email', as: 'електронна адреса')]
    public string $email = '';

    public function save(): void
    {
        $this->validate();   // правила беруться з атрибутів
    }
};

Правила поруч із властивістю, і поле перевіряється автоматично при кожному оновленні з браузера (якщо воно надсилається на сервер - з .live).

Показ помилок - звичайні директиви Blade:

<input wire:model="title" @error('title') aria-invalid="true" @enderror>
@error('title')
    <p class="text-sm text-red-600">{{ $message }}</p>
@enderror

Важливо: $this->validate() перед збереженням потрібен навіть з #[Validate] - атрибут перевіряє лише поля, які оновлювалися. Поле, якого користувач не торкався, без явного validate() залишиться неперевіреним.

Обмеження атрибутів: у PHP-атрибутах не можна використовувати об'єкти правил (Rule::unique(...), Password::defaults()). Для них - метод rules().

Українські повідомлення - через файли перекладів lang/uk/validation.php, як і в усьому Laravel; назву поля задає параметр as: чи метод validationAttributes().

Ручне керування помилками: $this->addError('email', 'Ця адреса вже зайнята'), $this->resetValidation('email').

Докладніше в документації: Валідація

Livewire відстежує, чи відрізняється стан у браузері від стану на сервері. Форма, в якій користувач щось змінив, але це ще не відправлено, вважається «брудною» (dirty).

wire:dirty показує елемент лише тоді, коли є незбережені зміни:

<form wire:submit="update">
    <input wire:model="title">
    <textarea wire:model="content"></textarea>

    <div wire:dirty class="text-amber-600">Є незбережені зміни</div>

    <button type="submit">Зберегти</button>
</form>

Щойно користувач почав вводити - повідомлення з'являється; після відправки форми й синхронізації з сервером - зникає.

Варіанти:

  • wire:dirty.remove - навпаки: показувати елемент, доки змін немає («Усе збережено»);
  • wire:dirty.class="border-amber-500" - додати клас замість показу елемента;
  • wire:target="title" - стежити лише за конкретною властивістю:
<input wire:model.live.blur="title" wire:dirty.class="border-amber-500" wire:target="title">
  • $dirty у виразах і $wire.$dirty('email') в Alpine - перевірити програмно:
<button x-bind:disabled="! $wire.$dirty()">Зберегти</button>

Кнопка «Зберегти» активна лише тоді, коли є що зберігати.

Що важливо розуміти: «чисто» означає «синхронізовано із сервером», а не «збережено в базу». Якщо поле з wire:model.live уже відправило значення на сервер, а метод збереження ще не викликано, wire:dirty уже не покаже змін. Для форм, де важливо саме збереження в базу, стан «змінено» краще вести явно (наприклад, порівнювати з початковими значеннями).

Корисне доповнення: попередження при спробі піти зі сторінки з незбереженими змінами - обробник beforeunload в Alpine, що перевіряє $wire.$dirty().

Докладніше в документації: wire:dirty

wire:confirm показує діалог підтвердження перед виконанням дії. Якщо користувач натиснув «Скасувати» чи Escape, запит на сервер не надсилається.

<button
    type="button"
    wire:click="delete({{ $post->id }})"
    wire:confirm="Видалити цей пост? Дію не можна скасувати."
>
    Видалити
</button>

Працює з будь-якою дією: wire:click, wire:submit тощо.

Підтвердження введенням тексту - для особливо руйнівних дій (видалення облікового запису чи проєкту). Модифікатор .prompt, очікуваний текст - після |:

<button
    wire:click="deleteAccount"
    wire:confirm.prompt="Щоб підтвердити, введіть ВИДАЛИТИ|ВИДАЛИТИ"
>
    Видалити обліковий запис
</button>

Дія виконається лише якщо введено точно ВИДАЛИТИ (з урахуванням регістру).

Що варто знати:

  • це стандартні діалоги браузера (confirm() / prompt()): виглядають по-різному в різних браузерах і не стилізуються. Для фірмового вигляду роблять власне модальне вікно (Alpine чи готові компоненти на кшталт Flux), а дію викликають з кнопки всередині нього;
  • підтвердження - це UX, а не захист. Метод delete() можна викликати напряму з консолі браузера ($wire.delete(5)), минаючи діалог. Перевірка прав і власності запису обов'язкова в самому методі:
public function delete(Post $post): void
{
    $this->authorize('delete', $post);
    $post->delete();
}
  • для масових чи незворотних операцій варто подумати про «м'яке» видалення (SoftDeletes) і можливість відновити - це надійніше за будь-які діалоги;
  • текст діалогу - звичайний рядок, тож його варто перекладати так само, як решту інтерфейсу (wire:confirm="{{ __('...') }}").

Докладніше в документації: wire:confirm