tohokuaikiのチラシの裏

技術的ネタとか。

Laravel11のテンプレートにCoreUIを導入して、Inertia.jsで管理者だけテンプレートレイアウトを分けてみる。

やりたいこと

  • 管理者を作って、管理画面は別レイアウトのダッシュボードにて作業できること。
  • Reactで開発できる
  • CoreUIを使ってデザインできる

ライブラリの選定

管理画面テンプレート/UI

MoonShineはReact使うのにエントリポイントを作りにくかったり、レイアウトテンプレートがあるわけじゃなくて(Overrideすればいいんだけど)Classファイルをがしがし生成して記述してっていうパッケージ方式だったので、やりにくいかも…と思ってLaravelとは疎結合なCoreUIを使ってみる。

ChatGPTに聞いてみたところいくつか候補が挙がって、最後に3つを比較したらこんな感じでChatGPTが回答した。

パッケージ名 メリット デメリット 向き 不向き
AdminLTE 豊富なコンポーネントサードパーティプラグインの対応 デザインがやや古い、設定が複雑になることがある 多機能なダッシュボード、レガシーシステム 最新のデザインやモダンな技術を使う場合
CoreUI モダンなデザイン、React/Vue/Angular対応、拡張性が高い フレームワーク依存、学習コスト モダンフロントエンド、SPA HTML/CSS/JSベースのみのプロジェクト
Tabler シンプルで軽量、モダンなUI、カスタマイズ容易 機能が少ない、プラグイン対応が少ない 小規模プロジェクト、シンプルな管理画面 大規模プロジェクト、多機能ダッシュボード

どのパッケージを選ぶかは、プロジェクトの規模や技術的な要件によります。簡単な管理画面が必要な場合はTabler、ReactやVueを使用したSPA管理画面にはCoreUI、多機能で大規模なプロジェクトにはAdminLTEが適しています。|

ということで、CoreUIにした。

認証・認可まわり

JetStreamにしようかなと思ったけど、なんかLaravel Permission でもいいんじゃないか?そこまで二要素認証とか使わないし、Reactに対応してないしで…ということで Laravel Permissionにした。スターターキットはBreeze

Laravelのログイン後の画面にCoreUIの導入

LaravelでBreezeのインストールまでは昨日の記事と同じまま。

インストール

Viteとの連携なので、CoreUI & Vite · CoreUIを見ながら。

npm i --save @coreui/coreui @popperjs/core
npm i --save-dev sass

vite.config.js

CoreUIのJavaScriptを使えるようにする。 importで~coreuiのエイリアスが使えるように設定。

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
import { resolve } from 'path'

export default defineConfig({
    resolve: {
        alias: {
            '~coreui': resolve(__dirname, 'node_modules/@coreui/coreui'),
        }
    },
    plugins: [
        laravel({
            input: ['resources/js/app.tsx'],
            refresh: true,
        }),
        react(),
    ],
    server: {
        host: true,
        hmr: {
            host: 'localhost',
        },
    }
});

CoreUIのCSSを読み込む

Reactの大元になるresources/js/app.tsxで読み込む。resources/css/app.cssresources/css/app.scss にファイル名を変更し、app..tsxの1行目のimport '../css/app.css';を import '../css/app.scss';にする。

import '../css/app.scss';

そして、resources/css/app.scss でcoreuiのcssをインポートする。

@tailwind base;
@tailwind components;
@tailwind utilities;

@import "@coreui/coreui/dist/css/coreui.min.css"

ここまでで、npm run dev してCoreUIのコンポーネントが使えればOK。

Laravel Permissionの導入

インストール

Breezeのインストールを「React with Inertia」「TypeScript」で行う。

Laravel Permissionは Installation in Laravel | laravel-permission | Spatieから

 composer require spatie/laravel-permission
 php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider
./artisan migrate

でインストール。(てか、Laravel Permissionはこの段階ではどうでもいいね…)

認証後のレイアウト周りを変更する。

認証後にはCoreUIのレイアウトになるようにする。試しに、認証後のdashboardとprofileを /admin/dashboard/admin/profile のrouteにしてログインしてないと入れないようにする。

ルーティング

prefixに/adminを付けて、nameにadmin.をつける。

<?php
Route::prefix('/admin')->name('admin.')->group(function () {
    Route::get('/dashboard', function () {
        return Inertia::render('Dashboard');
    })->middleware(['auth', 'verified'])->name('dashboard');
});

Route::middleware('auth')->prefix('/admin')->name('admin.')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

ログイン時のControllerを変更

./artisan route:list すると、

POST login .......................................................... Auth\AuthenticatedSessionController@store

なので、storeメソッドを先ほどのルーティングの変更に合わせて変える。route('admin.dashboard')のところ。

<?php
    /**
     * Handle an incoming authentication request.
     */
    public function store(LoginRequest $request): RedirectResponse
    {
        $request->authenticate();

        $request->session()->regenerate();

        return redirect()->intended(route('admin.dashboard', absolute: false));
    }

bladeファイルの変更

Inertia.jsは resources/views/app.blade.php を起点にするのだけど、管理者用はこれを変えたい場合には、app/Http/Middleware/HandleInertiaRequests.php を変更する。rootViewメソッドをoverrideして自前のview文字列を返す。

<?
    public function rootView(Request $request)
    {
        if (Auth::user()){
            return 'admin';
        }
        return parent::rootView($request);
    }

で、 resources/views/app.blade.php をコピーして、 管理者用のrootView resources/views/admin.blade.php を作成。このbladeでは、resources/js/admin.tsxコンパイルするようにしておく。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title inertia>User::{{ config('app.name', 'Laravel') }}</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

        <!-- Scripts -->
        @routes
        @viteReactRefresh
        @vite(['resources/js/admin.tsx', "resources/js/Pages/Admin/{$page['component']}.tsx"])
        @inertiaHead
    </head>
    <body class="font-sans antialiased">
        Admin Page
        @inertia
    </body>
</html>

このテンプレートにある $page['component'] というところは、どうもInertia::render()の第一引数で指定されているテンプレート名らしい。

Reactテンプレートの変更

テンプレートのコピー

上記のadmin.blade.phpに合わせて、」ログイン後にホーム http://localhost:8000/ に行ったときに表示する resources/js/Pages/Welcome.tsxresources/js/Pages/Admin/Welcome.tsx にコピーする。この「Welcome」の指定は、routes/web.php で、InertisjsがWelcomeページを表示するようにされているから。

<?php
Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

ルーティングに関係するところを変更。

Laravelのルーティングを変えたので、Laravelのroute()関数を使っているところは変更が必要になる。tsxファイルのroute()があるところを適宜変更。このroute()は、React/JavaScriptではなくLaravelのヘルパ関数。

とりあえず、以下のファイルを変更。

  • resources/js/Layouts/AuthenticatedLayout.tsx
  • resources/js/Pages/Welcome.tsx
  • resources/js/Pages/Admin/Welcome.tsx

Inertia.jsのレンダリング

先ほど修正した、Welcome.tsxのroute('admin.dashborad')のリンクをクリックすると、JSONでレスポンスが返ってくる。URLも http://localhost:8000/admin/dashboard になっている。

しかし、http://localhost:18000/admin/dashboard を直接アクセスして開くと、JSONではなくHTMLが返ってくる。この時のテンプレートは、resources/views/admin.blade.php である。そして、このテンプレートの中の $page['component']Dashboardになっている。

この2つのレスポンスは、routes/web.php

<?php
Route::prefix('/admin')->name('admin.')->group(function () {
    Route::get('/dashboard', function () {
        return Inertia::render('Dashboard');
    })->middleware(['auth', 'verified'])->name('dashboard');
});

return Inertia::render('Dashboard'); による振り分けで、JavaScriptでfetchされた時はJSON、ブラウザから通常アクセスならHTMLを返すようになっている。

Inertis.jsを使っているときの注意点

Inertia.jsを使うと、Laravel側のルートが全部見えてしまう。だから、「URLがわからないからいいよね」って秘密のコッソリルーティングを作って認証を怠っていたりするとあっさりと突破されてしまう。 普通ならやらないことだけど、何の考えなしに作っていたりするとどでかいセキュリティホールになりうる。

テンプレートを作成する。

次の記事に続く…