XIONCC

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

Migrator::run() の続きを読みましょう。

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

    /**
     * Run the pending migrations at a given path.
     *
     * @param  array|string  $paths
     * @param  array  $options
     * @return array
     */
    public function run($paths = [], array $options = [])
    {
        // Once we grab all of the migration files for the path, we will compare them
        // against the migrations that have already been run for this package then
        // run each of the outstanding migrations against a database connection.
        $files = $this->getMigrationFiles($paths);

        $this->requireFiles($migrations = $this->pendingMigrations(
            $files, $this->repository->getRan()
        ));

        // Once we have all these migrations that are outstanding we are ready to run
        // we will go ahead and run them "up". This will execute each migration as
        // an operation against a database. Then we'll return this list of them.
        $this->runPending($migrations, $options);

        return $migrations;
    }

$this->repository->getRan() の戻り値は以下のSQLの結果を配列にしたものでした。

SELECT migration FROM migrations ORDER BY batch ASC, migration ASC

pendingMigrations() メソッドを見てみましょう。

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

    /**
     * Get the migration files that have not yet run.
     *
     * @param  array  $files
     * @param  array  $ran
     * @return array
     */
    protected function pendingMigrations($files, $ran)
    {
        return Collection::make($files)
                ->reject(function ($file) use ($ran) {
                    return in_array($this->getMigrationName($file), $ran);
                })->values()->all();
    }

Illuminate\Support\Traits\EnumeratesValues::reject()

    /**
     * Create a collection of all elements that do not pass a given truth test.
     *
     * @param  callable|mixed  $callback
     * @return static
     */
    public function reject($callback = true)
    {
        $useAsCallable = $this->useAsCallable($callback);

        return $this->filter(function ($value, $key) use ($callback, $useAsCallable) {
            return $useAsCallable
                ? ! $callback($value, $key)
                : $value != $callback;
        });
    }

マイグレーションファイル名の配列を引数として渡して Collection::make() メソッドをスタティックコールし、Collection インスタンスを生成したものの reject() メソッドをコールします。
引数として
reject() メソッドは受け取った引数がコールバックならば、それを使ってフィルターします。
マイグレーションファイル名の配列のなかで、先程のSQL実行結果に無いもののみを配列にしてそれを引数として生成した Collection インスタンスを戻します。

戻された Collection インスタンスを $migrations に代入し、requireFiles() メソッドをコールします。

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

    /**
     * Require in all the migration files in a given path.
     *
     * @param  array  $files
     * @return void
     */
    public function requireFiles(array $files)
    {
        foreach ($files as $file) {
            $this->files->requireOnce($file);
        }
    }

Illuminate\Filesystem\Filesystem::requireOnce()

    /**
     * Require the given file once.
     *
     * @param  string  $file
     * @return mixed
     */
    public function requireOnce($file)
    {
        require_once $file;
    }

requireFiles() メソッドは受け取ったファイル名配列を foreach で回して require_once() するだけですね。

さて、runPending() を読みましょう!

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

    /**
     * Run an array of migrations.
     *
     * @param  array  $migrations
     * @param  array  $options
     * @return void
     */
    public function runPending(array $migrations, array $options = [])
    {
        // First we will just make sure that there are any migrations to run. If there
        // aren't, we will just make a note of it to the developer so they're aware
        // that all of the migrations have been run against this database system.
        if (count($migrations) === 0) {
            $this->fireMigrationEvent(new NoPendingMigrations('up'));

            $this->note('Nothing to migrate.');

            return;
        }

        // Next, we will get the next batch number for the migrations so we can insert
        // correct batch number in the database migrations repository when we store
        // each migration's execution. We will also extract a few of the options.
        $batch = $this->repository->getNextBatchNumber();

        $pretend = $options['pretend'] ?? false;

        $step = $options['step'] ?? false;

        $this->fireMigrationEvent(new MigrationsStarted);

        // Once we have the array of migrations, we will spin through them and run the
        // migrations "up" so the changes are made to the databases. We'll then log
        // that the migration was run so we don't repeat it next time we execute.
        foreach ($migrations as $file) {
            $this->runUp($file, $batch, $pretend);

            if ($step) {
                $batch++;
            }
        }

        $this->fireMigrationEvent(new MigrationsEnded);
    }

引数として渡されているのは、マイグレーションファイル名の配列と、MigrateCommand::handle() メソッドから渡された第二引数の配列です。

Illuminate\Database\Console\Migrations\MigrateCommand::handle()

$this->migrator->setOutput($this->output)
    ->run($this->getMigrationPaths(), [
        'pretend' => $this->option('pretend'),
          'step' => $this->option('step'),
    ]);

Illuminate\Database\Migrations\Migrator::fireMigrationEvent() | note()

    /**
     * Fire the given event for the migration.
     *
     * @param  \Illuminate\Contracts\Database\Events\MigrationEvent  $event
     * @return void
     */
    public function fireMigrationEvent($event)
    {
        if ($this->events) {
            $this->events->dispatch($event);
        }
    }
    /**
     * Write a note to the console's output.
     *
     * @param  string  $message
     * @return void
     */
    protected function note($message)
    {
        if ($this->output) {
            $this->output->writeln($message);
        }
    }

runPending() に渡された第一引数、マイグレーションファイルの配列が空の場合、NoPendingMigrations インスタンスを引数に「up」を渡して生成したものを引数に fireMigrationEvent() メソッドをコールします。NoPendingMigrations クラスはコンストラクタに渡されたメソッド名のストリングを保持するだけの小さなクラスです。
fireMigrationEvent() メソッドはイベントにディスパッチしているだけですね。
そして note() メソッドを 「Nothing to migrate.」 をいうメッセージを引数にコールします。
これは、output->writeln() のラッパー関数ですね。メッセージを出力します。
メッセージを出力後に return します。

マイグレーションファイル配列に値が入っていた場合、DatabaseMigrationRepository インスタンスの getNextBatchNumber() メソッドをコールします。

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

    /**
     * Get the next migration batch number.
     *
     * @return int
     */
    public function getNextBatchNumber()
    {
        return $this->getLastBatchNumber() + 1;
    }
    /**
     * Get the last migration batch number.
     *
     * @return int
     */
    public function getLastBatchNumber()
    {
        return $this->table()->max('batch');
    }
    /**
     * Get a query builder for the migration table.
     *
     * @return \Illuminate\Database\Query\Builder
     */
    protected function table()
    {
        return $this->getConnection()->table($this->table)->useWritePdo();
    }

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

    /**
     * Retrieve the maximum value of a given column.
     *
     * @param  string  $column
     * @return mixed
     */
    public function max($column)
    {
        return $this->aggregate(__FUNCTION__, [$column]);
    }
    /**
     * Execute an aggregate function on the database.
     *
     * @param  string  $function
     * @param  array  $columns
     * @return mixed
     */
    public function aggregate($function, $columns = ['*'])
    {
        $results = $this->cloneWithout($this->unions ? [] : ['columns'])
                        ->cloneWithoutBindings($this->unions ? [] : ['select'])
                        ->setAggregate($function, $columns)
                        ->get($columns);

        if (! $results->isEmpty()) {
            return array_change_key_case((array) $results[0])['aggregate'];
        }
    }

getNextBatchNumber() メソッドは getLastBatchNumber() メソッドの戻り値に1を加算した値を戻します。
getLastBatchNumber() メソッドは table() メソッドをコールして戻ってきた Builder インスタンスの max() メソッドをコールします。
table() メソッドは接続に対象テーブル名、今回は 「migrations」 をセットして書込PDO接続を指定しています。
max() メソッドは aggregate() メソッドを関数名「max」とカラム名を配列にしたもの、今回は 「['batch']」 を引数として渡してコールします。

aggregate() メソッドは SQL文を組み立てて実行していると推測されます。
一つずつ読んでみましょう。

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

    /**
     * Clone the query without the given properties.
     *
     * @param  array  $properties
     * @return static
     */
    public function cloneWithout(array $properties)
    {
        return tap(clone $this, function ($clone) use ($properties) {
            foreach ($properties as $property) {
                $clone->{$property} = null;
            }
        });
    }

cloneWithout()['columns'] を引数として渡してコールしています。
cloneWithout()tap() ヘルパ関数を使い、自身のクローンを加工しています。
引数として渡された配列、今回は ['columns']foreach で回し、その値をプロパティー名としてクローンのプロパティーを null で上書きしています。つまり、指定カラム名をリセットすることになります。戻り値はクローンした Builder インスタンスです。

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

    /**
     * Clone the query without the given bindings.
     *
     * @param  array  $except
     * @return static
     */
    public function cloneWithoutBindings(array $except)
    {
        return tap(clone $this, function ($clone) use ($except) {
            foreach ($except as $type) {
                $clone->bindings[$type] = [];
            }
        });
    }

次は cloneWithoutBindings() メソッドを ['select'] を引数として渡してコールしています。
cloneWithoutBindings()tap() ヘルパ関数を使い、自身のクローンを加工しています。
引数として渡された配列、今回は ['select']foreach で回し、クローンのバインドタイプを空配列に上書きしています。つまり、指定したバインドタイプをリセットすることになります。戻り値はクローンした Builder インスタンスです。

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

    /**
     * Set the aggregate property without running the query.
     *
     * @param  string  $function
     * @param  array  $columns
     * @return $this
     */
    protected function setAggregate($function, $columns)
    {
        $this->aggregate = compact('function', 'columns');

        if (empty($this->groups)) {
            $this->orders = null;

            $this->bindings['order'] = [];
        }

        return $this;
    }

setAggregate() メソッドは受け取った引数を compact() で配列化し、$this->aggregate に代入します。この $this はクローンした Builder インスタンスです。
もし、 groups パラメーターが空だった場合、 ordersnull に、bindings['order'] を空配列にセットし、自身を返します。

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

    /**
     * Execute the query as a "select" statement.
     *
     * @param  array|string  $columns
     * @return \Illuminate\Support\Collection
     */
    public function get($columns = ['*'])
    {
        return collect($this->onceWithColumns(Arr::wrap($columns), function () {
            return $this->processor->processSelect($this, $this->runSelect());
        }));
    }

最後に get() メソッドを引数に ['batch'] を渡しコールします。
collect() ヘルパ関数を使い、結果を引数にして生成した Collection インスタンスを戻しています。中を見てみましょう。
onceWithColumns() メソッドをコールしています。これは少し前に読みましたね。引数として渡しているのは Arr::wrap() staticメソッドとクロージャーです。
Arr::wrap() は引数を配列にキャストして戻します。今回は 「['batch']」 ですのでそのままですね。
クロージャーは MySqlProcessor::processSelect() をコールしています。
引数に自身と $this->runSelect() を渡しています。

MySqlProcessor::processSelect() メソッドは第二引数をそのまま戻すものでした。
つまり、主役は $this->runSelect() ということになります。

もう一度見てみましょう。

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

    /**
     * Run the query as a "select" statement against the connection.
     *
     * @return array
     */
    protected function runSelect()
    {
        return $this->connection->select(
            $this->toSql(), $this->getBindings(), ! $this->useWritePdo
        );
    }

今回のポイントは aggregate が設定されているところだと思います。そこを主点に見てみましょう。

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

    /**
     * Get the SQL representation of the query.
     *
     * @return string
     */
    public function toSql()
    {
        return $this->grammar->compileSelect($this);
    }

Illuminate\Database\Query\Grammars\Grammar::compileSelect() | 関連メソッド

    /**
     * The components that make up a select clause.
     *
     * @var array
     */
    protected $selectComponents = [
        'aggregate',
        'columns',
        'from',
        'joins',
        'wheres',
        'groups',
        'havings',
        'orders',
        'limit',
        'offset',
        'lock',
    ];
    /**
     * Compile a select query into SQL.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @return string
     */
    public function compileSelect(Builder $query)
    {
        if ($query->unions && $query->aggregate) {
            return $this->compileUnionAggregate($query);
        }

        // If the query does not have any columns set, we'll set the columns to the
        // * character to just get all of the columns from the database. Then we
        // can build the query and concatenate all the pieces together as one.
        $original = $query->columns;

        if (is_null($query->columns)) {
            $query->columns = ['*'];
        }

        // To compile the query, we'll spin through each component of the query and
        // see if that component exists. If it does we'll just call the compiler
        // function for the component which is responsible for making the SQL.
        $sql = trim($this->concatenate(
            $this->compileComponents($query))
        );

        if ($query->unions) {
            $sql = $this->wrapUnion($sql).' '.$this->compileUnions($query);
        }

        $query->columns = $original;

        return $sql;
    }
    /**
     * Compile the components necessary for a select clause.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @return array
     */
    protected function compileComponents(Builder $query)
    {
        $sql = [];

        foreach ($this->selectComponents as $component) {
            // To compile the query, we'll spin through each component of the query and
            // see if that component exists. If it does we'll just call the compiler
            // function for the component which is responsible for making the SQL.
            if (isset($query->$component) && ! is_null($query->$component)) {
                $method = 'compile'.ucfirst($component);

                $sql[$component] = $this->$method($query, $query->$component);
            }
        }

        return $sql;
    }

このあたりはだいぶ読みましたのでわりと読めるようになってきていると思います。
compileSelect() メソッドがコールされ、ロジックの手順を踏み compileComponents() メソッドがコールされます。
$this->selectComponents[] にはコンパイル対象のSQL句が格納されていてそれを foreach で回します。その中に今回のポイントとなる aggregate があります。ロジックの中で 「compileAggregate()」 というメソッド名が整形されそれを対象に Builder インスタンスと $query->aggregate が引数として渡されてコールされます。$query->aggregate の内容は以下のようなイメージです。

array(2) {
  ["function"]=>
  string(3) "max"
  ["columns"]=>
  string(5) "batch"
}

compileAggregate() メソッドを見てみましょう。

Illuminate\Database\Query\Grammars\Grammar::compileAggregate()

    /**
     * Compile an aggregated select clause.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @param  array  $aggregate
     * @return string
     */
    protected function compileAggregate(Builder $query, $aggregate)
    {
        $column = $this->columnize($aggregate['columns']);

        // If the query has a "distinct" constraint and we're not asking for all columns
        // we need to prepend "distinct" onto the column name so that the query takes
        // it into account when it performs the aggregating operations on the data.
        if (is_array($query->distinct)) {
            $column = 'distinct '.$this->columnize($query->distinct);
        } elseif ($query->distinct && $column !== '*') {
            $column = 'distinct '.$column;
        }

        return 'select '.$aggregate['function'].'('.$column.') as aggregate';
    }

columnize() してます。これは $this->wrap() したものを implode() するものでした。特に変化はなさそうです。
次に $query->distinct パラメータを見ています。 DISTINCT句を使っているところはありませんのでここも通りません。
戻されるのは以下のようなSQLだと思います。

SELECT MAX(batch) as aggregate FROM migrations

SQL文を戻された compileComponents() メソッドは変数 $sql[] 配列に代入して戻します。
配列を戻された compileSelect() メソッドは implode() して文字列に変換しトリムしたものを変数 $sql に代入したものを戻します。toSql() は受け取った戻り値をそのまま戻します。
runSelect() は受け取った SQL文を使いPDOステートメントを生成しクエリを実行し fetchAll() た結果の配列を戻します。 get() メソッドは受け取った配列を引数として生成した Collection インスタンスを戻します。
aggregate() メソッドは受け取った Collection インスタンスに情報が含まれていた場合、その結果のキーを全て小文字に変換した配列の 「aggregate」 がキーの値を戻します。
max() getLastBatchNumber() と値が戻されて、getNextBatchNumber() メソッドで戻り値に 1 を加算した数が戻されます。null のケースの例外処理が抜けているような気がします。気にせず進みましょう。

Migrator::runPending() メソッドの戻り値が $batch に代入されました。
これは、最終バッチ番号をインクリメントし、次のバッチ番号を求める値ですね。

次にオプションの $pretend$step を整えます。今回は使わなそうですね。
fireMigrationEvent() メソッドで MigrationsStarted インスタンスをディスパッチしてます。
MigrationsStarted は定義がほぼ空っぽのクラスです。イベント登録処理のためのものでしょう。

いよいよマイグレーションファイルの処理の開始ですね。
runPending() メソッドが引数として受け取ったマイグレーションファイルの配列を foreach で回します。
$this->runUp() メソッドをファイルとバッチ番号、pretend オプションを引数として渡しコールします。
ステップオプションが付いている場合は処理ごとにバッチ番号をインクリメントします。

その18に続きます。