はじめに
プログラミング学習4日目として、LaravelのCRUD機能(Create, Read, Update, Delete)実装の第一歩、「C(Create)」と「R(Read)」に挑戦しました。
前回(3日目)学んだMVCパターンの知識を活かし、より実践的な投稿機能を作成する中で発生した問題と、その解決プロセスを詳しく解説します。
開発環境
カテゴリ | バージョン/ツール |
---|---|
OS | macOS |
PHP | 8.4.1(MAMP使用) |
Laravel | 11.x |
データベース | SQLite |
4日目の学習計画
Part 1: 復習
- MVCパターンの再確認
- 既存機能(投稿一覧表示)の動作確認
- 個別投稿ページの表示機能実装
Part 2: 新機能実装
- 投稿作成フォームの実装
- フォームデータの保存処理
- ルーティングの最適化
Part1: 復習と個別投稿ページの実装
まずは3日目までの機能が正しく動作するかを確認しつつ、個別投稿ページ(/posts/{id}
)を完成させます。
ファイル構成の確認
現在の主要なファイル構成です。今回、個別投稿ページ用のpost.blade.php
を追加しました。
app/
├── Http/Controllers/HelloController.php # コントローラー
├── Models/Post.php # モデル
database/
├── migrations/
│ └── 2025_08_14_061955_create_posts_table.php # マイグレーション
resources/views/
├── posts.blade.php # 投稿一覧画面
├── post.blade.php # 個別投稿画面(今回追加)
└── create-post.blade.php # 投稿作成画面(今回追加)
routes/
└── web.php # ルーティング
MVCの役割(復習)
Model (Post.php
)
Eloquent ORMを使い、データベースのposts
テーブルと連携します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
// Mass Assignment(一括代入)を許可するカラムを指定
protected $fillable = ['title', 'content'];
}
ポイント
-
$fillable
:Post::create()
などで一括して値を設定できるカラムを定義し、意図しないカラムへの代入を防ぎます。
Controller (HelloController.php
)
Post
モデルを使ってデータを取得し、ビューに渡します。
<?php
// ... (他のuse文)
use App\Models\Post;
class HelloController extends Controller
{
// 投稿一覧を取得
public function posts()
{
$posts = Post::all(); // 全ての投稿を取得
return view('posts', ['posts' => $posts]); // ビューにデータを渡す
}
// 個別の投稿を取得
public function showPost($id)
{
$post = Post::findOrFail($id); // IDで投稿を検索(見つからない場合は404エラー)
return view('post', compact('post')); // compact関数でデータを渡す
}
}
ポイント
-
Post::all()
: 全てのレコードを取得します。 -
Post::findOrFail($id)
: 指定したIDのレコードを取得します。レコードが存在しない場合、自動的に404ページを表示してくれる便利なメソッドです。 -
compact('post')
:['post' => $post]
と同じ意味で、変数をビューに渡す際の短縮記法です。
問題1: 個別投稿ページが表示されない!
エラー: View [post] not found
URL: http://127.0.0.1:8000/posts/1
原因:
コントローラーはview('post', ...)
でpost.blade.php
を呼び出そうとしていましたが、単純なファイル名のタイポ(post2.blade.php
と作成していた)が原因でした。
解決策:
resources/views
ディレクトリに、正しい名前でpost.blade.php
を作成しました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>{{ $post->title }}</title>
</head>
<body>
<h1>{{ $post->title }}</h1>
<p>{!! nl2br(e($post->content)) !!}</p>
<small>作成日時: {{ $post->created_at }}</small>
<br><br>
<a href="{{ route('posts.index') }}">投稿一覧へ戻る</a>
</body>
</html>
ワンポイント
-
{!! nl2br(e($post->content)) !!}
:e()
でXSS対策としてHTMLエスケープを行った後、nl2br()
で改行文字(\n
)を<br>
タグに変換しています。{!! !!}
はHTMLをエスケープせずに出力するための記法です。
Part2: 新機能実装 - 投稿作成機能
次に、CRUDの「C」である投稿作成機能を実装していきます。
問題2: 投稿作成ページが404エラーになる!
エラー: 404 NOT FOUND
URL: http://127.0.0.1:8000/posts/create
原因:
ルーティング(routes/web.php
)の 順序 が問題でした。
// 間違った順序
// このルートが先に定義されていると...
Route::get('/posts/{id}', [HelloController::class, 'showPost']);
// ...'/posts/create' の "create" が {id} パラメータとして解釈されてしまう
Route::get('/posts/create', [HelloController::class, 'create']);
Laravelのルーターは、上から順にルート定義を評価します。そのため、動的なルート({id}
のようなパラメータを含む)を先に書くと、create
という文字列がIDとして扱われてしまい、create
メソッドを呼び出すルートに到達できませんでした。
解決策:
具体的で固定的なルートを、動的なルートよりも先に定義 します。
// 正しい順序
// 固定ルートを先に定義
Route::get('/posts/create', [HelloController::class, 'create'])->name('posts.create');
Route::post('/posts', [HelloController::class, 'store'])->name('posts.store');
// 動的ルートを後に定義
Route::get('/posts/{id}', [HelloController::class, 'showPost'])->name('posts.show');
実装の詳細
1. Controllerにcreate
とstore
メソッドを追加
HelloController.php
に、投稿フォーム表示用(create
)と、データ保存用(store
)の2つのメソッドを追加します。
<?php
// ... (省略)
use Illuminate\Http\Request; // Requestクラスをインポート
class HelloController extends Controller
{
// ... (既存のメソッドは省略) ...
/**
* 投稿作成フォームを表示する
*/
public function create()
{
return view('create-post');
}
/**
* フォームから送信されたデータをデータベースに保存する
*/
public function store(Request $request)
{
// Eloquentを使ってデータを保存
Post::create([
'title' => $request->title,
'content' => $request->content
]);
// 保存後は投稿一覧ページにリダイレクト
return redirect()->route('posts.index');
}
}
store
メソッドのポイント
-
引数
Request $request
: フォームから送信された全てのデータ(タイトルや内容など)をオブジェクトとして受け取ります。 -
Post::create([...])
:$fillable
で許可されたカラムのデータを一括でデータベースに保存します。 -
redirect()->route('posts.index')
: 処理完了後、名前付きルートposts.index
(投稿一覧ページ)にユーザーをリダイレクトさせます。
2. 投稿作成フォームのビューを作成
resources/views/create-post.blade.php
を新規作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>新しい投稿を作成</title>
</head>
<body>
<h1>新しい投稿を作成</h1>
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<div>
<label for="title">タイトル:</label><br>
<input type="text" id="title" name="title" required style="width: 300px; padding: 5px;">
</div>
<br>
<div>
<label for="content">内容:</label><br>
<textarea id="content" name="content" required style="width: 300px; height: 100px; padding: 5px;"></textarea>
</div>
<br>
<button type="submit" style="padding: 10px 20px; background: #007cba; color: white; border: none; cursor: pointer;">
投稿する
</button>
</form>
<br>
<a href="{{ route('posts.index') }}">投稿一覧へ戻る</a>
</body>
</html>
フォームの重要ポイント
-
method="POST"
: データを「作成」するので、HTTPメソッドはPOST
を指定します。 -
action="{{ route('posts.store') }}"
: フォームの送信先を、先ほど定義した名前付きルートで指定します。これにより、将来URLが変更されてもビューを修正する必要がありません。 -
@csrf
: CSRF(クロスサイトリクエストフォージェリ)攻撃 を防ぐための必須ディレクティブです。Laravelは自動的にトークンを生成し、不正なリクエストからアプリケーションを保護します。
データフローの全体像
ユーザーが投稿を作成するまでの一連の流れをまとめます。
1.【フォーム表示】
ユーザーが GET /posts/create にアクセス
=> ルーターが HelloController@create を呼び出す
=> create-post.blade.php (投稿フォーム) を表示
2.【データ送信】
ユーザーがフォームを入力し、「投稿する」ボタンをクリック
=> フォームデータが POST /posts に送信される
3.【データ保存】
=> ルーターが HelloController@store を呼び出す
=> storeメソッドがリクエストデータを受け取り、データベースに保存
4.【リダイレクト】
=> storeメソッドが redirect() で投稿一覧ページ (GET /posts) へのリダイレクトを指示
5.【一覧表示】
=> ルーターが HelloController@posts を呼び出す
=> posts.blade.php で、新しい投稿を含む一覧が表示される
このように、MVCパターンがそれぞれの役割を分担し、連携して一連の処理を実現しています。
達成したことと技術的な学び
今回実装した機能のURL一覧
HTTPメソッド | URL | アクション |
---|---|---|
GET |
/posts |
投稿一覧の表示 |
GET |
/posts/create |
投稿作成フォームの表示 |
POST |
/posts |
新規投稿の保存 |
GET |
/posts/{id} |
個別投稿の表示 |
技術的学習ポイント
-
ルーティングの順序の重要性
- 固定的なルート (
/posts/create
) は、動的なルート (/posts/{id}
) より先に定義しないと意図通りに動作しないことを学びました。
- 固定的なルート (
-
Eloquent ORMの便利なメソッド
-
::all()
(全件取得),::findOrFail($id)
(ID指定取得),::create()
(新規作成) といった基本的なCRUD操作をマスターしました。
-
-
Bladeテンプレートとセキュリティ
-
{{ route(...) }}
によるURL生成、@csrf
によるセキュリティ対策、{{ }}
によるXSS対策など、Bladeの強力な機能を活用できました。
-
-
基本的なセキュリティ対策
-
CSRF:
@csrf
ディレクティブで対策。 - SQLインジェクション: Eloquent ORMが自動的にプリペアドステートメントを使用するため、安全にDB操作ができます。
-
XSS: Bladeの
{{ }}
がデフォルトでHTMLをエスケープしてくれます。 -
マスアサインメント: Modelの
$fillable
プロパティで意図しないデータ更新を防ぎます。
-
CSRF:
コミットログ
今回の実装は、以下の内容でコミットしました。
feat: 投稿作成フォーム機能の実装
- 投稿作成フォームページ(/posts/create)を追加
- HelloControllerにcreate()とstore()メソッドを実装
- POST /postsルートでフォーム送信処理に対応
- ルーティング順序を修正 (createを{id}より先に定義)
- CSRFトークン付きフォームでセキュリティを確保
+ resources/views/create-post.blade.php
~ app/Http/Controllers/HelloController.php
~ routes/web.php
まとめ
プログラミング学習4日目は、LaravelにおけるCRUDの「C」と「R」を完全に実装しました。特にルーティングの順序でつまずいた経験は、フレームワークの挙動を深く理解する良い機会となりました。
-
今回の達成目標
- 投稿一覧表示(Read)
- 個別投稿表示(Read)
- 投稿作成フォーム(Create)
- フォーム送信処理(Create)
- 投稿編集機能(Update)※次回
- 投稿削除機能(Delete)※次回
Webアプリケーション開発の基本的な流れと、MVCパターンがどのように連携するのかを、手を動かしながら体感できた非常に有意義な1日でした。
次回予告: LaravelでCRUD完全版!バリデーションと更新・削除機能の実装 - 5日目
明日は残りの「U(Update)」と「D(Delete)」機能に加え、不正な入力を防ぐための サーバーサイドバリデーション を実装し、CRUDアプリケーションを完成させます!