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>
フォーム入力は、変更され、バリデーションレスポンスが受信された後にのみ、有効または無効として表示されます。
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の使用
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>}
フォーム入力は、変更され、バリデーションレスポンスが受信された後にのみ、有効または無効として表示されます。
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の使用
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>
フォーム入力は、変更され、バリデーションレスポンスが受信された後にのみ、有効または無効として表示されます。
これまで見てきたように、入力の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)
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());}