CakePHP の Shell と Command どっちが優先されるの?
CakePHP では下記のように、CakePHP3.6.0 から Shell が非推奨となり、 Command の利用が推奨されるようになりました。
バージョン 3.6.0 で非推奨: Shell は 3.6.0 で非推奨ですが、 5.x までは削除されません。 代わりに コンソールコマンド を使用してください。
ただ、Shell も Command も実行コマンドは同じで、例えば HelloShell.php または HelloCommand.php を実行する際はどちらもbin/cake hello
で実行できるとドキュメントには書いてあります。
基本的には Command を使えば良いので、これはこれで良さそうですが、実際移行する場合などは同じ名前の Shell と Command が共存するケースが存在するんじゃないかと思い、その場合はどうなるのかと気になりました。
本エントリでは、CakePHP の内部実装を見て、その疑問を解き明かそうと思います。
結論
コンソールコマンドが優先される(推奨されてるので当たり前っちゃ当たり前かも)
どうやって Command が優先されるのか
結論としてはコンソールコマンドが優先されるのですが、実際内部ではどのように動いてるのでしょうか?
ちょっと深ぼって見てみます。
まずは CommandRunner::run() → CommandCollection::autoDiscover() が実行
Shell や Command を実行すると\Cake\Console\CommandRunner::run
が実行されます。そこから\Cake\Console\CommandCollection::autoDiscover
が実行されます。autoDiscover()
は下記のようになっています。
/**
* Automatically discover shell commands in CakePHP, the application and all plugins.
*
* Commands will be located using filesystem conventions. Commands are
* discovered in the following order:
*
* - CakePHP provided commands
* - Application commands
*
* Commands defined in the application will overwrite commands with
* the same name provided by CakePHP.
*
* @return string[] An array of command names and their classes.
*/
public function autoDiscover(): array
{
$scanner = new CommandScanner();
$core = $this->resolveNames($scanner->scanCore());
$app = $this->resolveNames($scanner->scanApp());
return array_merge($core, $app);
}
PHP Doc にあるように core
というCakePHP標準のコマンドと app
というアプリケーション側のコマンドを見つけて、がっちゃんこしてくれるっぽいですね。
今回はアプリケーション側 に着目してみます。 $app = $this->resolveNames($scanner->scanApp());
では何が行われているのか見てみましょう。
scanApp() で shell と command を取得する
/**
* Scan the application for shells & commands.
*
* @return array A list of command metadata.
*/
public function scanApp(): array
{
$appNamespace = Configure::read('App.namespace');
$appShells = $this->scanDir(
App::classPath('Shell')[0],
$appNamespace . '\Shell\\',
'',
[]
);
$appCommands = $this->scanDir(
App::classPath('Command')[0],
$appNamespace . '\Command\\',
'',
[]
);
return array_merge($appShells, $appCommands);
}
\Cake\Console\CommandScanner::scanApp
ではPHP Docでアプリケーションの Shell と Command をスキャンする、と書いてます。実装を見ると scanDir()
というメソッドを使って Shell ディレクトリ配下と Command ディレクトリ配下のファイル名を取得しています。例えば、$appCommands
の場合、下記のような配列が返ってくるようです。
Array
(
[0] => Array
(
[file] => /var/www/html/src/Command/HelloCommand.php
[fullName] => hello
[name] => hello
[class] => App\Command\HelloCommand
)
)
そして、これを最終的にarray_merge() してがっちゃんこしてますね。なので scanApp()
をすると下記のような配列が出来上がります。
Array
(
[0] => Array
(
[file] => /var/www/html/src/Shell/HelloShell.php
[fullName] => hello
[name] => hello
[class] => App\Shell\HelloShell
)
[1] => Array
(
[file] => /var/www/html/src/Command/HelloCommand.php
[fullName] => hello
[name] => hello
[class] => App\Command\HelloCommand
)
)
この配列を resolveNames()
するとどうなるでしょうか。
resolveNames() で shell と command を整理
/**
* Resolve names based on existing commands
*
* @param array $input The results of a CommandScanner operation.
* @return string[] A flat map of command names => class names.
*/
protected function resolveNames(array $input): array
{
$out = [];
foreach ($input as $info) {
$name = $info['name'];
$addLong = $name !== $info['fullName'];
// If the short name has been used, use the full name.
// This allows app shells to have name preference.
// and app shells to overwrite core shells.
if ($this->has($name) && $addLong) {
$name = $info['fullName'];
}
$out[$name] = $info['class'];
if ($addLong) {
$out[$info['fullName']] = $info['class'];
}
}
return $out;
}
$input
に先程の配列が入ります。この $input
が foreach で色々処理されますが、最終的に
$out[$name] = $info['class'];
をして $out
に $name
をキー、 $info['class']
を値として入れられます。 scanApp()
で返ってきた配列を見ると、キーである name や fullName は同じ名前のため、後続で処理されたものが上書きされます。
Array
(
[hello] => Cake\Command\HelloCommand
)
なので、$out
には結果として上記のようになり、Shell と Command が共存する場合、Command が生き残るわけですね。
整理したshellやcommandを実行!
上記のような形で、CakePHP標準コマンドやアプリケーション側のコマンドが処理され、\Cake\Console\CommandRunner::run
内の下記で、$shellが \Cake\Console\Shell
のインスタンスなら runShell()
でシェルとして実行、\Cake\Console\CommandInterface
のインスタンスなら runCommand()
でコマンドとして実行される、という処理になっています。
$shell = $this->getCommand($io, $commands, $name);
if ($shell instanceof Shell) {
$result = $this->runShell($shell, $argv);
}
if ($shell instanceof CommandInterface) {
$result = $this->runCommand($shell, $argv, $io);
}
まとめ
今回は CakePHP の機能である Shell と Command に関する疑問について、内部実装を追ってみました。
よしなにやってくれるフレームワークですが、内部まで見てみると納得感もありますし、根拠を持って実装できますね。
Discussion