INFRA

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

Laravelプロジェクト作成

Terminal window
laravel new hoge

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

DB作成・設定

Terminal window
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

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

Terminal window
php artisan migrate
php artisan serve

Jetstreamのデフォルト画面

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

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

Terminal window
php artisan db:seed

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

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

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

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

maryUIを入れてみる

これです。

https://mary-ui.com/docs/installationmary-ui.com

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

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

config/mary.php

<?php
return [
'prefix' => 'mary-',
...

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

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

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

Terminal window
php artisan make:migration create_authors_table
php artisan make:migration create_posts_table

そして、migrateします。

Terminal window
php artisan migrate

中身を確認してみます。

Terminal window
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が出力した内容に置き換えます。

Terminal window
php artisan make:model Author
php artisan make:model Post

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

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

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

Terminal window
php artisan make:volt "author-manager"

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

Terminal window
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が使うレイアウトが作成されます。

Terminal window
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>