コンテンツへスキップ

Precognition

はじめに

Laravel Precognitionを使用すると、将来のHTTPリクエストの結果を予測できます。Precognitionの主なユースケースの1つは、アプリケーションのバックエンドバリデーションルールを複製することなく、フロントエンドJavaScriptアプリケーションに「ライブ」バリデーションを提供できることです。Precognitionは、LaravelのInertiaベースのスターターキットと特に相性が良いです。

Laravelが「先見的なリクエスト」を受信すると、ルートのミドルウェアをすべて実行し、フォームリクエストのバリデーションを含め、ルートのコントローラーの依存関係を解決しますが、ルートのコントローラーメソッド自体は実際には実行しません。

ライブバリデーション

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を介してVue用のLaravel Precognitionフロントエンドヘルパーをインストールする必要があります。

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>

入力のバリデーションに成功または失敗したかどうかは、それぞれフォームのvalid関数とinvalid関数に入力名を渡すことで判断できます。

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

フォーム入力は、変更され、バリデーションレスポンスが受信された後にのみ、有効または無効として表示されます。

Precognitionを使用してフォームの入力のサブセットをバリデーションしている場合、エラーを手動でクリアすると便利な場合があります。これを実現するには、フォームのforgetError関数を使用できます。

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

これまで見てきたように、入力のchangeイベントにフックして、ユーザーが操作するにつれて個々の入力をバリデーションできます。ただし、ユーザーがまだ操作していない入力をバリデーションする必要がある場合があります。これは、「ウィザード」を構築する場合に一般的であり、次のステップに進む前に、ユーザーが操作したかどうかに関係なく、すべての表示されている入力をバリデーションしたい場合です。

Precognitionでこれを行うには、touchメソッドに名前を渡すことで、バリデーションするフィールドを「タッチ済」としてマークする必要があります。その後、onSuccessまたはonValidationErrorコールバックを使用してvalidateメソッドを呼び出します。

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

もちろん、フォーム送信への応答に応じてコードを実行することもできます。フォームのsubmit関数はAxiosリクエストプロミスを返します。これにより、レスポンスペイロードへのアクセス、送信成功時のフォーム入力のリセット、または失敗したリクエストの処理が容易になります。

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の使用

lightbulb

VueとInertiaを使用してLaravelアプリケーションを開発する際に、先に進みたい場合は、スターターキットの1つを使用することを検討してください。Laravelのスターターキットは、新しいLaravelアプリケーションのバックエンドとフロントエンドの認証スキャフォールディングを提供します。

VueとInertiaでPrecognitionを使用する前に、VueでのPrecognitionの使用に関する一般的なドキュメントを確認してください。InertiaでVueを使用する場合は、NPMを介してInertia互換のPrecognitionライブラリをインストールする必要があります。

npm install laravel-precognition-vue-inertia

インストール後、PrecognitionのuseForm関数は、上記で説明したバリデーション機能で拡張されたInertia フォームヘルパーを返します。

フォームヘルパーのsubmitメソッドは簡素化されており、HTTPメソッドまたはURLを指定する必要がなくなりました。代わりに、Inertiaのvisitオプションを最初の引数として渡すことができます。さらに、submitメソッドは、上記のVueの例のようにPromiseを返しません。代わりに、submitメソッドに渡されたvisitオプションで、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の使用

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を介してReact用のLaravel Precognitionフロントエンドヘルパーをインストールする必要があります。

npm install laravel-precognition-react

Laravel Precognitionパッケージをインストールしたら、PrecognitionのuseForm関数を使用してフォームオブジェクトを作成できます。HTTPメソッド(post)、ターゲットURL(/users)、および初期フォームデータを提供します。

ライブバリデーションを有効にするには、各入力のchangeイベントとblurイベントをリッスンする必要があります。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>}

入力のバリデーションに成功または失敗したかどうかは、それぞれフォームのvalid関数とinvalid関数に入力名を渡すことで判断できます。

{form.valid('email') && <span></span>}
 
{form.invalid('email') && <span></span>}
exclamation

フォーム入力は、変更され、バリデーションレスポンスが受信された後にのみ、有効または無効として表示されます。

Precognitionを使用してフォームの入力のサブセットをバリデーションしている場合、エラーを手動でクリアすると便利な場合があります。これを実現するには、フォームのforgetError関数を使用できます。

<input
id="avatar"
type="file"
onChange={(e) => {
form.setData('avatar', e.target.value);
 
form.forgetError('avatar');
}}
>

これまで見てきたように、入力のblurイベントにフックして、ユーザーが操作する際に個々の入力を検証できます。しかし、ユーザーがまだ操作していない入力も検証する必要がある場合があります。「ウィザード」を作成する場合によくあります。これは、次のステップに進む前に、ユーザーが操作したかどうかに関係なく、表示されているすべての入力を検証する場合です。

Precognitionでこれを行うには、touchメソッドに名前を渡すことで、バリデーションするフィールドを「タッチ済」としてマークする必要があります。その後、onSuccessまたはonValidationErrorコールバックを使用してvalidateメソッドを呼び出します。

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

もちろん、フォーム送信へのレスポンスに応じてコードを実行することもできます。フォームのsubmit関数はAxiosリクエストプロミスを返します。これは、レスポンスペイロードへのアクセス、フォーム送信が成功した場合のフォーム入力のリセット、または失敗したリクエストの処理に便利な方法を提供します。

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の使用

lightbulb

ReactとInertiaを使用してLaravelアプリケーションを開発する際に、迅速に開始したい場合は、スターターキットのいずれかを使用することを検討してください。Laravelのスターターキットは、新しいLaravelアプリケーションのバックエンドとフロントエンドの認証スキャフォールディングを提供します。

ReactとInertiaでPrecognitionを使用する前に、ReactでのPrecognitionの使用に関する一般的なドキュメントを確認してください。InertiaでReactを使用する場合は、NPM経由でInertia互換のPrecognitionライブラリをインストールする必要があります。

npm install laravel-precognition-react-inertia

インストール後、PrecognitionのuseForm関数は、上記で説明したバリデーション機能で拡張されたInertia フォームヘルパーを返します。

フォームヘルパーのsubmitメソッドは合理化され、HTTPメソッドやURLを指定する必要がなくなりました。代わりに、Inertiaのvisitオプションを最初で唯一の引数として渡すことができます。さらに、submitメソッドは、上記のReactの例のようにPromiseを返しません。代わりに、submitメソッドに渡されたvisitオプションで、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の使用

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経由でAlpine用のLaravel Precognitionフロントエンドヘルパーをインストールする必要があります。

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>

これで、ユーザーがフォームに入力すると、Precognitionはルートのフォームリクエストのバリデーションルールによって提供されるライブバリデーション出力を提供します。フォームの入力が変更されると、デバウンスされた「先見的な」バリデーションリクエストがLaravelアプリケーションに送信されます。フォームのsetValidationTimeout関数を呼び出すことで、デバウンスタイムアウトを設定できます。

form.setValidationTimeout(3000);

バリデーションリクエストが進行中の場合、フォームのvalidatingプロパティはtrueになります。

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

バリデーションリクエスト中またはフォーム送信中に返されたバリデーションエラーは、フォームのerrorsオブジェクトに自動的に設定されます。

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

フォームにエラーがあるかどうかは、フォームのhasErrorsプロパティを使用して判断できます。

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

入力のバリデーションに成功または失敗したかどうかは、それぞれフォームのvalid関数とinvalid関数に入力名を渡すことで判断できます。

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

フォーム入力は、変更され、バリデーションレスポンスが受信された後にのみ、有効または無効として表示されます。

これまで見てきたように、入力のchangeイベントにフックして、ユーザーが操作するにつれて個々の入力をバリデーションできます。ただし、ユーザーがまだ操作していない入力をバリデーションする必要がある場合があります。これは、「ウィザード」を構築する場合に一般的であり、次のステップに進む前に、ユーザーが操作したかどうかに関係なく、すべての表示されている入力をバリデーションしたい場合です。

Precognitionでこれを行うには、touchメソッドに名前を渡すことで、バリデーションするフィールドを「タッチ済」としてマークする必要があります。その後、onSuccessまたはonValidationErrorコールバックを使用してvalidateメソッドを呼び出します。

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

フォーム送信リクエストが進行中かどうかは、フォームのprocessingプロパティを検査することで判断できます。

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

古いフォームデータの再入力

上記で説明したユーザー作成の例では、Precognitionを使用してライブ検証を実行していますが、フォームを送信するために従来のサーバーサイドフォーム送信を実行しています。そのため、フォームには、サーバーサイドフォーム送信から返された「古い」入力と検証エラーがすべて入力されている必要があります。

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

あるいは、XHR経由でフォームを送信したい場合は、フォームのsubmit関数を使用できます。これはAxiosリクエストプロミスを返します。

<form
x-data="{
form: $form('post', '/register', {
name: '',
email: '',
}),
submit() {
this.form.submit()
.then(response => {
form.reset();
 
alert('User created.')
})
.catch(error => {
alert('An error occurred.');
});
},
}"
@submit.prevent="submit"
>

Axiosの設定

Precognition検証ライブラリは、Axios HTTPクライアントを使用して、アプリケーションのバックエンドにリクエストを送信します。便宜上、アプリケーションで必要に応じてAxiosインスタンスをカスタマイズできます。たとえば、laravel-precognition-vueライブラリを使用する場合、アプリケーションのresources/js/app.jsファイルで、各発信リクエストに追加のリクエストヘッダーを追加できます。

import { client } from 'laravel-precognition-vue';
 
client.axios().defaults.headers.common['Authorization'] = authToken;

または、アプリケーションに既に構成済みのAxiosインスタンスがある場合は、Precognitionにそのインスタンスを使用するように指示できます。

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)
exclamation

Inertia対応のPrecognitionライブラリは、検証リクエストにのみ構成済みのAxiosインスタンスを使用します。フォーム送信は常にInertiaによって送信されます。

バリデーションルールのカスタマイズ

リクエストのisPrecognitiveメソッドを使用して、先読みリクエスト中に実行される検証ルールをカスタマイズできます。

たとえば、ユーザー作成フォームでは、最終的なフォーム送信時のみ、パスワードが「侵害されていない」ことを検証したい場合があります。先読み検証リクエストでは、パスワードが必要であり、8文字以上の最小文字数であることを検証するだけです。isPrecognitiveメソッドを使用すると、フォームリクエストで定義されたルールをカスタマイズできます。

<?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(),
],
// ...
];
}
}

ファイルアップロードの処理

デフォルトでは、Laravel Precognitionは、先読み検証リクエスト中にファイルのアップロードや検証を実行しません。これにより、大きなファイルが不必要に複数回アップロードされるのを防ぎます。

この動作のため、アプリケーションが対応するフォームリクエストの検証ルールをカスタマイズして、フィールドが完全なフォーム送信にのみ必要であることを指定する必要があります。

/**
* 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',
],
// ...
];
}

すべての検証リクエストにファイルを含めたい場合は、クライアント側のフォームインスタンスでvalidateFiles関数を呼び出すことができます。

form.validateFiles();

副作用の管理

ルートにHandlePrecognitiveRequestsミドルウェアを追加する際には、先読みリクエスト中にスキップする必要がある他のミドルウェアに副作用がないかどうかを検討する必要があります。

たとえば、ユーザーがアプリケーションと対話した総「インタラクション」数を増やすミドルウェアがある場合がありますが、先読みリクエストをインタラクションとしてカウントしたくない場合があります。これを実現するには、インタラクション数を増やす前に、リクエストのisPrecognitiveメソッドを確認します。

<?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);
}
}

テスト

テストで先読みリクエストを行いたい場合は、LaravelのTestCaseにはwithPrecognitionヘルパーが含まれており、これによりPrecognitionリクエストヘッダーが追加されます。

さらに、先読みリクエストが成功した(つまり、検証エラーが返されなかった)ことをアサートしたい場合は、レスポンスでassertSuccessfulPrecognitionメソッドを使用できます。

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());
}