LaravelDocs(中文)

Precognition

Laravel Precognition 允許你在使用者完成表單之前預先驗證

簡介 (Introduction)

Laravel Precognition 允許你預測未來 HTTP 請求的結果。Precognition 的主要用途之一是能夠為你的前端 JavaScript 應用程式提供「即時」驗證,而無需複製應用程式的後端驗證規則。

當 Laravel 收到「預知請求」時,它會執行路由的所有中介層並解析路由的控制器依賴項,包括驗證表單請求 - 但它實際上不會執行路由的控制器方法。

即時驗證 (Live Validation)

使用 Vue (Using Vue)

使用 Laravel Precognition,你可以為使用者提供即時驗證體驗,而無需在前端 Vue 應用程式中複製驗證規則。為了說明它的運作方式,讓我們建立一個用於在應用程式中建立新使用者的表單。

首先,要為路由啟用 Precognition,應將 HandlePrecognitiveRequests 中介層加入到路由定義中。你還應該建立一個表單請求來存放路由的驗證規則:

use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', function (StoreUserRequest $request) {
    // ...
})->middleware([HandlePrecognitiveRequests::class]);

接下來,你應該透過 NPM 安裝 Laravel Precognition 的 Vue 前端輔助工具:

npm install laravel-precognition-vue

安裝 Laravel Precognition 套件後,你現在可以使用 Precognition 的 useForm 函式建立表單物件,提供 HTTP 方法(post)、目標 URL(/users)和初始表單資料。

然後,要啟用即時驗證,請在每個輸入的 change 事件上呼叫表單的 validate 方法,並提供輸入的名稱:

<script setup>
import { useForm } from "laravel-precognition-vue";

const form = useForm("post", "/users", {
  name: "",
  email: "",
});

const submit = () => form.submit();
</script>

<template>
  <form @submit.prevent="submit">
    <label for="name">Name</label>
    <input id="name" v-model="form.name" @change="form.validate('name')" />
    <div v-if="form.invalid('name')">
      {{ form.errors.name }}
    </div>

    <label for="email">Email</label>
    <input
      id="email"
      type="email"
      v-model="form.email"
      @change="form.validate('email')"
    />
    <div v-if="form.invalid('email')">
      {{ form.errors.email }}
    </div>

    <button :disabled="form.processing">Create User</button>
  </form>
</template>

現在,當使用者填寫表單時,Precognition 將提供由路由表單請求中的驗證規則驅動的即時驗證輸出。當表單的輸入改變時,將向你的 Laravel 應用程式發送一個防抖的「預知」驗證請求。你可以透過呼叫表單的 setValidationTimeout 函式來設定防抖逾時:

form.setValidationTimeout(3000);

當驗證請求正在進行時,表單的 validating 屬性將為 true:

<div v-if="form.validating">Validating...</div>

在驗證請求或表單提交期間返回的任何驗證錯誤都會自動填入表單的 errors 物件:

<div v-if="form.invalid('email')">{{ form.errors.email }}</div>

你可以使用表單的 hasErrors 屬性來判斷表單是否有任何錯誤:

<div v-if="form.hasErrors">
  <!-- ... -->
</div>

你也可以透過將輸入的名稱傳遞給表單的 validinvalid 函式,來判斷輸入是否通過或未通過驗證:

<span v-if="form.valid('email')"> ✅ </span>

<span v-else-if="form.invalid('email')"> ❌ </span>

[!WARNING] 表單輸入只有在改變並收到驗證回應後才會顯示為有效或無效。

如果你使用 Precognition 驗證表單輸入的子集,手動清除錯誤可能會很有用。你可以使用表單的 forgetError 函式來實現這一點:

<input
  id="avatar"
  type="file"
  @change="(e) => {
        form.avatar = e.target.files[0]

        form.forgetError('avatar')
    }"
/>

如我們所見,你可以掛鉤到輸入的 change 事件,並在使用者與它們互動時驗證個別輸入;然而,你可能需要驗證使用者尚未互動的輸入。這在建立「精靈」時很常見,你希望在移至下一步之前驗證所有可見的輸入,無論使用者是否與它們互動。

要使用 Precognition 做到這一點,你應該呼叫 validate 方法,將你希望驗證的欄位名稱傳遞給 only 設定鍵。你可以使用 onSuccessonValidationError 回呼來處理驗證結果:

<button
  type="button"
  @click="form.validate({
        only: ['name', 'email', 'phone'],
        onSuccess: (response) => nextStep(),
        onValidationError: (response) => /* ... */,
    })"
>
  Next Step
</button>

當然,你也可以執行程式碼來回應表單提交的回應。表單的 submit 函式會返回一個 Axios 請求 promise。這提供了一種方便的方式來存取回應負載、在成功提交時重設表單輸入,或處理失敗的請求:

const submit = () =>
  form
    .submit()
    .then((response) => {
      form.reset();

      alert("User created.");
    })
    .catch((error) => {
      alert("An error occurred.");
    });

你可以透過檢查表單的 processing 屬性來判斷表單提交請求是否正在進行:

<button :disabled="form.processing">Submit</button>

使用 Vue 和 Inertia (Using Vue and Inertia)

[!NOTE] 如果你希望在使用 Vue 和 Inertia 開發 Laravel 應用程式時快速開始,請考慮使用我們的其中一個起始套件。Laravel 的起始套件為你的新 Laravel 應用程式提供後端和前端身份驗證腳手架。

在將 Precognition 與 Vue 和 Inertia 一起使用之前,請務必查閱我們關於將 Precognition 與 Vue 一起使用的一般文件。當將 Vue 與 Inertia 一起使用時,你需要透過 NPM 安裝與 Inertia 相容的 Precognition 程式庫:

npm install laravel-precognition-vue-inertia

安裝後,Precognition 的 useForm 函式將返回一個 Inertia 表單輔助工具,增強了上述討論的驗證功能。

表單輔助工具的 submit 方法已經簡化,無需指定 HTTP 方法或 URL。相反,你可以將 Inertia 的訪問選項作為第一個也是唯一的參數傳遞。此外,submit 方法不會像上面 Vue 範例中那樣返回 Promise。相反,你可以在給定給 submit 方法的訪問選項中提供 Inertia 支援的任何事件回呼:

<script setup>
import { useForm } from "laravel-precognition-vue-inertia";

const form = useForm("post", "/users", {
  name: "",
  email: "",
});

const submit = () =>
  form.submit({
    preserveScroll: true,
    onSuccess: () => form.reset(),
  });
</script>

使用 React (Using React)

使用 Laravel Precognition,你可以為使用者提供即時驗證體驗,而無需在前端 React 應用程式中複製驗證規則。為了說明它的運作方式,讓我們建立一個用於在應用程式中建立新使用者的表單。

首先,要為路由啟用 Precognition,應將 HandlePrecognitiveRequests 中介層加入到路由定義中。你還應該建立一個表單請求來存放路由的驗證規則:

use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', function (StoreUserRequest $request) {
    // ...
})->middleware([HandlePrecognitiveRequests::class]);

接下來,你應該透過 NPM 安裝 Laravel Precognition 的 React 前端輔助工具:

npm install laravel-precognition-react

安裝 Laravel Precognition 套件後,你現在可以使用 Precognition 的 useForm 函式建立表單物件,提供 HTTP 方法(post)、目標 URL(/users)和初始表單資料。

要啟用即時驗證,你應該監聽每個輸入的 changeblur 事件。在 change 事件處理程式中,你應該使用 setData 函式設定表單的資料,傳遞輸入的名稱和新值。然後,在 blur 事件處理程式中呼叫表單的 validate 方法,並提供輸入的名稱:

import { useForm } from "laravel-precognition-react";

export default function Form() {
  const form = useForm("post", "/users", {
    name: "",
    email: "",
  });

  const submit = (e) => {
    e.preventDefault();

    form.submit();
  };

  return (
    <form onSubmit={submit}>
      <label htmlFor="name">Name</label>
      <input
        id="name"
        value={form.data.name}
        onChange={(e) => form.setData("name", e.target.value)}
        onBlur={() => form.validate("name")}
      />
      {form.invalid("name") && <div>{form.errors.name}</div>}

      <label htmlFor="email">Email</label>
      <input
        id="email"
        value={form.data.email}
        onChange={(e) => form.setData("email", e.target.value)}
        onBlur={() => form.validate("email")}
      />
      {form.invalid("email") && <div>{form.errors.email}</div>}

      <button disabled={form.processing}>Create User</button>
    </form>
  );
}

現在,當使用者填寫表單時,Precognition 將提供由路由表單請求中的驗證規則驅動的即時驗證輸出。當表單的輸入改變時,將向你的 Laravel 應用程式發送一個防抖的「預知」驗證請求。你可以透過呼叫表單的 setValidationTimeout 函式來設定防抖逾時:

form.setValidationTimeout(3000);

當驗證請求正在進行時,表單的 validating 屬性將為 true:

{
  form.validating && <div>Validating...</div>;
}

在驗證請求或表單提交期間返回的任何驗證錯誤都會自動填入表單的 errors 物件:

{
  form.invalid("email") && <div>{form.errors.email}</div>;
}

你可以使用表單的 hasErrors 屬性來判斷表單是否有任何錯誤:

{form.hasErrors && <div><!-- ... --></div>}

你也可以透過將輸入的名稱傳遞給表單的 validinvalid 函式,來判斷輸入是否通過或未通過驗證:

{
  form.valid("email") && <span>✅</span>;
}

{
  form.invalid("email") && <span>❌</span>;
}

[!WARNING] 表單輸入只有在改變並收到驗證回應後才會顯示為有效或無效。

如果你使用 Precognition 驗證表單輸入的子集,手動清除錯誤可能會很有用。你可以使用表單的 forgetError 函式來實現這一點:

<input
    id="avatar"
    type="file"
    onChange={(e) => {
        form.setData('avatar', e.target.files[0]);

        form.forgetError('avatar');
    }}
>

如我們所見,你可以掛鉤到輸入的 blur 事件,並在使用者與它們互動時驗證個別輸入;然而,你可能需要驗證使用者尚未互動的輸入。這在建立「精靈」時很常見,你希望在移至下一步之前驗證所有可見的輸入,無論使用者是否與它們互動。

要使用 Precognition 做到這一點,你應該呼叫 validate 方法,將你希望驗證的欄位名稱傳遞給 only 設定鍵。你可以使用 onSuccessonValidationError 回呼來處理驗證結果:

<button
    type="button"
    onClick={() => form.validate({
        only: ['name', 'email', 'phone'],
        onSuccess: (response) => nextStep(),
        onValidationError: (response) => /* ... */,
    })}
>Next Step</button>

當然,你也可以執行程式碼來回應表單提交的回應。表單的 submit 函式會返回一個 Axios 請求 promise。這提供了一種方便的方式來存取回應負載、在成功提交表單時重設表單輸入,或處理失敗的請求:

const submit = (e) => {
  e.preventDefault();

  form
    .submit()
    .then((response) => {
      form.reset();

      alert("User created.");
    })
    .catch((error) => {
      alert("An error occurred.");
    });
};

你可以透過檢查表單的 processing 屬性來判斷表單提交請求是否正在進行:

<button disabled="{form.processing}">Submit</button>

使用 React 和 Inertia (Using React and Inertia)

[!NOTE] 如果你希望在使用 React 和 Inertia 開發 Laravel 應用程式時快速開始,請考慮使用我們的其中一個起始套件。Laravel 的起始套件為你的新 Laravel 應用程式提供後端和前端身份驗證腳手架。

在將 Precognition 與 React 和 Inertia 一起使用之前,請務必查閱我們關於將 Precognition 與 React 一起使用的一般文件。當將 React 與 Inertia 一起使用時,你需要透過 NPM 安裝與 Inertia 相容的 Precognition 程式庫:

npm install laravel-precognition-react-inertia

安裝後,Precognition 的 useForm 函式將返回一個 Inertia 表單輔助工具,增強了上述討論的驗證功能。

表單輔助工具的 submit 方法已經簡化,無需指定 HTTP 方法或 URL。相反,你可以將 Inertia 的訪問選項作為第一個也是唯一的參數傳遞。此外,submit 方法不會像上面 React 範例中那樣返回 Promise。相反,你可以在給定給 submit 方法的訪問選項中提供 Inertia 支援的任何事件回呼:

import { useForm } from "laravel-precognition-react-inertia";

const form = useForm("post", "/users", {
  name: "",
  email: "",
});

const submit = (e) => {
  e.preventDefault();

  form.submit({
    preserveScroll: true,
    onSuccess: () => form.reset(),
  });
};

使用 Alpine 和 Blade (Using Alpine and Blade)

使用 Laravel Precognition,你可以為使用者提供即時驗證體驗,而無需在前端 Alpine 應用程式中複製驗證規則。為了說明它的運作方式,讓我們建立一個用於在應用程式中建立新使用者的表單。

首先,要為路由啟用 Precognition,應將 HandlePrecognitiveRequests 中介層加入到路由定義中。你還應該建立一個表單請求來存放路由的驗證規則:

use App\Http\Requests\CreateUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', function (CreateUserRequest $request) {
    // ...
})->middleware([HandlePrecognitiveRequests::class]);

接下來,你應該透過 NPM 安裝 Laravel Precognition 的 Alpine 前端輔助工具:

npm install laravel-precognition-alpine

然後,在你的 resources/js/app.js 檔案中向 Alpine 註冊 Precognition 外掛:

import Alpine from "alpinejs";
import Precognition from "laravel-precognition-alpine";

window.Alpine = Alpine;

Alpine.plugin(Precognition);
Alpine.start();

安裝並註冊 Laravel Precognition 套件後,你現在可以使用 Precognition 的 $form 「魔術」建立表單物件,提供 HTTP 方法(post)、目標 URL(/users)和初始表單資料。

要啟用即時驗證,你應該將表單的資料繫定到其相關輸入,然後監聽每個輸入的 change 事件。在 change 事件處理程式中,你應該呼叫表單的 validate 方法,並提供輸入的名稱:

<form
  x-data="{
    form: $form('post', '/register', {
        name: '',
        email: '',
    }),
}"
>
  @csrf
  <label for="name">Name</label>
  <input
    id="name"
    name="name"
    x-model="form.name"
    @change="form.validate('name')"
  />
  <template x-if="form.invalid('name')">
    <div x-text="form.errors.name"></div>
  </template>

  <label for="email">Email</label>
  <input
    id="email"
    name="email"
    x-model="form.email"
    @change="form.validate('email')"
  />
  <template x-if="form.invalid('email')">
    <div x-text="form.errors.email"></div>
  </template>

  <button :disabled="form.processing">Create User</button>
</form>

Now, as the form is filled by the user, Precognition will provide live validation output powered by the validation rules in the route's form request. When the form's inputs are changed, a debounced "precognitive" validation request will be sent to your Laravel application. You may configure the debounce timeout by calling the form's setValidationTimeout function:

form.setValidationTimeout(3000);

When a validation request is in-flight, the form's validating property will be true:

<template x-if="form.validating">
  <div>Validating...</div>
</template>

Any validation errors returned during a validation request or a form submission will automatically populate the form's errors object:

<template x-if="form.invalid('email')">
  <div x-text="form.errors.email"></div>
</template>

You can determine if the form has any errors using the form's hasErrors property:

<template x-if="form.hasErrors">
  <div><!-- ... --></div>
</template>

You may also determine if an input has passed or failed validation by passing the input's name to the form's valid and invalid functions, respectively:

<template x-if="form.valid('email')">
  <span>✅</span>
</template>

<template x-if="form.invalid('email')">
  <span>❌</span>
</template>

[!WARNING] A form input will only appear as valid or invalid once it has changed and a validation response has been received.

As we have seen, you can hook into an input's change event and validate individual inputs as the user interacts with them; however, you may need to validate inputs that the user has not yet interacted with. This is common when building a "wizard", where you want to validate all visible inputs, whether the user has interacted with them or not, before moving to the next step.

To do this with Precognition, you should call the validate method passing the field names you wish to validate to the only configuration key. You may handle the validation result with onSuccess or onValidationError callbacks:

<button
  type="button"
  @click="form.validate({
        only: ['name', 'email', 'phone'],
        onSuccess: (response) => nextStep(),
        onValidationError: (response) => /* ... */,
    })"
>
  Next Step
</button>

You may determine if a form submission request is in-flight by inspecting the form's processing property:

<button :disabled="form.processing">Submit</button>

Repopulating Old Form Data

In the user creation example discussed above, we are using Precognition to perform live validation; however, we are performing a traditional server-side form submission to submit the form. So, the form should be populated with any "old" input and validation errors returned from the server-side form submission:

<form
  x-data="{
    form: $form('post', '/register', {
        name: '{{ old('name') }}',
        email: '{{ old('email') }}',
    }).setErrors({{ Js::from($errors->messages()) }}),
}"
></form>

Alternatively, if you would like to submit the form via XHR you may use the form's submit function, which returns an Axios request promise:

<form
  x-data="{
        form: $form('post', '/register', {
            name: '',
            email: '',
        }),
        submit() {
            this.form.submit()
                .then(response => {
                    this.form.reset();

                    alert('User created.')
                })
                .catch(error => {
                    alert('An error occurred.');
                });
        },
    }"
  @submit.prevent="submit"
></form>

Configuring Axios (Configuring Axios)

The Precognition validation libraries use the Axios HTTP client to send requests to your application's backend. For convenience, the Axios instance may be customized if required by your application. For example, when using the laravel-precognition-vue library, you may add additional request headers to each outgoing request in your application's resources/js/app.js file:

import { client } from "laravel-precognition-vue";

client.axios().defaults.headers.common["Authorization"] = authToken;

Or, if you already have a configured Axios instance for your application, you may tell Precognition to use that instance instead:

import Axios from "axios";
import { client } from "laravel-precognition-vue";

window.axios = Axios.create();
window.axios.defaults.headers.common["Authorization"] = authToken;

client.use(window.axios);

[!WARNING] The Inertia flavored Precognition libraries will only use the configured Axios instance for validation requests. Form submissions will always be sent by Inertia.

Customizing Validation Rules (Customizing Validation Rules)

It is possible to customize the validation rules executed during a precognitive request by using the request's isPrecognitive method.

For example, on a user creation form, we may want to validate that a password is "uncompromised" only on the final form submission. For precognitive validation requests, we will simply validate that the password is required and has a minimum of 8 characters. Using the isPrecognitive method, we can customize the rules defined by our form request:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;

class StoreUserRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    protected function rules()
    {
        return [
            'password' => [
                'required',
                $this->isPrecognitive()
                    ? Password::min(8)
                    : Password::min(8)->uncompromised(),
            ],
            // ...
        ];
    }
}

Handling File Uploads (Handling File Uploads)

By default, Laravel Precognition does not upload or validate files during a precognitive validation request. This ensure that large files are not unnecessarily uploaded multiple times.

Because of this behavior, you should ensure that your application customizes the corresponding form request's validation rules to specify the field is only required for full form submissions:

/**
 * Get the validation rules that apply to the request.
 *
 * @return array
 */
protected function rules()
{
    return [
        'avatar' => [
            ...$this->isPrecognitive() ? [] : ['required'],
            'image',
            'mimes:jpg,png',
            'dimensions:ratio=3/2',
        ],
        // ...
    ];
}

If you would like to include files in every validation request, you may invoke the validateFiles function on your client-side form instance:

form.validateFiles();

Managing Side-Effects (Managing Side-Effects)

When adding the HandlePrecognitiveRequests middleware to a route, you should consider if there are any side-effects in other middleware that should be skipped during a precognitive request.

For example, you may have a middleware that increments the total number of "interactions" each user has with your application, but you may not want precognitive requests to be counted as an interaction. To accomplish this, we may check the request's isPrecognitive method before incrementing the interaction count:

<?php

namespace App\Http\Middleware;

use App\Facades\Interaction;
use Closure;
use Illuminate\Http\Request;

class InteractionMiddleware
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): mixed
    {
        if (! $request->isPrecognitive()) {
            Interaction::incrementFor($request->user());
        }

        return $next($request);
    }
}

Testing (Testing)

If you would like to make precognitive requests in your tests, Laravel's TestCase includes a withPrecognition helper which will add the Precognition request header.

Additionally, if you would like to assert that a precognitive request was successful, e.g., did not return any validation errors, you may use the assertSuccessfulPrecognition method on the response:

it('validates registration form with precognition', function () {
    $response = $this->withPrecognition()
        ->post('/register', [
            'name' => 'Taylor Otwell',
        ]);

    $response->assertSuccessfulPrecognition();

    expect(User::count())->toBe(0);
});
public function test_it_validates_registration_form_with_precognition()
{
    $response = $this->withPrecognition()
        ->post('/register', [
            'name' => 'Taylor Otwell',
        ]);

    $response->assertSuccessfulPrecognition();
    $this->assertSame(0, User::count());
}