サーバーサイド側の設定
公式に従ってサーバーサイドのInertiaの設定をしていく
https://inertiajs.com/client-side-setup
https://laravel.com/docs/10.x/starter-kits を使わずに個別でインストールしていく
依存関係をインストール
sail composer require inertiajs/inertia-laravel
ルートテンプレートの作成
InertiaとLaravelでのプロジェクトは、デフォルトのルートテンプレートを app.blade.php
としている。
# /resources/views/app.blade.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
@vite('resources/js/app.js')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>
-
@inertiaHead
Inertia.jsを使用する上で必要なメタデータやリンク等を動的に挿入するタグ。
-
@inertia
フロントエンドのエントリポイント作成のためのタグ。ここに アプリケーションのメインjavascriptファイル(app.js等) が読み込まれ実行される。
ミドルウェアの設定
ArtisanコマンドでHandleInertiaRequestsミドルウェアをアプリケーションに公開する。
sail php artisan inertia:middleware
ミドルウェアが公開されたら、Web ミドルウェア グループの最後に、HandleInertiaRequests ミドルウェアを に登録する。
# /app/Http/Kernel.php
'web' => [
// ...
\App\Http\Middleware\HandleInertiaRequests::class,
],
レスポンスの作成
ルーターでやる場合
# /route/web.php
Route::get('/news/{news}', function () {
return Inertia::render('News/Show');
});
コントローラーでやる場合
# /app/Http/Controllers/NewsController.php
use Inertia\Inertia;
class NewsController extends Controller
{
/**
* Display the specified resource.
*/
public function show(News $news)
{
return Inertia::render('News/Show', [
'news' => $news->only(
'id',
'title',
'body',
),
]);
}
}
フロントエンド側の設定
ViteとVue3で実装します。
Laravel10では、Viteが標準のフロントエンドビルドツールとして採用されておりプロジェクトを作成すると、Viteに関連する設定ファイルが初めから含まれている。
ViteのVueプラグインをインストール
@vitejs/plugin-vue
をインストールすると、それ自体でVueを依存関係として持っているため、Vueも自動的にインストールされる。
Asset Bundling (Vite) - Laravel 10.x - The PHP Framework For Web Artisans
sail npm install @vitejs/plugin-vue --save-dev
// /vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'; //←追加
export default defineConfig({
plugins: [
vue(), //←追加
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
InertiaとVue3の依存関係のためのライブラリをインストール
sail npm install @inertiajs/vue3
Inertiaアプリの初期化
// /resources/js/app.js
import './bootstrap';
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
createInertiaApp({
resolve: (name) => {
const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
return pages[`./Pages/${name}.vue`];
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el);
},
});
-
resolve
ページコンポーネントの名前を受け取り、対応するVueコンポーネントを返す。ここでは
import.meta.glob
を使用して、*./Pages/*内の全Vueファイルを動的にインポートして返している。 -
setup
アプリケーションが起動する際に呼び出される。
resolve
で返されるコンポーネントを現在のページとしてレンダリングされる。従来のVueアプリケーションでApp.vueが固定のルートコンポーネントとして機能するのとは異なり、現在のルートに応じてルートコンポーネントが動的に変わる。
コンポーネントの作成
# /resources/js/Pages/News/Show.vue
<template>
<div>
Hello World!
</div>
</template>
フロントエンドサーバーを起動してアクセス
Viteのフロントエンドサーバーを起動して
sail npm run dev
http://localhost/news/1 にアクセス
CRUDの実装
共通
CRUD用のルートに修正する。
# /routes/web.php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\NewsController; //←追加
Route::get('/', function () {
return view('welcome');
});
// ↓削除
// Route::get('/news/{news}', function () {
// return Inertia::render('News/Show');
// });
// ↓以下6行追加
Route::resource('news', NewsController::class)
->except(['store', 'update']);
Route::post('news/create', [NewsController::class, 'store'])
->name('news.store');
Route::put('news/{news}/edit', [NewsController::class, 'update'])
->name('news.update');
-
resource
以下のルートを作成してくれる。
動詞 URI アクション ルート名 GET /news
index news.index GET /news/create
create news.create POST /news
store news.store GET /news/{news}
show news.show GET /news/{news}/edit
edit news.edit PUT/PATCH /news/{news}
update news.update DELETE /news/{news}
destroy news.destroy -
Route::post
、Route::put
storeとupdateを除外して別途ルートを設定している。sailでの開発環境でvalidationエラー時に、元いた画面ではなくindexにリダイレクトされてしまい、どうしても期待通りに動かなかった。なぜこうなるか、有識者の方教えてくださると幸いです。
表のstoreとupdateのURI部分が以下のように変更される。
動詞 URI アクション ルート名 POST /news/create
store news.store PUT/PATCH /news/{news}/edit
update news.update
Ziggyをインストールする。
Ziggy は、Laravelのroute()
と同じように動作するjavascript のRoute()
関数を提供しており、JavaScript で名前付きの Laravel ルートを簡単に使用できるようになる。
Breezeをインストールすると自動的に入ってくるが、今回はBreezeをインストールせずに入れてみる。
https://github.com/tighten/ziggy?tab=readme-ov-file#vue
https://iwasherefirst2.medium.com/how-to-setup-ziggy-on-laravel-inertija-vite-vue-19385f50848e
sail composer require tightenco/ziggy
# resources/views/app.blade.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
@vite('resources/js/app.js')
@routes //←追加
@inertiaHead
</head>
<body>
@inertia
</body>
</html>
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import path from 'path'; //←追加
export default defineConfig({
// ↓5行追加
resolve: {
alias: {
'ziggy-js': path.resolve('vendor/tightenco/ziggy'),
}
},
plugins: [
vue(),
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
// resources/js/app.js
import './bootstrap';
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { ZiggyVue } from 'ziggy-js'; //←追加
createInertiaApp({
resolve: (name) => {
const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
return pages[`./Pages/${name}.vue`];
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue) //←追加
.mount(el);
},
});
Read
一覧画面と詳細画面とそのそれぞれの処理を追加していく。
# /app/Http/Controllers/NewsController.php
class NewsController extends Controller
{
public function index()
{
$news = News::all();
return Inertia::render('News/Index', [
'news' => $news,
]);
}
...省略...
public function show(News $news)
{
return Inertia::render('News/Show', [
'news' => $news->only(
'title',
'body',
),
]);
}
...省略...
// resources/js/Pages/News/Index.vue
<script setup>
import { Link } from '@inertiajs/vue3'
defineProps({
news: Object,
})
</script>
<template>
<div v-if="news">
<div v-for="n in news">
<h1>{{ n.title }}</h1>
<p>{{ n.body }}</p>
<Link :href="route('news.show', n)">詳細</Link>
</div>
</div>
</template>
<!-- resources/js/Pages/News/Show.vue -->
<script setup>
import { Link } from '@inertiajs/vue3';
defineProps({
news: Object,
})
</script>
<template>
<div>
<h1>{{ news.title }}</h1>
<p>{{ news.body }}</p>
</div>
<Link :href="route('news.index')">一覧に戻る</Link>
</template>
Create
新規作成画面とその処理を追加していく。
一覧画面に「新規作成」へのリンクを追加。
// resources/js/Pages/News/Index.vue
...省略...
</script>
<template>
<Link :href="route('news.create')">新規作成</Link> <!-- ←追加 -->
<div v-if="news">
<div v-for="n in news">
<h1>{{ n.title }}</h1>
<p>{{ n.body }}</p>
<Link :href="route('news.show', n)">詳細</Link>
</div>
</div>
</template>
# app/Http/Controllers/NewsController.php
class NewsController extends Controller
{
...省略...
public function create()
{
return Inertia::render('News/Create', []);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required'],
'body' => ['required'],
]);
News::create($validated);
return redirect()->route('news.index');
}
...省略...
<!-- resources/js/Pages/News/Create.vue -->
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
title: null,
body: null,
})
const submit = () => {
form.post(route('news.store'));
}
</script>
<template>
<form @submit.prevent="submit">
<table>
<tr>
<th><label for="title">title:</label></th>
<td>
<div v-if="form.errors.title">{{ form.errors.title }}</div>
<input id="title" type="text" v-model="form.title">
</td>
</tr>
<tr>
<th><label for="body">body:</label></th>
<td>
<div v-if="form.errors.body">{{ form.errors.body }}</div>
<textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
</td>
</tr>
<tr>
<th></th>
<td>
<button type="submit" :disabled="form.processing">登録</button>
</td>
</tr>
</table>
</form>
</template>
Update
<!-- resources/js/Pages/News/Index.vue -->
...省略...
</script>
<template>
<Link :href="route('news.create')">新規作成</Link>
<div v-if="news">
<div v-for="n in news">
<h1>{{ n.title }}</h1>
<p>{{ n.body }}</p>
<div>
<Link :href="route('news.show', n)">詳細</Link>
</div>
<div>
<Link :href="route('news.edit', n)">編集</Link> <!-- ←追加 -->
</div>
</div>
</div>
</template>
# app/Http/Controllers/NewsController.php
class NewsController extends Controller
{
...省略...
public function edit(News $news)
{
return Inertia::render('News/Edit', [
'news' => $news,
]);
}
public function update(Request $request, News $news)
{
$validated = $request->validate([
'title' =>'required',
'body' => 'required',
]);
$news->update($validated);
return redirect()->route('news.index');
}
...省略...
<!-- resources/js/Pages/News/Edit.vue -->
<script setup>
import { useForm } from '@inertiajs/vue3'
const props = defineProps({
news: Object,
})
const form = useForm({
title: props.news.title,
body: props.news.body,
})
const submit = () => {
form.put(route('news.update', props.news.id ))
}
</script>
<template>
<form @submit.prevent="submit">
<table>
<tr>
<th><label for="title">title:</label></th>
<td>
<div v-if="form.errors.title">{{ form.errors.title }}</div>
<input id="title" type="text" v-model="form.title">
</td>
</tr>
<tr>
<th><label for="body">body:</label></th>
<td>
<div v-if="form.errors.body">{{ form.errors.body }}</div>
<textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
</td>
</tr>
<tr>
<th></th>
<td>
<button type="submit" :disabled="form.processing">更新</button>
</td>
</tr>
</table>
</form>
</template>
Delete
# app/Http/Controllers/NewsController.php
class NewsController extends Controller
{
public function destroy(News $news)
{
$news->delete();
return redirect()->route('news.index')->with('message', 'News Deleted Successfully');
}
<!-- resources/js/Pages/News/Index.vue -->
<script setup>
import { Link, useForm } from '@inertiajs/vue3' //←修正
defineProps({
news: Object,
})
// ↓以下6行追加
const form = useForm({});
const deleteNews = (news) => {
form.delete(route('news.destroy', news), {
preserveScroll: true,
})
}
</script>
<template>
<Link :href="route('news.create')">新規作成</Link>
<div v-if="news">
<div v-for="n in news">
<h1>{{ n.title }}</h1>
<p>{{ n.body }}</p>
<div>
<Link :href="route('news.show', n)">詳細</Link>
</div>
<div>
<Link :href="route('news.edit', n)">編集</Link>
</div>
<div>
<Link href="#" @click="deleteNews(n)">削除する</Link> <!-- ←追加 -->
</div>
</div>
</div>
</template>
Session
flash
//コントローラーの削除メソッド
public function destroy ($id)
{
Bookmark::destroy($id);
return to_route('bookmark.index')->with('message', '削除しました'); //削除後ブックマーク一覧ページへリダイレクト
}
# app/Http/Middleware/HandleInertiaRequests.php
//(略)
public function share(Request $request)
{
return array_merge(parent::share($request), [
//(略)
'flash' => [
'message' => fn () => $request->session()->get('message')
],
]);
}
//(略)
import { usePage } from '@inertiajs/vue3'
const page = usePage()
console.log(page.props.flash.message)
<div v-if="page.props.flash.message" class="bg-green-200 p-2 m-1">
{{ page.props.flash.message }}
</div>
File upload
バックエンドはLaravelで。
create/store
<script setup>
const form = useForm({
title: null,
body: null,
image: null,
})
const submit = () => {
form.post(route('news.store'));
}
</script>
<template>
<div>
<input type="file" id="image" @input="form.image = $event.target.files[0]">
<!-- inertiaはこっち -->
<img :src="'/upload/'+ form.image" alt="">
<!-- bladeはこっち -->
<img src="{{ asset($user->img_path) }}" >
</div>
</template>
public function store(Request $request)
{
$formData = $request->validate([
'title' => ['required'],
'body' => ['required'],
]);
if ($image = $request->file('image')) {
$imageName = $image->getFilename(). '.'. $image->getClientOriginalExtension();
$image->storeAs('public/upload', $imageName);
$formData['image'] = $imageName;
// or
// $formData['image'] = $image->store('public/upload');
}
News::create($formData);
}
※ $image->getClientOriginalExtension()
は使わない方がいいらしい。
https://laravel.com/docs/10.x/filesystem#other-uploaded-file-information
edit/update
<script setup>
const form = useForm({
title: props.news.title,
body: props.news.body,
image: null,
registerd_image: props.news.image
})
const submit = () => {
router.post(route('admin.ramen.update', props.ramen.id), {
_method: 'put',
title: form.title,
body: form.body,
image: form.image,
registerd_image: form.registerd_image,
});
}
</script>
<template>
<input type="file" name="image" @input="form.image = $event.target.files[0]">
<img :src="`/${form.registerd_image}`" alt="">
</template>
※ form.put(route('news.update', news.id))
はできない。
public function update(Request $request, News $news)
{
$formData = $request->validate([
'title' =>'required',
'body' => 'required',
'registerd_image' => 'nullable'
]);
if ($image = $request->file('image')) {
$imageName = $image->getFilename(). '.'. $image->getClientOriginalExtension();
$image->storeAs('public/upload', $imageName);
$formData['image'] = $imageName;
// or
// $formData['image'] = $image->store('upload')
if (!empty($formData['registerd_image'])) {
unlink(storage_path('app/public/upload/'). $formData['registerd_image']);
// or
// unlink(storage_path('app/'). $validated['registerd_image']);
}
}
unset($formData['registerd_image']);
$ramen->update($formData);
}
preview
<script setup>
const form = useForm({
name: props.ramen.name,
address: props.ramen.address,
type: props.ramen.type,
taste: props.ramen.taste,
image: null,
old_image: props.ramen.image,
time_open: props.ramen.time_open,
time_close: props.ramen.time_close,
date_open: props.ramen.date_open,
day_close: props.ramen.day_close,
})
const imagePreview = ref('');
const handleFileChange = (event) => {
const file = event.target.files[0];
form.image = file;
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.value = e.target.result;
}
reader.readAsDataURL(file);
}
}
</script>
<template>
<input type="file" name="image" @input="handleFileChange">
<div>
<img v-if="imagePreview" :src="imagePreview" alt="">
<img v-else-if="form.old_image" :src="'/upload/'+ form.old_image" alt="">
<img v-else src="/image/default.jpg" alt="">
</div>
</template>