larabel-sample

laravelで簡単な掲示板サイトを作る

2025/01/31
2025/01/31
関連ワード:PHPlaravel

phpとlaravelの勉強で簡単な掲示板サイトを作ったので解説します。

laravelとは

はじめにlaravelについて簡単に説明します。

laravelはPHPのweb開発フレームワークで、MVCモデルを採用しているのでフロントエンド、バックエンドの開発を並行して効率よく開発できます。

同じPHPのフレームワークのCakePHPと比べると、自由度が高く拡張性に優れています。

環境構築

laravelの環境構築をします。

下記のdocker-compose.ymlファイルを使用してPHP・laravel用のコンテナを作成します。最初はDBの環境も別途用意するつもりだったのでdocker-composeで書いていますが、Dockerfileで作成しても問題ないです。

macやlinux上で構築するならLaravel Sailを使ったほうが手軽だと思います。

yaml
1services:
2  dev:
3    image: php:8.2-fpm
4    container_name: dev
5    tty: true
6    stdin_open: true
7    volumes:
8      - ./work:/work
9    working_dir: /work
10    ports:
11      - "8000:8000"
12      - "5173:5173"

コンテナを立ち上げたら、コンテナ内で下記のコマンドを実行してnodejsとcomposerをインストールします。

shell
1# パッケージインストール
2apt update &&apt install -y nodejs npm && apt install -y git

3
4# composer インストール
5curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer && chmod +x /usr/local/bin/composer

6
7# laravelプロジェクト作成(リポジトリのプロジェクトをそのまま使う場合は不要)
8apt-get -y install unzip
9composer create-project "laravel/laravel=11.*" sample

10
11# パッケージインストール
12cd sample
13npm install && npm run build

サーバー起動には下記のコマンドを使います。

shell
1# フロントエンドの開発も同時に行うなら
2composer run dev

3# ただサーバーを立ち上げるだけなら
4php artisan serve --host=0.0.0.0

新しくプロジェクトを作成している場合はcompose.jsonのscript>devのphp artisan serveの部分に--host=0.0.0.0を追記してください。そうしないとコンテナ内のサーバーにアクセスできません。

json
1~~~~~~~~~~
2"scripts": {
3        "post-autoload-dump": [
4            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
5            "@php artisan package:discover --ansi"
6        ],
7        "post-update-cmd": [
8            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
9        ],
10        "post-root-package-install": [
11            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
12        ],
13        "post-create-project-cmd": [
14            "@php artisan key:generate --ansi",
15            "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
16            "@php artisan migrate --graceful --ansi"
17        ],
18        "dev": [
19            "Composer\\Config::disableProcessTimeout",
20            "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve --host=0.0.0.0\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
21        ]
22    },
23~~~~~~~~~~

モデル定義

最初にモデルを定義します。

今回実装する機能は

  • ユーザー登録・ログイン機能
  • 投稿機能

なので、ユーザー情報と投稿内容の記録を保持しておく必要があります。ユーザー情報はデフォルトで作成されるモデルをそのまま使っています。

投稿内容で保持しておく情報は

  • ユーザー名
  • 投稿内容
  • 投稿日時

の3つなのでこれに合うモデルを作っていきます。

php
1<?php
2namespace App\Models;
3use Illuminate\Database\Eloquent\Factories\HasFactory;
4use Illuminate\Database\Eloquent\Model;
5class Post extends Model
6{
7    use HasFactory;
8    protected $fillable = [
9        'content',
10        'user_id',
11    ];
12    public function user()
13    {
14        return $this->belongsTo(User::class);
15    }
16}

ユーザー名、投稿内容はデータを登録できる項目なので$fillableに記述します。

値が登録、更新されないようにする場合は$guardedに記述します。

Laravelでは自動的にデータの作成日時と更新日時のカラムが作成されるので、投稿日時については記述しなくても問題ないです。

また、投稿はユーザー1人に対して複数の投稿が紐づくのでuser()で一対多の外部キーとしています。今回はデフォルトで作成されるUserモデルに紐づけています。

HasFactoryをインポートしていますが、これはテストデータを生成するために使用するもので今回は使用していないので削除しても問題ないです。

モデルを作成したら以下のコマンドでマイグレーションを行います。

shell
1php artisan migrate

新規登録ページ

新規登録ページのテンプレートを作っていきます。laravelではbrade.phpファイルでテンプレートを作成しそれを適宜表示させます。

html
1<!DOCTYPE html>
2<html lang="ja">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>ユーザー登録</title>
7    @vite(['resources/css/app.css'])
8</head>
9<body class="bg-gray-100">
10    <div class="min-h-screen flex items-center justify-center">
11        <div class="max-w-md w-full bg-white rounded-lg shadow-md p-8">
12            <h2 class="text-2xl font-bold mb-6 text-center">ユーザー登録</h2>
13
14            @if ($errors->any())
15                <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
16                    <ul>
17                        @foreach ($errors->all() as $error)
18                            <li>{{ $error }}</li>
19                        @endforeach
20                    </ul>
21                </div>
22            @endif
23
24            <form method="POST" action="{{ route('register') }}">
25                @csrf
26                <div class="mb-4">
27                    <label for="name" class="block text-gray-700 text-sm font-bold mb-2">名前</label>
28                    <input type="text" name="name" id="name" value="{{ old('name') }}" required
29                        class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500">
30                </div>
31
32                <div class="mb-4">
33                    <label for="email" class="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
34                    <input type="email" name="email" id="email" value="{{ old('email') }}" required
35                        class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500">
36                </div>
37
38                <div class="mb-4">
39                    <label for="password" class="block text-gray-700 text-sm font-bold mb-2">パスワード</label>
40                    <input type="password" name="password" id="password" required
41                        class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500">
42                </div>
43
44                <div class="mb-6">
45                    <label for="password_confirmation" class="block text-gray-700 text-sm font-bold mb-2">パスワード(確認)</label>
46                    <input type="password" name="password_confirmation" id="password_confirmation" required
47                        class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500">
48                </div>
49
50                <button type="submit" class="w-full bg-blue-500 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-600">
51                    登録する
52                </button>
53            </form>
54            <div class="mt-4 text-center">
55                <a href="{{ route('login') }}" class="text-blue-500 hover:text-blue-600">ログインはこちら</a>
56            </div>
57        </div>
58    </div>
59</body>
60</html>

laravelではデフォルトでTailwind cssが使えるのでこれを使ってスタイルを充てています。

以下、laravel独自の構文のところを抜粋して説明します。

@vite(~.css)

cssの編集をブラウザに即時反映させるために書いています。これによって編集のたびにいちいちブラウザを更新せずともスタイルの確認がリアルタイムでできます。

viteはフロントエンドビルドツールでlaravelに限らずフロントエンド開発では利用することが多いと思います。

@if~@endif、@foreach~@endforeach

文字通り条件分岐と繰り返しをします。今回はフォームの入力にバリデーションエラーがあった時に列挙するために使っています。

@csrf

これはcsrfという認証機能の脆弱性をついた攻撃を防ぐために使用します。

larabelではフォームの実装時はこれを使用しないとフォーム送信時にエラーになります。

route()

ページの遷移先の指定で使用し、引数でページを使用します。ソースでは登録ページ、ログインページが設定されています。

この記述だけでは遷移せず、ルーティングの設定も必要ですがこれについては後程説明します。

old()

フォームの値を保持するために使用し、引数にフォームにname属性の値を指定します。今回はバリデーションエラーの発生時に値を保持するために使用しています。

ログインページ

続いてログインページを作成します。新規登録ページとほぼ同じ作りなので説明は省略します。

html
1<!DOCTYPE html>
2<html lang="ja">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>ログイン</title>
7    @vite(['resources/css/app.css'])
8</head>
9<body class="bg-gray-100">
10    <div class="min-h-screen flex items-center justify-center">
11        <div class="max-w-md w-full bg-white rounded-lg shadow-md p-8">
12            <h2 class="text-2xl font-bold mb-6 text-center">ログイン</h2>
13
14            @if ($errors->any())
15                <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
16                    <ul>
17                        @foreach ($errors->all() as $error)
18                            <li>{{ $error }}</li>
19                        @endforeach
20                    </ul>
21                </div>
22            @endif
23
24            @if (session('success'))
25                <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
26                    {{ session('success') }}
27                </div>
28            @endif
29
30            <form method="POST" action="{{ route('login') }}">
31                @csrf
32                <div class="mb-4">
33                    <label for="email" class="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
34                    <input type="email" name="email" id="email" value="{{ old('email') }}" required
35                        class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500">
36                </div>
37
38                <div class="mb-4">
39                    <label for="password" class="block text-gray-700 text-sm font-bold mb-2">パスワード</label>
40                    <input type="password" name="password" id="password" required
41                        class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500">
42                </div>
43
44                <div class="mb-6">
45                    <label class="flex items-center">
46                        <input type="checkbox" name="remember" class="mr-2">
47                        <span class="text-sm text-gray-700">ログイン状態を保持する</span>
48                    </label>
49                </div>
50
51                <button type="submit" class="w-full bg-blue-500 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-600">
52                    ログイン
53                </button>
54            </form>
55
56            <div class="mt-4 text-center">
57                <a href="{{ route('register') }}" class="text-blue-500 hover:text-blue-600">新規登録はこちら</a>
58            </div>
59        </div>
60    </div>
61</body>
62</html>

投稿画面

投稿画面を作成します。

html
1<!DOCTYPE html>
2<html lang="ja">
3
4<head>
5    <meta charset="UTF-8">
6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7    <title>掲示板</title>
8    @vite(['resources/css/app.css'])
9</head>
10
11<body class="bg-gray-100">
12    <nav class="navbar navbar-expand-lg navbar-light bg-light">
13        <div class="container">
14            <div class="ms-auto">
15                @auth
16                    <span class="navbar-text me-3">
17                        {{ Auth::user()->name }}
18                    </span>
19                    <form action="{{ route('logout') }}" method="POST" class="d-inline">
20                        @csrf
21                        <button type="submit" class="btn btn-outline-secondary">ログアウト</button>
22                    </form>
23                @else
24                    <a href="{{ route('login') }}" class="btn btn-outline-primary me-2">ログイン</a>
25                    <a href="{{ route('register') }}" class="btn btn-primary">新規登録</a>
26                @endauth
27            </div>
28        </div>
29    </nav>
30
31    <div class="container mx-auto px-4 py-8">
32        <div class="max-w-3xl mx-auto">
33            <h1 class="text-3xl font-bold mb-8">掲示板</h1>
34
35            @auth
36                <div class="bg-white rounded-lg shadow-md p-6 mb-8">
37                    <form action="{{ route('posts.store') }}" method="POST">
38                        @csrf
39                        <div class="mb-4">
40                            <label for="content" class="block text-gray-700 text-sm font-bold mb-2">投稿内容</label>
41                            <textarea name="content" id="content" rows="3" required
42                                class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500"></textarea>
43                        </div>
44                        <div class="text-right">
45                            <button type="submit"
46                                class="bg-blue-500 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-600">
47                                投稿する
48                            </button>
49                        </div>
50                    </form>
51                </div>
52            @else
53                <div class="bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded mb-8">
54                    投稿するには<a href="{{ route('login') }}" class="underline">ログイン</a>してください。
55                </div>
56            @endauth
57
58            <div class="space-y-6">
59                @foreach($posts as $post)
60                    <div class="bg-white rounded-lg shadow-md p-6">
61                        <div class="mb-4">
62                            <p class="whitespace-pre-wrap">{{ $post->content }}</p>
63                        </div>
64                        <div class="text-sm text-gray-600">
65                            <span class="font-semibold">{{ $post->user->name }}</span>
66                            <span class="mx-2">-</span>
67                            <span>{{ $post->created_at->format('Y/m/d H:i:s') }}</span>
68                        </div>
69                    </div>
70                @endforeach
71            </div>
72        </div>
73    </div>
74</body>
75
76</html>

@auth~@endauth

認証状態による出し分けに使用します。今回はログイン済のユーザーのみ投稿ができるようにするために使用しています。

{{ 変数名 }}

{{ }}で囲むことで変数の値を表示させることができます。

例として認証済ユーザーの場合は{{ Auth::user() }}になります。

コントローラー

ユーザー認証関連と投稿関連の2種作っていますがユーザー認証関連は長くなりそうなので投稿関連だけ説明します。

php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Post;
6use Illuminate\Http\Request;
7
8class PostController extends Controller
9{
10    public function index()
11    {
12        $posts = Post::with('user')->orderBy('created_at', 'desc')->get();
13        return view('posts.index', compact('posts'));
14    }
15
16    public function store(Request $request)
17    {
18        $request->validate([
19            'content' => 'required|string|max:1000',
20        ]);
21
22        $request->user()->posts()->create([
23            'content' => $request->content,
24        ]);
25
26        return redirect()->route('posts.index');
27    }
28}

データベースから全投稿を取得するindexメソッドと新しい投稿を保存するstoreメソッドの2つを実装しています。

indexメソッド

$postsに全投稿データを格納し、view()で指定されたテンプレートとデータを使って表示を行います(ここでは$postsのデータとposts/index.brade.phpテンプレートを使って表示をしています)。

Post::with('user')で取得するデータ(紐づけられているuserモデルのデータを含むpostモデルのデータ)を指定し、orderBy()で並び替えも指定した上でget()でデータを取得します。

compact()では投稿データを配列の形にしています。(laravel独自の関数ではなく標準実装されている関数です)

storeメソッド

引数で渡されている$requestにはhttpリクエストが入ります。

validate()で投稿内容のバリデーション(今回は最大1000文字の文字列であること)を行います。

$request->user()->posts()->create()でuserモデルと紐づけたうえで投稿データをデータベースに保存します。

保存後はredirect()->route( 'posts.index')で指定したページに遷移します。

ルーティング

ここでurlとテンプレートファイルを紐づけます。

php
1<?php
2
3use Illuminate\Support\Facades\Route;
4use App\Http\Controllers\PostController;
5use App\Http\Controllers\Auth\RegisteredUserController;
6use App\Http\Controllers\Auth\AuthenticatedSessionController;
7
8// メインページのルート
9Route::get('/', [PostController::class, 'index'])->name('posts.index');
10Route::post('/', [PostController::class, 'store'])->middleware('auth')->name('posts.store');
11
12// 認証関連のルート
13Route::middleware('guest')->group(function () {
14    Route::get('register', [RegisteredUserController::class, 'create'])
15        ->name('register');
16    Route::post('register', [RegisteredUserController::class, 'store']);
17    Route::get('login', [AuthenticatedSessionController::class, 'create'])
18        ->name('login');
19    Route::post('login', [AuthenticatedSessionController::class, 'store']);
20});
21
22Route::middleware('auth')->group(function () {
23    Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
24        ->name('logout');
25});

Route::get()、 Route::post()でそれぞれデータの取得と保存をしています。引数にはurlパスとコントローラーの関数を入れます。

name()で特定のルートに名前を付けています。これをすることでテンプレートで{{ ルートの名前 }}とすることでパスを直接書かずともリンクを設定することができます。

投稿やログイン関連では認証を行う必要があるのでmiddleware()を使っています。未認証の場合はmiddleware('guest')、認証済の場合はmiddleware('auth')を使用します。

group()ではルートのグルーピングをしています。今回は未認証状態と認証状態でのルートをグルーピングしています。

動作確認

shell
1composer run dev

でサーバーを立ち上げます。以下のような掲示板サイトが表示されると思います。