Connection::statement() メソッドを見てみましょう。
Illuminate\Database\Connection::statement()
/**
* Execute an SQL statement and return the boolean result.
*
* @param string $query
* @param array $bindings
* @return bool
*/
public function statement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return true;
}
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$this->recordsHaveBeenModified();
return $statement->execute();
});
}
Illuminate\Database\Connection::statement()
第一引数はストリング型でクエリ文です。
第二引数は配列でバインドする値です
戻り値はブーリアンです。
受け取った引数にクロージャーを加えて run() メソッドをコールしています。
見てみましょう。
Illuminate\Database\Connection::run()
/**
* Run a SQL statement and log its execution context.
*
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws \Illuminate\Database\QueryException
*/
protected function run($query, $bindings, Closure $callback)
{
$this->reconnectIfMissingConnection();
$start = microtime(true);
// Here we will run this query. If an exception occurs we'll determine if it was
// caused by a connection that has been lost. If that is the cause, we'll try
// to re-establish connection and re-run the query with a fresh connection.
try {
$result = $this->runQueryCallback($query, $bindings, $callback);
} catch (QueryException $e) {
$result = $this->handleQueryException(
$e, $query, $bindings, $callback
);
}
// Once we have run the query we will calculate the time that it took to run and
// then log the query, bindings, and execution time so we will report them on
// the event that the developer needs them. We'll log time in milliseconds.
$this->logQuery(
$query, $bindings, $this->getElapsedTime($start)
);
return $result;
}
Illuminate\Database\Connection::statement()
第一引数はストリング型でクエリ文です。
第二引数は配列でバインドする値です
第三引数はクロージャーです。
戻り値は mix です。
reconnectIfMissingConnection() メソッドは接続が切れていた場合再接続を試みるものでしたね。
次に $start にクエリ実行前のマイクロタイムを代入しています。
そして runQueryCallback() メソッドを TRY して $result に代入しています。
渡されたクロージャーを見てみましょう。
function ($query, $bindings) {
if ($this->pretending()) {
return true;
}
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$this->recordsHaveBeenModified();
return $statement->execute();
}
Illuminate\Database\Connection::runQueryCallback()
/**
* Run a SQL statement.
*
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws \Illuminate\Database\QueryException
*/
protected function runQueryCallback($query, $bindings, Closure $callback)
{
// To execute the statement, we'll simply call the callback, which will actually
// run the SQL against the PDO connection. Then we can calculate the time it
// took to execute and log the query SQL, bindings and time in our memory.
try {
$result = $callback($query, $bindings);
}
// If an exception occurs when attempting to run a query, we'll format the error
// message to include the bindings with SQL, which will make this exception a
// lot more helpful to the developer instead of just the database's errors.
catch (Exception $e) {
throw new QueryException(
$query, $this->prepareBindings($bindings), $e
);
}
return $result;
}
/**
* Determine if the connection is in a "dry run".
*
* @return bool
*/
public function pretending()
{
return $this->pretending === true;
}
/**
* Indicate if any records have been modified.
*
* @param bool $value
* @return void
*/
public function recordsHaveBeenModified($value = true)
{
if (! $this->recordsModified) {
$this->recordsModified = $value;
}
}
Illuminate\Database\Connection::runQueryCallback()
第一引数はストリング型でクエリ文です。
第二引数は配列でバインドする値です
第三引数はクロージャーです。
戻り値は mix です。
渡されたクロージャーをクエリ文とバインドを渡し TRY しています。
クロージャーは pretending() メソッドをコールしています。戻り値が true ならば true を戻しています。
pretending() は $this->pretending が true かどうかをブーリアンで返しています。 $this->pretending は初期値は false で pretend() メソッドで更新します。
今回は初期値 false なので次に行きます。
getPdo() メソッドをコールした戻り値の prepare() メソッドを引数にクエリ文を渡してコールしています。
getPdo() メソッドは PDO インスタンスを戻します。MySQL用のステートメントオブジェクトが生成され $statement に代入されます。 bindValues() は以前読みましたね。今回はバインド用の配列は空ですので処理はされません。
recordsHaveBeenModified() メソッドをコールして recordsModified に true を代入します。
準備された $statement インスタンスの execute() メソッドをコールして SQL を実行し結果を戻します。
run() メソッドに戻ります。
logQuery() をクエリ文とバインド内容と getElapsedTime() にクエリ返し時間を引数に渡しコールした戻り値を引数に渡しコールします。
getElapsedTime() からみてみましょう。
Illuminate\Database\Connection::getElapsedTime()
/**
* Get the elapsed time since a given starting point.
*
* @param int $start
* @return float
*/
protected function getElapsedTime($start)
{
return round((microtime(true) - $start) * 1000, 2);
}
現在時間からクエリ開始時間を引いて整形してます。クエリ実行にかかった時間を割り出してます。
次は logQuery() を見てみましょう。
Illuminate\Database\Connection::logQuery()
/**
* Log a query in the connection's query log.
*
* @param string $query
* @param array $bindings
* @param float|null $time
* @return void
*/
public function logQuery($query, $bindings, $time = null)
{
$this->event(new QueryExecuted($query, $bindings, $time, $this));
if ($this->loggingQueries) {
$this->queryLog[] = compact('query', 'bindings', 'time');
}
}
event() メソッドを QueryExecuted インスタンスを生成したものを引数として渡しコールしています。
QueryExecuted インスタンスは、SQL、実行時間、バインドした値、接続、接続名を記録するためのクラスのようです。
Illuminate\Database\Events\QueryExecuted::__construct()
/**
* Create a new event instance.
*
* @param string $sql
* @param array $bindings
* @param float|null $time
* @param \Illuminate\Database\Connection $connection
* @return void
*/
public function __construct($sql, $bindings, $time, $connection)
{
$this->sql = $sql;
$this->time = $time;
$this->bindings = $bindings;
$this->connection = $connection;
$this->connectionName = $connection->getName();
}
event() メソッドを見てみましょう。
Illuminate\Database\Connection::event()
/**
* Fire the given event if possible.
*
* @param mixed $event
* @return void
*/
protected function event($event)
{
if (isset($this->events)) {
$this->events->dispatch($event);
}
}
$this->events がセットされていたばあい、 dispatch() メソッドに $event を渡してコールしています。 $this->events は DatabaseManager::configure() で $this->app['events'] が渡されていました。 Dispatcher インスタンスです。渡された
QueryExecuted インスタンスをディスパッチしています。$this->loggingQueries が ture ならば、 $this->queryLog[] に クエリ文、バインドする値、時間を配列にして代入します。
logQuery() は戻りは無いので、 run() メソッドに戻ります。
run() メソッドは PDOステートメントを実行した戻り値を戻します。
statement() メソッドは run() メソッドの戻り値を戻します。
これで Blueprint::build() メソッドの処理は完了です。
長い道のりでしたが、Illuminate\Database\Migrations\DatabaseMigrationRepository::createRepository() メソッドの実行が完了しました。
コール元の Illuminate\Console\Command::handle() の続きです。
public function handle()
{
$this->repository->setSource($this->input->getOption('database'));
$this->repository->createRepository();
$this->info('Migration table created successfully.');
}
$this->info() メソッドを 「Migration table created successfully.」 というメッセージを引数として渡してコールしています。
これは、 Illuminate\Console\Concerns\InteractsWithIO トレイトに定義されています。見てみましょう。
Illuminate\Console\Concerns\InteractsWithIO::info() | 関連メソッド
/**
* Write a string as information output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function info($string, $verbosity = null)
{
$this->line($string, 'info', $verbosity);
}
/**
* Write a string as standard output.
*
* @param string $string
* @param string|null $style
* @param int|string|null $verbosity
* @return void
*/
public function line($string, $style = null, $verbosity = null)
{
$styled = $style ? "<$style>$string$style>" : $string;
$this->output->writeln($styled, $this->parseVerbosity($verbosity));
}
/**
* Get the verbosity level in terms of Symfony's OutputInterface level.
*
* @param string|int|null $level
* @return int
*/
protected function parseVerbosity($level = null)
{
if (isset($this->verbosityMap[$level])) {
$level = $this->verbosityMap[$level];
} elseif (! is_int($level)) {
$level = $this->verbosity;
}
return $level;
}
info() メソッドは受け取ったメッセージと 「info」 というスタイル定数と null を引数として渡して line() メソッドをコールします。
line() メソッドは受け取ったメッセージをスタイルを用いて装飾して、 出力の writeln() メソッドをコールしています。
第二引数で渡している parseVerbosity() メソッドですが、受け取った引数から OutputInterface の定数を返します。
writeln() メソッドは出力に受け取った文字列に改行を追加して表示するものでしたね。
コマンドラインに 「Migration table created successfully.」 と表示されます。
Illuminate/Database/Console/Migrations/MigrateCommand::prepareDatabase() メソッドの call() メソッドの処理が終わりました。続きを読みましょう。
prepareDatabase() メソッドをコールしていたのは Illuminate\Database\Console\Migrations\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;
}
Illuminate\Database\Migrations\Migrator::setOutput()
/**
* Set the output implementation that should be used by the console.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return $this
*/
public function setOutput(OutputInterface $output)
{
$this->output = $output;
return $this;
}
$this->migrator インスタンスの setOutput() メソッドを出力を引数として渡してコールし、戻ってくる Migrator インスタンスの run() メソッドをコールします。
引数として、getMigrationPaths() の戻り値とオプション pretend step を配列にしたものを渡します。
getMigrationPaths() メソッドを見てみましょう。
Illuminate\Database\Console\Migrations\BaseCommand::getMigrationPaths()
/**
* Get all of the migration paths.
*
* @return array
*/
protected function getMigrationPaths()
{
// Here, 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 the installation folder so our database
// migrations may be run for any customized path from within the application.
if ($this->input->hasOption('path') && $this->option('path')) {
return collect($this->option('path'))->map(function ($path) {
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$path
: $path;
})->all();
}
return array_merge(
$this->migrator->paths(), [$this->getMigrationPath()]
);
}
$this->input->hasOption('path') がコールされています。
これはなんでしょうか。MigrateCommand インスタンスが生成される時に、スーパークラスの Command クラスのコンストラクタで $this->signature が設定されていた場合 configureUsingFluentDefinition() メソッドがコールされます。そこで、 $this->signature が Parser::parse() メソッドを通してオプションの説明文の配列が取得されます。これを $this->getDefinition()->addOptions($options) したものを参照します。つまり、$this->input->hasOption('path') はコマンド説明を参照して 「--path」 があるか検証しています。
$this-option() メソッドは InteractsWithIO クラスで以下のように定義されています。
Illuminate\Console\Concerns\InteractsWithIO::option()
/**
* 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);
}
Symfony\Component\Console\Input\Input::getOption()
/**
* Returns the option value for a given option name.
*
* @return string|string[]|bool|null The option value
*
* @throws InvalidArgumentException When option given doesn't exist
*/
public function getOption(string $name)
{
if (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
}
return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
}
引数としてキーが渡されていなければ、getOptions() の戻り値を、渡されていれば getOption() の戻り値を戻しています。 getOption() メソッドはオプションに指定キーの値があればそれを、なければオプション定義のデフォルト値を返します。
オプション値が返された場合は、環境に合わせてパスを整形しそれを返します。
今回はここは通らず以下を戻します。
return array_merge(
$this->migrator->paths(), [$this->getMigrationPath()]
);
Illuminate\Database\Migrations\Migrator::run()
/**
* Get all of the custom migration paths.
*
* @return array
*/
public function paths()
{
return $this->paths;
}
Illuminate\Database\Console\Migrations\BaseCommand::getMigrationPath()
/**
* Get the path to the migration directory.
*
* @return string
*/
protected function getMigrationPath()
{
return $this->laravel->databasePath().DIRECTORY_SEPARATOR.'migrations';
}
paths() メソッドと getMigrationPath() メソッドの戻り値を array_merge() してます。 path() メソッドは $this->paths を戻すだけのものです。 $this->paths は代入した場所はありませんでしたので空です。 getMigrationPath() はアプリケーションのデータベースディレクトリ下の 「migrations」 を戻します。今回は、 「PROJECT_ROOT/database/migrations/」 のみの配列が戻る結果となります。
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;
}
/**
* Get all of the migration files in a given path.
*
* @param string|array $paths
* @return array
*/
public function getMigrationFiles($paths)
{
return Collection::make($paths)->flatMap(function ($path) {
return Str::endsWith($path, '.php') ? [$path] : $this->files->glob($path.'/*_*.php');
})->filter()->values()->keyBy(function ($file) {
return $this->getMigrationName($file);
})->sortBy(function ($file, $key) {
return $key;
})->all();
}
Illuminate\Support\Traits\EnumeratesValues::make()
/**
* Create a new collection instance if the value isn't one already.
*
* @param mixed $items
* @return static
*/
public static function make($items = [])
{
return new static($items);
}
Illuminate\Database\Migrations\Migrator::run()
第一引数はストリング型若しくは配列でマイグレーションファイルが配置されているディレクトリのパスです。
第二引数は配列でオプションです。
戻り値は配列です。
getMigrationFiles() メソッドを第一引数として受け取ったパス情報を引数として渡しコールしています。
パスは 「PROJECT_ROOT/database/migrations/」 でした。
getMigrationFiles() メソッドは受け取った引数を Collection::make() に引数として渡しスタティックコールしてます。 Collection::make() メソッドは EnumeratesValues トレイトで定義されている、受け取った引数を渡して自身を生成し戻すメソッドです。
戻された Collection インスタンスの flatMap() メソッドをコールバックを渡しコールします。
コールバックから見てみましょう。
function ($path) {
return Str::endsWith($path, '.php') ? [$path] : $this->files->glob($path.'/*_*.php');
}
Str::endsWith() が使われています。これは第一引数のストリング型変数の終わりの文字列が第二引数と等価だった場合 true をそうでなければ false を返すものでした。つまり、受け取ったパスが拡張子 「.php」 のファイルパスだった場合はそのパスを配列にして戻します。
そうでなかった場合、 Filesystem インスタンスの glob() メソッドを引数にパスと「/*_*.php 」 を連結した文字列を渡してコールしています。ファイル名をワイルドカードとPHP拡張子でフィルタルールを渡しています。
Illuminate\Filesystem\Filesystem::glob()
/**
* Find path names matching a given pattern.
*
* @param string $pattern
* @param int $flags
* @return array
*/
public function glob($pattern, $flags = 0)
{
return glob($pattern, $flags);
}
これは glob() しているだけですね。「PROJECT_ROOT/database/」 ディレクトリにあるマイグレーションコマンドで生成されたマイグレーション用のPHPファイルの一覧のパスを配列にして戻しています。
おそらく現状は、先頭にタイムスタンプが入った 「create_user_table.php」「create_failed_jobs_table.php」「create_customers_table.php」 の3ファイルが配置されていると思います。
flatMap() を見てみましょう。
Illuminate\Support\Traits\EnumeratesValues::flatMap()
/**
* Map a collection and flatten the result by a single level.
*
* @param callable $callback
* @return static
*/
public function flatMap(callable $callback)
{
return $this->map($callback)->collapse();
}
Illuminate\Support\Collection::map()
/**
* Run a map over each of the items.
*
* @param callable $callback
* @return static
*/
public function map(callable $callback)
{
$keys = array_keys($this->items);
$items = array_map($callback, $this->items, $keys);
return new static(array_combine($keys, $items));
}
引数として渡されたコールバックを自身の map() メソッドに渡して戻り値の collapse() メソッドをコールしています。
map() メソッドは $items を渡されたコールバックでフィルタリングした後、array_combine() したものを引数として生成した Collection インスタンスを戻します。
Illuminate\Support\Collection::filter() | values()
/**
* Run a filter over each of the items.
*
* @param callable|null $callback
* @return static
*/
public function filter(callable $callback = null)
{
if ($callback) {
return new static(Arr::where($this->items, $callback));
}
return new static(array_filter($this->items));
}
/**
* Reset the keys on the underlying array.
*
* @return static
*/
public function values()
{
return new static(array_values($this->items));
}
Illuminate\Support\Arr::where()
/**
* Filter the array using the given callback.
*
* @param array $array
* @param callable $callback
* @return array
*/
public static function where($array, callable $callback)
{
return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
}
filter() メソッドは、引数にコールバックがあった場合は Arr::where() に $this->items とコールバックを渡して戻り値を引数として渡した Collection インスタンスを戻します。
Arr::where() メソッドは array_filter() を引数に ARRAY_FILTER_USE_BOTH フラグを添えてコールした戻り値を戻すだけです。
filter() メソッドに引数が渡されていなかった場合は、そのまま array_filter() で $this->items 配列の値が false のものを削除した配列を引数として渡し生成した Collection インスタンスを戻します。
values() メソッドは渡された引数を array_values に渡してコールした戻り値を引数として生成した Collection インスタンスを戻します。
keyBy() メソッドを見てみましょう。
Illuminate\Support\Collection::keyBy()
/**
* Key an associative array by a field or using a callback.
*
* @param callable|string $keyBy
* @return static
*/
public function keyBy($keyBy)
{
$keyBy = $this->valueRetriever($keyBy);
$results = [];
foreach ($this->items as $key => $item) {
$resolvedKey = $keyBy($item, $key);
if (is_object($resolvedKey)) {
$resolvedKey = (string) $resolvedKey;
}
$results[$resolvedKey] = $item;
}
return new static($results);
}
渡されているコールバックは以下でした。
function ($file) {
return $this->getMigrationName($file);
}
getMigrationName() メソッドをコールしています。こちらも見てみましょう。
Illuminate\Database\Migrations\Migrator::getMigrationName()
/**
* Get the name of the migration.
*
* @param string $path
* @return string
*/
public function getMigrationName($path)
{
return str_replace('.php', '', basename($path));
}
引数として受け取ったパスの最後にある名前の部分だけ取り出し、「.php」 を削除しています。
続きの sortBy() を引数として渡すコールバックと一緒に見てみましょう。
function ($file, $key) {
return $key;
}
Illuminate\Support\Collection::sortBy()
/**
* Sort the collection using the given callback.
*
* @param callable|string $callback
* @param int $options
* @param bool $descending
* @return static
*/
public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
{
$results = [];
$callback = $this->valueRetriever($callback);
// First we will loop through the items and get the comparator from a callback
// function which we were given. Then, we will sort the returned values and
// and grab the corresponding values for the sorted keys from this array.
foreach ($this->items as $key => $value) {
$results[$key] = $callback($value, $key);
}
$descending ? arsort($results, $options)
: asort($results, $options);
// Once we have sorted all of the keys in the array, we will loop through them
// and grab the corresponding model so we can set the underlying items list
// to the sorted version. Then we'll just return the collection instance.
foreach (array_keys($results) as $key) {
$results[$key] = $this->items[$key];
}
return new static($results);
}
Illuminate\Support\Traits\EnumeratesValues::valueRetriever() | useAsCallable()
/**
* Get a value retrieving callback.
*
* @param callable|string|null $value
* @return callable
*/
protected function valueRetriever($value)
{
if ($this->useAsCallable($value)) {
return $value;
}
return function ($item) use ($value) {
return data_get($item, $value);
};
}
/**
* Determine if the given value is callable, but not a string.
*
* @param mixed $value
* @return bool
*/
protected function useAsCallable($value)
{
return ! is_string($value) && is_callable($value);
}
sortBy() メソッドは受け取ったコールバックを valueRetriever() メソッドに渡してコールします。
valueRetriever() メソッドは渡された引数が実行可能なコールバックか検証し、問題ない場合はそのまま戻します。
実行可能なコールバックでなかった場合、 data_get() ヘルパ関数を通すクロージャーを戻します。 data_get() は以前も出てきましたね。配列やオブジェクトにドットシンタックスでアクセス出来るようにするものです。
引数として渡されたクロージャーは第二引数をそのまま戻すものでした。
$this->items を foreach で回し、キーと値が同じ配列を生成します。
生成された配列を指定のソート方法でソートします。
ソートされた配列を foreach で回し、キーに対応した $this->items の値を代入し、出来た配列を引数として生成した Collection インスタンスを戻します。
最後の all() メソッドは $this->items をそのまま戻すだけでしたね。
結果、getMigrationFiles() の戻り値は、PROJECT_ROOT/database/migrations のディレクトリにあるマイグレーションファイルを昇順に並べ替えた配列ということですね。
その16に続きます。