XIONCC

【フレームワークを読む】Laravelのマイグレーションファイルは何をしているのか? :その18:Migrator::runUp()

runUp() メソッドです。見てみましょう。

Illuminate\Database\Migrations\Migrator::runUp() | 関連メソッド

    /**
     * Run "up" a migration instance.
     *
     * @param  string  $file
     * @param  int  $batch
     * @param  bool  $pretend
     * @return void
     */
    protected function runUp($file, $batch, $pretend)
    {
        // First we will resolve a "real" instance of the migration class from this
        // migration file name. Once we have the instances we can run the actual
        // command such as "up" or "down", or we can just simulate the action.
        $migration = $this->resolve(
            $name = $this->getMigrationName($file)
        );

        if ($pretend) {
            return $this->pretendToRun($migration, 'up');
        }

        $this->note("Migrating: {$name}");

        $startTime = microtime(true);

        $this->runMigration($migration, 'up');

        $runTime = round(microtime(true) - $startTime, 2);

        // Once we have run a migrations class, we will log that it was run in this
        // repository so that we don't try to run it next time we do a migration
        // in the application. A migration repository keeps the migrate order.
        $this->repository->log($name, $batch);

        $this->note("Migrated:  {$name} ({$runTime} seconds)");
    }
    /**
     * Resolve a migration instance from a file.
     *
     * @param  string  $file
     * @return object
     */
    public function resolve($file)
    {
        $class = Str::studly(implode('_', array_slice(explode('_', $file), 4)));

        return new $class;
    }
    /**
     * Get the name of the migration.
     *
     * @param  string  $path
     * @return string
     */
    public function getMigrationName($path)
    {
        return str_replace('.php', '', basename($path));
    }

以前読んだ処理の中で、実行対象のマイグレーションファイルは require_once() で読み込まれていました。
それを実行する手続きを踏むはずです。
getMigrationName() メソッドで拡張子を削除します。
resolve() メソッドでクラス名を取り出します。
マイグレーションファイルはファイルの前半に生成した日付の情報が数字とアンダースコアで連結してあります。まずこれを取り除きます。その後 Str::studly() メソッドでパスカルケースのクラス名に変換しつつキャッシュします。
出来たクラス名を使い生成したインスタンスを変数 $migration に代入します。
次に、 $pretendtrue ならば、pretendToRun() メソッドをコールします。
これはおそらくドライランのような使い方をするのでしょう。今回は通りません。
note() メソッドでメッセージを出力します。「Migrating: 2014_10_12_000000_create_users_table」 のような感じですね。

変数 $startTime にクエリ開始時間を代入します。

runMigration() メソッドを実行します。

Illuminate\Database\Migrations\Migrator::runMigration()

    /**
     * Run a migration inside a transaction if the database supports it.
     *
     * @param  object  $migration
     * @param  string  $method
     * @return void
     */
    protected function runMigration($migration, $method)
    {
        $connection = $this->resolveConnection(
            $migration->getConnection()
        );

        $callback = function () use ($migration, $method) {
            if (method_exists($migration, $method)) {
                $this->fireMigrationEvent(new MigrationStarted($migration, $method));

                $migration->{$method}();

                $this->fireMigrationEvent(new MigrationEnded($migration, $method));
            }
        };

        $this->getSchemaGrammar($connection)->supportsSchemaTransactions()
            && $migration->withinTransaction
                    ? $connection->transaction($callback)
                    : $callback();
    }

resolveConnection() メソッドで接続を変数 $connection に代入します。
変数 $callback にコールバックを代入しています。メイン処理を行うクロージャーです。
getSchemaGrammar() メソッドでスキーマグラマーを取得しています。
今回は MySqlGrammar ですね。
取得した MySqlGrammarsupportsSchemaTransactions() メソッドをコールしています。

Illuminate\Database\Schema\Grammars\Grammar::supportsSchemaTransactions()

    /**
     * Check if this Grammar supports schema changes wrapped in a transaction.
     *
     * @return bool
     */
    public function supportsSchemaTransactions()
    {
        return $this->transactions;
    }

supportsSchemaTransactions() メソッドは $this->transactions を戻しているだけです。
Grammar クラスのこのパラメータの初期値は false です。継承している MySqlGrammar もこのパラメータを上書きはしていません。おそらく、現状の Laravel のグラマーはトランザクションをカバーしていないのでしょう。
このパラメーターが true 且つ 対象マイグレーションインスタンスの $withinTransaction パラメーターが true だった場合、接続の transaction() メソッドにコールバックを渡します。
そうでない場合はコルバックをそのまま実行します。
マイグレーションファイルのスーパークラスとなる Illuminate\Database\Migrations\Migration クラスの $withinTransaction パラメータの初期値は true となっています。

Illuminate\Database\Concerns\ManagesTransactions::transactionI()

    /**
     * Execute a Closure within a transaction.
     *
     * @param  \Closure  $callback
     * @param  int  $attempts
     * @return mixed
     *
     * @throws \Throwable
     */
    public function transaction(Closure $callback, $attempts = 1)
    {
        for ($currentAttempt = 1; $currentAttempt beginTransaction();

            // We'll simply execute the given callback within a try / catch block and if we
            // catch any exception we can rollback this transaction so that none of this
            // gets actually persisted to a database or stored in a permanent fashion.
            try {
                $callbackResult = $callback($this);
            }

            // If we catch an exception we'll rollback this transaction and try again if we
            // are not out of attempts. If we are out of attempts we will just throw the
            // exception back out and let the developer handle an uncaught exceptions.
            catch (Throwable $e) {
                $this->handleTransactionException(
                    $e, $currentAttempt, $attempts
                );

                continue;
            }

            try {
                $this->commit();
            } catch (Throwable $e) {
                $this->handleCommitTransactionException(
                    $e, $currentAttempt, $attempts
                );

                continue;
            }

            return $callbackResult;
        }
    }

一応 transaction() メソッドも見てみましょう。 beginTransaction() メソッドをコールしてコールバックを実行し、 commit() メソッドをコールしています。
流れはわかりますね。今回はここは通りません。

コールバックの実行に入ります。クロージャーを読みましょう。以下のようの内容です。

function () use ($migration, $method) {
    if (method_exists($migration, $method)) {
        $this->fireMigrationEvent(new MigrationStarted($migration, $method));
        $migration->{$method}();
        $this->fireMigrationEvent(new MigrationEnded($migration, $method));
    }
};

引数として渡されたマイグレーションクラスとメソッド名が実行可能か検証し、可能なら処理をします。
fireMigrationEvent() はイベントをディスパッチするものでした。
マイグレーション開始と終了のクラスをディスパッチする間で対象メソッドが実行されています。

今回のマイグレーション実行予定は以下のようなものでした。

2014_10_12_000000_create_users_table.php
2019_08_19_000000_create_failed_jobs_table.php
2020_03_23_050035_create_customers_table.php

create_users_table.php を見てみましょう。

class=”codeTitle”>PROJECT_ROOT\database\magrations\2014_10_12_000000_create_users_table.php
class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Schema::create() を見てみましょう。
もはや懐かしいですね。初期の頃に Facade::__callStatic() から static::$app['db']->connection()->getSchemaBuilder() がコールされ、戻り値の create() メソッドがコールされるんでしたね。

Illuminate\Database\Schema\Builder::create() | createBlueprint()

    /**
     * Create a new table on the schema.
     *
     * @param  string  $table
     * @param  \Closure  $callback
     * @return void
     */
    public function create($table, Closure $callback)
    {
        $this->build(tap($this->createBlueprint($table), function ($blueprint) use ($callback) {
            $blueprint->create();

            $callback($blueprint);
        }));
    }
    /**
     * Create a new command set with a Closure.
     *
     * @param  string  $table
     * @param  \Closure|null  $callback
     * @return \Illuminate\Database\Schema\Blueprint
     */
    protected function createBlueprint($table, Closure $callback = null)
    {
        $prefix = $this->connection->getConfig('prefix_indexes')
                    ? $this->connection->getConfig('prefix')
                    : '';

        if (isset($this->resolver)) {
            return call_user_func($this->resolver, $table, $callback, $prefix);
        }

        return new Blueprint($table, $callback, $prefix);
    }
tap() ヘルパ関数をcreateBlueprint() メソッドの戻り値とクロージャーを渡してコールしています。
その戻り値を build() メソッドに引数として渡してコールしています。
ここの流れは 【フレームワークを読む】Laravelのマイグレーションファイルは何をしているのか? :その13:MigrateCommand::handle() で詳細に追いました。

createBlueprint() メソッドは、Blueprint インスタンスをテーブル名とクロージャー、プレフィックスを引数として渡して生成し戻しています。

tap() ヘルパ関数は第一引数を第二引数のクロージャーに引数として渡してコールします。
クロージャーでは、渡された Blueprint インスタンスの create() メソッドをコールしています。

Illuminate\Database\Schema\Blueprint::create()

    /**
     * Indicate that the table needs to be created.
     *
     * @return \Illuminate\Support\Fluent
     */
    public function create()
    {
        return $this->addCommand('create');
    }

create コマンドを追加していますね。
その後、 create() メソッドに渡された クロージャーを自身を引数として渡してコールしています。
ややこしいですね。
つまり、マイグレーションファイルのに定義されているクラス、今回は CreateUsersTableup() メソッドの第二引数として渡されているクロージャーです。テーブルにカラムを追加する処理が入っているようです。

tap() ヘルパ関数は第二引数に渡した第一引数を戻します。
こうして手続きを踏んだ Blueprint インスタンスを引数として渡して build() メソッドがコールされます。

SQL文を構築するメソッドを見てみましょう。

Illuminate\Database\Schema\Blueprint SQL生成用メソッド

    public function id($column = 'id')
    {
        return $this->bigIncrements($column);
    }
    public function bigIncrements($column)
    {
        return $this->unsignedBigInteger($column, true);
    }
    public function unsignedBigInteger($column, $autoIncrement = false)
    {
        return $this->bigInteger($column, $autoIncrement, true);
    }
    public function bigInteger($column, $autoIncrement = false, $unsigned = false)
    {
        return $this->addColumn('bigInteger', $column, compact('autoIncrement', 'unsigned'));
    }
    public function addColumn($type, $name, array $parameters = [])
    {
        $this->columns[] = $column = new ColumnDefinition(
            array_merge(compact('type', 'name'), $parameters)
        );

        return $column;
    }
    public function string($column, $length = null)
    {
        $length = $length ?: Builder::$defaultStringLength;

        return $this->addColumn('string', $column, compact('length'));
    }
    public function timestamp($column, $precision = 0)
    {
        return $this->addColumn('timestamp', $column, compact('precision'));
    }
    public function rememberToken()
    {
        return $this->string('remember_token', 100)->nullable();
    }
    public function timestamps($precision = 0)
    {
        $this->timestamp('created_at', $precision)->nullable();

        $this->timestamp('updated_at', $precision)->nullable();
    }

以下のようなSQLが生成されそうです。

CREATE TABLE users (
    id bigint unsigned auto_increment primary key not null,
    name varchar(255) not null,
    email varchar(255) not null,
    email_verified_at timestamp null,
    password varchar(255) not null,
    remember_token varchar(100) null,
    created_at timestamp null,
    updated_at timestamp null
);
ALTER TABLE users ADD unique users_email_unique (email);

残りのファイルも見てみましょう。

PROJECT_ROOT\database\magrations\2019_08_19_000000_create_failed_jobs_table.php

class CreateFailedJobsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('failed_jobs', function (Blueprint $table) {
            $table->id();
            $table->text('connection');
            $table->text('queue');
            $table->longText('payload');
            $table->longText('exception');
            $table->timestamp('failed_at')->useCurrent();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('failed_jobs');
    }
}

以下のようなSQLが生成されそうです。

CREATE TABLE failed_jobs (
    id bigint unsigned auto_increment primary key not null,
    connection text not null,
    queue text not null,
    payload longtext not null,
    exception longtext not null,
    failed_at timestamp  default CURRENT_TIMESTAMP
);

PROJECT_ROOT\database\magrations\2020_03_23_050035_create_customers_table.php

class CreateCustomersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('customers');
    }
}

以下のようなSQLが生成されそうです。

CREATE TABLE customers (
    id bigint unsigned auto_increment primary key not null,
    created_at timestamp null,
    updated_at timestamp null
);

生成されたSQLを実行しテーブルを3つ作成します。
生成後、現在時間から、処理開始時間を引き、実行にかかった時間を割り出します。
$this->repository 、つまり DatabaseMigrationRepository インスタンスの log() メソッドをマイグレーション名とバッチ番号を引数として渡しコールします。

Illuminate\Database\Migrations\DatabaseMigrationRepository::log()

    /**
     * Log that a migration was run.
     *
     * @param  string  $file
     * @param  int  $batch
     * @return void
     */
    public function log($file, $batch)
    {
        $record = ['migration' => $file, 'batch' => $batch];

        $this->table()->insert($record);
    }

Illuminate\Database\Query\Builder::insert()

    /**
     * Insert a new record into the database.
     *
     * @param  array  $values
     * @return bool
     */
    public function insert(array $values)
    {
        // Since every insert gets treated like a batch insert, we will make sure the
        // bindings are structured in a way that is convenient when building these
        // inserts statements by verifying these elements are actually an array.
        if (empty($values)) {
            return true;
        }

        if (! is_array(reset($values))) {
            $values = [$values];
        }

        // Here, we will sort the insert keys for every record so that each insert is
        // in the same order for the record. We need to make sure this is the case
        // so there are not any errors or problems when inserting these records.
        else {
            foreach ($values as $key => $value) {
                ksort($value);

                $values[$key] = $value;
            }
        }

        // Finally, we will run this query against the database connection and return
        // the results. We will need to also flatten these bindings before running
        // the query so they are all in one huge, flattened array for execution.
        return $this->connection->insert(
            $this->grammar->compileInsert($this, $values),
            $this->cleanBindings(Arr::flatten($values, 1))
        );
    }

Illuminate\Database\Query\Grammars\MySqlGrammar::compileInsert()

    /**
     * Compile an insert statement into SQL.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @param  array  $values
     * @return string
     */
    public function compileInsert(Builder $query, array $values)
    {
        if (empty($values)) {
            $values = [[]];
        }

        return parent::compileInsert($query, $values);
    }

Illuminate\Database\Query\Grammars\MySqlGrammar::compileInsert()

    /**
     * Compile an insert statement into SQL.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @param  array  $values
     * @return string
     */
    public function compileInsert(Builder $query, array $values)
    {
        // Essentially we will force every insert to be treated as a batch insert which
        // simply makes creating the SQL easier for us since we can utilize the same
        // basic routine regardless of an amount of records given to us to insert.
        $table = $this->wrapTable($query->from);

        if (empty($values)) {
            return "insert into {$table} default values";
        }

        if (! is_array(reset($values))) {
            $values = [$values];
        }

        $columns = $this->columnize(array_keys(reset($values)));

        // We need to build a list of parameter place-holders of values that are bound
        // to the query. Each insert should have the exact same amount of parameter
        // bindings so we will loop through the record and parameterize them all.
        $parameters = collect($values)->map(function ($record) {
            return '('.$this->parameterize($record).')';
        })->implode(', ');

        return "insert into $table ($columns) values $parameters";
    }

もう読むのは難しくありませんね。

以下のようなSQLが生成されそうです。

INSERT INTO migrations (migration, batch) values (FILE_NAME, BATCH_NO);

その後、note() メソッドを引数に 「Migrated: マイグレーション名 (実行時間 seconds)」 というメッセージを渡しコールしてメッセージを出力し Migrator::runUp() メソッドは完了です。

runUp() メソッドの処理が終わると、runPending() メソッドは fireMigrationEvent() メソッドでマイグレーション終了をディスパッチして終了します。
runPending() メソッドの実行が狩猟すると、Migrator::run() メソッドはマイグレーションファイル配列を戻します。
コール元は MigrateCommand::handle() でした。マイグレーション処理完了後、 0 を戻します。

MigrateCommand::handle() メソッドのコール元は Command::execute() でした。
受け取った値をそのまま戻します。
Command::execute() メソッドのコール元は Symfony\Component\Console\Command\Command::run() でした。
Symfony\Component\Console\Command\Command::run() メソッドは戻り値が整数ならそれを、そうでなけれが 0 を戻します。
Symfony\Component\Console\Command\Command::run() メソッドのコール元は Illuminate\Console\Command::run() でした。Illuminate\Console\Command::run() は受け取った戻り値をそのまま戻します。
Illuminate\Console\Command::run() のコール元は Symfony\Component\Console\Application::doRunCommand() です。
Symfony\Component\Console\Application::doRunCommand() は受け取った戻り値をそのまま戻します。
Symfony\Component\Console\Application::doRunCommand() のコール元は Symfony\Component\Console\Application::doRun() です。
Symfony\Component\Console\Application::doRun()$this->runningCommandnull に上書きし、戻り値を戻します。
Symfony\Component\Console\Application::doRun() のコール元は Symfony\Component\Console\Application::run() です。
Symfony\Component\Console\Application::run() は受け取った戻り値をそのまま戻します。
Symfony\Component\Console\Application::run() のコール元は Illuminate\Console\Application::run() です。
Illuminate\Console\Application::run() はイベントディスパッチャーにコマンド終了インスタンスをディスパッチした後、戻り値をそのまま戻します。
Illuminate\Console\Application::run() のコール元は Illuminate\Foundation\Console\Kernel::handle() です。
Illuminate\Foundation\Console\Kernel::handle() は戻り値をそのまま戻します。
Illuminate\Foundation\Console\Kernel::handle() のコール元は PROJECT_ROOT/artisan です。
PROJECT_ROOT/artisan は戻り値を変数 $status に代入します。
その後 terminate() メソッドを入力と戻り値のステータス値を引数に渡しコールします。

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

    /**
     * Terminate the application.
     *
     * @param  \Symfony\Component\Console\Input\InputInterface  $input
     * @param  int  $status
     * @return void
     */
    public function terminate($input, $status)
    {
        $this->app->terminate();
    }

Illuminate\Foundation\Application::terminate()

    /**
     * Terminate the application.
     *
     * @return void
     */
    public function terminate()
    {
        foreach ($this->terminatingCallbacks as $terminating) {
            $this->call($terminating);
        }
    }

terminate() メソッドはコマンド終了時にコールするコールバックが登録されていた場合はそれをコールします。
そしてとうとう、 exit() メソッドをステータス値を引数として渡してコールして終了です。
長い旅でした。お疲れさまです。