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


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

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

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

```php
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
<?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
<?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
<?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`
  1. 全タスクの`initialize`
  1. 親コマンドの`startup`
  1. 親コマンドの`main`
* サブコマンドを実行したとき
  1. 親コマンドの`initialize`
  1. 全タスクの`initialize`
  1. **親コマンドの`startup`**
  1. サブコマンド(タスク)の`startup`
  1. サブコマンド(タスク)の`main`

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

[サブコマンド]: https://book.cakephp.org/3.0/ja/console-and-shells.html#id27
[タスク]: https://book.cakephp.org/3.0/ja/console-and-shells.html#id3
[フックメソッド]: https://book.cakephp.org/3.0/ja/console-and-shells.html#id16