Laravel をまだ触っておらず、最初の 1 画面を作りながら Route / Controller / View / Model の役割をまとめてつかみたい読者向けです。
ゴールは、Laravel 13 の新規プロジェクト作成から SQLite を使った本一覧ページの表示まで。
補助導線として先に触っておくと入りやすい記事:
この 2 本を未読でも、以下の手順だけで進められる構成です。
前提環境
- Windows 11
- WSL2(Ubuntu)
- VS Code(Remote - WSL)
- Docker Desktop(WSL 連携有効)
以降のコマンドは、特記がない限り WSL 側ターミナルで実行します。
1. ゴールと非対象
この記事で到達する状態:
- Laravel の新規プロジェクトを作成できる
http://localhost:8000で開発サーバーを確認できるRoute/Controller/View/Modelを使って本一覧ページを表示できる
今回は、Laravel の最初の導線を 1 本でつかむことに絞ります。
認証やフロントエンド構築まで広げると、Route / Controller / View / Model の役割が見えにくくなるからです。
扱わない内容は次のとおりです。
- Breeze / Jetstream などの認証導入
- React / Vue / Inertia
- PostgreSQL / MySQL への切り替え
- テスト、CI、デプロイ
2. 作業ディレクトリを用意して Laravel プロジェクトを作る
最初に、WSL と Docker が見えているかを確認します。
PowerShell:
wsl -l -v
WSL(Ubuntu):
docker --version
docker compose version
開始位置は ~/projects/laravel-intro-guide-demo です。
mkdir -p ~/projects/laravel-intro-guide-demo
cd ~/projects/laravel-intro-guide-demo
code .
Laravel 13 の公式 Installation では Laravel Installer から starter kit を選ぶ流れが主線です。
ここでは既存シリーズを Docker 中心でそろえるため、ホスト側へ PHP や Composer を直接入れずに済む composer:2 コンテナから create-project を実行します。
docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app composer:2 create-project laravel/laravel .
-u "$(id -u):$(id -g)" を付けているのは、生成されたファイルの所有者を WSL 側の自分にそろえるためです。
ここを外すと Laravel 一式が root 所有になり、VS Code で保存するときに権限エラーになることがあります。
生成された composer.json には php:^8.3 と laravel/framework:^13.0。
最初は雛形のまま進めます。追加の package はまだ不要。
続いて compose.yml を作成。
以降のコマンドを docker compose exec app ... にそろえるためです。
compose.yml:
services:
app:
image: composer:2
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /app
volumes:
- ./:/app
ports:
- "8000:8000"
command: php artisan serve --host=0.0.0.0 --port=8000
同じシェルで次も実行して、docker compose 側でも自分の UID / GID を使うようにします。
export LOCAL_UID="$(id -u)"
export LOCAL_GID="$(id -g)"
起動前に Laravel の環境情報を見たい場合は、次を実行します。
docker compose run --rm app php artisan about --only=environment
run --rm はコンテナを一時起動して即削除します。以降の docker compose exec app ... はコンテナが起動済みの前提で使うコマンドです。
Laravel Version や PHP Version が表示されれば、雛形の作成は完了です。
3. 開発サーバーを起動して初期画面を確認する
ここでは、生成直後の Laravel がそのまま起動することを先に確かめます。
docker compose up -d
docker compose logs app --tail 20
ログに Server running on [http://0.0.0.0:8000] が出たら、ブラウザで http://localhost:8000 を開いてください。
初期の welcome 画面が表示されれば、Laravel 自体の起動確認は完了です。
curl -I http://localhost:8000
- 期待結果:
HTTP/1.1 200 OK
8000 番ポートが埋まっている場合は、compose.yml の左側を 8001:8000 に変えて docker compose up -d をやり直してください。
4. Model と migration を追加する
次は一覧表示に使う Book モデルです。
docker compose exec app php artisan make:model Book -m
このコマンドで、次の 2 つが生成されます。
app/Models/Book.phpdatabase/migrations/*_create_books_table.php
migration の timestamp は実行時刻ごとに変わるため、ファイル名の先頭は各自で異なります。
生成された *_create_books_table.php を、次の内容に置き換えてください。
database/migrations/*_create_books_table.php:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('author');
$table->unsignedSmallInteger('published_year');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('books');
}
};
app/Models/Book.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
protected $fillable = [
'title',
'author',
'published_year',
];
}
今回は追加 DB コンテナを使わず、Laravel 生成直後の SQLite 前提で進めます。
最初の 1 本では DB サーバーの構築より、Model と画面のつながりを先に見たほうが流れをつかみやすいからです。
5. Route / Controller / View をつなぐ
続けて controller を作成します。
docker compose exec app php artisan make:controller BookController
最終的に使うファイルは 3 つです。
routes/web.php(既存ファイルを編集)app/Http/Controllers/BookController.php(artisan が生成)resources/views/books/index.blade.php(自分で作成)
routes/web.php:
<?php
use App\Http\Controllers\BookController;
use Illuminate\Support\Facades\Route;
Route::get('/', [BookController::class, 'index']);
app/Http/Controllers/BookController.php:
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use Illuminate\Contracts\View\View;
class BookController
{
public function index(): View
{
$books = Book::query()
->orderBy('id')
->get();
return view('books.index', [
'books' => $books,
]);
}
}
次に View ファイルを用意します。resources/views/ 直下には welcome.blade.php しかないので、books/ ディレクトリを作ってその中にファイルを置きます。
mkdir -p resources/views/books
touch resources/views/books/index.blade.php
resources/views/books/index.blade.php(新規作成):
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel Intro Books</title>
<style>
body {
margin: 0;
font-family: "Segoe UI", sans-serif;
background: #f5f7fb;
color: #1f2937;
}
main {
max-width: 720px;
margin: 48px auto;
padding: 24px;
}
.card {
background: #ffffff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.08);
}
h1 {
margin-top: 0;
font-size: 2rem;
}
ul {
padding-left: 20px;
}
li + li {
margin-top: 12px;
}
.meta {
color: #4b5563;
font-size: 0.95rem;
}
</style>
</head>
<body>
<main>
<section class="card">
<h1>Laravelで作った最初の一覧ページ</h1>
<p>BookController が Model から受け取ったデータを Blade で表示しています。</p>
<ul>
@forelse ($books as $book)
<li>
<strong>{{ $book->title }}</strong><br>
<span class="meta">{{ $book->author }} / {{ $book->published_year }}</span>
</li>
@empty
<li>books テーブルにデータがまだありません。</li>
@endforelse
</ul>
</section>
</main>
</body>
</html>
Route は URL と処理の対応付け、Controller は取得したデータの受け渡し、View は HTML の描画に集中します。
この分け方が見えてくると、Laravel の基本構成が読みやすくなります。
View [books.index] not found. と出た場合は、resources/views/books/index.blade.php のディレクトリ名とファイル名を見直してください。
6. サンプルデータを入れる
このままでは books テーブルが空なので、一覧に何も出ません。
最初のデータ投入は Seeder で行います。
docker compose exec app php artisan make:seeder BookSeeder
database/seeders/BookSeeder.php:
<?php
namespace Database\Seeders;
use App\Models\Book;
use Illuminate\Database\Seeder;
class BookSeeder extends Seeder
{
public function run(): void
{
Book::query()->insert([
[
'title' => 'Laravelの教科書',
'author' => 'Sato',
'published_year' => 2024,
'created_at' => now(),
'updated_at' => now(),
],
[
'title' => '実践PHPアプリ設計',
'author' => 'Suzuki',
'published_year' => 2025,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
}
ここでは 2 件の固定データをまとめて入れるため insert を使用。
insert は Eloquent の $fillable チェックを通らず、created_at / updated_at も自動では入らないため、その 2 つは明示しています。
Laravel 13 の create-project では database/database.sqlite と初回 migration が用意されるため、ここではそのまま migration -> seed の順に進めます。
順番は migration -> seed です。
docker compose exec app php artisan migrate
docker compose exec app php artisan db:seed --class=BookSeeder
Seeder を何度も実行すると同じ行が増える点には注意。
入れ直したいときは docker compose exec app php artisan migrate:fresh を使ってテーブルを作り直してから、もう一度 db:seed を流してください。
Laravel 13 の新規アプリは SQLite 前提で始めやすく、create-project 直後の状態からそのまま一覧ページの導線へ進めます。
7. 一覧ページを確認しながら役割を整理する
最後は、ルートと画面の確認。
docker compose exec app php artisan route:list
/ が BookController@index に向いていれば、ルーティングはつながっています。
ブラウザで http://localhost:8000 を開くと、2 件の本が並んだ一覧ページになるはずです。
ここで見えている 4 つの役割は次のとおりです。
Route:/へ来たアクセスをBookController@indexへ渡すController:Bookモデルから一覧を取得し、View へ渡すModel:booksテーブルを Eloquent で扱うView: 受け取った一覧を HTML として表示する
画面が空のままなら、次の順番で見ると切り分けやすくなります。
docker compose exec app php artisan migrateが成功しているかdocker compose exec app php artisan db:seed --class=BookSeederを実行したかBookControllerのBook::query()->orderBy('id')->get()が本文どおりか
8. よくある詰まりと次の一歩
docker compose up -d が通らないとき:
docker compose logs app
artisan や追加クラスが見つからないとき:
docker compose exec app composer dump-autoload
よくある詰まりは次の 4 つです。
- 8000 番ポートが使用中
compose.ymlの左側を8001:8000などへ変更する
View [books.index] not found.resources/views/books/index.blade.phpの配置を確認する
- migration ファイル名が本文と違う
- 先頭 timestamp は毎回変わるので、
*_create_books_table.phpを編集すればよい
- 先頭 timestamp は毎回変わるので、
- データが表示されない
migrateとdb:seedの順で実行できているか確認する
ここまで進めば、Laravel の最初の 4 要素は一度つながりました。
次に広げるなら、フォーム作成 -> バリデーション -> 作成 / 編集 / 削除の順で進めると、CRUD の流れが自然に見えてきます。