Kodokon kodokon.com

手作りのMVCアーキテクチャ

完全なマイクロMVCフレームワーク、つまり正規表現のルーター、注入されたコントローラー、バッファリングされたビューを書き、フレームワークがあなたの代わりに何をしているのかを理解しましょう。

11 分 · 3 問

このレッスンを Kodokon で開く

フレームワークを理解するとは、小さなものを書けるようになるということです。すべてのモダンなMVCアーキテクチャはフロントコントローラーに支えられています。単一のindex.phpがすべてのリクエストを受け取り(サーバーがあらゆるURLをそこへ書き換えます)、次にルーターがHTTPメソッドとパスをハンドラーに対応づけます。私たちのルーターは、/posts/{id}のようなパターンを名前付きキャプチャグループを持つ正規表現に変換します。するとpreg_matchが、URLを手作業で分割することなく、直接$found['id']を埋めてくれます。

PHP
<?php
declare(strict_types=1);

final class Router
{
    private array $routes = [];

    public function add(
        string $method,
        string $pattern,
        callable $handler
    ): void {
        $regex = preg_replace(
            '#\{(\w+)\}#',
            '(?P<$1>[^/]+)',
            $pattern
        );
        $this->routes[] = [
            $method, "#^$regex$#", $handler
        ];
    }

    public function dispatch(
        string $method,
        string $uri
    ): mixed {
        foreach ($this->routes as $route) {
            [$m, $re, $handler] = $route;
            if ($m !== $method) {
                continue;
            }
            if (preg_match($re, $uri, $found) === 1) {
                return $handler($found);
            }
        }
        http_response_code(404);
        return 'Page not found';
    }
}
パターン/posts/{id}は#^/posts/(?P<id>[^/]+)$#になる

コントローラーが全体を指揮します。パラメーターを読み、モデル、つまりデータベースと話すことを許された唯一の層に問い合わせ、ビューを選びます。依存関係はコンストラクターを通して入ってきます。これはコンテナのない依存性注入であり、readonlyプロパティに昇格させることで、それらが構築後に決して変わらないことが保証されます。モデルはリソースが存在しないとき、falseではなく明示的なnullを返します。そうすればコントローラーが404を判断できます。

PHP
<?php
declare(strict_types=1);

final class PostModel
{
    public function __construct(
        private readonly PDO $pdo
    ) {}

    public function find(int $id): ?array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM posts WHERE id = :id'
        );
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row === false ? null : $row;
    }
}

final class PostController
{
    public function __construct(
        private readonly PostModel $posts
    ) {}

    public function show(array $params): string
    {
        $id = (int) $params['id'];
        $post = $this->posts->find($id);
        if ($post === null) {
            http_response_code(404);
            return render('not-found');
        }
        return render('post', ['post' => $post]);
    }
}
render関数は次のブロックで定義される

ビューは、すでに用意された変数を表示するだけの、素のPHPファイルです。二つの内部的な仕組みがそれを可能にします。extractはデータ配列をローカル変数に変え、出力バッファリングob_startのあとob_get_clean)は、出力を即座に送る代わりに、それを文字列として返すために捕捉します。入れ子になったテンプレートを組み立てたり、レンダリングのあとでヘッダーを決めたりするのに欠かせません。

PHP
<?php
declare(strict_types=1);

function render(
    string $view,
    array $data = []
): string {
    extract($data, EXTR_SKIP);
    ob_start();
    require __DIR__ . "/views/$view.php";
    return (string) ob_get_clean();
}

$pdo = new PDO('sqlite:blog.db');
$controller = new PostController(
    new PostModel($pdo)
);

$router = new Router();
$router->add(
    'GET',
    '/posts/{id}',
    fn (array $p): string => $controller->show($p)
);

$path = parse_url(
    $_SERVER['REQUEST_URI'],
    PHP_URL_PATH
);

echo $router->dispatch(
    $_SERVER['REQUEST_METHOD'],
    (string) $path
);
完全なフロントコントローラー。レンダリング、配線、ディスパッチ

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. このルーターで、パターン/posts/{id}を変換すると何が生成されますか?
    • 名前付きキャプチャグループを持つ正規表現。その後$found['id']でアクセスできる
    • URLセグメントの連想配列
    • セグメントごとの単純な文字列比較
  2. render関数はなぜob_startを使うのですか?
    • 送信する前にHTML出力を圧縮するため
    • ビューの出力を捕捉し、それを即座にブラウザへ送る代わりに文字列として返すため
    • ビューがPHPコードを実行するのを防ぐため
    • ビューのレンダリングをディスクにキャッシュするため
  3. フロントコントローラーの役割は何ですか?
    • 静的ファイルをより速く配信すること
    • すべてのページの前にユーザーの権限を確認すること
    • アプリケーションの単一の入口であること。あらゆるリクエストがそこへ書き換えられ、次にルーターへ委譲される