いよいよ MigrateCommand::handle() を読むときです。少しばかり長かったですね。
見てみましょう。
Illuminate\Database\Console\Migrations\MigrateCommand::handle()
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
$this->migrator->usingConnection($this->option('database'), function () {
$this->prepareDatabase();
// Next, we will check to see if a path option has been defined. If it has
// we will use the path relative to the root of this installation folder
// so that migrations may be run for any path within the applications.
$this->migrator->setOutput($this->output)
->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->option('seed') && ! $this->option('pretend')) {
$this->call('db:seed', ['--force' => true]);
}
});
return 0;
}
/**
* Resolve the database connection instance.
*
* @return \Illuminate\Database\Connection
*/
public function getConnection()
{
return $this->resolver->connection($this->connection);
}
Illuminate\Database\Console\Migrations\MigrateCommand()
引数はありません。
戻り値は整数型です。
まず、$this->confirmToProceed() メソッドがコールされています。
これは、Illuminate\Console\ConfirmableTrait トレイトに定義されています。
見てみましょう。
Illuminate\Console\ConfirmableTrait::confirmToProceed()
/**
* Confirm before proceeding with the action.
*
* This method only asks for confirmation in production.
*
* @param string $warning
* @param \Closure|bool|null $callback
* @return bool
*/
public function confirmToProceed($warning = 'Application In Production!', $callback = null)
{
$callback = is_null($callback) ? $this->getDefaultConfirmCallback() : $callback;
$shouldConfirm = value($callback);
if ($shouldConfirm) {
if ($this->hasOption('force') && $this->option('force')) {
return true;
}
$this->alert($warning);
$confirmed = $this->confirm('Do you really wish to run this command?');
if (! $confirmed) {
$this->comment('Command Canceled!');
return false;
}
}
return true;
}
/**
* Get the default confirmation callback.
*
* @return \Closure
*/
protected function getDefaultConfirmCallback()
{
return function () {
return $this->getLaravel()->environment() === 'production';
};
}
Illuminate\Console\ConfirmableTrait::confirmToProceed()
第一引数はストリング型で警告メッセージです。
第二引数はコールバック関数若しくはブーリアン若しくは null です。
戻り値はブーリアンです。
引数として渡されたコールバック関数が null だった場合、デフォルトのコールバック関数をセットします。
value() メソッドは Laravel のヘルパ関数です。引数がクロージャならばそれの戻り値、そうでなければそのままを戻します。
今回はコールバックは指定されていないのでデフォルトコールバックが入っています。内容は コマンドアプリケーションコンテナが production であるかの検証がブーリアンで返されるものですね。現在 APP_ENV は production に設定されているので true です。
value($callback) が true だった場合、オプションに force 指定があるか検証します。もし、あった場合は true を戻します。
つまりコマンド実行確認のメソッドですね。
キャンセルされた場合は false 実行承認した場合は true が戻されます。
handle() メソッドの続きに戻ります。
$this->confirmToProceed() でキャンセルされた場合は 整数 1 を戻します。
キャンセルされなかった場合は以下が実行されます。
今回の記事の主題ですね。
Illuminate\Database\Console\Migrations\MigrateCommand::handle() 抜粋
$this->migrator->usingConnection($this->option('database'), function () {
$this->prepareDatabase();
// Next, we will check to see if a path option has been defined. If it has
// we will use the path relative to the root of this installation folder
// so that migrations may be run for any path within the applications.
$this->migrator->setOutput($this->output)
->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->option('seed') && ! $this->option('pretend')) {
$this->call('db:seed', ['--force' => true]);
}
});
$this->migrator は Illuminate\Database\MigrationServiceProvider::registerMigrator() で以下のようにコマンドアプリケーションコンテナにシングルトン結合したものを、 Illuminate\Database\MigrationServiceProvider::registerMigrateCommand() で引数として渡して MigrateCommand インスタンスを生成し、コンストラクタで $migrator に代入したものです。
Illuminate\Database\MigrationServiceProvider::registerMigrator() | registerMigrateCommand()
/**
* Register the migrator service.
*
* @return void
*/
protected function registerMigrator()
{
// The migrator is responsible for actually running and rollback the migration
// files in the application. We'll pass in our database connection resolver
// so the migrator can resolve any of these connections when it needs to.
$this->app->singleton('migrator', function ($app) {
$repository = $app['migration.repository'];
return new Migrator($repository, $app['db'], $app['files'], $app['events']);
});
}
/**
* Register the command.
*
* @return void
*/
protected function registerMigrateCommand()
{
$this->app->singleton('command.migrate', function ($app) {
return new MigrateCommand($app['migrator']);
});
}
Illuminate\Database\Console\Migrations\MigrateCommand::__construct() | registerMigrateCommand()
/**
* Create a new migration command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
Illuminate\Database\Migrations\Migrator を見てみましょう。
Illuminate\Database\Migrations\Migrator::__construct()
/**
* Create a new migrator instance.
*
* @param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param \Illuminate\Filesystem\Filesystem $files
* @param \Illuminate\Contracts\Events\Dispatcher|null $dispatcher
* @return void
*/
public function __construct(MigrationRepositoryInterface $repository,
Resolver $resolver,
Filesystem $files,
Dispatcher $dispatcher = null)
{
$this->files = $files;
$this->events = $dispatcher;
$this->resolver = $resolver;
$this->repository = $repository;
}
コンストラクタで引数として渡されたものを変数に代入していますね。リポジトリ、データベース、ファイルシステム、イベント、一通りあります。
usingConnection() を見てみます。
Illuminate\Database\Migrations\Migrator::usingConnection()
/**
* Execute the given callback using the given connection as the default connection.
*
* @param string $name
* @param callable $callback
* @return mixed
*/
public function usingConnection($name, callable $callback)
{
$previousConnection = $this->resolver->getDefaultConnection();
$this->setConnection($name);
return tap($callback(), function () use ($previousConnection) {
$this->setConnection($previousConnection);
});
}
Illuminate\Database\Migrations\Migrator::usingConnection()
第一引数はストリングでデータベース名です。
第二引数はコールバック関数です。
戻り値は です。
$this->resolver には $app['db'] が代入されています。エイリアス設定されていて、中身は Illuminate\Database\DatabaseManager でした。DatabaseManager::getDefaultConnection() の内容は以下です。
/**
* Get the default connection name.
*
* @return string
*/
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}
つまり、 PROJECT_ROOT/config/database.app の 「default」キーの内容です。デフォルトでは「mysql」ですね。次に
setConnection() に $name を渡しています。$name は MigrateCommand() から $this->option('database') が渡されています。option() は、Illuminate\Console\Concerns\InteractsWithIO トレイトで以下のように定義されています。
/**
* Get the value of a command option.
*
* @param string|null $key
* @return string|array|bool|null
*/
public function option($key = null)
{
if (is_null($key)) {
return $this->input->getOptions();
}
return $this->input->getOption($key);
}
$this->input は Illuminate\Console\Command::run() メソッドがコールされた時に渡された Symfony\Component\Console\Input\InputInterface が入っています。 getOption() に 「database」 が引数に渡しています。このオプションを設定した箇所は見当たりませんでしたのでデフォルト値 null が入っていると思われます。setConnection() を引数に null が渡されコールされます。見てみましょう。
Illuminate\Database\Migrations\Migrator::setConnection()
/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setConnection($name)
{
if (! is_null($name)) {
$this->resolver->setDefaultConnection($name);
}
$this->repository->setSource($name);
$this->connection = $name;
}
Illuminate\Database\Migrations\Migrator::setConnection()
第一引数はストリングで接続名です。
戻り値はありません。
$name が null でなければ $this->resolver->setDefaultConnection() メソッドをコールしています。
resolver は DatabaseManager インスタンスでした。 setDefaultConnection()は以下です。
/**
* Set the default connection name.
*
* @param string $name
* @return void
*/
public function setDefaultConnection($name)
{
$this->app['config']['database.default'] = $name;
}
$this->app['config']['database.default'] を引数で上書きしてます。今回は
null ですのでここは通りません。
$this->repository->setSource()
$this->repository は Illuminate\Database\Migrations\DatabaseMigrationRepository インスタンスです。setSource() は引数を変数に代入しているだけです。
/**
* Set the information source to gather data.
*
* @param string $name
* @return void
*/
public function setSource($name)
{
$this->connection = $name;
}
$this->connection に $name を代入しています。setConnection() メソッドは以上です。
usingConnection() の続きです。
return tap($callback(), function () use ($previousConnection) {
$this->setConnection($previousConnection);
});
tap() メソッドをコールしています。これは Laravelのヘルパ関数です。
/**
* Call the given Closure with the given value then return the value.
*
* @param mixed $value
* @param callable|null $callback
* @return mixed
*/
function tap($value, $callback = null)
{
if (is_null($callback)) {
return new HigherOrderTapProxy($value);
}
$callback($value);
return $value;
}
第二引数として渡されたコールバック関数に第一引数を渡してコールしています。戻り値は第一引数です。今回はコールパック関数として
setConnection() するクロージャーが入っています。クロージャーの引数として渡されるのは usingConnection() の第二引数として渡されたコールバック関数です。これをコールした戻り値を setConnection() する流れですね。
では Migrations\MigrateCommand() の続きに戻りましょう。
usingConnection() の第二引数にクロージャーが渡されています。見てみましょう。
Illuminate\Database\Console\Migrations\MigrateCommand::handle() 抜粋
function () {
$this->prepareDatabase();
// Next, we will check to see if a path option has been defined. If it has
// we will use the path relative to the root of this installation folder
// so that migrations may be run for any path within the applications.
$this->migrator->setOutput($this->output)
->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->option('seed') && ! $this->option('pretend')) {
$this->call('db:seed', ['--force' => true]);
}
}
Illuminate\Database\Console\Migrations\MigrateCommand::prepareDatabase()
/**
* Prepare the migration database for running.
*
* @return void
*/
protected function prepareDatabase()
{
if (! $this->migrator->repositoryExists()) {
$this->call('migrate:install', array_filter([
'--database' => $this->option('database'),
]));
}
}
Illuminate\Database\Console\Migrations\Migrator::repositoryExists()
/**
* Determine if the migration repository exists.
*
* @return bool
*/
public function repositoryExists()
{
return $this->repository->repositoryExists();
}
Illuminate\Database\Console\Migrations\DatabaseMigrationRepository::repositoryExists()
/**
* Determine if the migration repository exists.
*
* @return bool
*/
public function repositoryExists()
{
$schema = $this->getConnection()->getSchemaBuilder();
return $schema->hasTable($this->table);
}
Illuminate\Database\Console\Migrations\DatabaseMigrationRepository::getConnections()
/**
* Resolve the database connection instance.
*
* @return \Illuminate\Database\Connection
*/
public function getConnection()
{
return $this->resolver->connection($this->connection);
}
Illuminate\Database\DatabaseManager::connection()
/**
* Get a database connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function connection($name = null)
{
[$database, $type] = $this->parseConnectionName($name);
$name = $name ?: $database;
// If we haven't created this connection, we'll create it based on the config
// provided in the application. Once we've created the connections we will
// set the "fetch mode" for PDO which determines the query return types.
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->configure(
$this->makeConnection($database), $type
);
}
return $this->connections[$name];
}
Illuminate\Database\MySqlConnection::getSchemaBuilder()
/**
* Get a schema builder instance for the connection.
*
* @return \Illuminate\Database\Schema\MySqlBuilder
*/
public function getSchemaBuilder()
{
if (is_null($this->schemaGrammar)) {
$this->useDefaultSchemaGrammar();
}
return new MySqlBuilder($this);
}
Illuminate\Database\Schema\Builder::getSchemaBuilder()
/**
* Create a new database Schema manager.
*
* @param \Illuminate\Database\Connection $connection
* @return void
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->grammar = $connection->getSchemaGrammar();
}
$this->prepareDatabase() メソッドがコールされています。
$this->prepareDatabase() メソッドでは $this->migrator->repositoryExists() の戻り値を判定しています。
$this->migrator->repositoryExists() メソッドは $this->repository->repositoryExists() の戻り値を戻しています。$this->repository->repositoryExists() は $this->getConnection()->getSchemaBuilder() の戻り値を変数 $schema に代入しています。
getConnection() は resolver つまり DatabaseManager の connection() に接続名を渡し Connection インスタンスを受け取ります。
今回はデータベースが MySQL ですので、MySqlConnection::getSchemaBuilder() をコールします。
getSchemaBuilder() は $this->schemaGrammar が null ならば、useDefaultSchemaGrammar() メソッドでスキーマグラマーをデフォルトにセットします。
SchemaBuilder インスタンスを自身を引数に渡し生成したものを返します。
SchemaBuilder インスタンスは生成時にコンストラクタで接続とグラマーを変数に代入ます。
随分とたらい回しをしました。スキーマが生成されたのを DatabaseMigrationRepository::repositoryExists() が変数に代入し、hasTable() メソッドを $this->table を引数に渡しコールします。
$this->table とは何でしょうか。この$this は DatabaseMigrationRepository インスタンスです。生成されたタイミングは Illuminate\Database\MigrationServiceProvider::registerRepository() です。見てみましょう。
Illuminate\Database\MigrationServiceProvider::registerRepository()
/**
* Register the migration repository service.
*
* @return void
*/
protected function registerRepository()
{
$this->app->singleton('migration.repository', function ($app) {
$table = $app['config']['database.migrations'];
return new DatabaseMigrationRepository($app['db'], $table);
});
}
Illuminate\Database\Migrations\DatabaseMigrationRepository::__construct()
/**
* Create a new database migration repository instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param string $table
* @return void
*/
public function __construct(Resolver $resolver, $table)
{
$this->table = $table;
$this->resolver = $resolver;
}
DatabaseMigrationRepository 生成時にコンストラクタに第二引数として $app['config']['database.migrations'] を渡しています。$app['config']['database.migrations'] は PROJECT_ROOT/config/database.app に記述されている配列のキー「migrations」の値ですつまり、
$this->table はデフォルトでは「migrations」 です。
次は以下が実行されます。
return $schema->hasTable($this->table);
Builder インスタンスの hasTable() がコールされています。
Illuminate\Database\Schema\MySqlBuilder::hasTable()
/**
* Determine if the given table exists.
*
* @param string $table
* @return bool
*/
public function hasTable($table)
{
$table = $this->connection->getTablePrefix().$table;
return count($this->connection->select(
$this->grammar->compileTableExists(), [$this->connection->getDatabaseName(), $table]
)) > 0;
}
Illuminate\Database\Schema\MySqlBuilder::hasTable()
第一引数はストリングでテーブル名です。
戻り値はブーリアンです。
テーブルプレフィックスを取得して対象テーブル名に連結し $table に代入します。
テーブルプレフィックスはおそらくこれを切り替えることで環境を簡単に変更できるというものでしょう。
select() メソッドをコールしています。引数を見てみましょう。
$this->grammar->compileTableExists()
$this は ConnectionFactory::createConnection() で $driver を switch で検証して生成された MySqlConnection でした。
ということで、 MySqlConnection::compileTableExists() を見てみましょう。
Illuminate\Database\MySqlConnection::getDefaultSchemaGrammar() | 関連事項
use Illuminate\Database\Schema\Grammars\MySqlGrammar as SchemaGrammar;
class MySqlConnection extends Connection
{
/**
* Get the default schema grammar instance.
*
* @return \Illuminate\Database\Schema\Grammars\MySqlGrammar
*/
protected function getDefaultSchemaGrammar()
{
return $this->withTablePrefix(new SchemaGrammar);
}
Illuminate\Database\Connection::withTablePrefix()
/**
* Set the table prefix and return the grammar.
*
* @param \Illuminate\Database\Grammar $grammar
* @return \Illuminate\Database\Grammar
*/
public function withTablePrefix(Grammar $grammar)
{
$grammar->setTablePrefix($this->tablePrefix);
return $grammar;
}
Illuminate\Database\Grammarb::setTablePrefix()
/**
* Set the grammar's table prefix.
*
* @param string $prefix
* @return $this
*/
public function setTablePrefix($prefix)
{
$this->tablePrefix = $prefix;
return $this;
}
Illuminate\Database\Schema\Grammars\MySqlGrammar::compileTableExists()
/**
* Compile the query to determine the list of tables.
*
* @return string
*/
public function compileTableExists()
{
return "select * from information_schema.tables where table_schema = ? and table_name = ? and table_type = 'BASE TABLE'";
}
MySqlConnection クラスは名前空間のエイリアス設定で SchemaGrammar を Illuminate\Database\Schema\Grammars\MySqlGrammar としています。
getDefaultSchemaGrammar() メソッドで MySqlGrammar を生成し、setTablePrefix() でテーブルプレフィックスを設定しています。
先程取得しようとしていたクエリは、 MySqlGrammar::compileTableExists() の戻り値となります。
以下がそのクエリです。
select * from information_schema.tables where table_schema = ? and table_name = ? and table_type = 'BASE TABLE'
やっとSQLが見れましたね。
このSQLを第一引数、データベース名、テーブル名を配列にしたものを第二引数として渡して select() メソッドとコールします。
Illuminate\Database\Connection::select()
/**
* Run a select statement against the database.
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return array
*/
public function select($query, $bindings = [], $useReadPdo = true)
{
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
if ($this->pretending()) {
return [];
}
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->prepared($this->getPdoForSelect($useReadPdo)
->prepare($query));
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();
return $statement->fetchAll();
});
}
Illuminate\Database\Connection::selectFromWriteConnection()
第一引数はストリング型でクエリです。
第二引数は配列型でバインド対象です。
戻り値は配列です。
run() メソッドをクエリとバインド配列とクロージャーを渡してコールしています。
クロージャーを読んでみましょう。
引数にクエリとバインド配列を受け取ります。
pretending() メソッドをコールしています。これは単に $this->pretending を見ているだけです。このパラメーターはドライランの時に true にセットすることでクエリ実行をブロックする仕様のようです。
pretending() メソッドの戻り値が true だった場合、空の配列を戻します。
続きを読みます。
$statement = $this->prepared($this->getPdoForSelect($useReadPdo)
->prepare($query));
prepared() してますね。中から見ましょう。getPdoForSelect() メソッドをコールしています。
Illuminate\Database\getPdoForSelect | 関連メソッド
/**
* Get the PDO connection to use for a select query.
*
* @param bool $useReadPdo
* @return \PDO
*/
protected function getPdoForSelect($useReadPdo = true)
{
return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
}
/**
* Get the current PDO connection.
*
* @return \PDO
*/
public function getPdo()
{
if ($this->pdo instanceof Closure) {
return $this->pdo = call_user_func($this->pdo);
}
return $this->pdo;
}
/**
* Get the current PDO connection used for reading.
*
* @return \PDO
*/
public function getReadPdo()
{
if ($this->transactions > 0) {
return $this->getPdo();
}
if ($this->recordsModified && $this->getConfig('sticky')) {
return $this->getPdo();
}
if ($this->readPdo instanceof Closure) {
return $this->readPdo = call_user_func($this->readPdo);
}
return $this->readPdo ?: $this->getPdo();
}
getPdoForSelect() メソッドが受け取る引数は読み取り専用接続を使うかどうかのブーリアンのようです。以前データベース接続のコードを読んだ際に、通常の接続 $this->pdo と 読み取り専用接続 $this->readPdo に分けるロジックがありましたね。それの切り替えを行っています。検証した適切な PDO インスタンスが戻されます。
戻された PDO インスタンスに prepare() メソッドがコールされ PDOStatement が戻されます。
戻された PDOStatement を引数にして prepared() メソッドをコールします。
Illuminate\Database\Connection::prepared()
/**
* Configure the PDO prepared statement.
*
* @param \PDOStatement $statement
* @return \PDOStatement
*/
protected function prepared(PDOStatement $statement)
{
$statement->setFetchMode($this->fetchMode);
$this->event(new StatementPrepared(
$this, $statement
));
return $statement;
}
Illuminate\Database\Events\StatementPrepared 抜粋
namespace Illuminate\Database\Events;
class StatementPrepared
{
public $connection;
public $statement;
public function __construct($connection, $statement)
{
$this->statement = $statement;
$this->connection = $connection;
}
}
Illuminate\Database\Connection::prepared()
第一引数は PDOStatement です。
戻り値は PDOStatement です。
setFetchMode() でフェッチモードをセットします。
StatementPrepared インスタンスを Connection と PDOStatement を引数に渡し生成します。 StatementPrepared は Connection と PDOStatement を記憶しておくだけのシンプルなクラスです。
生成した StatementPrepared をイベントにディスパッチします。
PDOStatement を戻します。
select() の続きです。
$this->bindValues($statement, $this->prepareBindings($bindings));
prepareBindings() を見てみましょう。
Illuminate\Database\Connection::prepareBindings()
/**
* Prepare the query bindings for execution.
*
* @param array $bindings
* @return array
*/
public function prepareBindings(array $bindings)
{
$grammar = $this->getQueryGrammar();
foreach ($bindings as $key => $value) {
// We need to transform all instances of DateTimeInterface into the actual
// date string. Each query grammar maintains its own date string format
// so we'll just ask the grammar for the format to get from the date.
if ($value instanceof DateTimeInterface) {
$bindings[$key] = $value->format($grammar->getDateFormat());
} elseif (is_bool($value)) {
$bindings[$key] = (int) $value;
}
}
return $bindings;
}
Illuminate\Database\Connection::prepareBindings()
第一引数は配列でバインドする値です。
戻り値は配列です。
バインドする値が DateTimeInterface インスタンスである場合、データベースの種類に合わせたフォーマットに変更します。
バインドする値がブーリアンの場合は整数型に変換します。
bindValues() を見てみましょう。
Illuminate\Database\Connection::bindValues()
/**
* Bind values to their parameters in the given statement.
*
* @param \PDOStatement $statement
* @param array $bindings
* @return void
*/
public function bindValues($statement, $bindings)
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1, $value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
);
}
}
Illuminate\Database\Connection::bindValues()
第一引数は PDOStatement インスタンスです。
第二引数は配列でバインドする値です。
戻り値はありません。
PDOStatement とバインドする配列を受け取り、 foreach でバインドする配列を回してバインド処理していきます。ストリング型と整数型で処理を変えていますね。
PDOStatement をもう一度見てみましょう。
select * from information_schema.tables where table_schema = ? and table_name = ? and table_type = 'BASE TABLE'
このクエリに配列 [データベース名, テーブル名] がバインドされます。完成形のSQLが見れましたね。
slect() の続きです。
$statement->execute(); return $statement->fetchAll();
execut() が実行され fetchAll() が戻されました。とうとうクエリが叩かれましたね。
hasTable に戻ります。
return count($this->connection->select(
$this->grammar->compileTableExists(), [$this->connection->getDatabaseName(), $table]
)) > 0;
select() の戻り値が 0 より大きいかを検証した結果を戻しています。migrations テーブルの存在確認ですね。prepareDatabase() メソッドに戻ります。
protected function prepareDatabase()
{
if (! $this->migrator->repositoryExists()) {
$this->call('migrate:install', array_filter([
'--database' => $this->option('database'),
]));
}
}
「’migarate’」 テーブルが存在しなかった場合、call() メソッドがコールされます。
Illuminate\Console\Concerns\CallsCommands::call()
/**
* Call another console command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @param array $arguments
* @return int
*/
public function call($command, array $arguments = [])
{
return $this->runCommand($command, $arguments, $this->output);
}
Illuminate\Console\Concerns\CallsCommands::runCommand()
/**
* Run the given the console command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @param array $arguments
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
protected function runCommand($command, array $arguments, OutputInterface $output)
{
$arguments['command'] = $command;
return $this->resolveCommand($command)->run(
$this->createInputFromArguments($arguments), $output
);
}
Illuminate\Console\Concerns\CallsCommands::call()
第一引数は Command インスタンス若しくはストリング型で コマンドです。
第二引数は配列で引数です。
戻り値は です。
call() メソッドに受け取ったコマンドとオプションにデータベースキーの情報があればそれを、なければ空の配列を引数に渡します。
call() メソッドは受け取った引数に出力を足して runCommand() メソッドに渡します。
runCommand() メソッドは resolveCommand() メソッドを引数にコマンドを渡しコールし、その戻り値のrun() メソッドをコールします。
resolveCommand() メソッドを見てみましょう。
Illuminate\Console\Concerns\CallsCommands:: resolveCommand()
/**
* Resolve the console command instance for the given command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @return \Symfony\Component\Console\Command\Command
*/
protected function resolveCommand($command)
{
if (! class_exists($command)) {
return $this->getApplication()->find($command);
}
$command = $this->laravel->make($command);
if ($command instanceof SymfonyCommand) {
$command->setApplication($this->getApplication());
}
if ($command instanceof self) {
$command->setLaravel($this->getLaravel());
}
return $command;
}
Symfony\Component\Console\Command::getApplication()
/**
* Gets the application instance for this command.
*
* @return Application|null An Application instance
*/
public function getApplication()
{
return $this->application;
}
resolveCommand() は受け取った引数を class_exists() で判定してクラス定義されていなければ getApplication()->find() を戻します。
getApplication() はアプリケーションインスタンスを戻します。返されたインスタンスの find() メソッドをコールします。以前、find() メソッドは読みましたね。渡されるコマンドは 「migrate:install」です。find() メソッドは、Illuminate\Database\MigrationServiceProvider::$commands から対象のコマンドがあるか検索します。
Illuminate\Database\MigrationServiceProvider::$commands | 関連メソッド
protected $commands = [
'Migrate' => 'command.migrate',
'MigrateFresh' => 'command.migrate.fresh',
'MigrateInstall' => 'command.migrate.install',
'MigrateRefresh' => 'command.migrate.refresh',
'MigrateReset' => 'command.migrate.reset',
'MigrateRollback' => 'command.migrate.rollback',
'MigrateStatus' => 'command.migrate.status',
'MigrateMake' => 'command.migrate.make',
];
/**
* Register the migration repository service.
*
* @return void
*/
protected function registerRepository()
{
$this->app->singleton('migration.repository', function ($app) {
$table = $app['config']['database.migrations'];
return new DatabaseMigrationRepository($app['db'], $table);
});
}
/**
* Register the command.
*
* @return void
*/
protected function registerMigrateInstallCommand()
{
$this->app->singleton('command.migrate.install', function ($app) {
return new InstallCommand($app['migration.repository']);
});
}
ありましたね。InstallCommand インスタンスが返ってくるようです。
InstallCommand::run() は先程読みました。ぐるぐるまわって自身の execute() メソッドがコールされ、コマンドアプリケーションから自身の handle() をコールする手続きでした。
InstallCommand::handle() を見てみましょう。
Illuminate\Database\Console\Migrations\InstallCommand::handle() | 関連メソッド
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->repository->setSource($this->input->getOption('database'));
$this->repository->createRepository();
$this->info('Migration table created successfully.');
}
/**
* Create a new migration install command instance.
*
* @param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository
* @return void
*/
public function __construct(MigrationRepositoryInterface $repository)
{
parent::__construct();
$this->repository = $repository;
}
MigrationServiceProvider::registerMigrateInstallCommand() でコマンドをアプリケーションコンテナに結合する際に、 InstallCommand インスタンスに引数 $app['migration.repository'] が渡されていました。
InstallCommand クラスではコンストラクタで $repository に引数を入れています。
つまり、handle() メソッド内の $this->repository は Illuminate\Database\Migrations\DatabaseMigrationRepository インスタンスです。見てみましょう。
Illuminate\Database\Migrations\DatabaseMigrationRepository::setSource() | createRepository ()
/**
* Set the information source to gather data.
*
* @param string $name
* @return void
*/
public function setSource($name)
{
$this->connection = $name;
}
/**
* Create the migration repository data store.
*
* @return void
*/
public function createRepository()
{
$schema = $this->getConnection()->getSchemaBuilder();
$schema->create($this->table, function ($table) {
// The migrations table is responsible for keeping track of which of the
// migrations have actually run for the application. We'll create the
// table to hold the migration file's path as well as the batch ID.
$table->increments('id');
$table->string('migration');
$table->integer('batch');
});
}
/**
* Create a new database migration repository instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param string $table
* @return void
*/
public function __construct(Resolver $resolver, $table)
{
$this->table = $table;
$this->resolver = $resolver;
}
/**
* Resolve the database connection instance.
*
* @return \Illuminate\Database\Connection
*/
public function getConnection()
{
return $this->resolver->connection($this->connection);
}
setSource() に $this->input->getOption('database') を引数として渡しています。接続名ですね。
次に repository->createRepository() をコールしています。
createRepository() では、スキーマビルダーに create() メソッドをコールしています。
引数は $this->table とクロージャーです。
$this->table は MigrationServiceProvider::registerRepository() で DatabaseMigrationRepository を生成する際に第二引数として $app['config']['database.migrations'] を渡しています。DatabaseMigrationRepository のコンストラクタで渡された引数を $this->table に代入しています。つまり、デフォルトでは 「migrations」 です。
同様に this->resolver は $app['db'] つまり、DatabaseManager ですね。
DatabaseManager::connection() メソッドに接続名を渡し接続を受け取ります。受け取った接続の getSchemaBuilder() をコールします。接続は MySqlConnection ですから戻ってくるのは MySqlBuilder ですね。だいぶ読めるようになってきましたね。
では、Builder::create() を見てみましょう。
Illuminate\Database\Schema\Builder::create() | 関連メソッド
/**
* 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);
}));
}
/**
* Execute the blueprint to build / modify the table.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @return void
*/
protected function build(Blueprint $blueprint)
{
$blueprint->build($this->connection, $this->grammar);
}
/**
* 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);
}
Illuminate\Database\Schema\Builder::create()
第一引数はストリング型でテーブル名です。
第二引数はクロージャーでコールバックです
戻り値はありません。
tap() メソッドに createBlueprint() メソッドをコールしたものとクロージャーを渡した戻り値を build() メソッドに渡しています。
Illuminate\Database\Schema\Builder::create()
第一引数はストリング型でテーブル名です。
第二引数はクロージャーでコールバックです
戻り値は Blueprint インスタンスです。
接続設定の prefix_indexes が true なら 接続設定の prefix を false なら空文字を $prefix に代入します。
接続設定は PROJECT_ROOT/config/database.php の内容です。
$this->resolver が null でなければ、call_user_func() にコールバックとして $this->resolver にテーブル名、コールバック関数、プレフィックスを渡しコールし戻ってきたものを戻します。
セットされていなければ、Blueprint インスタンスをテーブル名、コールバック関数、プレフィックスを渡して生成したものを返します。
Builder::resolver の更新インターフェースは Builder::blueprintResolver() メソッドのみですので今回は null ですね。
Blueprint インスタンスが生成されます。Blueprint クラスのコンストラクタを見てみましょう。
Illuminate\Database\Schema\Blueprint::__construct()
/**
* Create a new schema blueprint.
*
* @param string $table
* @param \Closure|null $callback
* @param string $prefix
* @return void
*/
public function __construct($table, Closure $callback = null, $prefix = '')
{
$this->table = $table;
$this->prefix = $prefix;
if (! is_null($callback)) {
$callback($this);
}
}
Illuminate\Database\Schema\Blueprint::__construct()
第一引数はストリング型でテーブル名です。
第二引数はクロージャーか null です。
第三引数はストリングでプレフィックスです。
戻り値はありません。
引数として受け取ったテーブル名とプレフィックスを変数に代入します。引数にクロージャーが渡されていた場合は自身を引数にコールします。
今回はクロージャーは渡されていません。
ここで生成されたクロージャーが Builder::create() メソッドの tap() ヘルパ関数に渡されます。第二引数のクロージャーを第一引数を引数として渡してコールするんでしたね。
$this->build(tap($this->createBlueprint($table), function ($blueprint) use ($callback) {
$blueprint->create();
$callback($blueprint);
}));
このクロージャーに渡されている $callback は DatabaseMigrationRepository::createRepository() で渡されている以下です。
$schema->create($this->table, function ($table) {
// The migrations table is responsible for keeping track of which of the
// migrations have actually run for the application. We'll create the
// table to hold the migration file's path as well as the batch ID.
$table->increments('id');
$table->string('migration');
$table->integer('batch');
});
まず、渡された 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');
}
/**
* Add a new command to the blueprint.
*
* @param string $name
* @param array $parameters
* @return \Illuminate\Support\Fluent
*/
protected function addCommand($name, array $parameters = [])
{
$this->commands[] = $command = $this->createCommand($name, $parameters);
return $command;
}
/**
* Create a new Fluent command.
*
* @param string $name
* @param array $parameters
* @return \Illuminate\Support\Fluent
*/
protected function createCommand($name, array $parameters = [])
{
return new Fluent(array_merge(compact('name'), $parameters));
}
addCommand() メソッドに 「create」という文字列を引数として渡しています。addCommand() メソッドは $this->commands[] に createCommand() メソッドの戻り値を代入しそれを戻しています。createCommand() は引数にコマンド名と引数配列を受け取り、compact() で「name 」というキーにコマンド名「create 」を代入した配列と引数配列を array_merge() したものを引数として Fluent インスタンスを生成して戻しています。
Illuminate\Support\Fluent::__construct | 関連メソッド
class Fluent implements Arrayable, ArrayAccess, Jsonable, JsonSerializable
{
/**
* All of the attributes set on the fluent instance.
*
* @var array
*/
protected $attributes = [];
/**
* Create a new fluent instance.
*
* @param array|object $attributes
* @return void
*/
public function __construct($attributes = [])
{
foreach ($attributes as $key => $value) {
$this->attributes[$key] = $value;
}
}
/**
* Get an attribute from the fluent instance.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null)
{
if (array_key_exists($key, $this->attributes)) {
return $this->attributes[$key];
}
return value($default);
}
Illuminate\Support\Fluent::__construct
第一引数は配列で格納したいパラメーターです。
戻り値はありません。
Fluent クラスはコードをざっと見た感じ、コンストラクタや _set() で設定した値を以下の形で取得できるクラスのようです。
$fluent = new Fluent(['key' => 'value']); var_dump($fluent->key); // string(5) "value"
ArrayAccess も可能で格納する情報は JSON 型でもいいみたいですね。
続きを見ましょう。
$this->table にテーブル名「migrations 」を代入し、$this->commands[] に「'name' => 'create' 」というコマンドが登録された blueprint インスタンスを引数にコールバックをコールします。
コールバックの内容を追いましょう。
Illuminate\Database\Schema\Blueprint::increments() | 関連メソッド
/**
* Create a new auto-incrementing integer (4-byte) column on the table.
*
* @param string $column
* @return \Illuminate\Database\Schema\ColumnDefinition
*/
public function increments($column)
{
return $this->unsignedInteger($column, true);
}
/**
* Create a new unsigned integer (4-byte) column on the table.
*
* @param string $column
* @param bool $autoIncrement
* @return \Illuminate\Database\Schema\ColumnDefinition
*/
public function unsignedInteger($column, $autoIncrement = false)
{
return $this->integer($column, $autoIncrement, true);
}
/**
* Create a new integer (4-byte) column on the table.
*
* @param string $column
* @param bool $autoIncrement
* @param bool $unsigned
* @return \Illuminate\Database\Schema\ColumnDefinition
*/
public function integer($column, $autoIncrement = false, $unsigned = false)
{
return $this->addColumn('integer', $column, compact('autoIncrement', 'unsigned'));
}
/**
* Add a new column to the blueprint.
*
* @param string $type
* @param string $name
* @param array $parameters
* @return \Illuminate\Database\Schema\ColumnDefinition
*/
public function addColumn($type, $name, array $parameters = [])
{
$this->columns[] = $column = new ColumnDefinition(
array_merge(compact('type', 'name'), $parameters)
);
return $column;
}
increments() > unsignedInteger() > integer() とカラム名に加えてパラメーターをつなぎ合わせて addColumn() メソッドをコールしています。
addColumn() メソッドは受け取った引数を渡して ColumnDefinition インスタンスを生成しています。
ColumnDefinition インスタンスを生成する時に渡される引数は以下のようなイメージです。
array(4) {
["type"]=> string(7) "integer"
["name"]=> string(2) "id"
["autoIncrement"]=> bool(true)
["unsigned"]=> bool(true)
}
ColumnDefinition は先程の Fluent クラスを継承するクラスです。テーブル情報を管理するためのインスタンスでしょう。生成された ColumnDefinition インスタンスを columns[] に代入し、そのまま戻しています。
string() メソッド、integer() メソッド共に同様の流れです。
tap() ヘルパ関数の戻り値はクロージャーに引数で渡した値でした。つまり、いろいろな手続きを踏んだ $blueprint が戻ってきます。これを引数に build() する流れとなります。
その14に続く。