JUGEM PHP

レガシーPHPプロダクトのアプリケーションアーキテクチャ改善活動(第2回/全3回)

JUGEM PHP

こちらは前回の記事からの続きとなっております。

前回「上と下から同時に攻める」という方針を掲げ、そのうち上から攻める(アプリケーション全体で保証される仕組みの整備)の部分の改善をこの記事でご紹介していきます。

上から攻める

1. Composerを導入する

アプリケーション全体で保証される仕組み作りにおいて最重要項目がComposerを導入することだと考えています。Composer自体が昨今のPHP開発においてほぼ必須のものになりつつありますし、なにより外部から優れたコードを入手して既存のコードと置換することで既存の問題の多くを解消できるようになります。

Composerの導入はPHPバージョン数さえ満たせていればすぐ出来ますし、導入方法は散々世に溢れている情報なので特には触れません。

2. HTTPリクエスト/HTTPレスポンスのクラス化

WebアプリケーションとはHTTPリクエストをIN、HTTPレスポンスをOUTとしたアプリケーションです。LaravelでもHTTPリクエストを表すRequestクラス、HTTPレスポンスを表すResponseクラス、アプリケーションそのものを表すApplicationクラスが主な構成要素となり、グローバルスコープではほぼこの3つのオブジェクトだけで処理が行われています。(厳密に言ってしまうと、Requestを受け取って実際の処理を実行するのはKernelというクラスで、ApplicationはKernelから使用されることになります)JUGEMでもこの形に倣った作りを目指しました。ただしApplicationクラスは巨大な仕組みのためなかなか手をつけることが出来ません。今回はHTTPリクエスト/HTTPレスポンスのクラス化のみを行いました。

web_application.png

PHPは $_GET$_POST といったスーパーグローバル変数によってどこからでもHTTPリクエストにアクセスでき、またHTTPレスポンスもどこからでも返却できる言語ですが、これらをクラス化することには大きな意味があります。それはグローバルな情報へ依存すると結局アプリケーション全体が密結合な状態となってしまうため、それを抑止する効果があります。またHTTPリクエストを入力として受け取る、またはHTTPレスポンスを出力として返却するクラスがあった場合、スーパーグローバル変数のままだとこれらをテストはできませんが、オブジェクトとして渡すようにしてやればテストすることが可能になります。

今回これらをクラス化するにあたって、Laravelのコンポーネントをそのまま使うことも考慮しましたが、内部の実装を見る限りSymfonyのコンポーネントを継承して少しメソッドを増やしたぐらいのもので、ほとんどの機能はSymfonyの時点で完成されていました。依存は少ないに越したことは無いので、今回はSymfonyのコンポーネントを使うこととしました。後にLaravel化したときにもインターフェースは一緒なので、そのまま移行できるはずです。

HTTPリクエスト/HTTPレスポンスのクラス化前後でコードは以下のように変わりました。グローバルな情報に依存していないというだけでもかなり安心感があります。またどのクラスがリクエスト・レスポンスの管理に責務を持つべきかがハッキリしたため、例えば「今回のリクエストがスマートフォンからのアクセスかどうか確認するためのメソッドを定義したい」なんて時にどのクラスにメソッドを定義すればいいかもハッキリします。まだHTTPリクエスト/HTTPレスポンスをクラス化していないWebアプリケーションを運用されてる方がいらっしゃいましたら是非クラス化することをお勧めします。

クラス化前

class Controller
{
    function someAction()
    {
        // ローカルスコープ内でグローバル変数を参照している・・・
        $body = $_POST['body'];

        // ローカルスコープ内でHTTPレスポンスを返してしまっている・・・
        header('Location: /entry/');
        exit();
    }
}

(new Controller)->someAction();

クラス化後

class Controller
{
    function someAction(Request $request)
    {
        $body = $request->request->get('body');

        return new RedirectResponse('/entry/');
    }
}

// スーパーグローバル変数($_GET, $_POST, $_COOKIE, $_FILE, $_SERVER)からRequestオブジェクトを作ってくれるメソッド
$request = Request::createFromGlobals();
$response = (new Controller)->someAction($request);
// レスポンスオブジェクトを実際のHTTPレスポンスへ変換するメソッド
$response->send();

3. 初期化処理のパーツ化

一般的なWebアプリケーションであれば、処理を開始して初めに何らかの初期化処理を行っているでしょう。コンフィグの読み込みやロガーの準備などが考えられます。特にログイン認証を行っているようなサービスであればここで何らかの認証処理を行っていると思います。

これらを「初期化処理」と乱暴に括って呼んでしまいましたが、実際のところLaravelにおいてはアプリケーションの初期化を行う目的のBootstrapクラスと、Requestの精査を行う目的のMiddlewareクラスに分かれます。Bootstrapクラスはどのルーティングでも必ず実行されますが、Middlewareクラスはルーティング毎に実行の有無を選択できます。コンフィグの読み込みやロガーの準備はLaravelでは必ず実行する必要のある処理のためBootstrapクラスとして実装されています、逆にログイン認証は非ログイン状態でもアクセス出来るようにしたいルーティングもあるため、ルーティング毎に実行の有無を選択できなければならず、Middlewareクラスとして実装されています。

JUGEMの既存のコードで行っていた初期化処理を意味ある塊にわけ、それぞれを上記2つのどちらかに分類してクラス化していきます。LaravelのBootstrapクラスとMiddlewareクラスはInterface(LaravelではContractsと呼んでいる)が公開されているわけではありませんが、中身は単純な作りなのでメソッド名だけ合わせてさくっと作ります。本来LaravelのServiceProviderとして実装すべき処理もいくつか含まれていましたが、ServiceProviderはフレームワークの拡張ポイントとしての性質が大きく、そこまで高度な仕組みを今のJUGEMに取り入れるまではないとして、そういう処理はBootstrapクラスとして実装しました。

初期化処理をパーツ化することはLaravelへ移行することにおいて必要な作業ではあるのですが、JUGEMではそれ以外のメリットもありました。というのもJUGEMはエントリポイントとなるPHPファイルが17個ほどあって、それぞれで実行している初期化処理がバラバラで、ビジネスロジック部分でどの初期化処理が行われているのを期待していいのか分からない状態でした。うっかり初期化されてないような処理を呼び出すとエラーが発生してしまいます。初期化処理をパーツ化することで使い回し可能となり、エントリポイントごとの初期化処理を均質化することに成功しました。エントリポイントごとの初期化処理がバラバラで同じ苦しみ抱えているサービスがありましたらやってみることをお勧めします。

4. 初期化処理の追加

上記の初期化処理のパーツ化によって得られたメリットはまだあります。それは初期化処理の追加が容易になり、アプリケーション全体で保証される仕組みをどんどん追加できるようになったことです。

JUGEMでは現在のところコンフィグ取得用オブジェクトやログインユーザ取得用オブジェクトの準備を行い、それらはシングルトン管理でアプリケーションのどこからでも参照できるようにしています。またそれ以外にも必要になったタイミングで随時追加しています。

第3回の記事へ