1
1

More than 3 years have passed since last update.

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!その9「Kernel::handle()」

Last updated at Posted at 2020-04-26

INDEX

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!

Kernel::handle()

前回はKernel::handle()がコールされた際に渡される引数

  • Symfony\Component\Console\Input\ArgvInput
  • Symfony\Component\Console\Output\ConsoleOutput

を軽く見てみました。
では、handle()メソッドを見てみましょう。

Illuminate\Foundation\Console\Kernel::handle()
    /**
     * Run the console application.
     *
     * @param  \Symfony\Component\Console\Input\InputInterface  $input
     * @param  \Symfony\Component\Console\Output\OutputInterface|null  $output
     * @return int
     */
    public function handle($input, $output = null)
    {
        try {
            $this->bootstrap();
            return $this->getArtisan()->run($input, $output);
        } catch (Throwable $e) {
            $this->reportException($e);
            $this->renderException($output, $e);
            return 1;
        }
    }
    /**
     * Get the Artisan application instance.
     *
     * @return \Illuminate\Console\Application
     */
    protected function getArtisan()
    {
        if (is_null($this->artisan)) {
            return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
                                ->resolveCommands($this->commands);
        }
        return $this->artisan;
    }

Illuminate\Foundation\Console\Kernel::handle()

第一引数はSymfony\Component\Console\Input\ArgvInputインスタンスです。
第二引数はSymfony\Component\Console\Output\ConsoleOutputインスタンスです。
戻り値は整数型です。

まずbootstrap()メソッドがコールされます。ここは以前読みましたのでもう理解できています。
次にgetArtisan()メソッドがコールされます。
getArtisan()メソッドの戻り値はIlluminate\Console\Applicationインスタンスです。
getArtisan()メソッドは$this->artisannullかどうか検証します。nullでなければそれを返します。nullならば、アプリケーション、イベント、バージョンを渡しArtisanインスタンスを生成し、resolveCommands()を実行した戻り値を返します。
Artisanの実体はIlluminate\Console\Applicationです。見てみましょう。

Illuminate\Console\Application::__construct()
    /**
     * Create a new Artisan console application.
     *
     * @param  \Illuminate\Contracts\Container\Container  $laravel
     * @param  \Illuminate\Contracts\Events\Dispatcher  $events
     * @param  string  $version
     * @return void
     */
    public function __construct(Container $laravel, Dispatcher $events, $version)
    {
        parent::__construct('Laravel Framework', $version);
        $this->laravel = $laravel;
        $this->events = $events;
        $this->setAutoExit(false);
        $this->setCatchExceptions(false);
        $this->events->dispatch(new ArtisanStarting($this));
        $this->bootstrap();
    }
    /**
     * Bootstrap the console application.
     *
     * @return void
     */
    protected function bootstrap()
    {
        foreach (static::$bootstrappers as $bootstrapper) {
            $bootstrapper($this);
        }
    }
Symfony\Component\Console\Application::__construct()|関連メソッド
    public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
    {
        $this->name = $name;
        $this->version = $version;
        $this->terminal = new Terminal();
        $this->defaultCommand = 'list';
    }
    /**
     * Sets whether to automatically exit after a command execution or not.
     */
    public function setAutoExit(bool $boolean)
    {
        $this->autoExit = $boolean;
    }
    /**
     * Sets whether to catch exceptions or not during commands execution.
     */
    public function setCatchExceptions(bool $boolean)
    {
        $this->catchExceptions = $boolean;
    }

Illuminate\Console\Application::__construct()

第一引数はアプリケーションコンテナです。
第二引数はイベントディスパッチャーです。
第三引数はストリング型でバージョン番号です。
戻り値はありません。

まず、スーパークラスSymfony\Component\Console\Applicationのコンストラクタをアプリケーション名「Laravel Framework」とバージョン情報を引数にコールします。
Symfony\Component\Console\Applicationのコンストラクタは受け取ったアプリケーション名とバージョン名を変数に代入し、$this->terminalTerminalインスタンスを生成したものを代入し、デフォルトコマンドをセットします。Terminalにコンストラクタはありません。保持ファンクションの一覧を見てみましょう。

Symfony\Component\Console\Terminal変数|ファンクション一覧
    private static $width;
    private static $height;
    private static $stty;
    public function getWidth()
    public function getHeight()
    private static function initDimensions()
    private static function hasVt100Support(): bool
    private static function initDimensionsUsingStty()
    private static function getConsoleMode(): ?array
    private static function getSttyColumns(): ?string
    private static function readFromProcess(string $command): ?string

Symfony\Component\Console\Terminal

縦幅横幅の取得、Sttyが利用可能か、サイズの初期化、VT100をサポートしているか、Stty利用時のサイズ初期化、mode CONが使用可能か(コマンドプロンプトウィンドウの変更)。Sttyの行数の取得、コマンドの実行、等のメソッドを実装しているようです。端末に関することを担うクラスのようです。

Illuminate\Console\Application::__construct()に戻ります。
アプリケーションコンテナとイベントディスパッチャーを変数に代入します。
setAutoExit()メソッドをfalseを渡しコールします。
setAutoExit()メソッドは$this->autoExitをセットします。
コマンド実行後に自動的に終了するかどうかを設定するパラメータのようです。
setCatchExceptions()メソッドをfalseを渡しコールします。
setCatchExceptions()メソッドは$this->catchExceptionsをセットします。
コマンドの実行中に例外をキャッチするかどうかを設定するパラメータのようです。
次にイベントディスパッチャーにArtisanStartingを引数に$thisを渡して生成したものを登録します。
ArtisanStartingクラスは非常に小さな定義のクラスです。

Illuminate\Console\Events\ArtisanStarting
<?php
namespace Illuminate\Console\Events;
class ArtisanStarting
{
    /**
     * The Artisan application instance.
     *
     * @var \Illuminate\Console\Application
     */
    public $artisan;
    /**
     * Create a new event instance.
     *
     * @param  \Illuminate\Console\Application  $artisan
     * @return void
     */
    public function __construct($artisan)
    {
        $this->artisan = $artisan;
    }
}

コンストラクタでコンソールアプリケーションを受け取り$this->artisanに代入します。
Illuminate\Console\Application::__construct()の続きです。
bootstrap()メソッドをコールしています。
bootstrap()メソッドは、foreachstatic::$bootstrappersを回して$bootstrapper()を引数に自身を渡してコールしています。static::$bootstrappersは生成の過程で仕込まれている様子はありませんでした。おそらくKernelが初期化される工程で準備されるのでしょう。

Kernel初期化の流れの中でApplication::registerConfiguredProviders()がコールされます。そこでは$this->config['app.providers']つまり、PROJECT_ROOT/config/app.phpで定義されている配列のキーprovidersつまりプロバイダーのリストを引数にProviderRepository::load()をコールしていました。その処理の中で、アプリケーションコンテナのregister()に引数としてプロバイダーを一つずつ渡し、そこでプロバダー自体のregister()がコールされます。
$this->config['app.providers']の中にはIlluminate\Foundation\Providers\ConsoleSupportServiceProviderが含まれます。
こちらを見てみましょう。

Illuminate\Foundation\Providers\ConsoleSupportServiceProvider
<?php
namespace Illuminate\Foundation\Providers;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Database\MigrationServiceProvider;
use Illuminate\Support\AggregateServiceProvider;
class ConsoleSupportServiceProvider extends AggregateServiceProvider implements DeferrableProvider
{
    /**
     * The provider class names.
     *
     * @var array
     */
    protected $providers = [
        ArtisanServiceProvider::class,
        MigrationServiceProvider::class,
        ComposerServiceProvider::class,
    ];
}

コンストラクタもregister()も見当たりません。
$providersが定義されているだけです。その中にArtisanServiceProviderとそれっぽいクラスが記述されています。
ConsoleSupportServiceProviderクラスはIlluminate\Support\AggregateServiceProviderクラスを継承しています。こちらを見てみましょう。

Illuminate\Support\AggregateServiceProviderL::register()
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->instances = [];
        foreach ($this->providers as $provider) {
            $this->instances[] = $this->app->register($provider);
        }
    }

register()メソッドがありました。
ConsoleSupportServiceProviderでオーバーライドした$providersforeachで回してアプリケーションコンテナのregister()でプロバイダーを登録して$this->instances[]に入れいていく処理です。
ということは、ArtisanServiceProviderインスタンスがアプリケーションコンテナにプロバイダー登録され、自身のregister()メソッドがコールされるはずです。
見てみましょう。

Illuminate\Foundation\Providers\ArtisanServiceProvider::register()|関連メソッド
    protected $commands = [
        'CacheClear' => 'command.cache.clear',
        'CacheForget' => 'command.cache.forget',
        'ClearCompiled' => 'command.clear-compiled',
        'ClearResets' => 'command.auth.resets.clear',
        'ConfigCache' => 'command.config.cache',
        'ConfigClear' => 'command.config.clear',
        'DbWipe' => 'command.db.wipe',
        'Down' => 'command.down',
        'Environment' => 'command.environment',
        'EventCache' => 'command.event.cache',
        'EventClear' => 'command.event.clear',
        'EventList' => 'command.event.list',
        'KeyGenerate' => 'command.key.generate',
        'Optimize' => 'command.optimize',
        'OptimizeClear' => 'command.optimize.clear',
        'PackageDiscover' => 'command.package.discover',
        'QueueFailed' => 'command.queue.failed',
        'QueueFlush' => 'command.queue.flush',
        'QueueForget' => 'command.queue.forget',
        'QueueListen' => 'command.queue.listen',
        'QueueRestart' => 'command.queue.restart',
        'QueueRetry' => 'command.queue.retry',
        'QueueWork' => 'command.queue.work',
        'RouteCache' => 'command.route.cache',
        'RouteClear' => 'command.route.clear',
        'RouteList' => 'command.route.list',
        'Seed' => 'command.seed',
        'ScheduleFinish' => ScheduleFinishCommand::class,
        'ScheduleRun' => ScheduleRunCommand::class,
        'StorageLink' => 'command.storage.link',
        'Up' => 'command.up',
        'ViewCache' => 'command.view.cache',
        'ViewClear' => 'command.view.clear',
    ];
    /**
     * The commands to be registered.
     *
     * @var array
     */
    protected $devCommands = [
        'CacheTable' => 'command.cache.table',
        'ChannelMake' => 'command.channel.make',
        'ComponentMake' => 'command.component.make',
        'ConsoleMake' => 'command.console.make',
        'ControllerMake' => 'command.controller.make',
        'EventGenerate' => 'command.event.generate',
        'EventMake' => 'command.event.make',
        'ExceptionMake' => 'command.exception.make',
        'FactoryMake' => 'command.factory.make',
        'JobMake' => 'command.job.make',
        'ListenerMake' => 'command.listener.make',
        'MailMake' => 'command.mail.make',
        'MiddlewareMake' => 'command.middleware.make',
        'ModelMake' => 'command.model.make',
        'NotificationMake' => 'command.notification.make',
        'NotificationTable' => 'command.notification.table',
        'ObserverMake' => 'command.observer.make',
        'PolicyMake' => 'command.policy.make',
        'ProviderMake' => 'command.provider.make',
        'QueueFailedTable' => 'command.queue.failed-table',
        'QueueTable' => 'command.queue.table',
        'RequestMake' => 'command.request.make',
        'ResourceMake' => 'command.resource.make',
        'RuleMake' => 'command.rule.make',
        'SeederMake' => 'command.seeder.make',
        'SessionTable' => 'command.session.table',
        'Serve' => 'command.serve',
        'StubPublish' => 'command.stub.publish',
        'TestMake' => 'command.test.make',
        'VendorPublish' => 'command.vendor.publish',
    ];
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerCommands(array_merge(
            $this->commands, $this->devCommands
        ));
    }
    /**
     * Register the given commands.
     *
     * @param  array  $commands
     * @return void
     */
    protected function registerCommands(array $commands)
    {
        foreach (array_keys($commands) as $command) {
            call_user_func_array([$this, "register{$command}Command"], []);
        }
        $this->commands(array_values($commands));
    }
Illuminate\Support\ServiceProvider::commands()
    /**
     * Register the package's custom Artisan commands.
     *
     * @param  array|mixed  $commands
     * @return void
     */
    public function commands($commands)
    {
        $commands = is_array($commands) ? $commands : func_get_args();
        Artisan::starting(function ($artisan) use ($commands) {
            $artisan->resolveCommands($commands);
        });
    }

$commands$devCommandsに連想配列が沢山代入されています。
register()メソッドはregisterCommands()$commands$devCommandsarray_mergeしたものを渡してコールしています。
registerCommands()メソッドは受け取った配列をarray_keysでキーのみ配列で取り出し、それをforeachで回し、call_user_func_arrayでコールバック関数を実行します。実行するコールバックは自身に定義してある関数で、関数名はregister{$command}Commandで生成されたものです。引数は空の配列となっています。
このクラスにはクロージャーをアプリケーションコンテナにシングルトンで結合するメソッドが沢山定義されてます。
以下は例です。

定義例
    /**
     * Register the command.
     *
     * @return void
     */
    protected function registerUpCommand()
    {
        $this->app->singleton('command.up', function () {
            return new UpCommand;
        });
    }

アプリケーションコンテナにcommand.upという名前でUpCommandインスタンスを生成して返すクロージャーをシングルトン結合しています。
$commands$devCommandsに代入されている沢山のコマンドがシングルトン結合されます。
次にcommands()メソッドを$commandsarray_valuesで値のみの配列にしたものを引数にコールします。
Artisan::starting()をクロージャーを引数に渡してコールしています。ArtisanIlluminate\Console\Applicationのことです。見てみましょう。

Illuminate\Console\Application::starting()
    /**
     * Register a console "starting" bootstrapper.
     *
     * @param  \Closure  $callback
     * @return void
     */
    public static function starting(Closure $callback)
    {
        static::$bootstrappers[] = $callback;
    }

Illuminate\Console\Application::starting()

第一引数はクロージャーです。
戻り値はありません。

static::$bootstrappers[]配列に受け取ったクロージャーを登録しています。
先程疑問だった、static::$bootstrappersに格納されている中身の正体がわかりました。
static::$bootstrappers[]に登録されたクロージャー全てに$thisを渡したものをアプリケーションコンテナにシングルトン結合するという手順になります。

せっかくここまで読んだので、Artisan::starting()メソッドに引数として渡されているクロージャーも読んでみましょう。

        Artisan::starting(function ($artisan) use ($commands) {
            $artisan->resolveCommands($commands);
        });

このクロージャーをコールするArtisan::bootstrap()では引数として$thisを渡しています。つまり、$this->resolveCommands(コマンド名)が実行されるクロージャーです。resolveCommands()を見てみましょう。

Illuminate\Console\Application::resolveCommands()|関連メソッド
    /**
     * Resolve an array of commands through the application.
     *
     * @param  array|mixed  $commands
     * @return $this
     */
    public function resolveCommands($commands)
    {
        $commands = is_array($commands) ? $commands : func_get_args();
        foreach ($commands as $command) {
            $this->resolve($command);
        }
        return $this;
    }
    /**
     * Add a command, resolving through the application.
     *
     * @param  string  $command
     * @return \Symfony\Component\Console\Command\Command
     */
    public function resolve($command)
    {
        return $this->add($this->laravel->make($command));
    }
    /**
     * Add a command to the console.
     *
     * @param  \Symfony\Component\Console\Command\Command  $command
     * @return \Symfony\Component\Console\Command\Command
     */
    public function add(SymfonyCommand $command)
    {
        if ($command instanceof Command) {
            $command->setLaravel($this->laravel);
        }
        return $this->addToParent($command);
    }
    /**
     * Set the Laravel application instance.
     *
     * @param  \Illuminate\Contracts\Container\Container  $laravel
     * @return void
     */
    public function setLaravel($laravel)
    {
        $this->laravel = $laravel;
    }
    /**
     * Add the command to the parent instance.
     *
     * @param  \Symfony\Component\Console\Command\Command  $command
     * @return \Symfony\Component\Console\Command\Command
     */
    protected function addToParent(SymfonyCommand $command)
    {
        return parent::add($command);
    }

Illuminate\Console\Application::resolveCommands()

第一引数は コマンドです。
戻り値は $this です。

受け取ったコマンドをforeachで回し、resolve()メソッドを引数にコマンド名を渡してコールし、自身を返しています。
resolve()はコマンド名を受け取りそれをアプリケーションコンテナでmake()した戻り値をadd()しています。
add()メソッドでは、引数として渡されたコマンドがCommandインスタンス か検証し、そうであった場合はsetLaravel()メソッドにアプリケションコンテナを引数に渡しコールします。
そうでない場合はaddToParent()メソッドを通し、スーパークラスのadd()メソッドをコールします。
setLaravel()メソッドはコマンドの変数$laravelにアプリケーションコンテナを代入します。
スーパークラスのadd()メソッドとはSymfony\Component\Console\Application::add()の事です。
Symfony\Component\Console\Application::add()が引数として受け取るのはSymfony\Component\Console\Command\Commandインスタンスで、これはIlluminate\Console\Commandのスーパークラスです。つまり Illuminate\Console\CommandSymfony\Component\Console\Command\CommandをLaravel用に拡張したコマンドクラスなのでしょう。
Symfony\Component\Console\Applicationを読みたいところですが、少しボリュームがあるので、まずは本筋に戻ります。


Illuminate\Foundation\Console\Kernel::handle()から$this->getArtisan()がコールされその処理の途中でした。

Illuminate\Foundation\Console\Kernel::getArtisan()
    /**
     * Get the Artisan application instance.
     *
     * @return \Illuminate\Console\Application
     */
    protected function getArtisan()
    {
        if (is_null($this->artisan)) {
            return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
                                ->resolveCommands($this->commands);
        }
        return $this->artisan;
    }

生成したArtisanインスタンスのresolveCommands$this->commandsを引数に渡しコールします。おそらく$this->commandsは空の配列なのでただ戻り値に自身が返ってくるだけでしょう。
そのまま生成されたArtisanインスタンスが戻されます。

Illuminate\Foundation\Console\Kernel::handle()に戻ります。

Illuminate\Foundation\Console\Kernel::handle()
    public function handle($input, $output = null)
    {
        try {
            $this->bootstrap();
            return $this->getArtisan()->run($input, $output);
        } catch (Throwable $e) {
            $this->reportException($e);
            $this->renderException($output, $e);
            return 1;
        }

$this->getArtisan()の戻り値にrun()しています。引数に入力と出力を渡しています。
次はこちらを見ていきましょう。

次回

今回はartisanインスタンスの生成の流れを読んでみました。次回はConsole\Application::run()を追ってみたいと思います!

続く☆

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1