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

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, але об'єкт форми ще й тримає стан між запитами компонента.

Докладніше в документації: Форми: об'єкти форм

Валідація в реальному часі - перевірка поля під час заповнення форми, а не лише при відправці.

Що для цього потрібно:

  1. правила на властивості через #[Validate] - Livewire перевіряє властивість при кожному оновленні з браузера;
  2. оновлення має доходити до сервера - 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), скидання властивості не очистить сам віджет - йому треба надіслати подію.

Докладніше в документації: Форми: скидання полів