この記事は、 CakePHP Advent Calendar 2019 17日目の記事です。
Symfony のコマンドラインアプリケーションを CakePHP コマンドラインアプリケーションとして動かす方法を解説します。
(余談ここから)昨日(12/16 JST)ついに、CakePHPの次期バージョンである4.0がstableリリースされました! > CakePHP 4.0.0 Released — Bakery
3.xから色々と整理され、さらに良いフレームワークになったと思います。(余談ここまで)
TL;DR: cakephp/migrations の実装が参考になるよ。
現在開発中のWebアプリケーションで、定時にいくつかのタスクを実行したいという要求がありました。
これまではcrontabへタスクごとに登録していましたが、今後タスク数が増えることと、実行の有無や実行時間をWebアプリ側から制御できるようにしたかったので、良い方法がないか検討していました。
lavary/crunz
そのような要件を満たすPHPライブラリとして lavary/crunz: A PHP-based job scheduler を使用すればよいのではと考えました。
このライブラリは、Symfonyのコマンドラインアプリケーションとして作られていますが、どうせならCakePHPと統合して実行できるようにしたい。というのが今回のお話です。
類似の仕組み
実は、cakephp/migrations プラグインも同じように、Symfonyアプリケーションである Phinx をCakePHPのShellでラップして動作するようになっています。
今回の実装にあたり、Migrations\Shell\MigrationsShell
の手法を参考にしました。
Symfonyのコマンドラインアプリケーションを、CakePHPコマンドラインアプリケーションとして動かすためには、以下の3つを行います。
- Shell::initializeメソッドで、Symfonyアプリケーションを起動するために必要な定数などの初期化
- Shell::runCommandメソッドをオーバーライドして、Symfonyアプリケーションへコマンドラインの引数をバイパスできるようにする
- Shell::mainメソッドで、Symfonyアプリケーションを生成して実行する
以下、今回作成したラッパーである CrobJobsShell を例示として説明します。
1. initializeで定数などを準備する
通常、crunzは、vendor/bin/crunz
のスクリプトを通じて実行します。ここにcrunzを実行するために必要な定数が定義されています。
// (前略
// @TODO Remove in v2
if (!\defined('CRUNZ_BIN_DIR')) {
\define('CRUNZ_BIN_DIR', __DIR__);
}
if (!\defined('CRUNZ_BIN')) {
\define('CRUNZ_BIN', __FILE__);
}
// (後略
crunz/crunz at 1.12.x · lavary/crunz
CRUNZ_BIN
, CRUNZ_BIN_DIR
がcrunz実行に必要な定数です。これをShellの initialize
にて初期化するよう記述します。(CRUNZ_VERSION
は、このShell独自の定数です。
public function initialize()
{
if (!defined('CRUNZ_BIN')) {
define('CRUNZ_BIN', ROOT . '/vendor/bin/crunz');
}
// NOTE: Remove in crunz v2
if (!defined('CRUNZ_BIN_DIR')) {
define('CRUNZ_BIN_DIR', dirname(CRUNZ_BIN));
}
if (!defined('CRUNZ_VERSION')) {
define('CRUNZ_VERSION', '1.12.2');
}
parent::initialize();
}
Shell/CronJobsShell.php#L30-L44
2. runCommandをオーバーライドしてコマンド引数をSymfonyアプリケーションへバイパスする
Symfonyのコンソールアプリケーションもコマンドラインから引数を与えて実行することができます。例えばcrunzでは、
# スケジュールされたタスクの実行
vendor/bin/crunz schedule:run
# スケジュールされたタスクの表示
vendor/bin/crunz schedule:list
といったサブコマンドがあります。これをCakePHPのシェルを通しても同じように扱えるようにします。
# これで動くようにしたい
bin/cake CronJobs schedule:run
bin/cake CronJobs schedule:list
そのままですと、CakePHPのShellでは第2引数(schedule:run
の部分)はサブコマンドとして解釈され、同名のメソッドが実行されてしまいます。
今回は、Symfonyアプリケーションに全てを任せたいので、CakePHPのコマンドライン引数のパースを回避し、Symfonyアプリケーションへ引数を渡してやる必要があります。
CakePHPのShellでは、runCommand
メソッドにより引数のパースと実行するメソッドの決定が行われています。(Web側でいうルーティング処理)
この runCommand
を書き換えることで、ルーティング処理をスキップして、Symfonyアプリケーションへ引数を渡すことができるようになります。
public function runCommand($argv, $autoMethod = false, $extra = [])
{
array_unshift($argv, 'crunz');
$this->argv = $argv;
return parent::runCommand($argv, $autoMethod, $extra);
}
Shell/CronJobsShell.php#L52-L61
上記のように、コマンドライン引数 $argv
に、サブコマンドとして存在しない任意の文字列(今回は “crunz”)を先頭に付与することで、ルーティング処理をスキップします。
また、後で引数をSymfonyアプリケーションへ渡すために、一時的にオブジェクトプロパティに格納します。
3. mainでSymfonyアプリケーションを実行する
上記の2点で、Symfonyアプリケーションを実行するための下準備が整いました。
最後に、mainメソッドでSymfonyアプリケーションの起動を行います。
public function main()
{
$app = $this->getApp(); // 起動したいSymfonyアプリケーションインスタンスを取得
$input = new ArgvInput($this->argv); // コマンドライン引数を渡す
$app->setAutoExit(false); // 終了コードを取得するためAutoExitはoff
$exitCode = $app->run($input, $this->getOutput()); // アプリケーションを実行
return $exitCode === 0; // 終了コード判定
}
/**
* @return \Crunz\Application
*/
protected function getApp()
{
return new Application('CakePHP Cron Scheduler via Crunz', CRUNZ_VERSION);
}
/**
* @return \Symfony\Component\Console\Output\ConsoleOutput
*/
protected function getOutput()
{
return new ConsoleOutput();
}
Shell/CronJobsShell.php#L69-L99
mainメソッドでは、起動したいSymfonyアプリケーションを取得して、コマンドライン引数とコンソール出力をセットして実行するだけです。
(アプリケーションインスタンス取得とコンソール出力メソッドを別メソッドにわけているのはテスト時のモック注入のためです。
また、実行結果の判定を行うためにSymfonyアプリケーションの終了ステータスコードを取得できるよう、autoExitを無効化しています。
以上のようにして作成したのが、 elstc/cakephp-cron-jobs となります。
このプラグインはCrunzの単純なラッパーですので、Crunzの機能はそのまま使えます。
また、プラグイン独自の機能として、タスクの登録はイベントを通じて行えるようになっています。
Symfonyで作成された便利なライブラリはたくさんありますので、皆さんもCakePHPと統合するプラグインを書いてみましょう。
それではクリスマスまであと1週間、今年もあと2週間。ちょっと早いですがみなさまよいお年を。