ディレクトリ構造
本テキストで追加されるファイルです。
※srcはLaravelアプリケーションのルートディレクトリです。
src/
├── app
│ ├── Http
│ │ ├── Controllers
│ │ │ ├── CommentsController.php
│ │ │ └── PostsController.php
│ │ └── Requests
│ ├── Models
│ │ ├── Comment.php
│ │ └── Post.php
│ └── Providers
│
├── database
│ └── migrations
│ ├── YYYY_MM_DD_HHMMSS_create_comments_table.php
│ ├── YYYY_MM_DD_HHMMSS_create_posts_table.php
│ └── YYYY_MM_DD_HHMMSS_create_sessions_table.php
│
├── resources
│ └── views
│ ├── layout.blade.php
│ └── posts
│ ├── create.blade.php
│ ├── edit.blade.php
│ ├── index.blade.php
│ └── show.blade.php
│
├── routes
│ └── web.php
│
└── etc.
前提条件
phpコンテナ内
は、コンテナ内の/var/www/html
ディレクトリを指します。docker compose exec php bash
コマンドでphpコンテナに入ります。
コンテナ外
は、docker-compose.yml
ファイルが存在するディレクトリを指します。- コンテナから抜けるには
exit
コマンドを実行します。
- コンテナから抜けるには
MVCモデル図
以下は、このLaravel BBSアプリケーションのMVCモデル図です:
この図は、LaravelのMVCアーキテクチャを示しています:
- Model: データベースとの対話を担当(Post.php, Comment.php)
- View: ユーザーインターフェースを表示(layout.blade.php, index.blade.php, create.blade.php, edit.blade.php, show.blade.php)
- Controller: ビジネスロジックを処理し、ModelとViewを連携(PostsController.php, CommentsController.php)
また、ルーティング(web.php)とデータベース(posts, comments, sessionsテーブル)も図に含まれています。これらの要素が連携して、BBSアプリケーションの機能を実現しています。
データベースマイグレーションの設定と実行
既存のマイグレーションファイルの削除
まず、PHPコンテナ内で既存のマイグレーションファイルを削除します。以下の手順に従ってください:
PHPコンテナに入る コンテナ外
docker-compose exec php bash
マイグレーションのリセットと削除 phpコンテナ内
php artisan migrate:reset
php artisan migrate:rollback --step=1000
rm database/migrations/*.php
これらのコマンドを実行することで、データベースのテーブルとマイグレーションファイルが完全に削除されます。
新しいマイグレーションファイルの作成
以下のコマンドを実行し、新しいマイグレーションファイルを作成します。
phpコンテナ内
php artisan make:migration create_posts_table --create=posts
php artisan make:migration create_comments_table --create=comments
重要:上記のコマンドは同時に実行しないでください。
実行順序が重要なため、各コマンドの間に少なくとも1秒の間隔を設けてください。
マイグレーションファイルの実行順序は作成時刻に基づいて決定されます。そのため、posts テーブルが先に作成され、その後 comments テーブルが作成されるように順序付けられます。これにより、外部キー制約が正しく設定され、データベースの整合性が保たれます。
セッションテーブルの作成
セッションテーブルは、ユーザーセッション情報を保存するために使用されます。これにより、ユーザーの状態を追跡し、セッション間でデータを保持することができます。セッションテーブルを作成することで、アプリケーションのセキュリティと機能性が向上します。
phpコンテナ内
php artisan session:table
上記3つのコマンドを実行すると、以下のファイルが生成されます:
- database/migrations/YYYY_MM_DD_HHMMSS_create_posts_table.php
- database/migrations/YYYY_MM_DD_HHMMSS_create_comments_table.php
- database/migrations/YYYY_MM_DD_HHMMSS_create_sessions_table.php
マイグレーションファイルの編集
ファイルの所有権と権限の変更
コンテナをrootで起動して作成したファイルは外部から変更できません。コンテナ外から変更するには、以下のコマンドをターミナルで実行してください。
コンテナ外
sudo chown -R $(whoami):$(whoami) ./src
sudo chmod -R 775 ./src
create_posts_table.php
<?php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 50);
$table->text('body');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
create_comments_table.php
<?php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
if (Schema::hasTable('comments')) {
// テーブルが存在していればリターン
return;
}
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->text('body');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('comments');
}
};
このマイグレーションファイルでは、comments
テーブルを作成し、post_id
を外部キーとして設定しています。onDelete('cascade')
を使用することで、関連する投稿が削除された場合に、そのコメントも自動的に削除されるようになります。
モデル、コントローラー、ビューファイルの作成
モデルとコントローラー
以下のコマンドを実行し、モデルとコントローラーを作成します。
phpコンテナ内
php artisan make:model Post
php artisan make:model Comment
php artisan make:controller PostsController
php artisan make:controller CommentsController
このコマンドで、以下のファイルが生成されます。
- app/Models/Post.php
- app/Models/Comment.php
- app/Http/Controllers/PostsController.php
- app/Http/Controllers/CommentsController.php
ビュー
必要なディレクトリ・ファイルを一括で作成します。
phpコンテナ内
mkdir -p app/Http/Controllers app/Models resources/views/posts
touch app/Models/Comment.php app/Models/Post.php
touch resources/views/layout.blade.php resources/views/posts/create.blade.php resources/views/posts/edit.blade.php resources/views/posts/index.blade.php resources/views/posts/show.blade.php
touch routes/web.php
外部から編集できるよう、ファイルの所有権と権限を変更します。
コンテナ外
sudo chown -R $(whoami):$(whoami) ./src
sudo chmod -R 775 ./src
コントローラーファイル
app/Http/Controllers/CommentsController.php
<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use App\\Models\\Post;
class CommentsController extends Controller
{
public function store(Request $request)
{
$validatedData = $request->validate([
'post_id' => 'required|exists:posts,id',
'body' => 'required|max:2000',
]);
$post = Post::findOrFail($validatedData['post_id']);
$post->comments()->create($validatedData);
return redirect()->route('posts.show', $post);
}
}
CommentsController
クラスは、コメントの保存を管理します。store
メソッドでは、リクエストデータの検証、コメントの作成、そして投稿詳細ページへのリダイレクトを行います。
app/Http/Controllers/PostsController.php
<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use App\\Models\\Post;
use DB;
class PostsController extends Controller
{
public function index()
{
$posts = Post::with('comments')->latest()->paginate(10);
return view('posts.index', compact('posts'));
}
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|max:50',
'body' => 'required|max:2000',
]);
Post::create($validatedData);
return redirect()->route('top');
}
public function show(Post $post)
{
return view('posts.show', compact('post'));
}
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$validatedData = $request->validate([
'title' => 'required|max:50',
'body' => 'required|max:2000',
]);
$post->update($validatedData);
return redirect()->route('posts.show', $post);
}
public function destroy(Post $post)
{
DB::transaction(function () use ($post) {
$post->comments()->delete();
$post->delete();
});
return redirect()->route('top');
}
}
PostsController
クラスは、投稿(Post
)に関連する様々な機能を提供します。主な機能には、投稿の一覧表示(index
)、新規作成(create
、store
)、詳細表示(show
)、編集(edit
、update
)、削除(destroy
)が含まれています。
モデルファイル
app/Models/Comment.php
<?php
namespace App\\Models;
use Illuminate\\Database\\Eloquent\\Model;
class Comment extends Model
{
protected $fillable = [
'post_id',
'body',
];
public function post()
{
return $this->belongsTo(Post::class);
}
}
Comment
モデルは、コメントに関連するデータベーステーブルとの対応を担当します。$fillable
配列では、一括代入可能な属性を指定しています。ここでは’post_id’と’body’が設定されており、これらの属性のみが一括代入可能となります。post
メソッドは、このコメントが属する投稿(Post
)との関係を定義しています。
app/Models/Post.php
<?php
namespace App\\Models;
use Illuminate\\Database\\Eloquent\\Model;
class Post extends Model
{
protected $fillable = [
'title',
'body',
];
public function comments()
{
return $this->hasMany(Comment::class);
}
}
Postモデルは、投稿(Post)に関連するデータベーステーブルとの対応を担います。$fillable配列では、一括代入可能な属性を指定しています。commentsメソッドは、この投稿に属する複数のコメント(Comment)との関係を定義しています。
ビューファイル
resources/views/layout.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyBBS</title>
<link rel="stylesheet" href="<https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css>">
</head>
<body>
<header class="navbar navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">MyBBS</a>
</div>
</header>
<div class="container mt-4">
@yield('content')
</div>
<script src="<https://code.jquery.com/jquery-3.5.1.min.js>"></script>
<script src="<https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js>"></script>
</body>
</html>
resources/views/posts/create.blade.php
@extends('layout')
@section('content')
<div class="border p-4">
<h1 class="h5 mb-4">新規作成</h1>
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<div class="form-group">
<label for="title">タイトル</label>
<input id="title" name="title" class="form-control @error('title') is-invalid @enderror" value="{{ old('title') }}" type="text">
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="body">本文</label>
<textarea id="body" name="body" class="form-control @error('body') is-invalid @enderror" rows="4">{{ old('body') }}</textarea>
@error('body')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mt-5">
<a class="btn btn-secondary" href="{{ route('posts.create') }}">キャンセル</a>
<button type="submit" class="btn btn-primary">登録する</button>
<a class="btn btn-info" href="{{ url('') }}">ホームに戻る</a>
</div>
</form>
</div>
@endsection
resources/views/posts/edit.blade.php
@extends('layout')
@section('content')
<div class="border p-4">
<h1 class="h5 mb-4">投稿の編集</h1>
<form method="POST" action="{{ route('posts.update', ['post' => $post]) }}">
@csrf
@method('PUT')
<div class="form-group">
<label for="title">タイトル</label>
<input id="title" name="title" class="form-control @error('title') is-invalid @enderror" value="{{ old('title', $post->title) }}" type="text">
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="body">本文</label>
<textarea id="body" name="body" class="form-control @error('body') is-invalid @enderror" rows="4">{{ old('body', $post->body) }}</textarea>
@error('body')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mt-5">
<a class="btn btn-secondary" href="{{ route('posts.show', ['post' => $post]) }}">キャンセル</a>
<button type="submit" class="btn btn-primary">更新する</button>
</div>
</form>
</div>
@endsection
resources/views/posts/index.blade.php
@extends('layout')
@section('content')
<div class="mb-4">
<a href="{{ route('posts.create') }}" class="btn btn-primary">投稿の新規作成</a>
</div>
@forelse ($posts as $post)
<div class="card mb-4">
<div class="card-header">
<a href="{{ route('posts.show', ['post' => $post]) }}">{{ $post->title }}</a>
</div>
<div class="card-body">
<p class="card-text">{!! nl2br(e(Str::limit($post->body, 200))) !!}</p>
</div>
<div class="card-footer">
<span class="mr-2">投稿日時 {{ $post->created_at->format('Y.m.d') }}</span>
@if ($post->comments->count())
<span class="badge badge-primary">コメント数 {{ $post->comments->count() }}</span>
@endif
</div>
</div>
@empty
<p>投稿はまだありません。</p>
@endforelse
<div class="d-flex justify-content-center mb-5">
{{ $posts->links() }}
</div>
@endsection
resources/views/posts/show.blade.php
@extends('layout')
@section('content')
<div class="border p-4">
<div class="mb-4 text-right">
<a class="btn btn-primary" href="{{ route('posts.edit', ['post' => $post]) }}">編集</a>
<form style="display: inline-block;" method="POST" action="{{ route('posts.destroy', ['post' => $post]) }}">
@csrf
@method('DELETE')
<button class="btn btn-danger">削除</button>
</form>
</div>
<h1 class="h5 mb-4">{{ $post->title }}</h1>
<p class="mb-5">{!! nl2br(e($post->body)) !!}</p>
<section>
<h2 class="h5 mb-4">コメント</h2>
<form class="mb-4" method="POST" action="{{ route('comments.store') }}">
@csrf
<input name="post_id" type="hidden" value="{{ $post->id }}">
<div class="form-group">
<label for="body">本文</label>
<textarea id="body" name="body" class="form-control @error('body') is-invalid @enderror" rows="4">{{ old('body') }}</textarea>
@error('body')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">コメントする</button>
</form>
@forelse($post->comments as $comment)
<div class="border-top p-4">
<time class="text-secondary">{{ $comment->created_at->format('Y.m.d H:i') }}</time>
<p class="mt-2">{!! nl2br(e($comment->body)) !!}</p>
</div>
@empty
<p>コメントはまだありません。</p>
@endforelse
</section>
</div>
@endsection
ルーティング
routes/web.php
<?php
use Illuminate\\Support\\Facades\\Route;
use App\\Http\\Controllers\\PostsController;
use App\\Http\\Controllers\\CommentsController;
Route::get('/', [PostsController::class, 'index'])->name('top');
Route::resource('posts', PostsController::class);
Route::post('/comments', [CommentsController::class, 'store'])->name('comments.store');
このコードは、BBSアプリケーションのルーティング設定を定義しています。主に3つのルートが設定されています:
- トップページ(
/
)へのGETリクエストをPostsController
のindex
メソッドにマッピングし、'top'
という名前を付与しています。 'posts'
リソースに対してRESTfulなルートを自動生成しています。これにより、投稿の一覧表示、作成、編集、削除などの操作が可能になります。- コメントの投稿(
/comments
)に対するPOSTリクエストをCommentsController
のstore
メソッドにマッピングし、'comments.store'
という名前を付与しています。
以上が、docker-composeを使用したLaravel環境での基本的なBBSアプリケーションの構築に必要なルーティング設定です。これらの設定により、アプリケーションの主要な機能が実現可能になります。
パーミッションの変更
以下のコマンドを実行して、Webサーバーが適切にファイルにアクセスできるようにします:
コンテナ外
sudo chown -R www-data:www-data ./src/storage
sudo chmod -R 775 ./src/storage
www-dataとは
www-dataは、LinuxのWebサーバー(主にApache)で使用される標準ユーザー・グループ名です。Webサーバー実行時に使用され、アクセス対象のファイルやディレクトリの所有者として設定されます。これにより、Webサーバーの最小権限アクセスが可能となり、セキュリティが向上します。
マイグレーションの実行
phpコンテナ内
php artisan migrate
このコマンドにより、マイグレーションファイルが実行され、データベースに必要なテーブルが生成されます。
最後に、localhostにアクセスして、アプリケーションの動作を確認します。
動作確認
以下のページが表示されたら完了です。
ご自身でコードを書き換えて外観を変えたり、機能を追加してみてください。