• 投稿カテゴリー:Docker / Laravel / 技術 / 開発
  • 投稿の最終変更日:12月 26, 2024

ディレクトリ構造

本テキストで追加されるファイルです。

※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)、新規作成(createstore)、詳細表示(show)、編集(editupdate)、削除(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リクエストをPostsControllerindexメソッドにマッピングし、'top'という名前を付与しています。
  • 'posts'リソースに対してRESTfulなルートを自動生成しています。これにより、投稿の一覧表示、作成、編集、削除などの操作が可能になります。
  • コメントの投稿(/comments)に対するPOSTリクエストをCommentsControllerstoreメソッドにマッピングし、'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にアクセスして、アプリケーションの動作を確認します。

動作確認

以下のページが表示されたら完了です。

ご自身でコードを書き換えて外観を変えたり、機能を追加してみてください。