【Laravel入門】LaravelでCRUD機能を学ぶ!掲示板アプリ完成編

【Laravel入門】LaravelでCRUD機能を学ぶ!掲示板アプリ完成編

Iori Suzuki

Iori Suzuki

こんにちは、新卒エンジニアの鈴木です!

このブログでは、Laravelを学んでいく過程を発信しています。前回はLaravelの認証機能を実装しました。

今回は、Webアプリケーションの基本となるCRUD(クラッド)機能を実装していきます。

💡 CRUDとは?
CRUDとは、Create(作成)、Read(読み取り)、Update(更新)、Delete(削除)の頭文字を取った言葉で、データの基本操作のことです。

実装にあたっては、前回作成した認証機能を活用して、「ログインしているユーザーだけが投稿できる」「自分の投稿のみを編集・削除できる」といった、実際のWebアプリケーションで必須となる権限管理を含む実装を行っていきます。

機能が豊富で、安全なアプリケーションを作り上げていきましょう!

手順1:準備

1. モデルの設定

機能の実装に入る前に、土台となるデータの保存設定を確認しておきます。

Laravelにはマスアサインメント対策というセキュリティ機能があり、明示的に許可したカラム以外への一括保存(createupdate)が禁止されています。

正しく保存できるように、投稿者情報user_idと投稿内容contentを許可リストに登録しましょう。

php

// App/Models/Post.php
class Post extends Model {
    protected $fillable = [
        'content',
        'user_id'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

 
重要なのが、$fillableプロパティです。これは先ほど説明した許可リストで、今後登録するカラムをここに記述していきます。

もしuser_idを記述し忘れると、後ほどの投稿処理で「user_idには値を入れられません」というエラーが発生するので、必ず追加しておきましょう。

2. ログイン後用レイアウトの作成

今回は「ログイン後の画面」を作るため、ヘッダーにログアウトボタンが付いた共通レイアウトを新しく作成します。

php

 {{-- resources/views/layouts/app.blade.php --}} 
<!DOCTYPE html>
<html lang="ja">
<head> 
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>@yield('title', '掲示板')</title>
	<style> 
	* { box-sizing: border-box; margin: 0; padding: 0; } 
	body { font-family: sans-serif; background-color: #f5f5f5; color: #333; }
    header { background: #fff; padding: 1rem 2rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
    .site-title { font-size: 1.5rem; font-weight: bold; color: #333; text-decoration: none; }
    .nav-links { display: flex; align-items: center; gap: 1rem; }
    .user-name { font-weight: bold; }
    .btn-logout { border: 1px solid #dc2626; color: #dc2626; background: white; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; }
    .btn-logout:hover { background: #dc2626; color: white; }
    .container { max-width: 800px; margin: 0 auto; padding: 0 1rem; }
    .btn-submit { background-color: #2563eb; color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }
    .btn-submit:hover { background-color: #1d4ed8; }   
    .text-danger { color: #dc2626; font-size: 0.875rem; margin-top: 0.25rem; }
    .alert-success { background-color: #dcfce7; color: #166534; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; border: 1px solid #bbf7d0; }    
    .form-control { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 4px; font-size: 1rem; margin-top: 0.5rem; }
</style>
</head>
<body>
	<header> 
		<a href="{{ route('posts') }}" class="site-title">ひとこと掲示板</a>
		<nav class="nav-links">
			@auth
				<span class="user-name">ようこそ、{{ Auth::user()->name }}さん</span>
				<form action="{{ route('logout') }}" method="POST">
					@csrf
					<button type="submit" class="btn-logout">ログアウト</button>
				</form>
			@endauth

			@guest
				<a href="{{ route('login') }}" class="btn-submit">ログイン</a>
			@endguest
		</nav>
	</header>

	<main class="container">
		{{-- フラッシュメッセージ --}}
		@if(session('success'))
			<div class="alert-success">{{ session('success') }}</div>
		@endif

		@yield('content')
	</main>
</body> 
</html> 

 
このレイアウトファイルでは、以下の機能を持たせています。

  • ヘッダーの表示:常に画面上部にサイト名を表示し、トップページ(一覧)へ戻れるようにしています。
  • ユーザー情報の表示{{ Auth::user()->name }}で、現在ログインしているユーザーの名前を表示します。
  • ログアウト機能:前回作成したログアウト処理(POST送信)を実行するボタンを配置しています。

手順2:新規投稿機能の作成

掲示板の基本となる「投稿機能」から作っていきます。

1. バリデーションの作成

データの入り口となるバリデーションから作成します。

前回解説しましたが、バリデーションはユーザーが入力したデータが正しい形式やルールに沿っているかを検証します。もしこれがないと、空のデータが登録されてしまったり、データベースにエラーが発生したりする可能性があります。安全なアプリケーションを作るためにも、正確に実装しましょう。

プロンプト

php artisan make:request PostRequest

 

php

// App/Http/Requests/PostRequest.php 
public function authorize(): bool { 
	// ログイン判定はルーティング側で行うためtrue
	return true;
}

public function rules(): array
{
    return [
        'content' => ['required', 'string', 'max:140'],
    ];
}

 

2. コントローラーの実装

続いて、データの保存処理を行うコントローラーを実装します。

既存のPostControllerを修正し、storeメソッドに記述していきます。

php

// App/Http/Controllers/PostController.php 
use App\Http\Requests\PostRequest; // 忘れずに追加 
use App\Models\Post; // 忘れずに追加 
use Illuminate\Support\Facades\Auth; // 忘れずに追加

/**
 * 新規投稿処理
 */
public function store(PostRequest $request)
{
    $validated = $request->validated();

    // ログインユーザーのIDをデータに追加
    $validated['user_id'] = Auth::id();

    Post::create($validated);

    return back()->with('success', '投稿しました!');
}

 
バリデーション作成時にuser_idを記述していませんでした。これは、投稿フォームから「私はユーザーID:1です」と送信させるのは、なりすましの危険があるためです(そもそも、ユーザーは自分のユーザーIDを知りません)。

そのため、コントローラーでuser_idを登録します。Auth::id()を使うことで、サーバー側でセッションから「現在ログインしているユーザーのID」を自動的に取得し、セットすることができます。

これにより、安全に投稿者情報を記録でき、誰がどの投稿をしたかを正確に判断できるようになりました。

3. ルーティングの定義

機能ができたので、ルーティングを設定します。

ここでmiddlewareという仕組みを使います。これはルートの入り口に設置する「関所」のようなもので、「ログイン済みの人は通す」「未ログインの人はログイン画面へ飛ばす」という制御をLaravelが自動で行ってくれるようになります。

php

 // routes/web.php
//省略
Route::middleware(['auth'])->group(function () {
	// 新規投稿
	Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
});

 
ポイントはRoute::middleware(['auth'])->group(...)で囲んでいる点です。

このブロックのなかに書かれたルートはすべて「ログイン必須」になります。今後、編集や削除機能を追加する際も、このブロックのなかに追記するだけでセキュリティ対策が完了するので、非常に便利です。

4. ビューの改修

最後に、画面に投稿フォームを設置して仕上げましょう。

ここでも、ログイン状態に応じた表示の切り替えを行います。layoutを使うよう修正しているので既存のコードは無視し、コード全体を張り付けてください。

php

// resources/views/posts/index.blade.php 
@extends('layouts.app')

@section('content')
{{-- 投稿フォーム(ログイン時のみ表示) --}}
@auth
    <div class="post-form-card">
        <form action="{{ route('posts.store') }}" method="POST">
            @csrf
            <div class="form-group">
                <label>投稿内容</label>
                <textarea name="content" class="form-control" rows="3" placeholder="今なにしてる?">{{ old('content') }}</textarea>

                {{-- エラーメッセージ --}}
                @error('content')
                   <div class="text-danger">{{ $message }}</div>
                @enderror
            </div>
            <div class="text-right">
                <button type="submit" class="btn-submit">投稿する</button>
            </div>
        </form>
    </div>
@endauth

{{-- 投稿一覧エリア --}}
<div class="container">
    @forelse ($posts as $post)
            <div class="post">
                <div class="post-header">
                    <strong>投稿者: {{ $post->user->name }}</strong> -
                    <time>{{ $post->created_at->format('Y年m月d日 H:i') }}</time>
                </div>
                <p class="post-content">
                    {!! nl2br(e($post->content)) !!}
                </p>

                <div class="comments">
                    <h4>コメント</h4>
                    @forelse ($post->comments as $comment)
                        <div class="comment">
                            <p>{!! nl2br(e($comment->content)) !!}</p>
                            <div class="comment-meta">
                                <strong>{{ $comment->user->name }}</strong> -
                                <time>{{ $comment->created_at->format('Y/m/d H:i') }}</time>
                            </div>
                        </div>
                    @empty
                        <p>コメントはまだありません。</p>
                    @endforelse
                </div>
            </div>
        @empty
            <p>投稿はまだありません。</p>
        @endforelse
</div>


<style>
    .container { max-width: 800px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
	.post-form-card {padding: 20px 0;}
    .post { border-bottom: 1px solid #eee; padding: 20px 0; }
    .post:last-child { border-bottom: none; }
    .post-header { font-size: 0.9em; color: #555; margin-bottom: 10px; }
    .post-content { margin-top: 10px; }
    .comments { margin-top: 20px; margin-left: 20px; border-left: 3px solid #e9ecef; padding-left: 15px; }
    .comment { margin-top: 10px; padding: 10px 0; font-size: 0.95em; }
    .comment + .comment { border-top: 1px solid #ddd; }
    .comment-meta { font-size: 0.8em; color: #777; }
</style>
@endsection

 
コードが長くなったので、重要な部分だけ切り出します。

php

@auth
    <div class="post-form-card">
        <form action="{{ route('posts.store') }}" method="POST">
            @csrf
            <div class="form-group">
                <label>投稿内容</label>
                <textarea name="content" class="form-control" rows="3" placeholder="今なにしてる?" >
					{{ old('content') }}
				</textarea>

                {{-- エラーメッセージ --}}
                @error('content')
                    <div class="text-danger">{{ $message }}</div>
                @enderror
            </div>
            <div class="text-right">
                <button type="submit" class="btn-submit">投稿する</button>
            </div>
        </form>
    </div>
@endauth

 
@auth@endauthで囲むことで、ログイン中のユーザーにだけフォームを表示しています。

逆に、未ログインユーザーに対しては@guestを使って、「投稿するにはログインしてください」といった案内を出すのも良いかもしれません。

また、<textarea>のなかにある{{ old('content') }}も非常に重要です。これを記述しておくと、バリデーションエラーで画面が戻ってきたときに、直前に入力していた内容が再表示されます。これがないと、エラーのたびに文章が全部消えてしまい、ユーザーにストレスを与えてしまうため、必ず入れるようにしましょう。

これで投稿機能は完成です。

手順3:編集機能の作成

続いて、一度投稿した内容を編集する機能を作成します。ここでの重要なテーマは「自分の投稿のみ編集できる」という点です。

1. コントローラーの実装

編集画面を表示するeditと、更新処理を行うupdateを追加します。ここで重要なのが「権限チェック」です。他人の投稿を勝手に編集されないように、ガードする処理を組み込みましょう。

php

// App/Http/Controllers/PostController.php 
/**
 * 編集画面表示 
 */ 
public function edit(Post $post) {
	// 自分の投稿かチェック 
	if (Auth::id() !== $post->user_id) {
		abort(403, '権限がありません'); 
	}
    return view('posts.edit', compact('post'));
}

 
上のコードは、編集画面を表示するためのものです。

abort(403)は処理をその場で強制終了させ、ブラウザに「403 Forbidden(アクセス禁止)」というエラー画面を返す、Laravelのヘルパー関数です。

もし悪意のあるユーザーが、URLのID部分を勝手に書き換えて他人の投稿画面にアクセスしようとしても、このチェックがあれば「あなたには権限がありません」と門前払いすることができます。これはセキュリティ的に重要なガード処理です。

また、updateメソッドの引数でPostRequestを型指定することで、更新処理が走る前に自動的にバリデーションが行われます。

php

// App/Http/Controllers/PostController.php 
/**
 * 更新処理
 */
public function update(PostRequest $request, Post $post)
{
    // 自分の投稿かチェック
    if (Auth::id() !== $post->user_id) {
        abort(403, '権限がありません');
    }

    $validated = $request->validated();
    $post->update($validated);

    return redirect()->route('posts')->with('success', '更新しました');
}

 
このメソッドでは、Laravelの便利な機能がいくつも連携しています。

まず引数でPostRequestを指定しているため、自動的にバリデーションが行われます。もし入力内容に不備があれば、即座にエラーメッセージとともに前の画面に戻されるため、コントローラー内で検証ロジックを書く必要がありません。

また、Post $postと書くことで、URLに含まれるIDに対応した投稿データが自動的に検索され、変数として渡されてきます。

メソッドの冒頭にあるif (Auth::id() !== $post->user_id) は、セキュリティ上とても重要です。悪意のあるユーザーがURLを直接操作して他人の投稿を書き換えようとしても、ログイン中のユーザーが投稿したものであるかを検証し、別のユーザーであれば拒否(403エラー)をすることで、不正な更新を確実に防いでいます。

最後に$post->update($validated)を実行することで、安全なデータを一括で更新することができます。

2. ルーティングの定義

編集用のルートを追加します。データの更新には PUT メソッドを使用します。

php

// routes/web.php
Route::middleware(['auth'])->group(function () {
	// 新規投稿(作成済み) 
	Route::post('/posts', [PostController::class, 'store'])->name('posts.store');

	// 編集画面・更新処理(追加)
	Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
	Route::put('/posts/{post}', [PostController::class, 'update'])->name('posts.update');
}); 

 
URLのなかにある{post}は、具体的なID(1, 2, 3……)が入る場所を表すプレースホルダーです。たとえば/posts/1/editにアクセスすれば、{post}の部分は 1 になります。

「なぜ{id} ではなく {post}と書くのか?」と疑問に思うかもしれません。

これは、先ほどコントローラーでedit(Post $post)と引数を定義したことに関係しています。

Laravelにはルートモデル結合という機能があり、ルートのパラメータ名({post})とコントローラーの引数名($post)を一致させることで、URLに入っているIDに対応するデータをデータベースから自動的に検索して取得してくれます。

これにより、自分でPost::find($id)と書く手間が省けるのです。

3. ビューの作成

編集専用の画面resources/views/posts/edit.blade.phpを新規作成します。

構成は新規投稿フォームと似ていますが、以下の2点が異なります。

  • 初期値の表示<textarea>のなかに{{ old('content', $post->content) }}と書くことで、元の投稿内容を表示させます。
  • PUTメソッド:HTMLフォームはGETPOSTしか送れないため、@method('PUT')を記述してLaravelに「これはPUT送信ですよ」と伝えます。
php

//resources/views/posts/edit.blade.php
@extends('layouts.app')
@section('content')

<h2>投稿編集</h2>
<form action="{{ route('posts.update', $post) }}" method="POST">
    @csrf
    @method('PUT')
    <div class="form-group">
        <label>投稿内容</label>
        <textarea name="content" class="form-control">{{ old('content', $post->content) }}</textarea>
    </div>
    <button type="submit" class="btn-submit">更新する</button>
</form>
@endsection

 
actionにはroute('posts.update', $post)と記述することで、この投稿自体を変数としてコントローラーに渡すことができます。

4. 一覧画面へのリンク追加

最後に、一覧画面(index.blade.php)から編集画面へ飛べるようにリンクを設置します。

ここでもif文を使い、投稿者本人にしか編集リンクが見えないように制御します。

php

// resources/views/posts/index.blade.php 
{{-- 投稿ループのなかで --}} 
<div class="post-item">
	<p>投稿者: {{ $post->user->name }}</p>
	<p>{{ $post->content }}</p>
	
    {{-- 自分の投稿のみ編集リンクを表示 --}}
    @if (Auth::id() === $post->user_id)
        <div class="actions">
            <a href="{{ route('posts.edit', $post) }}">編集</a>
        </div>
    @endif
</div>

 
こちらも{{ route('posts.edit', $post) }}と記述することで、この投稿自体を変数で渡すことができています。

手順4:削除機能の作成

次に、不要になった投稿を削除する機能です。

確認ダイアログを実装し、間違えて投稿を消してしまうことを防ぎます。

1. バリデーション

削除処理には入力フォームがないため、バリデーションクラスは不要です。

2. コントローラーの実装

削除処理はdestroyメソッドを記述します。

ここでも、自分の投稿以外は削除できないようにチェックが必須です。これを忘れると、URLを直接入力して他人の投稿を削除できてしまう脆弱性につながります。

php

// App/Http/Controllers/PostController.php
/** 
 * 削除処理
 */
public function destroy(Post $post) { 
 	// 自分の投稿かチェック 
	if (Auth::id() !== $post->user_id) {
		abort(403, '権限がありません');
	}
    $post->delete();
    return back()->with('success', '削除しました');
}

 

3. ルーティングの定義

削除用のルートを追加します。削除にはDELETEメソッドを使用します。

php

// routes/web.php
Route::middleware(['auth'])->group(function () { 
	// ...これまでのルート...

	// 削除処理(追加)
	Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
}); 

 

4. ビューの改修

一覧画面(index.blade.php)の各投稿に、削除ボタンを設置します。@ifを使って、「投稿者本人にだけ削除ボタンが見える」ように制御します。

php

// resources/views/posts/index.blade.php 
@foreach ($posts as $post) 
	<div class="post-item">
		<p>投稿者: {{ $post->user->name }}</p>
		<p>{{ $post->content }}</p>

		{{-- 編集・削除ボタン(自分の投稿のみ表示) --}}
		@if (Auth::id() === $post->user_id)
			<div class="actions">
				<a href="{{ route('posts.edit', $post) }}">編集</a>

				<form action="{{ route('posts.destroy', $post) }}" method="POST" style="display:inline;">
					@csrf
					@method('DELETE')
					<button type="submit" onclick="return confirm('削除しますか?')">削除</button>
				</form>
			</div>
		@endif
	</div>
@endforeach 

 

手順5:コメント機能の作成

最後に、掲示板の醍醐味である「コメント機能」を追加しましょう。 テーブルは作成済みですが、機能としてはまだ動いていません。

コメントを投稿できるように、まずは正しく保存できるよう、許可リストに登録しましょう。

php

// App/Models/Comment.php 
class Comment extends Model {
    protected $fillable = [
        'content', 
        'user_id',
        'post_id'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

 

1. バリデーションの作成

これまでと同様に、コメント用のバリデーションを作成します。投稿と同じくcontentカラムのチェックが必要です。

プロンプト
php artisan make:request CommentRequest

 

php

// App/Http/Requests/CommentRequest.php 
public function authorize(): bool { 
	return true;
}

public function rules(): array
{
    return [
        'content' => ['required', 'string', 'max:140'],
    ];
}

 

2. コントローラーの実装

コメント専用のコントローラーを作成します。

基本は投稿機能と同じですが、コメント投稿はuser_idのほかに、紐付くpostも外部キーとして登録する必要があります。

プロンプト
php artisan make:controller CommentController

 
ポイントは引数です。どの投稿へのコメントかを判断するために、$postを受け取ります。

php

// App/Http/Controllers/CommentController.php 
use App\Http\Requests\CommentRequest; // 忘れずに追加 
use App\Models\Post; // 忘れずに追加 
use Illuminate\Support\Facades\Auth; // 忘れずに追加 

class CommentController extends Controller { 
	/**
	 * コメント投稿処理 
	 */ 
	 public function store(CommentRequest $request, Post $post) {
	 	$validated = $request->validated();

		// ログインユーザーのIDを追加
		$validated['user_id'] = Auth::id();

		// 投稿に紐付いたコメントとして作成
		// $post->comments()->create(...) と書くと、自動でpost_idが入ります
		$post->comments()->create($validated);

		return back()->with('success', 'コメントしました!');
	}
} 

 
$post->comments()->create(...)と記述することで、Laravelがこのコメントはこの$postに紐付いていると判断し、post_idを自動的にセットしてくれます。

手動でIDを指定する必要がないため、非常に安全です。

3. ルーティングの定義

どの投稿に対するコメントなのかをURLで表現するために、ネストしたURLにします。

php

// routes/web.php use App\Http\Controllers\CommentController; // 追加

Route::middleware(['auth'])->group(function () { 
	// ...既存のルート...

	// コメント投稿(/posts/{投稿ID}/comments というURLになります)
	Route::post('/posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store');
}); 

 
/posts/{post}/commentsというURL構造にすることで、URLを見るだけで「投稿ID◯◯番へのコメント」という意味が明確になります。

編集機能のところでも触れたLaravelの「ルートモデル結合」という機能により、URLのなかにあるID({post})が自動的にPostモデルのインスタンスに変換されて、コントローラーに渡されます。

4. ビューの改修

一覧画面の各投稿の下に、コメント一覧と入力フォームを追加します。

php

// resources/views/posts/index.blade.php
{{-- 投稿ループのなかで、本文の下あたりに追加 --}}
<div class="comments">
    <h4>コメント</h4>
    
    {{-- リレーションで紐付いたコメントを表示 --}}
    @foreach ($post->comments as $comment)
        <div class="comment">
            <strong>{{ $comment->user->name }}</strong>: {{ $comment->content }}
        </div>
    @endforeach

    {{-- コメント投稿フォーム --}}
    @auth
        <form action="{{ route('comments.store', $post) }}" method="POST" class="mt-2">
            @csrf
            <input type="text" name="content" placeholder="コメントを書く" class="form-control">
            <button type="submit" class="btn-sm">送信</button>
        </form>
    @endauth
</div>

 
$post->commentsと書くだけで、その投稿に関連するコメント一覧が取得できるのは、モデルでhasManyリレーションを設定をしたおかげです。

また、コメントフォームも@authで囲んでいるため、未ログインユーザーには表示されず、レイアウト崩れも防げます。

まとめ

環境構築から始まり、認証機能の実装、そして今回のCRUD機能。長い道のりでしたが「ひとこと掲示板」がついに完成しました!

Laravelを使えば、これだけ本格的な機能を持つWebアプリも、短期間で安全に構築することができます。このブログシリーズはこれで完結ですが、Laravelにはまだまだおもしろい機能がたくさんあります。ぜひオリジナルアプリ開発に挑戦してみてください!

最後まで読んでいただき、本当にありがとうございました。

この記事のシェア数

DX部に所属しWeb制作・開発業務を担当。学生時代はHTML/CSS/JavaScript/PHP/Java/pythonなどの基礎的なプログラミングスキルの習得に加え、データベースやクラウドサービスに関する技術も習得。Vue.jsやReact、Laravelを用いたWebアプリケーションの開発経験を積み卒業後LIGに新卒入社。

このメンバーの記事をもっと読む
Laravelで掲示板を作ろう | 3 articles
10年以上の開発実績があるLIGが、最適な開発体制や見積もりをご提案します
相談する サービス詳細を見る