公開日 2026-09-11

CodeIgniter 4入門(Docker + PHP 8.2 で Route / Controller / View まで)

CodeIgniter 4 の新規プロジェクトを Docker だけで作り、spark serve の起動から Route / Controller / View をつないだ一覧・詳細ページまでを通す。

目次

  1. 1. CodeIgniter 4 の現在地
  2. CodeIgniter とは
  3. CodeIgniter 3 とは別物として入り直す
  4. Laravel / Slim との距離感
  5. 選ばれる場面
  6. 2. 作業ディレクトリと Docker を用意する
  7. コードのポイント
  8. 3. CodeIgniter 4 をインストールする
  9. 4. .env を作って開発サーバーを起動する
  10. --host 0.0.0.0 を省略するとブラウザから届かない
  11. 5. Route / Controller / View をつなぐ
  12. ルートを登録する
  13. コードのポイント
  14. コントローラを作る
  15. コードのポイント
  16. ビューを作る
  17. コードのポイント
  18. 登録されたルートを確認する
  19. ここまでの動作確認
  20. 6. CI_ENVIRONMENT で表示がどう変わるか
  21. まとめ
  22. 参考リンク

CodeIgniter をまだ触っていない読者、または CodeIgniter 3 で止まっている読者向けの導入記事です。Docker だけで CodeIgniter 4 の新規プロジェクトを作り、開発サーバーの起動から Route / Controller / View をつないだ一覧ページ・詳細ページの表示までを扱います。データベース接続、Model、認証は範囲外です。

補助導線として、先に読んでおくと入りやすい記事:

どちらも未読のまま、以下の手順だけで進められる構成にしています。

1. CodeIgniter 4 の現在地

CodeIgniter とは

CodeIgniter は、PHP で Web アプリケーションや API を作るためのフレームワークです。1 つのリクエストを、経路を決める Route・処理を担う Controller・画面を組み立てる View の 3 つで分担する MVC 構成をとります。この記事で 5 章まで作るのが、その Route → Controller → View をつなぐ最小の 1 往復です。

CodeIgniter 3 とは別物として入り直す

CodeIgniter 4 は 3 系の延長線上にはありません。3 系のコードをそのまま動かす互換レイヤーは同梱されておらず、入り口の書き方から変わっています。

この記事の手順でインストールした vendor/codeigniter4/framework を読むと、断絶の具体が確認できます。

  • コントローラの基底クラスは CodeIgniter\Controller。3 系の CI_Controller というクラスは存在しない
  • load プロパティが無い。$this->load->view(...) は view() 関数へ、$this->load->model(...) は new でのインスタンス化か model() ヘルパでの取得に変わった(use は完全修飾名を短く書くためのインポートで、ロードそのものではない)
  • クラスは PSR-4 オートロードに載り、アプリ側のコードは App\ 名前空間へ入る

3 系の記憶のまま $this->load->view() と書くと、そのプロパティが無いため実行時に落ちます。移行の続きではなく、再入門として読むほうが近道です。

Laravel / Slim との距離感

同梱範囲で並べると、CodeIgniter 4 は Laravel と Slim の中間に位置します。

フレームワーク同梱している範囲自分で決める量
Laravel認証、ORM、キュー、テンプレート、スケジューラまで広く同梱少ない(規約に乗る)
CodeIgniter 4ルーティング、Query Builder と Model、バリデーション、セッションを同梱。認証は別途中くらい
SlimHTTP とルーティングのみ。DI もテンプレートも自分で選ぶ多い

依存パッケージの量にも差が出ます。codeigniter4/framework の composer.json の require のうち、第三者パッケージは laminas/laminas-escaper と psr/log の 2 つだけ(ほかは php 本体と ext-intl などの拡張)。フレームワーク本体が持ち込む第三者コードが少ないため、vendor/ の中を追いかけるときに読む量が抑えられます。

選ばれる場面

  • CodeIgniter 3 の資産を抱えた現場で、同じ系列の現行バージョンへ載せ替えたい
  • 認証やテンプレートの選定は自分で決めたいが、ルーティングや DB 層まで自作はしたくない
  • フレームワーク本体の依存を薄く保ち、vendor/ を読める規模に留めたい

認証やスターターキットを最初から使いたいなら Laravel のほうが早く、HTTP 層だけ足したいなら Slim のほうが素直に収まります。

2. 作業ディレクトリと Docker を用意する

Windows のターミナルから WSL2 に入ります。

wsl

作業ディレクトリを作り、VS Code で開きます。CodeIgniter 本体を置く src と、コンテナ定義を置く docker/php を先に作ります。

mkdir -p ~/projects/codeigniter4-intro-guide-demo/src
mkdir -p ~/projects/codeigniter4-intro-guide-demo/docker/php
cd ~/projects/codeigniter4-intro-guide-demo
code .

docker/php/Dockerfile を作成します。CodeIgniter 4 の本体は ext-intl と ext-mbstring を要求するため、intl をここで追加します(mbstring は php:8.2-cli に同梱済み)。

FROM php:8.2-cli

RUN apt-get update && apt-get install -y --no-install-recommends \
        git unzip libicu-dev libzip-dev \
    && docker-php-ext-install intl zip \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /workspace

compose.yml を作成します。

services:
  app:
    build: ./docker/php
    user: "1000:1000"
    working_dir: /workspace
    volumes:
      - ./src:/workspace
    environment:
      COMPOSER_HOME: /tmp/composer
    ports:
      - "8080:8080"
    command: ["sleep", "infinity"]

コードのポイント

① user を指定してホスト側のファイル所有者をそろえる

    user: "1000:1000"

この指定が無いと、コンテナ内の Composer は root として動き、バインドマウント越しに作られる src/ 配下が root 所有になります。所有者がずれると、WSL 側のユーザーからは編集も削除もできません(Permission denied)。VS Code で開いても保存が通らないので、create-project の前に決めておく設定です。WSL2 で最初に作ったユーザーは 1000:1000。id -u と id -g の出力が違う場合は、その値へ置き換えてください。

② マウント先を ./src にして create-project の空ディレクトリ要件を満たす

    volumes:
      - ./src:/workspace

composer create-project は、対象ディレクトリが空でないと停止します(Project directory "/workspace/." is not empty.)。./:/workspace としてプロジェクト直下をマウントすると compose.yml と docker/ が見えるため、この条件を満たせません。コンテナ定義を src の外へ置けば、/workspace は空のままです。

③ COMPOSER_HOME を書き込める場所へ逃がす

    environment:
      COMPOSER_HOME: /tmp/composer

user を指定すると、コンテナ内のホームディレクトリは書き込み不可になります。Composer はキャッシュの置き場を失うため、/tmp 配下を明示して回避します。

コンテナをビルドして起動します。

docker compose up -d --build

起動を確認します。

docker compose exec app php -v
docker compose exec app composer --version
docker compose exec app php -m | grep -E '^(intl|mbstring)$'
  • 期待結果: PHP 8.2 系と Composer 2 系のバージョンが表示され、intl と mbstring の両方が並ぶ。

PHP・Composer・spark を動かすコマンドは docker compose exec app ... の形で実行します。一方、ls や cp、ファイルの編集などのファイル操作は、コンテナではなく WSL 側のプロジェクトルートで実行します。コンテナ内の /workspace はホストの src/ を指すため、src/... というパスはホスト側で解釈してください(docker compose exec app を付けて src/... を触ると、コンテナ内には src/ が無く失敗します)。

3. CodeIgniter 4 をインストールする

空の /workspace へ、スターターアプリを展開します。

docker compose exec app composer create-project codeigniter4/appstarter . --no-interaction

インストールされた構成を確認します。

ls src/

主なディレクトリの役割:

パス役割
app/Controllers/コントローラ。App\Controllers 名前空間に載る
app/Views/ビューテンプレートの置き場
app/Config/設定クラス群。Routes.php もここに入る
public/公開ディレクトリ。index.php が置かれる
writable/ログとキャッシュの書き込み先
sparkコマンドラインツールの実行ファイル

src/composer.json を開くと、要求バージョンが読み取れます。php は ^8.2、codeigniter4/framework は ^4.7。App Starter の 4.7 系が php: ^8.2 を要求するため、この記事は PHP 8.2 のコンテナで最新の 4.7.4 を対象にしています。PHP 8.1 で同じ create-project を実行すると、composer は 4.7 系を選べず、8.1 を許容する旧系(App Starter 4.6 系、framework 4.6 系)を代わりに入れます。入るバージョンを固定したいときは docker compose exec app composer create-project codeigniter4/appstarter:4.7.4 . のようにバージョンを明示します。

4. .env を作って開発サーバーを起動する

スターターアプリが同梱しているのは .env ではなく env というファイル名です。コピーして使います。

cp src/env src/.env

src/.env を開き、CI_ENVIRONMENT の行のコメントを外して development へ変更します。

CI_ENVIRONMENT = development

開発サーバーを起動します。

docker compose exec app php spark serve --host 0.0.0.0 --port 8080

このコマンドはターミナルを占有したまま動き続けます。停止は Ctrl+C。次章の docker compose exec app php spark routes やブラウザでの確認は、このサーバーを止めずに、VS Code で 2 つ目の WSL ターミナルを開いて実行します。

--host 0.0.0.0 を省略するとブラウザから届かない

php spark serve の既定の待ち受けアドレスは localhost です。コンテナ内の localhost はコンテナ自身を指すため、ports で 8080 を公開していてもホスト側からの接続は拒否されます。

手元で確認した挙動:

# 既定(--host なし)
コンテナ内から      -> 応答あり
ホスト側から curl   -> 接続拒否(status=000)

# --host 0.0.0.0 を付けた場合
ホスト側から curl   -> status=200

ブラウザで http://localhost:8080/ を開くと、スターターアプリの初期ページが表示されます。インストールされたバージョンがそのまま見出しに出るため、4.7.4 のようなパッチバージョンまで確認できます。

ブラウザで http://localhost:8080/ を開いた CodeIgniter 4 の初期ページ

画面右下の炎のアイコンがデバッグツールバーで、CI_ENVIRONMENT が development のときだけ出ます。production との差は 6 章で確認します。

5. Route / Controller / View をつなぐ

初期ページは、app/Config/Routes.php の $routes->get('/', 'Home::index'); が App\Controllers\Home::index() を呼び、その中の view('welcome_message') が app/Views/welcome_message.php を描画した結果です。同じ流れで、レポートの一覧ページと詳細ページを足します。

リクエストが通る経路:

flowchart LR
    B[ブラウザ] -->|GET /reports| R[app/Config/Routes.php]
    R -->|Reports::index| C[app/Controllers/Reports.php]
    C -->|view reports/index| V[app/Views/reports/index.php]
    V -->|HTML| B

ルートを登録する

src/app/Config/Routes.php を、次の内容へ書き換えます。

<?php

use CodeIgniter\Router\RouteCollection;

/** @var RouteCollection $routes */
$routes->get('/', 'Home::index');
$routes->get('reports', 'Reports::index');
$routes->get('reports/(:num)', 'Reports::show/$1');

コードのポイント

① (:num) で数字だけを受け取り、$1 で引数へ渡す

$routes->get('reports/(:num)', 'Reports::show/$1');

(:num) は数字にのみ一致するプレースホルダで、内部では ([0-9]+) の正規表現へ展開されます。一致した部分が $1 の位置に入り、Reports::show() の第 1 引数として渡ります。/reports/abc はこのルート自体に一致しないため、コントローラへ到達せず 404 になります。

コントローラを作る

src/app/Controllers/Reports.php を作成します。データベースは使わず、定数の配列を表示元にします。

<?php

declare(strict_types=1);

namespace App\Controllers;

use CodeIgniter\Exceptions\PageNotFoundException;

class Reports extends BaseController
{
    private const REPORTS = [
        1 => ['title' => 'レポートA', 'owner' => 'ユーザー1', 'status' => '公開'],
        2 => ['title' => 'レポートB', 'owner' => 'ユーザー2', 'status' => '下書き'],
        3 => ['title' => 'レポートC', 'owner' => 'ユーザー1', 'status' => '公開'],
    ];

    public function index(): string
    {
        return view('reports/index', ['reports' => self::REPORTS]);
    }

    public function show(int $id): string
    {
        if (! isset(self::REPORTS[$id])) {
            throw PageNotFoundException::forPageNotFound('レポート ' . $id . ' は存在しません');
        }

        return view('reports/show', [
            'id'     => $id,
            'report' => self::REPORTS[$id],
        ]);
    }
}

コードのポイント

① BaseController を継承する

class Reports extends BaseController

app/Controllers/BaseController.php はスターターアプリが用意する抽象クラスで、CodeIgniter\Controller を継承しています。全コントローラ共通の初期化を置く場所として用意されているため、新規のコントローラもこちらを継承します。

② view() の第 2 引数がビューの変数になる

        return view('reports/index', ['reports' => self::REPORTS]);

第 1 引数は app/Views/ からの相対パスで、拡張子 .php は省きます。第 2 引数の連想配列はキーがそのまま変数名へ展開されるので、ビュー側では $reports として参照できます。

③ 存在しない ID は PageNotFoundException で 404 にする

            throw PageNotFoundException::forPageNotFound('レポート ' . $id . ' は存在しません');

この例外はフレームワークが 404 応答へ変換するため、ステータスコードを自分で組み立てる必要はありません。渡したメッセージが画面に出るかどうかは CI_ENVIRONMENT で変わります(6 章)。

ビューを作る

src/app/Views/reports/ ディレクトリを作り、その中に index.php を作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>レポート一覧</title>
</head>
<body>
    <h1>レポート一覧</h1>
    <ul>
    <?php foreach ($reports as $id => $report): ?>
        <li>
            <a href="/reports/<?= $id ?>"><?= esc($report['title']) ?></a>
            (<?= esc($report['status']) ?>)
        </li>
    <?php endforeach; ?>
    </ul>
</body>
</html>

続けて src/app/Views/reports/show.php を作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title><?= esc($report['title']) ?></title>
</head>
<body>
    <h1><?= esc($report['title']) ?></h1>
    <dl>
        <dt>ID</dt><dd><?= esc((string) $id) ?></dd>
        <dt>担当</dt><dd><?= esc($report['owner']) ?></dd>
        <dt>状態</dt><dd><?= esc($report['status']) ?></dd>
    </dl>
    <p><a href="/reports">一覧へ戻る</a></p>
</body>
</html>

コードのポイント

① 出力は esc() を通す

            <a href="/reports/<?= $id ?>"><?= esc($report['title']) ?></a>

esc() はフレームワークが用意するグローバル関数で、内部で laminas/laminas-escaper を呼びます。既定は HTML 文脈のエスケープ。<?= ?> へ生の値を渡した場合、値に含まれるタグは描画時に解釈されます。この記事の表示元は定数配列なので実害はありませんが、値が利用者の入力や DB 由来へ変わった時点で、この一手間が境界線です。掲載行で href の $id を素で出しているのは、ルートを (:num) に限定しており数字しか渡ってこないためです。文字列を出す title などは必ず esc() を通します。属性・JavaScript・CSS・URL のように HTML テキスト以外へ出すときは、esc($value, 'attr') のように第 2 引数で出力文脈を指定します。

登録されたルートを確認する

docker compose exec app php spark routes
+--------+------------------+------+-----------------------------------+
| Method | Route            | Name | Handler                           |
+--------+------------------+------+-----------------------------------+
| GET    | /                | »    | \App\Controllers\Home::index      |
| GET    | reports          | »    | \App\Controllers\Reports::index   |
| GET    | reports/([0-9]+) | »    | \App\Controllers\Reports::show/$1 |
+--------+------------------+------+-----------------------------------+

(:num) が ([0-9]+) へ展開された結果と、show/$1 の引数渡しがこの表から読み取れます。

ここまでの動作確認

ブラウザで http://localhost:8080/reports を開きます。

  • 期待結果: 3 件のレポートがリンク付きで並ぶ。
  • リンクをたどると http://localhost:8080/reports/2 で詳細が表示される。
  • http://localhost:8080/reports/99 は 404 になる。
ブラウザで http://localhost:8080/reports を開いた一覧ページ

リンク先の URL は /reports/2 の形になり、(:num) に一致した 2 が Reports::show() の引数として渡ります。

ブラウザで http://localhost:8080/reports/2 を開いた詳細ページ

6. CI_ENVIRONMENT で表示がどう変わるか

.env の CI_ENVIRONMENT は、エラー表示とデバッグツールバーの出方を切り替えます。src/.env を production へ変更して比べます。

CI_ENVIRONMENT = production

変更したら、spark serve を実行中のターミナルで Ctrl+C を押して停止し、同じコマンドで起動し直します。.env は起動時に読み込まれるため、走らせたままでは反映されません。

同じ URL を開いたときの差:

確認した項目developmentproduction
デバッグツールバー(通常のページ)画面右下にアイコンが出る出ない
/reports/99 の 404 画面PageNotFoundException::forPageNotFound() へ渡したメッセージがそのまま出る定型の文言に差し替わる

development では、5 章のコードで渡した「レポート 99 は存在しません」が画面に出ます。

development で /reports/99 を開いたときの404画面。渡したメッセージがそのまま表示される

production では同じ URL が定型の文言(Sorry! Cannot seem to find the page you were looking for.)へ差し替わり、アプリ側が例外へ渡した文字列は画面から消えます。存在しない ID を突いた利用者に、内部の事情を説明した文言が出ることはありません。

production で /reports/99 を開いたときの404画面。定型の文言に差し替わっている

確認が済んだら development へ戻し、production のときと同じように Ctrl+C で停止してから開発サーバーを起動し直します。

CI_ENVIRONMENT = development

.env はバージョン管理へ入れません。スターターアプリの .gitignore は .env を除外済みで、リポジトリに残るのは雛形の env だけです。

まとめ

この記事で組み立てた構成:

  • intl を追加した PHP 8.2 コンテナと、user 指定でホスト側の所有者をそろえた Docker 環境(2 章)
  • composer create-project によるスターターアプリの展開(3 章)
  • .env の用意と、spark serve --host 0.0.0.0 での起動(4 章)
  • Route から Controller、View への導線と、(:num) を使った ID 付き詳細ページ(5 章)
  • CI_ENVIRONMENT によるエラー表示の切り替え(6 章)

次の一歩は、app/Config/Database.php と .env の database.default.* を使った DB 接続、そして Model と Query Builder による一覧の置き換えになります。

同じ題材を別のフレームワークで比べる場合は、次の 2 本が対応します。

参考リンク