こんにちは、新卒エンジニアの鈴木です!
このブログでは、Laravelを学んでいく過程を発信しています。前回の記事ではLaravelのインストールと、プロジェクトの作成まで進めました。 【初心者向け】PHPインストールから始めるLaravel開発環境構築ガイド
今回は、簡単な「ひとこと掲示板」を開発していきます。ユーザーが文章を投稿できて、その投稿に対して他のユーザーがコメントできるという、基本的な機能を持つWebアプリケーションを想定しています。
この掲示板を作る過程で、Laravel 12でのデータベースの扱い方やリレーション、そして画面表示の基本をマスターしていきましょう!
※本記事は、データベースの基本知識があり、Laravelの初期設定が完了している方を対象としています。
また、本記事の環境は以下の通りです。
-
- Laravel:12.24.0
- PHP:8.2.12
- MySQL:8.4.0
少し専門用語が並びますが、丁寧に解説するのでご安心ください! それでは、さっそく始めていきましょう。
目次
開発を始める前に:「MVCモデル」について
開発を始める前に、Laravelの構造を理解するうえで絶対に欠かせない「MVCモデル」について解説します。
MVCとは、ソフトウェアを開発する際の「役割分担の考え方」のことです。プログラムの機能をモデル(Model)、ビュー(View)、コントローラー(Controller)の3つのパーツに分けて整理することで、コードをきれいに保ち、効率よく開発を進めるための設計パターンです。
この「役割分担」を徹底することで、コードがどこに書かれているか分かりやすくなり、複数人での開発がスムーズに進められます。また、後からの修正や機能追加も楽になるという大きなメリットがあります。
Model(モデル)
モデルは、アプリケーションが扱うデータやデータベースとのやり取りを担当します。
主な仕事はデータベースとのやり取りです。ユーザー情報の登録や、特定の記事をデータベースから取ってくるといった、データの保存(Create)、取得(Read)、更新(Update)、削除(Delete)など、いわゆるCRUD処理はすべてモデルが担当します。
View(ビュー)
ビューは、ユーザーが直接目にする見た目を担当し、HTMLの生成が主な仕事です。
後述するコントローラーから渡されたデータ(たとえば、ユーザーの名前や記事のタイトル)を、HTMLのなかに埋め込んで、最終的なWebページとして完成させます。
ビューの重要なルールは、余計なデータ処理はしないことです。あくまでコントローラーから受け取った情報を「見せる」ことに徹します。デザインの変更が必要になった場合、開発者はこのビューのファイルだけを修正すれば良いため、他のロジック部分を壊してしまう心配がありません。
Controller(コントローラー)
コントローラーは、ユーザーのリクエストを最初に受け取る窓口のようなものです。モデルとビューの間に立ち、両者の橋渡しをします。
コントローラーの仕事の流れは以下の通りです。
- コントローラーの仕事の流れ
-
- ユーザーからのリクエストを受け取る:
ユーザーがURLにアクセスすると、Laravelのルーターが適切なコントローラーを呼び出します。 - モデルに指示を出す:
リクエストの内容に応じて、「IDが5の記事データを取ってきて」のようにモデルに必要なデータ処理を依頼します。 - モデルからデータを受け取る:
モデルがデータベースから取得したデータを受け取ります。 - ビューにデータを渡す:
受け取ったデータをビューに渡し、「このデータを使ってページを表示してください」と指示を出します。
- ユーザーからのリクエストを受け取る:
このようにユーザーのリクエストに応じて全体の処理の流れを管理するのが、コントローラーの重要な役割です。
MVCモデルの概要はつかめたでしょうか?
次の章からいよいよ実践です。まずは最初のステップとして、データベースへの接続から始めていきましょう!
手順1:データベースと接続しよう
まずは、Laravelがデータベースと通信できるように設定を行います。設定情報はプロジェクトのルートディレクトリにある.envファイルに記述します。
.envの設定
.envファイルは多くのプロジェクトで初期設定を記述するファイルとして使われています。コメントを外し、自分の環境に合わせてデータベース情報を設定してください。
DB_CONNECTION=mysql # ← データベースの種 DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel_blog # ← 自分で作成したデータベース名 DB_USERNAME=root # ← 自分のDBユーザー名 DB_PASSWORD=password # ← 自分のDBパスワード
設定ができたら接続の確認をします。以下のコマンドを実行してください。
C:\xampp\htdocs\my-laravel-project>php artisan tinker
Psy Shell v0.12.9 (PHP 8.2.12 — cli) by Justin Hileman
> DB::select(‘select 1’);
select 1はダミーのクエリで、常に1を返します。レスポンスが返ってくればデータベース接続が正しくできています!
手順2:モデルの作成
データベースと接続できたので、ここからは最初に解説したMVCモデルの考え方に沿って開発を進めます。
それでは、MVCの「M」であるモデルから作成していきましょう!
Laravelは最初からUserモデルを用意してくれています。今回はPostモデルとCommentモデル、関連ファイルをコマンドで一気に作成します。
php artisan make:model Post -mf
php artisan make:model Comment -mf
make:modelは、モデルを作成するコマンドです。ここに付けている-mfというオプションは、「m(migration)とf(factory)も同時に生成してください」という意味になります。
これにより、モデル(app/Models)・マイグレーション(database/migrations)・ファクトリー(database/factories)が生成できました。
しかし、今のモデルはただの独立した箱にすぎません。「誰が」「どの記事に」「どんなコメントをしたか」という関連性をモデルに教える必要があります。
そのために、以下のコードを追加してください。
// app/Models/User.php
// 省略
/**
* ユーザーが所有する投稿を取得する (1対多)
*/
public function posts()
{
return $this->hasMany(Post::class);
}
/**
* ユーザーが作成したコメントを取得する (1対多)
*/
public function comments()
{
return $this->hasMany(Comment::class);
}
Userモデルには、「1人のユーザーは、たくさんの投稿とコメントを持つことができる」という関係を定義します。
return $this->hasMany(モデル名::class)は「このモデルは、指定したモデルをたくさん持っています」という意味で、1対多のリレーションを表すことができます。
// app/Models/Post.php
// 省略
/**
* この投稿を所有するユーザーを取得する (多対1)
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* この投稿に紐づくコメントを取得する (1対多)
*/
public function comments()
{
return $this->hasMany(Comment::class);
}
Postモデルは、UserモデルとCommentモデルの中間に位置します。
return $this->belongsTo(モデル名::class)は「このモデルは、指定したモデルに属しています」という意味で、hasManyとは逆の多対1のリレーションを定義します。
// app/Models/Comment.php
// 省略
/**
* このコメントを作成したユーザーを取得する (多対1)
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* このコメントが紐づいている投稿を取得する (多対1)
*/
public function post()
{
return $this->belongsTo(Post::class);
}
Commentモデルは、書いた人(Userモデル)と、書かれた場所(Postモデル)の両方に属しています。
これで、3つのモデルがお互いの関係を理解できるようになりました!
手順3:テーブルを設計しよう(マイグレーション)
マイグレーションとは、データベースの設計図をPHPコードで管理する仕組みです。SQLを直接書かずにデータベースのテーブルを作成したり、カラムを追加・削除したりといった変更をコードで記録・実行できます。Gitがソースコードのバージョンを管理するように、マイグレーションではデータベースの構造のバージョンを管理できます。
先ほど作成したマイグレーション(database/migrations)を以下のように編集してください。
// database/migrations/YYYY_MM_DD_HHMMSS_create_posts_table
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
// user_idカラムを作成し、usersテーブルのidカラムに外部キー制約を設定
// ユーザーが削除されたら、そのユーザーの投稿も一緒に削除する (onDelete('cascade'))
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
// database/migrations/YYYY_MM_DD_HHMMSS_create_comments_table
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('comments');
}
};
スキーマを記述したら、以下のコマンドでマイグレーションを実行します。
php artisan migrate
このコマンドを実行すると、Laravel側でまだ実行されていないマイグレーションファイルを検出し、up() メソッドを実行してくれます。
データベースには、作成したテーブル(この例ではpostsテーブルとcommentsテーブル)と、どのマイグレーションが実行済みかを記録するための migrationsテーブルが作られます。
これでマイグレーションは完了です。データベースを確認するとテーブルが作成されているはずです!
手順4:データを用意しよう(ファクトリーとシーディング)
次にアプリケーションの表示に必要なダミーデータを作成します。この作業では、「ファクトリー」と「シーディング」という2つの機能を使います。
ファクトリー(Factory)は、モデルのダミーデータを簡単に生成するための設計図です。テストや開発の初期段階で、アプリケーションに表示するためのデータがほしいときに、手作業でデータベースにデータを投入する手間を省いてくれます。
シーディング(Seeding)は、データベースに初期データを投入する仕組みです。アプリケーションを動かすために最初に必要となるデータや、開発中に便利なダミーデータを、あらかじめ用意したクラス(シーダークラス)を使って一括で登録する機能です。先に作成したファクトリーのルールに従ってデータを挿入してくれます。
ファクトリー
先ほどモデル作成時に、同時にファクトリーを生成したので、編集していきましょう。
UserFactoryはデフォルトで用意されていますが、書き換えます。
// database/factories/UserFactory.php
public function definition(): array
{
// 1. 英語の姓と名を生成します
$firstName = fake()->firstName(); // 例: "John"
$lastName = fake()->lastName(); // 例: "Doe"
// 2. 姓と名を小文字にしてユーザー名を組み立てる
$username = strtolower($firstName) . '.' . strtolower($lastName); // 例: "john.doe"
return [
// 姓と名を結合してフルネームをセット
'name' => $firstName . ' ' . $lastName,
// 組み立てたユーザー名を使い、重複しないようにEmailを生成
'email' => fake()->unique()->numerify($username . '_##') . '@' . fake()->safeEmailDomain(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
このようにreturnのなかにルールを定義することで、シーディングのときにこのルールに沿ったデータを挿入してくれます。
リアルなダミーデータの生成を可能にしてくれているのがfake()です。fake() というのは、Laravelに標準で搭載されているFakerというライブラリを呼び出すためのヘルパー関数です。Fakerはその名の通り、本物そっくりの偽のデータを生成してくれる、非常に便利なツールです。
これを使うことで、fake()->name()で名前を、fake()->address()で住所を、といった具合に、さまざまな種類のリアルなデータを簡単に作り出すことができます。
今回のコードは、「名前」と「メールアドレス」に一貫性を持たせるためのものです。
修正前はfake()->name()で名前を、'email' => fake()->unique()->safeEmail(),でメールアドレスを生成していました。これでも正常に名前とメールアドレスを生成することは可能ですが、fake()を別々に実行してしまい、名前と関連性のないメールアドレスが生成されてしまいます。
今回は次のような処理になっています。
firstName()とlastName()を使って、名と姓を別々に生成し、それぞれ変数に保存する。- 生成した名と姓をすべて小文字に変換し、
.で連結してメールアドレスの元になる文字列を$usernameとします。 $usernameをメールアドレスに使用してメールアドレスを生成する。
このように処理を行うことで、名前とメールアドレスに一貫性を持たせることができます。
続いてはPostFactoryに記述していきます。
// database/Factories/PostFactory.php
<?php
namespace Database\Factories;
use App\Models\User; // Userモデルをインポート
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
*/
class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
// 既存のユーザーからランダムに1人選び、そのIDを取得する
'user_id' => User::inRandomOrder()->first()->id,
'content' => $this->faker->paragraph(2), // 2段落のランダムな文章を本文に
];
}
}
postsテーブルのデータ作成時にはuser_idとcontentが必要になります。
user_idは挿入時にusersテーブルに存在するidのなかからランダムで取得し挿入しています。
これで、選ばれたユーザーが投稿した状況を再現できます。
ランダムで取得するためにuse App\Models\User;Userモデルをインポートすることを忘れないでください。
contentは投稿の本文に当たる部分であり、複数行のテキストを挿入すれば良いです。$this->faker->paragraph(2)を定義することで2段落の文章をランダムで生成してくれます。
// Database/Factories/CommentFactory.php
<?php
namespace Database\Factories;
use App\Models\User; // Userモデルをインポート
use App\Models\Post; // Postモデルをインポート
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Comment>
*/
class CommentFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::inRandomOrder()->first()->id,
'post_id' => Post::inRandomOrder()->first()->id,
'content' => $this->faker->paragraph(2), // 2段落のランダムな文章を本文に
];
}
}
CommentFactoryもほとんど変わりません。post_idにはpostsテーブルからランダムでidを取得し外部キーにすることでコメントをしているように見せます。本文がランダム同士なので会話はかみ合っていませんが、ダミーデータなので文章があればいいでしょう。
こちらもUserモデルとPostモデルのインポートを忘れないようにしてください。
これでファクトリーは完了です。
シーディング
ファクトリーでダミーデータのルールを定めたので、次はそのルールに従って実際にデータを投入するシーダーを作成しましょう。
まずは、以下のコマンドでUser、Post、Commentのシーダーファイルをそれぞれ作成します。
php artisan make:seed UserSeeder
php artisan make:seed PostSeeder
php artisan make:seed CommentSeeder
これで database/seeders ディレクトリ内に3つのファイルが作成されました。では、それぞれのファイルにデータを生成する処理を記述していきましょう。
// database/seeders/UserSeeder.php
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
User::factory(10)->create();
}
}
一行追加するだけで、Userを10件作成する定義ができました。factory()の引数に数値を入れることで、その件数分データを挿入してくれます。
残りのPostとCommentも定義しましょう。
// database/seeders/PostSeeder.php
class PostSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Post::factory(20)->create();
}
}
// database/seeders/CommentSeeder.php
class CommentSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Comment::factory(50)->create();
}
}
これで、個別のシーダーファイルの準備が整いました。
しかし、このままではphp artisan db:seedコマンドを実行しても、これらのシーダーは呼び出されません。個別に作成したシーダーを実行するには、それらを統括する司令塔の役割を持つDatabaseSeederファイルに実行するシーダーを登録する必要があります。
database/seedersディレクトリにあるDatabaseSeeder.phpを開き、以下のように記述しましょう。
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
$this->call([
UserSeeder::class,
PostSeeder::class,
CommentSeeder::class
]);
}
}
これでデータを投入する準備がすべて整いました! 以下のコマンドを実行して、データベースにダミーデータを一括で流し込みましょう。
php artisan db:seed
ここのコマンドを実行すると、まず司令塔であるDatabaseSeederのrun()メソッドが呼び出されます。次に、DatabaseSeederに登録した各シーダー(UserSeeder、PostSeeder、CommentSeeder)が順番に実行され、ファクトリーで定義したルールに従ってダミーデータが生成されます。
手順5:データを表示しよう
それでは生成したダミーデータを実際に表示してみましょう。
Controller(コントローラー)
まずはコントローラーの準備です。
コントローラーはモデルから情報を受け取り、ビューに渡すという役割を持っていましたね。以下が実際のコードになります。
//app/Http/Controllers/PostController.php
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Post; //Postモデルをインポート
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$posts = Post::with(['user', 'comments.user'])->get();
return view('posts.index', compact('posts'));
}
//省略
}
Post::get();でpostsテーブルにあるすべてのデータを取得できます。
取得するときにwith(['user', 'comments.user'])を合わせることによって、投稿したユーザー情報とコメントを同時に取得できます。これはモデルで外部キーの設定をしたおかげで、簡単に外部キー参照ができるのです。SQLを叩くよりはるかに簡単ですよね。
view('posts.index', compact('posts'))では取得してきたデータをposts.indexに渡します。
このようにモデルとビューの橋渡しができました。
View(ビュー)
それでは画面を作っていきましょう。
Laravelでは、HTMLのなかにPHPの変数を埋め込んだり、繰り返し処理を行ったりできるBlade(ブレード)というテンプレートエンジンを使います。{{ $変数名 }}のように書くだけで安全にデータを表示でき、@forelseのような便利な構文が使えるのが特徴です。
Bladeファイルは手動で作成します。resources/viewsディレクトリのなかにpostsという名前のフォルダを作成し、そのなかにindex.blade.phpというファイルを作りましょう。
- ファイルパス
resources/views/posts/index.blade.php
このようにファイルを作成することで、コントローラーからview('posts.index')のように呼び出せます。
以下は、投稿とコメントを一覧表示するためのサンプルコードです。コントローラーから渡された$postsを@forelseで一つずつ表示し、さらに各投稿に紐づくコメントも表示しています。
// resources/views/posts/index.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>投稿一覧</title>
{{-- 簡単なスタイリング --}}
<style>
body { font-family: sans-serif; line-height: 1.6; color: #333; background-color: #f8f9fa; margin: 0; padding: 20px; }
h1 { text-align: center; color: #444; }
.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 { 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; font-size: 0.95em; }
.comment + .comment { border-top: 1px solid #ddd; }
.comment-meta { font-size: 0.8em; color: #777; }
</style>
</head>
<body>
<div class="container">
<h1>投稿一覧</h1>
@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>
</body>
</html>
このビュー(Blade)の詳しい使い方については、次回の記事で詳しく解説します!
Routing(ルーティング)
最後に、ユーザーがアクセスするURLと、実行されるべきコントローラーの処理を結びつけるルーティングを設定します。
ルーティングとは受付のようなもので、ユーザーが特定のURLにアクセスしてきたときに対応するコントローラーに指示を出します。リクエストを適切な処理に振り分けてくれるのです。
現状、MVCの準備ができたものの、まだ受付に誰もいない状態です。これでは、ユーザーがどのURLにアクセスしても、誰も対応してくれません。これから、受付役となるルートを定義して、最初のリクエストを受け取るための発火点を作成していきましょう。
//routes/web.php
use App\Http\Controllers\PostController; //コントローラーをインポート
Route::get('/posts', [PostController::class, 'index']);
ここでは、Route::get()というメソッドを使って、特定のURLへのGETリクエストに対する処理を定義しています。
Route::get('アクセス先のURL', [呼び出すコントローラー::class, '呼び出すメソッド名']);のように記述します。
この記述により、「ユーザーが /posts というURLにアクセスしたら、PostControllerのindexメソッドを呼び出す」というルートが設定されました。
これでhttp://127.0.0.1:8000/postsにアクセスすると投稿一覧画面が表示されます。
まとめ
今回は、Laravelを使ってデータベースに接続し、モデル、マイグレーション、シーディングを使ってデータの準備を行い、最終的にコントローラーとビューを使って画面に表示するまでの一連の流れを解説しました。MVCモデルの各要素がどのように連携して動くのか、少しイメージがつかめたのではないでしょうか。
次回の記事ではCRUD処理を行っていきます。最後までお読みいただき、ありがとうございました!