CakePHP3: Shellにタスクをサブコマンドとして登録したとき、フックメソッドが呼ばれる順序はどうなるのか?


CakePHP3はウェブフレームワークとしてだけでなく、コマンドラインアプリケーションを開発するためのフレームワークも備えている。開発者はCake\Console\Shellを継承することで、CLIアプリのコマンドを実装できる。

CakePHPのコンソールは、サブコマンドを実装する仕組み、処理を再利用可能にするためにタスクという仕組みを提供している。また、シェルの起動時に呼び出されるフックメソッドというものがある。

タスクはCake\Console\Shellを継承したクラスで、コマンドクラスの$tasksにクラス名を追加するとそのコマンドのコンポーネントとして使うことができるようになる。さらに、そのタスクをサブコマンドとして宣言すると、コンソールからタスククラスを呼び出せるようになる。

final class FooShell extends Shell
{
    public $tasks = ['Bar', 'Hoge']; // BarTaskとHogeTaskクラスをタスクとして登録

    public function getOptionParser()
    {
        $parser = parent::getOptionParser();
        // サブコマンドとして宣言
        $parser->addSubcommand('bar', [
            'parser' => $this->Bar->getOptionParser(),
        ]);
        $parser->addSubcommand('hoge', [
            'parser' => $this->Hoge->getOptionParser(),
        ]);
        return $parser;
    }
}

フックメソッドはシェルの起動時に何かを初期化したりする目的に用意されているものだ。今のところCakeには2つのフックメソッドがある。

  • Cake\Console\Shell::initialize

シェルを初期化し、サブクラスのコンストラクターとして動作します。またシェルの実行に先立って、 タスクの設定を行います。

  • Cake\Console\Shell::startup

シェルを起動して、ウェルカムメッセージを表示します。 コマンドや main の実行に先立ってチェックや設定を可能とします。

タスクのフックメソッドとシェルのフックメソッドの呼び出される順はどうなるか?

「タスク」に「サブコマンド」そして「フックメソッド」を組み合わせるとこのような疑問が湧いてきた。公式ドキュメントを見た限りこの疑問に対する答えはなさそうである。そこで、どの順で呼び出されるのか調査してみた。なおCakePHPのバージョンは3.5.15である。

まずコマンドクラスを作る:

<?php

declare(strict_types=1);

namespace App\Shell;

use App\Shell\Task\BarTask;
use App\Shell\Task\HogeTask;
use Cake\Console\Shell;

/**
 * @property-read BarTask $Bar
 * @property-read HogeTask $Hoge
 */
final class FooShell extends Shell
{
    public $tasks = ['Bar', 'Hoge'];

    public function initialize()
    {
        $this->info(__METHOD__);
        parent::initialize();
    }

    public function startup()
    {
        $this->info(__METHOD__);
        parent::startup();
    }

    public function main()
    {
        $this->info(__METHOD__);
    }

    public function getOptionParser()
    {
        $parser = parent::getOptionParser();
        $parser->addSubcommand('bar', [
            'parser' => $this->Bar->getOptionParser(),
        ]);
        $parser->addSubcommand('hoge', [
            'parser' => $this->Hoge->getOptionParser(),
        ]);
        return $parser;
    }
}

つぎに、タスククラスを2つ作る。ひとつめ:

<?php

declare(strict_types=1);

namespace App\Shell\Task;

use Cake\Console\Shell;

final class BarTask extends Shell
{
    public function initialize()
    {
        $this->info(__METHOD__);
        parent::initialize();
    }

    public function startup()
    {
        $this->info(__METHOD__);
        parent::startup();
    }

    public function main()
    {
        $this->info(__METHOD__);
    }
}

ふたつめ:

<?php

declare(strict_types=1);

namespace App\Shell\Task;

use Cake\Console\Shell;

final class HogeTask extends Shell
{
    public function initialize()
    {
        $this->info(__METHOD__);
        parent::initialize();
    }

    public function startup()
    {
        $this->info(__METHOD__);
        parent::startup();
    }

    public function main()
    {
        $this->info(__METHOD__);
    }
}

--helpを実行してみる。コマンド→サブコマンド1→サブコマンド2の順でinitializeが呼び出された。

$ cake foo --help
App\Shell\FooShell::initialize
App\Shell\Task\BarTask::initialize
App\Shell\Task\HogeTask::initialize
Usage:
cake foo [subcommand] [-h] [-q] [-v]

Subcommands:

bar
hoge

To see help on a subcommand use `cake foo [subcommand] --help`

Options:

--help, -h     Display this help.
--quiet, -q    Enable quiet output.
--verbose, -v  Enable verbose output.

コマンドのmainを実行してみる。すると、全コマンドのinitialize→コマンドのstartupの順で呼び出された。

$ cake foo
App\Shell\FooShell::initialize
App\Shell\Task\BarTask::initialize
App\Shell\Task\HogeTask::initialize
App\Shell\FooShell::startup
App\Shell\FooShell::main

次にサブコマンドのmainを実行してみる。全コマンドのinitialize親コマンドのstartup →サブコマンドのstartupの順になった。ここで注目したいのが、サブコマンドを実行したときでも親コマンドのstartupが呼び出されるところである。

$ cake foo bar
App\Shell\FooShell::initialize
App\Shell\Task\BarTask::initialize
App\Shell\Task\HogeTask::initialize
App\Shell\FooShell::startup
App\Shell\Task\BarTask::startup
App\Shell\Task\BarTask::main

# 結論

まとめると、CakePHPのコマンドとサブコマンドになったタスクのフックメソッドの呼び出し順は次のとおりである。

  • コマンドを実行したとき
    1. 親コマンドのinitialize
    2. 全タスクのinitialize
    3. 親コマンドのstartup
    4. 親コマンドのmain
  • サブコマンドを実行したとき
    1. 親コマンドのinitialize
    2. 全タスクのinitialize
    3. 親コマンドのstartup
    4. サブコマンド(タスク)のstartup
    5. サブコマンド(タスク)のmain

このうち、特筆すべきは後者のサブコマンドを実行したときに、親コマンドのstartupが呼び出されることだ。startupを実装するときはサブコマンドが実行されても差し支えないように考慮する必要があるだろう。