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;
}
getMigrationFiles() メソッドの戻り値を $files に代入するところまで読みました。
次は requireFiles() メソッドがコールされています。引数から見てみましょう。
pendingMigrations() メソッドをマイグレーションファイル配列と $this->repository->getRan() の戻り値を渡してコールした戻り値を $migrations に代入しています。
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\Database\Migrations\DatabaseMigrationRepository::getRan()
/**
* Get the completed migrations.
*
* @return array
*/
public function getRan()
{
return $this->table()
->orderBy('batch', 'asc')
->orderBy('migration', 'asc')
->pluck('migration')->all();
}
/**
* Get a query builder for the migration table.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function table()
{
return $this->getConnection()->table($this->table)->useWritePdo();
}
/**
* Resolve the database connection instance.
*
* @return \Illuminate\Database\Connection
*/
public function getConnection()
{
return $this->resolver->connection($this->connection);
}
Illuminate\Database\ConnectionResolver::connection()
/**
* Get a database connection instance.
*
* @param string|null $name
* @return \Illuminate\Database\ConnectionInterface
*/
public function connection($name = null)
{
if (is_null($name)) {
$name = $this->getDefaultConnection();
}
return $this->connections[$name];
}
まず、getRan() メソッドを読みましょう。
table() メソッドは、getConnection で接続を取得し、戻り値のtable() メソッドをコールします。
Illuminate\Database\Connection::table() | 関連メソッド
/**
* Begin a fluent query against a database table.
*
* @param \Closure|\Illuminate\Database\Query\Builder|string $table
* @param string|null $as
* @return \Illuminate\Database\Query\Builder
*/
public function table($table, $as = null)
{
return $this->query()->from($table, $as);
}
/**
* Get a new query builder instance.
*
* @return \Illuminate\Database\Query\Builder
*/
public function query()
{
return new QueryBuilder(
$this, $this->getQueryGrammar(), $this->getPostProcessor()
);
}
/**
* Get the query grammar used by the connection.
*
* @return \Illuminate\Database\Query\Grammars\Grammar
*/
public function getQueryGrammar()
{
return $this->queryGrammar;
}
/**
* Get the query post processor used by the connection.
*
* @return \Illuminate\Database\Query\Processors\Processor
*/
public function getPostProcessor()
{
return $this->postProcessor;
}
/**
* Create a new database connection instance.
*
* @param \PDO|\Closure $pdo
* @param string $database
* @param string $tablePrefix
* @param array $config
* @return void
*/
public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])
{
$this->pdo = $pdo;
// First we will setup the default properties. We keep track of the DB
// name we are connected to since it is needed when some reflective
// type commands are run such as checking whether a table exists.
$this->database = $database;
$this->tablePrefix = $tablePrefix;
$this->config = $config;
// We need to initialize a query grammar and the query post processors
// which are both very important parts of the database abstractions
// so we initialize these to their default values while starting.
$this->useDefaultQueryGrammar();
$this->useDefaultPostProcessor();
}
/**
* Set the query post processor to the default implementation.
*
* @return void
*/
public function useDefaultPostProcessor()
{
$this->postProcessor = $this->getDefaultPostProcessor();
}
Illuminate\Database\MySqlConnection::getDefaultPostProcessor() | 関連メソッド
/**
* Get the default post processor instance.
*
* @return \Illuminate\Database\Query\Processors\MySqlProcessor
*/
protected function getDefaultPostProcessor()
{
return new MySqlProcessor;
}
table() メソッドは query() メソッドの戻り値の from() メソッドをコールします。
from() メソッドは後で読みましょう。
query() メソッドは 自身と、 getQueryGrammar() メソッドの戻り値、今回は MySqlGrammar インスタンスと、 getPostProcessor() メソッドの戻り値、今回は MySqlProcessor インスタンスを引数に渡し QueryBuilder インスタンスを生成し戻します。
MySqlProcessor は Connection クラスのコンストラクタで useDefaultPostProcessor() メソッドがコールされ useDefaultPostProcessor() メソッドから getDefaultPostProcessor() メソッドがコールされ MySqlConnection::getDefaultPostProcessor で MySqlProcessor インスタンスが生成され戻されます。
QueryBuilder::from() メソッドを見てみましょう。
Illuminate\Database\Query\Builder::from()
/**
* Set the table which the query is targeting.
*
* @param \Closure|\Illuminate\Database\Query\Builder|string $table
* @param string|null $as
* @return $this
*/
public function from($table, $as = null)
{
if ($this->isQueryable($table)) {
return $this->fromSub($table, $as);
}
$this->from = $as ? "{$table} as {$as}" : $table;
return $this;
}
/**
* Determine if the value is a query builder instance or a Closure.
*
* @param mixed $value
* @return bool
*/
protected function isQueryable($value)
{
return $value instanceof self ||
$value instanceof EloquentBuilder ||
$value instanceof Closure;
}
/**
* Makes "from" fetch from a subquery.
*
* @param \Closure|\Illuminate\Database\Query\Builder|string $query
* @param string $as
* @return $this
*
* @throws \InvalidArgumentException
*/
public function fromSub($query, $as)
{
[$query, $bindings] = $this->createSub($query);
return $this->fromRaw('('.$query.') as '.$this->grammar->wrapTable($as), $bindings);
}
/**
* Add a raw from clause to the query.
*
* @param string $expression
* @param mixed $bindings
* @return $this
*/
public function fromRaw($expression, $bindings = [])
{
$this->from = new Expression($expression);
$this->addBinding($bindings, 'from');
return $this;
}
/**
* Creates a subquery and parse it.
*
* @param \Closure|\Illuminate\Database\Query\Builder|string $query
* @return array
*/
protected function createSub($query)
{
// If the given query is a Closure, we will execute it while passing in a new
// query instance to the Closure. This will give the developer a chance to
// format and work with the query before we cast it to a raw SQL string.
if ($query instanceof Closure) {
$callback = $query;
$callback($query = $this->forSubQuery());
}
return $this->parseSub($query);
}
/**
* Create a new query instance for a sub-query.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function forSubQuery()
{
return $this->newQuery();
}
/**
* Get a new instance of the query builder.
*
* @return \Illuminate\Database\Query\Builder
*/
public function newQuery()
{
return new static($this->connection, $this->grammar, $this->processor);
}
/**
* Parse the subquery into SQL and bindings.
*
* @param mixed $query
* @return array
*
* @throws \InvalidArgumentException
*/
protected function parseSub($query)
{
if ($query instanceof self || $query instanceof EloquentBuilder) {
return [$query->toSql(), $query->getBindings()];
} elseif (is_string($query)) {
return [$query, []];
} else {
throw new InvalidArgumentException(
'A subquery must be a query builder instance, a Closure, or a string.'
);
}
}
isQueryable() メソッドをコールして戻り値を検証しています。
isQueryable() は引数が、 Builder インスタンス若しくは EloquentBuilder インスタンス若しくはクロージャーであれば true そうでなければ false を戻します。
戻り値が true の場合、 fromSub() メソッドを引数をそのまま渡してコールします。
false の場合は、第二引数の $as が渡されていた場合はテーブル名にエイリアス設定を追加したSQL文を $this->from に代入し 自身を戻します。
fromSub() メソッドを読みましょう。
createSub() メソッドを第一引数を渡してコールした戻り値をクエリ文とバインド値に代入しています。
createSub() メソッドは受け取った引数がクロージャーだった場合、 forSubQuery() メソッドをコールし forSubQuery() から newQuery() をコールし Builder インスタンスを生成して戻します。
クロージャーでない場合は parseSub() メソッドに引数を渡してコールした戻り値を戻します。
parseSub() メソッドは受け取った引数が Builder インスタンス若しくは EloquentBuilder インスタンスだった場合は、 toSql() メソッドと getBuildings() メソッドの戻り値を配列にしたものを戻します。toSql() から見てみましょう。
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()
/**
* 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 a union aggregate query into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compileUnionAggregate(Builder $query)
{
$sql = $this->compileAggregate($query, $query->aggregate);
$query->aggregate = null;
return $sql.' from ('.$this->compileSelect($query).') as '.$this->wrapTable('temp_table');
}
Grammar インスタンスの compileSelect() メソッドに自身を引数として渡してコールしています。
$query->unions と $query->aggregate を検証しています。このパラメーターを更新するのは union() と setAggregate() です。こちらをコールした場所はありませんでしたので今回はここは通りません。
ここを通る場合は、 compileUnionAggregate() メソッドがコールされます。見てみましょう。
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';
}
/**
* Convert an array of column names into a delimited string.
*
* @param array $columns
* @return string
*/
public function columnize(array $columns)
{
return implode(', ', array_map([$this, 'wrap'], $columns));
}
columnize() メソッドを $aggregate['columns'] を引数として渡しコールします。
columnize() メソッドは array_map() で渡されたカラム配列を $this->wrap() を通し、 implode() で連結し戻します。wrap() を見てみましょう。
Illuminate\Database\Query\Grammars\Grammar::wrap()
/**
* Wrap a value in keyword identifiers.
*
* @param \Illuminate\Database\Query\Expression|string $value
* @param bool $prefixAlias
* @return string
*/
public function wrap($value, $prefixAlias = false)
{
if ($this->isExpression($value)) {
return $this->getValue($value);
}
// If the value being wrapped has a column alias we will need to separate out
// the pieces so we can wrap each of the segments of the expression on its
// own, and then join these both back together using the "as" connector.
if (stripos($value, ' as ') !== false) {
return $this->wrapAliasedValue($value, $prefixAlias);
}
// If the given value is a JSON selector we will wrap it differently than a
// traditional value. We will need to split this path and wrap each part
// wrapped, etc. Otherwise, we will simply wrap the value as a string.
if ($this->isJsonSelector($value)) {
return $this->wrapJsonSelector($value);
}
return $this->wrapSegments(explode('.', $value));
}
/**
* Determine if the given value is a raw expression.
*
* @param mixed $value
* @return bool
*/
public function isExpression($value)
{
return $value instanceof Expression;
}
/**
* Get the value of a raw expression.
*
* @param \Illuminate\Database\Query\Expression $expression
* @return string
*/
public function getValue($expression)
{
return $expression->getValue();
}
Illuminate\Database\Query\Expression::getValue()
/**
* Get the value of the expression.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Wrap the given JSON selector.
*
* @param string $value
* @return string
*
* @throws \RuntimeException
*/
protected function wrapJsonSelector($value)
{
throw new RuntimeException('This database engine does not support JSON operations.');
}
このメソッドは見覚えありますね。スキーマーのグラマーにも同じようなものがありました。処理も似ているのかもしれません。読んでみましょう。
isExpression() メソッドで引数を調べています。 Expression インスタンスだった場合は、getValue() メソッドに引数を渡し、getValue() メソッドで引数の getValue() メソッドをコールした戻り値を戻しています。Expression::getValue() は $this->value を返しているだけですね。
見たところ、スキーマの時の処理とほぼ同じようです。
1点違う部分は、 isJsonSelector() メソッドを検証しています。見てみましょう。
Illuminate\Database\Query\Grammars\Grammar::isJsonSelector()
/**
* Determine if the given string is a JSON selector.
*
* @param string $value
* @return bool
*/
protected function isJsonSelector($value)
{
return Str::contains($value, '->');
}
Illuminate\Support\Str::contains()
/**
* Determine if a given string contains a given substring.
*
* @param string $haystack
* @param string|string[] $needles
* @return bool
*/
public static function contains($haystack, $needles)
{
foreach ((array) $needles as $needle) {
if ($needle !== '' && mb_strpos($haystack, $needle) !== false) {
return true;
}
}
return false;
}
Str::contains() に引数と 「->」 を渡しています。
Str::contains() メソッドは第二引数を foreach で回し 第一引数の中にその文字列があった場合 true を、ない場合は false を返します。つまり mb_strpos() の配列対応関数ですね。
集計カラム用のSQL文が生成されました。
次は $this->distinct が配列か検証します。配列だった場合はこちらも columnize() メソッドでSQL文を生成します。
配列でない場合で且つ null でなく、整形しているSQL文が 「*」 でなかった場合は、 「distinct」文にカラム名を連結しSQL文を生成します。
出来上がったSQL文を使いセブクエリの文字列を戻します。
compileSelect() に戻りましょう。
引数として受け取った Builder インスタンスのカラムパラメータを検証し、null だった場合、「*」 を代入します。
concatenate() に引数として compileComponents() に引数として受け取った Builder インスタンスを渡してコールした戻り値を渡してコールしています。
Illuminate\Database\Query\Grammars\Grammar::compileComponents() | 関連メソッド
/**
* The components that make up a select clause.
*
* @var array
*/
protected
$selectComponents = [
'aggregate',
'columns',
'from',
'joins',
'wheres',
'groups',
'havings',
'orders',
'limit',
'offset',
'lock',
];
/**
* 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;
}
/**
* Concatenate an array of segments, removing empties.
*
* @param array $segments
* @return string
*/
protected function concatenate($segments)
{
return implode(' ', array_filter($segments, function ($value) {
return (string) $value !== '';
}));
}
compileComponents() を見てみましょう。
selectComponents をforeach で回して Builder インスタンスに $selectComponents のパラメーターが設定されていた場合、文字列 「compile」 と$selectComponents の値を結合しメソッド名を生成し、そのメソッドをコールした戻り値を SQL配列に代入したものを戻します。
concatenate() メソッドは受け取ったSQL文の配列を implode() したものを戻します。
compileSelect() の続きです。
引数として受け取った Builder インスタンスに unions のパラメータがあった場合は wrapUnion() メソッドと compileUnions() メソッドの戻り値を連結してSQL文を生成します。
見てみましょう。
Illuminate\Database\Query\Grammars\Grammar::wrapUnion()
/**
* Wrap a union subquery in parentheses.
*
* @param string $sql
* @return string
*/
protected function wrapUnion($sql)
{
return '('.$sql.')';
}
/**
* Compile the "union" queries attached to the main query.
*
* @param \Illuminate\Database\Query\Builder $query
* @return string
*/
protected function compileUnions(Builder $query)
{
$sql = '';
foreach ($query->unions as $union) {
$sql .= $this->compileUnion($union);
}
if (! empty($query->unionOrders)) {
$sql .= ' '.$this->compileOrders($query, $query->unionOrders);
}
if (isset($query->unionLimit)) {
$sql .= ' '.$this->compileLimit($query, $query->unionLimit);
}
if (isset($query->unionOffset)) {
$sql .= ' '.$this->compileOffset($query, $query->unionOffset);
}
return ltrim($sql);
}
/**
* Compile a single union statement.
*
* @param array $union
* @return string
*/
protected function compileUnion(array $union)
{
$conjunction = $union['all'] ? ' union all ' : ' union ';
return $conjunction.$this->wrapUnion($union['query']->toSql());
}
/**
* Compile the "order by" portions of the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $orders
* @return string
*/
protected function compileOrders(Builder $query, $orders)
{
if (! empty($orders)) {
return 'order by '.implode(', ', $this->compileOrdersToArray($query, $orders));
}
return '';
}
/**
* Compile the "limit" portions of the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @param int $limit
* @return string
*/
protected function compileLimit(Builder $query, $limit)
{
return 'limit '.(int) $limit;
}
/**
* Compile the "offset" portions of the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @param int $offset
* @return string
*/
protected function compileOffset(Builder $query, $offset)
{
return 'offset '.(int) $offset;
}
wrapUnion() メソッドは受け取ったSQL文を「()」で囲んで戻すだけです。
compileUnions() は受け取った Builder インスタンスをforeachで回し、パラメーターごとに文字列整形してSQL文を組み立てていく流れになっていますね。
こうして整えられた SQL文を compileSelect() メソッドは戻しています。
parseSub() メソッドに戻ります。
もう一つの戻り値 $query->getBindings() を見てみましょう。
Illuminate\Database\Query\Builder::getBindings()
/**
* Get the current query value bindings in a flattened array.
*
* @return array
*/
public function getBindings()
{
return Arr::flatten($this->bindings);
}
Illuminate\Support\Arr::flatten()
/**
* Flatten a multi-dimensional array into a single level.
*
* @param iterable $array
* @param int $depth
* @return array
*/
public static function flatten($array, $depth = INF)
{
$result = [];
foreach ($array as $item) {
$item = $item instanceof Collection ? $item->all() : $item;
if (! is_array($item)) {
$result[] = $item;
} else {
$values = $depth === 1
? array_values($item)
: static::flatten($item, $depth - 1);
foreach ($values as $value) {
$result[] = $value;
}
}
}
return $result;
}
Arr::flatten() メソッドを引数にバインド値を渡して静的コールしています。
Arr::flatten() は多次元配列を再帰的に一次配列に変換して返す関数ですね。
parseSub() メソッドの続きです。
受け取った引数が Builder インスタンスでも EloquentBuilder インスタンスでもなかった場合、それがストリング型か検証し、そうだった場合は引数と空の配列を配列に入れて戻します。どれでもなかった場合は「A subquery must be a query builder instance, a Closure, or a string.」 をメッセージを添えて例外 InvalidArgumentException をスローします。
parseSub() メソッドから戻されたSQL文配列を createSub() メソッドはそのまま fromSub() に戻します。fromSub() は受け取ったクエリ文を整形したものとバインド値を引数として fromRow() メソッドに渡し戻り値を戻します。
fromRaw() メソッドは 受け取ったSQL文を元に Expression インスタンスを生成し、$this->from に代入します。
次に受け取ったバインド値を引数として渡して addBinding() メソッドをコールします。
addBinding() を見てみましょう。
Illuminate\Database\Query\Builder::addBinding()
/**
* The current query value bindings.
*
* @var array
*/
public $bindings = [
'select' => [],
'from' => [],
'join' => [],
'where' => [],
'groupBy' => [],
'having' => [],
'order' => [],
'union' => [],
'unionOrder' => [],
];
/**
* Add a binding to the query.
*
* @param mixed $value
* @param string $type
* @return $this
*
* @throws \InvalidArgumentException
*/
public function addBinding($value, $type = 'where')
{
if (! array_key_exists($type, $this->bindings)) {
throw new InvalidArgumentException("Invalid binding type: {$type}.");
}
if (is_array($value)) {
$this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value));
} else {
$this->bindings[$type][] = $value;
}
return $this;
}
第二引数として受けたっとタイプが $this->bindings に含まれていない場合は、「Invalid binding type: {$type}.」 とメッセージを添えて例外 InvalidArgumentException をスローします。
受け取った第一引数が配列の場合、$this->bindings[] の第二引数をキーにした値に第一引数を array_merge() したものを array_values() したものを代入します。
配列でなかった場合、$this->bindings[] の第二引数をキーにした配列に第一引数を追加します。
$this->bindings[] へ値の追加をした後自身を戻します。
addBinding() メソッドから自身を戻された fromRaw() メソッドは fromSub() に自身を戻し、fromSub() メソッドはさらにそれを戻します。
Builder::from() メソッドに戻ります。結構経ってしまったのでもう一度見てみましょう。
Illuminate\Database\Query\Builder::from()
/**
* Set the table which the query is targeting.
*
* @param \Closure|\Illuminate\Database\Query\Builder|string $table
* @param string|null $as
* @return $this
*/
public function from($table, $as = null)
{
if ($this->isQueryable($table)) {
return $this->fromSub($table, $as);
}
$this->from = $as ? "{$table} as {$as}" : $table;
return $this;
}
/**
* Determine if the value is a query builder instance or a Closure.
*
* @param mixed $value
* @return bool
*/
protected function isQueryable($value)
{
return $value instanceof self ||
$value instanceof EloquentBuilder ||
$value instanceof Closure;
}
今回は第一引数として渡されている $table には $this->table 、つまり 「migrations」 が入っていました。 isQueryable() の戻り値は false ですのでここは通りません。
第二引数 $as は null ですので、$this->from には 「migrations」 が代入されます。
$this->from に適切な値が代入された後に Connection::table() に自身を戻します。
Connection::table() は受け取った戻り値をそのまま DatabaseMigrationRepository::table() メソッドに戻します。
もう一度見てみましょう。
Illuminate\Database\Migrations\DatabaseMigrationRepository::table() | 関連メソッド
/**
* Get the completed migrations.
*
* @return array
*/
public function getRan()
{
return $this->table()
->orderBy('batch', 'asc')
->orderBy('migration', 'asc')
->pluck('migration')->all();
}
/**
* Get a query builder for the migration table.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function table()
{
return $this->getConnection()->table($this->table)->useWritePdo();
}
/**
* Resolve the database connection instance.
*
* @return \Illuminate\Database\Connection
*/
public function getConnection()
{
return $this->resolver->connection($this->connection);
}
DatabaseMigrationRepository::table() メソッドは受け取った戻り値の useWritePdo() メソッドをコールし戻り値を戻します。
Illuminate\Database\Query\Builder::useWritePdo()
/**
* Use the write pdo for query.
*
* @return $this
*/
public function useWritePdo()
{
$this->useWritePdo = true;
return $this;
}
useWritePdo() メソッドは、 useWritePdo パラメータを true に上書きし自身を返します。
DatabaseMigrationRepository::table() メソッドは受け取った戻り値を DatabaseMigrationRepository::getRan() メソッドに戻します。
とても長かったですが、ようやく戻ってきました。戻されるのは Builder インスタンスですね。
Builder インスタンスに、 OrderBy() メソッドをコールしたあと、 pluck() して all() してます。OrderBy() を見てみましょう。
Illuminate\Database\Query\Builder::orderBy()
/**
* Add an "order by" clause to the query.
*
* @param \Closure|\Illuminate\Database\Query\Builder|string $column
* @param string $direction
* @return $this
*
* @throws \InvalidArgumentException
*/
public function orderBy($column, $direction = 'asc')
{
if ($this->isQueryable($column)) {
[$query, $bindings] = $this->createSub($column);
$column = new Expression('('.$query.')');
$this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order');
}
$direction = strtolower($direction);
if (! in_array($direction, ['asc', 'desc'], true)) {
throw new InvalidArgumentException('Order direction must be "asc" or "desc".');
}
$this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
'column' => $column,
'direction' => $direction,
];
return $this;
}
isQueryable() メソッドで検証しています。このメソッドは、引数が Builder インスタンス若しくは EloquentBuilder インスタンス若しくはクロージャーの場合は true を、そうでなければ false を戻すものでしたね。
もしその場合は、先程読んだ流れのようにサブクエリを生成する処理をします。
ソート指定をSQL文として整形します。もし、昇順降順以外の文字列が指定されていた場合は「Order direction must be "asc" or "desc".」 とメッセージを添えて例外 InvalidArgumentException をスローします。
$this->unions を検証し、true なら 「unionOrders」、false なら 「orders」 のパラメーター配列に第一引数をカラムとして、第二引数をソート順として代入し、自身を戻します。
今回は、 $this->orders[] に ['column' => 'batch', 'direction' => 'asc'], ['column' => 'migration', 'direction' => 'asc'] が追加される感じでしょう。
その後、戻された Builder インスタンスの pluck() メソッドがコールされる流れですね。
Illuminate\Database\Query\Builder::pluck() | 関連メソッド
/**
* Get an array with the values of a given column.
*
* @param string $column
* @param string|null $key
* @return \Illuminate\Support\Collection
*/
public function pluck($column, $key = null)
{
// First, we will need to select the results of the query accounting for the
// given columns / key. Once we have the results, we will be able to take
// the results and get the exact data that was requested for the query.
$queryResult = $this->onceWithColumns(
is_null($key) ? [$column] : [$column, $key],
function () {
return $this->processor->processSelect(
$this, $this->runSelect()
);
}
);
if (empty($queryResult)) {
return collect();
}
// If the columns are qualified with a table or have an alias, we cannot use
// those directly in the "pluck" operations since the results from the DB
// are only keyed by the column itself. We'll strip the table out here.
$column = $this->stripTableForPluck($column);
$key = $this->stripTableForPluck($key);
return is_array($queryResult[0])
? $this->pluckFromArrayColumn($queryResult, $column, $key)
: $this->pluckFromObjectColumn($queryResult, $column, $key);
}
/**
* Execute the given callback while selecting the given columns.
*
* After running the callback, the columns are reset to the original value.
*
* @param array $columns
* @param callable $callback
* @return mixed
*/
protected function onceWithColumns($columns, $callback)
{
$original = $this->columns;
if (is_null($original)) {
$this->columns = $columns;
}
$result = $callback();
$this->columns = $original;
return $result;
}
まず、onceWithColumns() メソッドをコールしています。引数にはカラム名とクロージャーを渡しています。
onceWithColumns() は、自身のcolumns の値を一時的に逃してコールバックを実行します。
その後 $this->columns をもとに戻し、コールバックの結果を戻しています。
コールバックを見てみましょう。
$this->processor の processSelect() メソッドをコールしています。
$this->processor は MySqlProcessor でしたね。
引数として渡しているのは自身と $this->runSelect() です。
$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
);
}
MySqlConnection::select() をコールしています。引数に $this->toSql() 、$this->getBindings() 、$this->useWritePdo を渡しています。この3つは先程読みましたね。MySqlConnection::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();
});
}
このメソッドは以前読みましたね。クエリ文とバインド値からPDOステートメントを生成して実行するコールバックを実行して処理時間を計測する処理でした。今回は 「ORDER BY」句が入っています。クエリ生成メソッドを見てみましょう。
Illuminate\Database\Query\Grammars\Grammar::compileOrders() | compileOrdersToArray()
/**
* Compile the "order by" portions of the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $orders
* @return string
*/
protected function compileOrders(Builder $query, $orders)
{
if (! empty($orders)) {
return 'order by '.implode(', ', $this->compileOrdersToArray($query, $orders));
}
return '';
}
/**
* Compile the query orders to an array.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $orders
* @return array
*/
protected function compileOrdersToArray(Builder $query, $orders)
{
return array_map(function ($order) {
return $order['sql'] ?? $this->wrap($order['column']).' '.$order['direction'];
}, $orders);
}
渡された第二引数 $orders が設定されていた場合、compileOrdersToArray() メソッドの戻り値の配列を implode() したものを 「ORDER BY 」 と連結して戻します。
設定されていない場合は空文字を戻します。
compileOrdersToArray() は渡された第二引数に「’sql’」がキーの値があればそれを、なければカラム名とソート順を半角スペースで連結した文字列を戻します。おそらく以下のようなSQL文が出来るのでしょう。
「SELECT migration FROM migrations ORDER BY batch ASC, migration ASC」
runSelect() は出来上がったSQL文の実行結果を配列にしたものを pluck() メソッドの onceWithColumns() の第二引数のコールバックの戻り値としてコールされる関数の第二引数として戻します。
processSelect() メソッドを見てみましょう。
Illuminate\Database\Query\Builder\Processor::processSelect()
/**
* Process the results of a "select" query.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $results
* @return array
*/
public function processSelect(Builder $query, $results)
{
return $results;
}
単純に第二引数を戻しているだけですね。
onceWithColumns() メソッドか渡されるのは migration と 先程のSQLの結果を配列で戻すコールバックということになります。
onceWithColumns() メソッドはコールバックの結果をそのまま戻していますので、pluck() メソッド内の $queryResult には先程のSQLの実行結果配列が代入されます。
pluck() メソッドをもう一度見てみましょう。
Illuminate\Database\Query\Builder::pluck() | 関連メソッド
public function pluck($column, $key = null)
{
// First, we will need to select the results of the query accounting for the
// given columns / key. Once we have the results, we will be able to take
// the results and get the exact data that was requested for the query.
$queryResult = $this->onceWithColumns(
is_null($key) ? [$column] : [$column, $key],
function () {
return $this->processor->processSelect(
$this, $this->runSelect()
);
}
);
if (empty($queryResult)) {
return collect();
}
// If the columns are qualified with a table or have an alias, we cannot use
// those directly in the "pluck" operations since the results from the DB
// are only keyed by the column itself. We'll strip the table out here.
$column = $this->stripTableForPluck($column);
$key = $this->stripTableForPluck($key);
return is_array($queryResult[0])
? $this->pluckFromArrayColumn($queryResult, $column, $key)
: $this->pluckFromObjectColumn($queryResult, $column, $key);
}
/**
* Strip off the table name or alias from a column identifier.
*
* @param string $column
* @return string|null
*/
protected function stripTableForPluck($column)
{
if (is_null($column)) {
return $column;
}
$seperator = strpos(strtolower($column), ' as ') !== false ? ' as ' : '\.';
return last(preg_split('~'.$seperator.'~i', $column));
}
Illuminate\Support\helpers.php 抜粋
/**
* Get the last element from an array.
*
* @param array $array
* @return mixed
*/
function last($array)
{
return end($array);
}
$queryResult が空の場合は情報の入っていない Collection インスタンスを生成してそのまま戻します。
空でない場合は、stripTableForPluck() メソッドをコールします。
カラムとキーをどちらとも通します。
stripTableForPluck() は引数が null の場合そのまま戻します。
null でない場合は引数に 「 as 」が含まれている場合はセパレーターとして 「 as 」に、そうでない場合は 「\.」に設定します。
引数をセパレーターで preg_split() で配列にし、最後の値を戻します。
今回の場合はカラムが「migration」、キーは null ですね。
SQLの実行結果配列の 0番目が配列ならば、pluckFromArrayColumn() メソッド、そうでなければ pluckFromObjectColumn() メソッドをコールします。
Illuminate\Database\Query\Builder::pluckFromArrayColumn() | pluckFromObjectColumn()
/**
* Retrieve column values from rows represented as arrays.
*
* @param array $queryResult
* @param string $column
* @param string $key
* @return \Illuminate\Support\Collection
*/
protected function pluckFromArrayColumn($queryResult, $column, $key)
{
$results = [];
if (is_null($key)) {
foreach ($queryResult as $row) {
$results[] = $row[$column];
}
} else {
foreach ($queryResult as $row) {
$results[$row[$key]] = $row[$column];
}
}
return collect($results);
}
/**
* Retrieve column values from rows represented as objects.
*
* @param array $queryResult
* @param string $column
* @param string $key
* @return \Illuminate\Support\Collection
*/
protected function pluckFromObjectColumn($queryResult, $column, $key)
{
$results = [];
if (is_null($key)) {
foreach ($queryResult as $row) {
$results[] = $row->$column;
}
} else {
foreach ($queryResult as $row) {
$results[$row->$key] = $row->$column;
}
}
return collect($results);
}
pluckFromArrayColumn() を見てみましょう。
渡されたキーが null だった場合は、foreach で回して結果配列のカラム名キーを取得します。
null でない場合は、foreach で回して結果配列の指定キーを取得します。
取得した配列を引数として渡した Collection インスタンスを生成して戻します。
ということで、 getRan() メソッドは以下のSQLが実行された結果の配列が取得されます。
SELECT migration FROM migrations ORDER BY batch ASC, migration ASC
その17に続きます