以前から、laravel + livewire + tailwind で chat を使ったカンファレンスを作りたいと思っていました。
本来であれば画像をアップロードすべきですが、それはとても難しいのでまずは文字だけのカンファレンスサイトを作成します。
データベース名は database.sqlite で以下の 3 つのテーブルを作成します。
themes
id: 主キー
user_id: 外部キー (users.id)
title: テーマのタイトル名
messages
id: 主キー
user_id: 外部キー (users.id)
theme_id: 外部キー (theme.id)
content: メッセージ内容
まずはプロジェクトを作成します。
composer create-project --prefer-dist laravel/laravel laraconf
livewire を設定します。
cd laraconfcomposer require livewire/livewire
tailwind を設定します。
npm install -D tailwindcss postcss autoprefixernpx tailwindcss init -p
データベースはデフォルトでは sqlite です。
/database/database.sqlite で自動作成されます。
composer require laravel/breeze --devphp artisan breeze:install
サーバーを立ち上げます。
php artisan serve
users テーブルはデフォルトで作成されます。
<?php
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); });
こんな感じです。
最初は chatGPT に従って新たに作成しようとしたのですが、どうしてもエラーで新規作成できませんでした。おそらく users テーブルを削除することはできないのだろうと思います。
次に、themes テーブルを作成します。
php artisan make:migration create_themes_table
マイグレーションファイルを編集します。
<?php
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;
return new class extends Migration{ public function up() { Schema::create('themes', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('title'); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('themes'); }};
messages テーブルを作成します。
php artisan make:migration create_messages_table
マイグレーションファイルを編集します。
<?php
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;
return new class extends Migration{ public function up() { Schema::create('messages', function (Blueprint $table) { $table->id(); $table->foreignId('theme_id')->constrained()->onDelete('cascade'); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->text('content'); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('messages'); }};
マイグレーションを実行します。
php artisan migrate
モデルを作成します。
User モデルはデフォルトで作成されていますが、一部編集します。
最後の方に、以下の function messages() を追加します。
<?php....class User extends Authenticatable{ .... public function messages() { return $this->hasMany(Message::class); }}
Theme モデルを作成します。
php artisan make:model Theme
app/Models/Theme.php を編集します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;
class Theme extends Model{ use HasFactory; protected $fillable = ['user_id', 'title'];
public function user() { return $this->belongsTo(User::class); } public function messages() { return $this->hasMany(Message::class); }}
Message モデルを作成します。
php artisan make:model Message
app/Models/Message.php を編集します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;
class Message extends Model{ use HasFactory;
protected $fillable = ['user_id', 'theme_id', 'content'];
public function theme() { return $this->belongsTo(Theme::class); } public function user() { return $this->belongsTo(User::class); }}
php artisan make:livewire ThemeCreate
編集。
<?php
namespace App\Livewire;
use Livewire\Component;use App\Models\Theme;use Illuminate\Support\Facades\Auth;
class ThemeCreate extends Component{ public $title;
public function render() { return view('livewire.theme-create')->layout('livewire.base'); }
public function saveTheme() { $this->validate([ 'title' => 'required|max:255', ]);
Theme::create([ 'user_id' => Auth::id(), 'title' => $this->title, ]);
$this->reset('title'); // フォームをリセット session()->flash('success', 'テーマが作成されました!'); }}
ThemeList 作成。
php artisan make:livewire ThemeList
編集。
<?php
namespace App\Livewire;use Livewire\Component;use App\Models\Theme;
class ThemeList extends Component{ public $themes;
public function mount() { $this->themes = Theme::with('user')->orderBy('created_at', 'desc')->get(); } public function render() { return view('livewire.theme-list')->layout('livewire.base'); }}
ThemeShow 作成。
php artisan make:livewire ThemeShow
編集。
<?php
namespace App\Livewire;
use Livewire\Component;use App\Models\Theme;use App\Models\Message;
class ThemeShow extends Component{ public $theme; public $content;
public function mount($theme_id) { $this->theme = Theme::with('messages.user')->findOrFail($theme_id); }
public function saveMessage() { $this->validate([ 'content' => 'required|string|max:1000', ]);
Message::create([ 'user_id' => auth()->id(), 'theme_id' => $this->theme->id, 'content' => $this->content, ]);
$this->reset('content');
session()->flash('message', 'メッセージが送信されました!'); }
public function render() { $messages = $this->theme->messages()->orderBy('created_at', 'desc')->get(); return view('livewire.theme-show', compact('messages'))->layout('livewire.base'); }}
MessageCreate 作成。
php artisan make:livewire MessageCreate
編集。
<?php
namespace App\Livewire;
use Livewire\Component;use App\Models\Message;use Illuminate\Support\Facades\Auth;
class MessageCreate extends Component{ public $theme_id; public $content;
public function mount($theme_id) { $this->theme_id = $theme_id; }
public function saveMessage() { $this->validate([ 'content' => 'required|string|max:1000', ]);
Message::create([ 'user_id' => Auth::id(), 'theme_id' => $this->theme_id, 'content' => $this->content, ]);
$this->reset('content');
session()->flash('message', 'メッセージが送信されました!'); }
public function render() { return view('livewire.message-create')->layout('livewire.base'); }}
resources/views/livewire/theme-create.blade.php
<div class="p-4 bg-white shadow-md rounded"> <h2 class="text-xl font-bold mb-4">テーマを作成</h2>
@if (session()->has('success')) <div class="p-4 mb-4 text-green-700 bg-green-100 rounded"> {{ session('success') }} </div> @endif
<form wire:submit.prevent="saveTheme"> <div class="mb-4"> <label for="title" class="block text-sm font-medium text-gray-700">テーマのタイトル</label> <input type="text" id="title" wire:model="title" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="テーマのタイトルを入力してください"> @error('title') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror </div>
<div> <button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> 保存 </button> </div> </form></div>
resources/views/livewire/theme-list.blade.php
<div class="p-4"> <h2 class="text-xl font-bold mb-4">テーマ一覧</h2>
@foreach ($themes as $theme) <div class="mb-4 p-4 bg-white shadow-md rounded border border-slate-200"> <h3 class="text-lg font-semibold"> <!-- テーマIDをURLパラメータとして渡してテーマ詳細ページへ遷移 --> <a href="{{ route('theme.show', ['theme_id' => $theme->id]) }}" class="text-blue-600 hover:text-blue-800"> {{ $theme->title }} </a> </h3> <p class="text-sm text-gray-500">作成者: {{ $theme->user->name }}</p> <!-- ユーザー名 --> <p class="text-sm text-gray-500">作成日時: {{ $theme->created_at->format('Y-m-d H:i') }}</p> </div> @endforeach
@if ($themes->isEmpty()) <p class="text-gray-500">まだテーマが作成されていません。</p> @endif</div>
resources/views/livewire/theme-show.blade.php
<div class="p-4"> <h2 class="text-xl font-bold mb-4">{{ $theme->title }}</h2> <p class="text-sm text-gray-500">作成者: {{ $theme->user->name }}</p> <p class="text-sm text-gray-500">作成日時: {{ $theme->created_at->format('Y-m-d H:i') }}</p>
<!-- メッセージ入力フォーム --> <div class="mt-4"> <form wire:submit.prevent="saveMessage"> <div class="mb-4"> <textarea wire:model="content" class="w-full p-2 border rounded" rows="4" placeholder="メッセージを入力してください..."></textarea> @error('content') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror </div> <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"> メッセージを送信 </button> </form> </div>
<!-- メッセージ一覧 --> <h3 class="mt-8 text-lg font-semibold">メッセージ一覧</h3> <div class="bg-white p-4 shadow-md rounded border border-slate-200"> @foreach ($messages as $message) <div class="mb-4"> <p><span class="font-semibold">{{ $message->user->name }}</span>:<span class="text-xs text-gray-600">{{ $message->created_at->format('Y-m-d H:i') }}</span></p> <p>{{ $message->content }}</p> </div> @endforeach </div>
@if (session()->has('message')) <div class="bg-green-100 text-green-700 p-4 mb-4 rounded"> {{ session('message') }} </div> @endif
</div>
resources/views/livewire/message-create.blade.php
<div class="p-4"> <form wire:submit.prevent="saveMessage"> @if (session()->has('message')) <div class="bg-green-100 text-green-700 p-4 mb-4 rounded"> {{ session('message') }} </div> @endif
<div class="mb-4"> <textarea wire:model="content" class="w-full p-2 border rounded" rows="4" placeholder="メッセージを入力してください..."></textarea> @error('content') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror </div>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"> メッセージを送信 </button> </form></div>
resources/views/livewire/base.blade.php
<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> @vite('resources/css/app.css') <title>laravel</title></head><body> {{ $slot }}</body></html>
<?php
use App\Http\Controllers\ProfileController;use Illuminate\Support\Facades\Route;use App\Livewire\ThemeCreate;use App\Livewire\ThemeList;use App\Livewire\ThemeShow;use App\Livewire\MessageCreate;
Route::get('/', function () { return view('welcome');});
Route::get('/dashboard', function () { return view('dashboard');})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->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'); Route::get('/theme/create', ThemeCreate::class)->name('theme.create'); Route::get('/theme/list', ThemeList::class)->name('theme.list'); Route::get('/theme/{theme_id}', ThemeShow::class)->name('theme.show');});
require __DIR__.'/auth.php';
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">{{ __('Menu') }}</h2> <ul class="list-disc leading-10 mx-10 px-2"> <li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('theme.create') }}">テーマ作成</a></li> <li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('theme.list') }}">テーマ一覧</a></li> </ul> </x-slot></x-app-layout>
npm run buildphp artisan serve
アドレスバーからアクセスします。
http://127.0.0.1:8000/ここまでで一応動くプログラムは作成できたのですが、特に Model に関しては全く理解できず、ほとんど chatGPT の言う通りに作ってきただけです。
全く理解できていないのに前に進んでいいのかわからないので、chatGPT に訊いてみました。
これが最大のポイントかと思います。
chatGPT の答えは「Laravelで3つのテーブル(users, themes, messages)が結合できるのは、Eloquent ORM(Object Relational Mapping)のリレーション機能によるものです。」
hasMany | 一つのレコードが複数の関連レコードを持つ場合に使う(例: User が複数の Message を持つ) |
---|---|
belongsTo | 一つのレコードが他のテーブルの特定のレコードに属する場合に使う(例: Message が特定の User に属する) |
class User extends Authenticatable{ public function messages() { return $this->hasMany(Message::class); }}
User モデルは、自分に関連付けられた messages テーブルの複数レコードを取得できます。
Laravelは、デフォルトで messages テーブルの user_id カラムを使って、User と Message を関連付けます。
class Message extends Model{ public function user() { return $this->belongsTo(User::class); }
public function theme() { return $this->belongsTo(Theme::class); }}
Message モデルは、自分が属する User と Theme を取得できます。
user() メソッドは、messages テーブルの user_id カラムを使って users テーブルの該当レコードを取得します。
同様に、theme() メソッドは、messages テーブルの theme_id カラムを使って themes テーブルの該当レコードを取得します。
class Theme extends Model{ public function user() { return $this->belongsTo(User::class); }
public function messages() { return $this->hasMany(Message::class); }}
Theme モデルは、作成者の情報(User)や、関連するメッセージのリスト(messages)を取得できます。
user() メソッドは、themes テーブルの user_id カラムを使って users テーブルの該当レコードを取得します。
messages() メソッドは、messages テーブルの theme_id カラムを使って該当するメッセージをすべて取得します。
LaravelのEloquentでは、belongsTo や hasMany を使用した場合、自動的にテーブル名(スネークケース)+ _id というカラム名を探して関連付けを行います。
例: user() メソッドは user_id を使用して結合を試みます。
例: theme() メソッドは theme_id を使用して結合を試みます。
テーブル間の結合は、データベースの外部キー制約(FOREIGN KEY)をもとに行われます。
テーブル構造:
messages.theme_id は themes.id に関連。
messages.user_id は users.id に関連。
themes.user_id は users.id に関連。
定義されたリレーションは、簡単にチェーンさせて利用可能です。
テーマに関連するメッセージの最初のメッセージを取得し、それを投稿したユーザーを取得する。
ユーザーが投稿したメッセージの最初のメッセージのテーマを取得する。
…
以上が chatGPT の説明ですが、これは全く理解できない。
SQL で書くと以下のような感じになるそうです。
SELECT messages.id AS message_id, messages.content AS message_content, users.id AS user_id, users.name AS user_name, themes.id AS theme_id, themes.title AS theme_titleFROM messagesJOIN users ON messages.user_id = users.idJOIN themes ON messages.theme_id = themes.idWHERE themes.id = ?;
最後に chatGPT に訊いてみました。
chatGPT の答えは、
Eloquentモデルとそのリレーションの理解は必要なようです、当然ですが。これは難しい。