目次

目次

bake modelコマンドについて、ChatGPTと一緒にコード読んでみた

西村拓海
西村拓海
最終更新日2024/03/15 投稿日2024/03/15

株式会社レコチョクのエンジニアをしております西村拓海です。 現在サーバーサイドエンジニアとしてmurketというサービスに携わっております。

はじめに

コードを読むのが億劫なことってないですか…?僕はあります! つい先日CakePHPの bake modelコマンドを利用する機会があり、内部の実装について 気になるけど、読み始めると時間がかかるし面倒だなと思い、腰が重く、なかなか動けていませんでした。

そんな時思いつきました!ChatGPTに読んでもらえばよくね!?と そこで今回はChatGPTと一緒に bake modelコマンドの内容についてコードを読みました。 その結果からChatGPTを利用してコード読むことについて、良かった点をお伝えできたらと思います。

目標: CakePHPのbake modelコマンドについて、どのようにしてTableファイルが生成されるか理解する

備考

  • 利用したChatGPTバージョン: 4
  • 対象のCakePHPバージョン: 4.2.4
  • ChatGPTの出力は一部要約・省略しています。

ChatGPTとの会話

1ターン目

まず bake modelコマンドを実行した際の処理の全体像を知るために、以下の質問をしました。

質問

question_first.png

回答

- ChatGPT
CakePHPのbake modelを入力すると、
その入力はCakePHPのコマンドラインインターフェイス (CLI) によって受け取られ、処理されます。

1. コマンドライン入力の解析: 
    - bin/cake bake model [オプション] [引数]と入力すると、CakePHPのCLIツールがこの入力を受け取ります。

2. コマンドの識別: 
    - CakePHPのCLIツールはbakeというキーワードを認識すると、bakeプラグインに処理を譲渡します。

3. コマンドオブジェクトの実行
    - bake modelコマンドに対応するコマンドオブジェクトがインスタンス化されます

4. オプションと引数の処理
    - コマンドオブジェクトは入力されたオプションと引数を解析し、それに基づいて処理を行います。

5. モデルの生成
    - bake modelコマンドは指定されたオプションや引数に従って、モデルクラスのファイルを生成します

6. 結果の出力
    - 生成が完了すると、CLIはユーザーに対して結果を報告します。

得られた情報

  • bake modelコマンドに関する大枠の処理の流れについて(6つの手順)


2ターン目

bake modelコマンドの大枠の処理についてなんとなく理解できました。 しかしコードを読み進めるための情報が足りなかった為、以下の質問をしました。

質問

question_second.png

回答

- ChatGPT
一般的な流れは以下の通りです

1. エントリポイント(bin/cake)
    - bin/cakeスクリプトが実行され、CakePHPのコンソールアプリケーションを起動させる

2. コンソールアプリケーション(src/Console/CommandRunner.php)
    - CommandRunner.phpはコマンドラインからの入力を解析し、適切なコマンドオブジェクトを見つけて実行する

3. Bakeプラグインのコマンドオブジェクト(vendor/cakephp/bake/src/Command/ModelCommand.php)
    - ModelCommand.phpはbake modelコマンドの具体的な実装を含むクラス。
    - モデルのスケルトンを生成するロジックを実装しています。
    - このファイル内でデータベースの構造を解析し生成するモデルクラスの属性やメソッドを決定する。

4. テンプレートファイル(vendor/cakephp/bake/templates/bake/Model/table.twig)
    - モデル、ビュー、コントローラーなどのコード生成にはTwigテンプレートエンジンを使用している
    - これらのテンプレートは生成されるコードの構造を定義しており、ModelCommandクラスによって利用される

得られた情報

  • bake modelコマンドに関する大枠の処理の流れについて
    • 各ファイルの定義位置とその処理内容

イメージ図

bake_model.png


3ターン目

bake modelコマンドに関する大枠の処理は理解できたので、各手順の中身について見ていきます。

質問

question_code.png
#!/usr/bin/php -q
<?php
// Check platform requirements
require dirname(__DIR__) . '/config/requirements.php';
require dirname(__DIR__) . '/vendor/autoload.php';

use App\Application;
use Cake\Console\CommandRunner;

// Build the runner with an application and root executable name.
$runner = new CommandRunner(new Application(dirname(__DIR__) . '/config'), 'cake');
exit($runner->run($argv));

回答

- ChatGPT
このコードはCakePHPフレームワークで使用されるコマンドラインインターフェース(CLI)スクリプトの一例です
CakePHPのアプリケーションでコンソールコマンドを実行するためのエントリポイントとして機能します。

スクリプトの概要

---- 省略 ----

4. CommandRunnerのインスタンス化
---------------------------------------------------------------------------------------
$runner = new CommandRunner(new Application(dirname(__DIR__) . '/config'), 'cake');
exit($runner->run($argv));
---------------------------------------------------------------------------------------
CommandRunnerクラスのインスタンスを作成し、Applicationクラスのインスタンスを引数としています。
Applicationクラスはアプリケーションの設定情報を含み、/configディレクトリに基づいて構成されます。
cakeという文字列はCLIからこのスクリプトを実行する際のルート実行可能ファイル名を指定しています。

runメソッドはコマンドライン引数($argv)を使用してコマンドを実行します。
このメソッドから返された終了コードコードを使用してスクリプトを終了させます(exit関数)

---- 省略 ----

得られた情報

  • bin/cake.phpについて
    • コンソールコマンドを実行するためのエントリファイル
    • アプリケーションの設定情報を持って、コマンドを解析するファイルをインスタンス化している


4ターン目

bin/cake.php内で定義されているCommandRunner::run関数にてコマンドを実行することが分かった為、次はその関数について見ていきます。

質問

question_code.png

/**
 * Run the command contained in $argv.
 *
 * Use the application to do the following:
 *
 * - Bootstrap the application
 * - Create the `CommandCollection` using the console() hook on the application.
 * - Trigger the Console.buildCommands event of auto-wiring plugins.
 * - Run the requested command.
 *
 * @param array $argv The arguments from the CLI environment.
 * @param \Cake\Console\ConsoleIo|null $io The ConsoleIo instance. Used primarily for testing.
 * @return int The exit code of the command.
 * @throws \RuntimeException
 */

public function run(array $argv, ?ConsoleIo $io = null): int
{
    $this->bootstrap();

    $commands = new CommandCollection([
        'help' => HelpCommand::class,
    ]);
    if (class_exists(VersionCommand::class)) {
        $commands->add('version', VersionCommand::class);
    }
    $commands = $this->app->console($commands);

    if ($this->app instanceof PluginApplicationInterface) {
        $commands = $this->app->pluginConsole($commands);
    }
    $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);
    $this->loadRoutes();

    if (empty($argv)) {
        throw new RuntimeException('Cannot run any commands. No arguments received.');
    }
    // Remove the root executable segment
    array_shift($argv);

    $io = $io ?: new ConsoleIo();

    try {
        [$name, $argv] = $this->longestCommandName($commands, $argv);
        $name = $this->resolveName($commands, $io, $name);
    } catch (MissingOptionException $e) {
        $io->error($e->getFullMessage());

        return CommandInterface::CODE_ERROR;
    }

    $result = CommandInterface::CODE_ERROR;
    $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);
    }

    if ($result === null || $result === true) {
        return CommandInterface::CODE_SUCCESS;
    }
    if (is_int($result) && $result >= 0 && $result <= 255) {
        return $result;
    }

    return CommandInterface::CODE_ERROR;
}

回答

- ChatGPT
このコードはCakePHPフレームワークの'CommandRunner'クラス内のrunメソッドの一部であり、
CLI(コマンドラインインターフェース)環境からの引数を受け取って特定のコマンドを実行するプロセスを定義しています。

# メソッドの説明
## パタメータ
- $argv コマンドラインから渡された引数
- $io ConsoleIoインスタンス。主にテスト目的で利用
## 戻り値
- コマンドの実行結果として終了コードを返します。

## メソッドの処理フロー
1. アプリケーションのブートストラップ
    - bootsrtrapメソッドを呼び出してアプリケーションを初期化します。

2. コマンドコレクションの作成
    - CommandCollectionインスタンスを作成する
    - アプリケーションのconsoleフックを使って、アプリケーション固有のコマンドをコレクションに追加します

3. プラグインコマンドの自動配線
    - PluginApplicationInterfaceを実装している場合、プラグインからコマンドのコレクションに追加します

4. ルートの読み込み
    - loadRoutesメソッドを呼び出して,CLIコマンド用のルーティング設定を読み込みます

5. 引数の検証と処理
    - 引数が空の場合、例外をスローしてエラーメッセージを表示します。
    - コマンド名の部分を削除します。

6. コマンドの解決と実行
    - longestCommandNameメソッドを使って最も長いマッチするコマンド名を解析します
    - resolveNameメソッドを使って実行するコマンドの名前を解決します。
    - getCommandメソッドで、解決したコマンド名に基づいて実際のコマンドオブジェクト(またはシェル)を取得する
    - 取得したコマンドオブジェクトの種類に合わせてrunShellまたはrunCommandメソッドを呼び出してコマンド実行する。

7. 終了コードの決定
    - コマンド実行の結果に基づいて適切な終了コードを返します。

得られた情報

  • CommandRunner::run関数について
    • 処理2~3にてCakePHPで実行できるコマンドの情報をまとめる
    • 処理6にてまとめたコマンド情報とコマンドライン引数から実行するコマンドを決定・実行する


5ターン目

次にコマンドオブジェクトの取得箇所、CommandRunner::getCommand関数について詳しく見ていきます。

質問

question_code.png
/**
 * Get the shell instance for a given command name
 *
 * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
 * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in.
 * @param string $name The command name to find
 * @return \Cake\Console\Shell|\Cake\Console\CommandInterface
 */
protected function getCommand(ConsoleIo $io, CommandCollection $commands, string $name)
{
    $instance = $commands->get($name);
    if (is_string($instance)) {
        $instance = $this->createCommand($instance, $io);
    }
    if ($instance instanceof Shell) {
        $instance->setRootName($this->root);
    }
    if ($instance instanceof CommandInterface) {
        $instance->setName("{$this->root} {$name}");
    }
    if ($instance instanceof CommandCollectionAwareInterface) {
        $instance->setCommandCollection($commands);
    }

    return $instance;
}

回答

- ChatGPT
このコードはCakePHPフレームワークのCommandRunnerクラス内のgetCommandメソッドの定義です。
このメソッドは指定されたコマンド名に対するシェルインスタンスまたはコマンドを取得するために使用されます。

## パラメータ
- $io ConsoleIoオブジェクト。コマンドの実行時の入出力操作をラップするために使用されます。
- $commands CommandCollectionオブジェクト、利用可能なコマンドのコレクションを表します。
- $name 取得したいコマンドの名前
## 戻り値
- このメソッドは指定されたコマンド名に対するShellまたはCommandInterfaceを実装するオブジェクトを返します。

## 処理の流れ
1. コマンドの取得:
    - CommandCollectionから指定された名前のコマンドを返します
2. コマンドインスタンスの生成
    - 取得したコマンドが文字列の場合、createCommandメソッドを使用し、そのクラスのインスタンスを作成します。
    - ここでは定義されていませんが、おそらく指定されたクラス名からコマンドオブジェクトを生成し、必要な依存関係(この場合は$io)を注入する役割を持っています。

得られた情報

  • getCommand関数について
    • シェルまたはコマンドを取得するための関数
    • 取得したコマンド名を引数にcreateCommand関数を実行し、コマンドオブジェクト(ModelCommand.php)を生成する


6ターン目

getCommand関数内で定義されているcreateCommnad関数内で、ModelCommandがインスタンス化されることが分かりました。なので次にcreateCommand関数を見ていきたい所なのですが、 処理を理解する上で必要な箇所は読めた & あまり深追いすると大変なので、createCommand関数は飛ばして、次はbake modelコマンドの核となるModelCommand.phpを見ていきたいと思います。 ここで困ったことに、 どのようにModelCommand.phpが呼び出されるのか分からないのでAIに頼ります!

質問

question_sixth.png

回答

- ChatGPT
CakePHPのModelCommand.phpは、CakePHPフレームワーク内のコンソール
コマンドを担当するクラスの一つで、モデルファイルを生成するためのコマンドを提供します。

ModelCommand.phpのエントリポイント、つまりコマンドが実行される際の開始点は、
クラス内のececuteメソッドです。このメソッドはCakePHPのコマンドラインインターフェース(CLI)
から呼び出され、モデルファイルの生成プロセスを開始します。

---- 省略 ----

得られた情報

  • ModelCommand.phpについて
    • エントリポイントはexecute関数


7ターン目

ModelCommand.phpのエントリポイントはexecute関数だとChatGPTに教えてもらったので、そこからコードを読んでいきます。

質問

question_code.png
/**
 * Execute the command.
 *
 * @param \Cake\Console\Arguments $args The command arguments.
 * @param \Cake\Console\ConsoleIo $io The console io
 * @return int|null The exit code or null for success
 */
public function execute(Arguments $args, ConsoleIo $io): ?int
{
    $this->extractCommonProperties($args);
    $name = $this->_getName($args->getArgument('name') ?? '');

    if (empty($name)) {
        $io->out('Choose a model to bake from the following:');
        foreach ($this->listUnskipped() as $table) {
            $io->out('- ' . $this->_camelize($table));
        }

        return static::CODE_SUCCESS;
    }

    $this->bake($this->_camelize($name), $args, $io);

    return static::CODE_SUCCESS;
}

回答

- ChatGPT
このコードはCakePHPフレームワークにおけるコンソールコマンドの一例で、
特定のモデルを生成するためのコマンドを実装しています。以下にコードの書く部分の解説を示します。

## コードの流れ
1. 共通プロパティの抽出($this->extractCommonProperties関数)
    - コマンド実行に必要な共通プロパティを引数から抽出するメソッドを呼び出します。

2. モデル名の取得($this->_getName関数)
    - コマンドライン引数からモデル名を取得します。
    - 引数が指定されていない場合は、デフォルトで空文字列が使用されます。

3. モデル名が空の場合の処理
    - モデル名が空の場合、利用可能なテーブルの一覧を表示し、ユーザーに選択を促します。
    - スキップされていない(生成対象の)テーブルのリストを取得し、コンソールに表示します。

4. モデルの生成($this->bake関数)
    - 指定されたモデル名でモデルファイルを生成します。

5. 正常終了の戻り値
    - コマンドが成功したことを示すために成功コードを返します。

得られた情報

  • ModelCommand::execute関数について
    • ModelCommand.phpに関する準備を行う関数
    • bake関数でファイルの生成を行う


8ターン目

次にファイル生成を行うModelCommand::bake関数について見ていきます。

質問

question_code.png
/**
 * Generate code for the given model name.
 *
 * @param string $name The model name to generate.
 * @param \Cake\Console\Arguments $args Console Arguments.
 * @param \Cake\Console\ConsoleIo $io Console Io.
 * @return void
 */
public function bake(string $name, Arguments $args, ConsoleIo $io): void
{
    $table = $this->getTable($name, $args);
    $tableObject = $this->getTableObject($name, $table);
    $data = $this->getTableContext($tableObject, $table, $name, $args, $io);
    $this->bakeTable($tableObject, $data, $args, $io);
    $this->bakeEntity($tableObject, $data, $args, $io);
    $this->bakeFixture($tableObject->getAlias(), $tableObject->getTable(), $args, $io);
    $this->bakeTest($tableObject->getAlias(), $args, $io);
}

回答

- ChatGPT
このコードはCakePHPフレームワークにおけるモデル生成のためのbakeメソッドの実装を示しています。
指定されたモデル名に基づいて必要なコード(モデル、テーブル、エンティティ、フィクスチャ、テスト)を自動生成します。

## メソッドパラメータ
- $name 生成するモデルの名前
- $args コマンドラインからの引数。特定のオプションや設定を取得するのに使用されます。
- $io 入出力操作(メッセージの出力、ユーザー入力の読み取りなど)を扱うオブジェクト。

## メソッドの処理フロー
1. テーブル名の取得
    - モデル名からテーブル名を取得します

2. テーブルオブジェクトの取得
    - 指定されたテーブル名に対応するTableクラスのインスタンスを取得します。

3. 名前の検証
    - テーブルスキーマの名前がCakePHPの命名規則に従っているか検証します。

4. テーブルコンテキスト取得
    - オブジェクト、テーブル名、モデル名を元に、生成に必要なコンテキスト情報を取得します。

5. 各種コードを生成
    - テーブルクラス、エンティティクラス、フィクスチャクラス、モデルのテストコードを作成します。

得られた情報

  • ModelCommand::bake関数について
    • モデルに関するファイルを生成をする関数
    • モデルに関する情報の取得し、各ファイルを生成する(テーブル・エンティティ・フィクスチャ・テスト)


9ターン目

ついにファイル生成箇所まで読み進めることができました。今回はTableファイルがどうやって作成されているかを知りたいので、ModelCommandl::bakeTable関数に絞って中身を見ていきます。

質問

question_code.png
/**
 * Bake a table class.
 *
 * @param \Cake\ORM\Table $model Model name or object
 * @param array $data An array to use to generate the Table
 * @param \Cake\Console\Arguments $args CLI Arguments
 * @param \Cake\Console\ConsoleIo $io CLI Arguments
 * @return void
 */
public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo $io): void
{
    if ($args->getOption('no-table')) {
        return;
    }

    $namespace = Configure::read('App.namespace');
    $pluginPath = '';
    if ($this->plugin) {
        $namespace = $this->_pluginNamespace($this->plugin);
    }

    $name = $model->getAlias();
    $entity = $this->_entityName($model->getAlias());
    $data += [
        'plugin' => $this->plugin,
        'pluginPath' => $pluginPath,
        'namespace' => $namespace,
        'name' => $name,
        'entity' => $entity,
        'associations' => [],
        'primaryKey' => 'id',
        'displayField' => null,
        'table' => null,
        'validation' => [],
        'rulesChecker' => [],
        'behaviors' => [],
        'connection' => $this->connection,
    ];

    $renderer = new TemplateRenderer($this->theme);
    $renderer->set($data);
    $out = $renderer->generate('Bake.Model/table');

    $path = $this->getPath($args);
    $filename = $path . 'Table' . DS . $name . 'Table.php';
    $io->out("\n" . sprintf('Baking table class for %s...', $name), 1, ConsoleIo::QUIET);
    $io->createFile($filename, $out, $args->getOption('force'));

    // Work around composer caching that classes/files do not exist.
    // Check for the file as it might not exist in tests.
    if (file_exists($filename)) {
        require_once $filename;
    }
    $this->getTableLocator()->clear();

    $emptyFile = $path . 'Table' . DS . '.gitkeep';
    $this->deleteEmptyFile($emptyFile, $io);
}

回答

- ChatGPT
このコードはCakePHPフレームワークの一部であり、
データベーステーブルに対応する「Tableクラス」を自動生成するための機能を提供しています。

## パラメーター
$model ORMのTableオブジェクトです。
$data Tableクラスの生成に使用される配列です。
$args コマンドラインからの引数を表します。
$io コマンドラインI/Oインターフェースを提供します。

## 処理の流れ
1. 早期リターン
    - no-tableオプジョンが指定されている場合処理を中断する

2. 名前空間の設定
    - アプリケーションの設定からデフォルトの名前空間を読み込みます。
    - プラグインが使用されている場合は、そのプラグインの名前空間に変更する

3. データの準備
    - モデルのエイリアスを取得し、その他の必要なデータを$data配列に追加します。

4. テンプレートのレンダリング
    - TemplateRendererクラスを使用して、指定されたテーマに基づきTableクラスのコードを生成します。
    - setメソッドでデータをテンプレートエンジンに渡し、generateメソッドでクラスのソースコードを生成します。

5. ファイルの生成
    - 生成したTableクラスのコードをファイルに書き込みます。
    - ファイルのパスはgetPathメソッドで取得し、ファイル名はモデルのエイリアスに基づいています。
    - $io->createFileメソッドでファイルを作成します。

---- 省略 ----

得られた情報

  • ModelCommand::bakeTable関数
    • テーブルに関する情報を$dataに追加し、set関数を用いてテンプレートエンジンに渡す
    • テンプレートエンジン(table.twig)に情報を配置し、createFile関数にてファイル生成を行う
    • これにてコード読み終了

ChatGPTと一緒にコードを読んでみて

今回ChatGPTと一緒に bake modelコマンドに関するコードを読みました。所要時間は1時間程度でコードを読むことができました。自分としては短時間で内容を理解することができたのではないかとおもいます。またChatGPTを使ったことで以下3つの良さを感じました。

1. 全体像の把握

「コードを読む上で大まかな流れを把握しておくと理解が深まる」というのはよく言われますが、それは仕様書がある、もしくはまず読んでみないことには全体像を理解できないと思います。その点、ChatGPTはコード読まずして全体像を教えてくれるので、常に全体像を把握しながら効率よくコードを読むことができました。

2. 必要な箇所だけ読める

コードの全てを理解するというのはそれだけの時間を要します。効率的にコードを読むには、そのコードを理解する必要性があるのかを考えて、コードを読む必要があると思います。ChatGPTはその際に便利です。ChatGPTがコードの概要について教えてくれるので、コードを読む必要があるのか取捨選択ができます。例えば5ターン目で出てきたcreateCommand関数はChatGPTの出力結果から読まないという選択ができ、時間短縮に繋がりました。

3. 困りごとの解決

コードの意味・定義場所など聞いたら全て教えてくれます(たまに間違う)。

まとめ

「bake modelコマンドについてChatGPTと一緒にコード読んでみた」でした。ここまで読んでいただきありがとうございました。今回はOSSのコードをChatGPTと一緒に読みましたが、運用しているプロジェクトでも同じようにコードが読めることができたら嬉しいですね。今後もAIを利用して模索してみます。

西村拓海

目次