- はじめに
- レガシーなプロダクトに Laravel を導入する
- Laravel について
- なぜ部分的にでも Laravel に移行していきたいのか
- どうやってレガシーコードから Laravel DI と Facade を使うのか
- Kernel の中身をちょっと見てみる
- まとめ
はじめに
この記事は、Qiita の Laravel Advent Calendar 2日目の記事です。
昨日は、 @ucan-lab さんでした。
レガシーなプロダクトに Laravel を導入する
僕は最近、あるレガシーなプロダクトに Laravel を導入する、ということをしています。
- URLエンドポイントごとに php ファイルがあり
- 依存性の注入が不可能でユニットテストができない
というものになっています。
コードベースも巨大で、スパゲッティ状態なので、まるごと Laravel に載せ替えるのは相当大変です。
URLエンドポイントごとに php ファイルがあるので、例えば、 do_something.php
といった php ファイルがあり、中身がこんな感じになっています。
<?php // なんか色々 require_once とか function log(string $message) { error_log($message, 3, '/tmp/application.log'); } // なんか色々意味不明な処理 log('なにか残したい情報'); // なにか色々な処理 ...
そこで、とりあえずログを吐くクラスを分割します。
<?php class Logger { public function __construct(なんかDIしたいクラス $something) { ... } public function log($message) {...} }
do_something.php
はこんな感じになります(イメージです)。
<?php // なんか色々 require_once とか $logger = LaravelDIResolver::make(App\Log\Logger::class); // なんか色々意味不明な処理 $logger->info('なにかログ'); // なにか色々な処理 ...
こうして、 Logger
がテスタブルになるので、テストをつけます。
徐々に Laravel にコードを移していき、最終的に Router や Controller まで Laravel 化できたら完了です。
Laravel について
Laravel にはエントリーポイントが2つあります。
- HTTPのエントリーポイント
public/index.php
- コマンドラインのエントリーポイント
artisan
artisan
ファイルは、 php
拡張子は付いていないが PHP のファイルです。メインの処理部分はこうなっています。
<?php $kernel = $app->make(Illuminate\Contracts\Console\Kernel::class); $status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, new Symfony\Component\Console\Output\ConsoleOutput ); $kernel->terminate($input, $status); exit($status);
Console/Kernel
の handle メソッドに、引数と、コンソール出力を渡していおり、最後に exit で、コンソールに終了コードを渡しています。
一方で、 public/index.php
は、こうなっています。
<?php $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send(); $kernel->terminate($request, $response);
Http\Kernel
の handle メソッドに、リクエスト内容を渡しています。最後にレスポンスを出力して終了します。
今回は Web のエンドポイントの方だけを考えることにします。
なぜ部分的にでも Laravel に移行していきたいのか
レガシーコードの一部のモジュールを Laravel に移行したい理由は、Laravel の DI 機能と、Facade の機能が使いたいからです。
Laravel の DI 機能
Laravel では、以下のように書くと、勝手にコンストラクタインジェクションしてくれます。
<?php class Envoronment { } class ApiClient { // こう書くだけで、 Environment クラスのインスタンスが引数で渡ってくる public function __construct(Environment $environment) { $this->environment = $environment; } }
Laravel の Facade
以下のように書くと、Laravel の config/logging.php
にある single
の設定に対応する Logger が取得できます。
<?php $logger = Illuminate\Support\Facades\Log::channel('single');
どうやってレガシーコードから Laravel DI と Facade を使うのか
do_something.php
でこのようにします。(Laravel のpublic/index.php
を参考に書いています)
<?php <?php require_once __DIR__ . '/Laravelのディレクトリ/vendor/autoload.php'; $app = require_once __DIR__ . '/Laravelのディレクトリ/bootstrap/app.php'; $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); // DI を解決してインスタンスを生成してくれる $apiClient = $app->make(ApiClient::class); // Facade とかもいい感じに解決してくれる // kernel の handle メソッドを一回呼んでいないと、これを解決してくれない $logger = Illuminate\Support\Facades\Log::channel('single');
$kernel->handle
を呼ぶことで、Laravel の DI 設定や、 Facade の設定が読み込まれて、レガシーコードからも、Laravel DI や、 Facade が使えるようになります。
$response->send();
を呼ぶと Laravel のレスポンスが出力されてしまいますが、このメソッドを呼ばずに、 handle メソッドだけ呼ぶことで、Laravel の Facade などを利用できるようにします。
実際に毎回こんなことはしたくないので、こんなクラスを作りました。
<?php namespace App; use Illuminate\Contracts\Http\Kernel; use Illuminate\Foundation\Application; use Illuminate\Http\Request; class LaravelAppForLegacy { /** * @var Application */ private static $app; /** * @return Application */ public static function get(): Application { // 色んな所からこのメソッドを呼ぶことを想定しており // その度に Laravel の初期化をしているとオーバーヘッドがすごそうなので // すでに初期化済みであればそれを返すようにする if (isset(static::$app)) { return static::$app; } static::$app = require __DIR__ . '/../bootstrap/app.php'; // Laravel の機能が正常に利用できるように、 kernel の handle を呼ぶ // 返ってくるレスポンスは無視する $kernel = static::$app->make(Kernel::class); // Kernel の handle メソッドを呼ぶと error_reporting の値が書き換えられるので、 // 事前の値を保持しておき、 handle メソッドを読んだ後にもとに戻す // レガシーコードではこれをしないと動かない部分が多数ある $prevErrorReportingValue = error_reporting(); // handle の結果、Body が空の response オブジェクトが返ってくるが // 空っぽになるので無視する $kernel->handle( $request = Request::capture() ); // error_reporting の値をもとに戻す error_reporting($prevErrorReportingValue); return static::$app; } }
これを do_something.php
から使う場合はこうです。
<?php require_once __DIR__ . '/Laravelのディレクトリ/vendor/autoload.php'; $app = App\LaravelAppForLegacy::get(); $apiClient = $app->make(ApiClient::class); $logger = Illuminate\Support\Facades\Log::channel('single');
Kernel の中身をちょっと見てみる
Kernel の実装は Illuminate\Foundation\Http\Kernel
になります。
handle
から sendRequestThroughRouter
メソッドが呼ばれ、その中で bootstrap
メソッドが呼ばれています。
bootstrap
の中では、 $bootstrappers
変数に登録された bootstraper が呼ばれることになります。
実際は handle
をしなくても、これで行けるのかも。
<?php $kernel = static::$app->make(Kernel::class); $kernel->bootstrap();
以前これで試した時は、Log ファサードがうまく解決されなかった気がするんですが、ファサードの解決は、$bootstrapers
に登録されている RegisterFacades
がファサードの設定をしてくれるはずなので、なんかできる気もしてきた。
そして今やってみたらできた気がする(最後になんか曖昧ですみません。この記事を書くにあたりもう一度検証してみた結果、bootstrap
呼ぶだけでも動いた気がします)。
まとめ
とりあえずこんな感じにやたら、Laravel外のコードから、Laravel DI等が使えるので、皆さんやってみてください。
「こういう方法のほうが良いよ」とか「Kernelのbootstrap呼ぶだけだとこういう場合にだめ!」とかあれば、コメントやtwitterで教えてくれると嬉しいです。