Logicky Blog

Logickyの開発ブログです

Jetstream入りのLaravelでmaryUI(Volt)を使って管理画面を作ってみる

Laravelプロジェクト作成

laravel new hoge

上記実行中に、JetstreamやLivewireなどを選択しました。Databaseもpostgresqlを選択しました。 しばらくすると、プロジェクトが作成されており、.envも作成済みでした。

DB作成・設定

psql -U postgres
# create database hoge

上記でhogeデータベースを作成し、.envの下記を設定します。

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=hoge
DB_USERNAME=postgres
DB_PASSWORD=password

マイグレーションを実行して、テーブルを作成し、サーバを起動します。

php artisan migrate
php artisan serve

Jetstreamのデフォルト画面

Jetstreamが入ってますので、ホームページの右上にlogin, registerというのがあります。

Seederでユーザを作成しログインしましょー。

php artisan db:seed

これで、下記ユーザでログインできます。

test@example.com
password

ログインするとこのようなDashboardが表示されます。

プロフィール編集画面はこのような感じです。

もちろんこのまま使えますが、今回は、maryUIを入れて、管理画面をデザインを変えながら作っていきたいと思います。

maryUIを入れてみる

これです。

mary-ui.com

composer require robsontenorio/mary
php artisan mary:install
pnpm dev

これでインストールは基本完了です。 インストール中に、どうもJetstreamが入っていることを検知して、config/mary.phpを自動で作成し、プレフィックス設定までしてくれているっぽいです。多分。下記が自動生成されていました。

config/mary.php

<?php

return [
    'prefix' => 'mary-',

...

本と著者のDBテーブルを作る

本と著者を作ります。構造は適当でよいので、Claudeでお願いします。

マイグレーションファイルを作り、中身をClaudeでもらったやつに変更します。

php artisan make:migration create_authors_table
php artisan make:migration create_posts_table

そして、migrateします。

php artisan migrate

中身を確認してみます。

psql -U postgres
# \c hoge
# \d posts
      列      |             タイプ             | 照合順序 | Null 値を許容 |            デフォルト
--------------+--------------------------------+----------+---------------+-----------------------------------
 id           | bigint                         |          | not null      | nextval('posts_id_seq'::regclass)
 author_id    | bigint                         |          | not null      |
 title        | character varying(255)         |          | not null      |
 content      | text                           |          | not null      |
 slug         | character varying(255)         |          | not null      |
 status       | character varying(255)         |          | not null      | 'draft'::character varying
 published_at | timestamp(0) without time zone |          |               |
 created_at   | timestamp(0) without time zone |          |               |
 updated_at   | timestamp(0) without time zone |          |               |

# \d authors
    列     |             タイプ             | 照合順序 | Null 値を許容 |             デフォルト
------------+--------------------------------+----------+---------------+-------------------------------------
 id         | bigint                         |          | not null      | nextval('authors_id_seq'::regclass)
 name       | character varying(255)         |          | not null      |
 email      | character varying(255)         |          | not null      |
 password   | character varying(255)         |          | not null      |
 bio        | text                           |          |               |
 created_at | timestamp(0) without time zone |          |               |
 updated_at | timestamp(0) without time zone |          |               |

本と著者のモデルを作る

これもClaudeにお願いします。下記で作成されたファイルをClaudeが出力した内容に置き換えます。

php artisan make:model Author
php artisan make:model Post

本と著者のVoltコンポーネントを作ってみる

これはどうも、Claudeさんは良く知らないようでした。VoltとmaryUIがよく知らないようです。そこで、Claudeのプロジェクトを使って、ナレッジとして、Volt, maryUIのドキュメントを入れてみようと思います。と思いましたが、一旦、Cursorでやってみます。CursorのDocにmaryUIを追加してみます。

まずは、下記コマンドでVoltコンポーネントを作成します。

php artisan make:volt "author-manager"

変なエラーが出たけど、キャッシュクリアしたら出なくなりました。

  php artisan config:cache
  php artisan route:cache
  php artisan view:cache

ただ、レイアウト関連でエラーがでました。JetstreamはLivewireを使わない前提で、resources/views/layouts/app.blade.php にレイアウトを用意してくれます。一方でLivewire(Volt)は、resources/views/components/layouts/app.blade.php をデフォルトでは使うようになっているのかなと思っています。多分。下記でLivewireが使うレイアウトが作成されます。

php artisan livewire:layout

上記で作成されたレイアウトに @vite(['resources/css/app.css', 'resources/js/app.js']) を下記のように追加します。

resources/views/components/layouts/app.blade.php

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

        <title>{{ $title ?? 'Page Title' }}</title>

        @vite(['resources/css/app.css', 'resources/js/app.js'])
    </head>
    <body>
        {{ $slot }}
    </body>
</html>

これでエラーでなくなったので、Voltコンポーネントを作成していきます。 著者の一覧、追加、編集、削除です。もちろん、バリデーションチェック・権限管理等を加える必要があります。Voltのお試しです。

<?php

use function Livewire\Volt\{state};
use App\Models\Author;

state([
    'authors' => Author::all(),
    'name' => '',
    'email' => '',
    'password' => '',
    'bio' => '',
    'selectedAuthor' => null,
    'tableHeaders' => [
        ['key' => 'id', 'label' => 'ID'],
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'actions', 'label' => '']
    ]
]);

$addAuthor = function () {
    Author::create([
        'name' => $this->name,
        'email' => $this->email,
        'password' => bcrypt($this->password),
        'bio' => $this->bio,
    ]);
    $this->resetForm();
    $this->authors = Author::all();
};

$editAuthor = function ($id) {
    $author = Author::find($id);
    $this->selectedAuthor = $author;
    $this->name = $author->name;
    $this->email = $author->email;
    $this->bio = $author->bio;
};

$updateAuthor = function ($id) {
    $author = Author::find($id);
    $author->update([
        'name' => $this->name,
        'email' => $this->email,
        'bio' => $this->bio,
    ]);
    $this->resetForm();
    $this->authors = Author::all();
};

$deleteAuthor = function ($id) {
    Author::destroy($id);
    $this->authors = Author::all();
};

$resetForm = function () {
    $this->name = '';
    $this->email = '';
    $this->password = '';
    $this->bio = '';
    $this->selectedAuthor = null;
};

?>

<div class="p-5">
    <h1 class="text-3xl">著者</h1>

    <div class="p-5">
        <h2 class="text-2xl font-bold pb-5">著者一覧</h2>
        <x-mary-table :headers="$tableHeaders" :rows="$authors" striped>
            @scope('cell_name', $author)
              {{ $author->name }}
            @endscope

            @scope('cell_email', $author)
              {{ $author->email}}
            @endscope

            @scope('cell_actions', $author)
                <x-mary-button class="btn-primary" wire:click="editAuthor({{ $author->id }})">編集</x-mary-button>
                <x-mary-button class="btn-error" wire:click="deleteAuthor({{ $author->id }})">削除</x-mary-button>
            @endscope
        </x-mary-table>
    </div>

    <x-mary-card title="{{ $selectedAuthor ? '著者編集' : '著者追加' }}">
        <x-mary-form wire:submit.prevent="{{ $selectedAuthor ? 'updateAuthor(' . $selectedAuthor->id . ')' : 'addAuthor' }}">
            <div>
                <label for="name">名前</label>
                <x-mary-input type="text" id="name" wire:model="name"/>
            </div>
            <div>
                <label for="email">メール</label>
                <x-mary-input type="email" id="email" wire:model="email"/>
            </div>
            @if (!$selectedAuthor)
                <div>
                    <label for="password">パスワード</label>
                    <x-mary-input type="password" id="password" wire:model="password"/>
                </div>
            @endif
            <div>
                <label for="bio">バイオ</label>
                <x-mary-textarea id="bio" wire:model="bio"/>
            </div>
            <x-slot:actions>
                <x-mary-button class="btn-primary" type="submit">{{ $selectedAuthor ? '更新' : '追加' }}</x-mary-button>
                <x-mary-button class="btn-warning" type="button" wire:click="resetForm">キャンセル</x-mary-button>
            </x-slot:actions>
        </x-mary-form>
    </x-mary-card>
</div>