21
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravel入門者が マイグレーション処理の流れを クソ丁寧に追ったら、追いきれなくなった話

Posted at

結論から申し上げます

本記事はなにもまとまっておらず、ただただmigrationに係るファイルを一つ一つ追っていったログ です。

本当は↓↓こんな感じの図を交えつつ

image.png

「Laravelのmigration処理を解き明かしたった!」みたいなタイトルで一世を風靡しようと思ったのですが、挫折しました。

せっかく途中まで調べたので恥を忍んで公開しておこうと思います。
悔しいからまた挑戦します!!

ということで、以下ログです。

migrationってなに?

まずはmigrationの本来の意味を調べます。

mi·gra·tion

  1. seasonal movement of animals from one region to another.
  2. movement from one part of something to another.

ざっくり**"場所から場所への移動"**と捉えておけば問題ないでしょう。

次に、公式ドキュメントを確認。
https://readouble.com/laravel/5.5/ja/migrations.html

...横文字だらけでわかりづらいのですが、要するにデータベースの定義をLaravelからできるものです。

  1. Migrationファイルを作って
  2. そのファイルの中身に指令を書き込んで
  3. その指令通りにLaravelにデータベースを操作してもらう

この3パターンで完了です!

ということで、それぞれの処理について詳細をみていきましょう。

登場人物

開発者とLaravel、そしてデータベースです。
image.png

migrationファイルを作る

まずは、マイグレーション処理をするために、migrationファイルを作成しましょう。

image.png

マイグレーションファイルを作るためには、php artisanコマンドを使います。Laravel用のコマンドですね。
(artisanは職人という意味らしいです)

ここでは、testというテーブルを作成したいとします。

root@8c8e2d4a121f:/var/www# php artisan make:migration create_tests_table
Created Migration: 2019_03_01_085359_create_tests_table

すると、database/migrations 配下に新しいファイルができているのがわかります。

ファイルの中身をみてみましょう。

2019_03_01_085359_create_tests_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTestsTable extends Migration
{
    public function up()
    {
        Schema::create('tests', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('tests');
    }
}

Migrationクラスを継承する形でCreateTestsTableクラスが作成されていることがわかると思います。

中身をみてみると、up()という関数とdown()という関数があることがわかります。

up()メソッドは新規テーブル、カラム、インデックスをデータベースに追加することができます。down()は逆に、up()メソッドの処理を元に戻す機能を記述します。

そして、両メソッド - up()down() - の中身はSchemaファサードが使えます。Schemaとは何か。それを次にみていきます。

回り道: Migration.phpを覗いてみる

では、少し回り道をしてMigrationクラスを覗いてみましょう。

抽象クラスのようですね。
コメントを日本語化して掲載してみます。

※抽象クラスの解説はこちらの記事が詳しいです。
https://laraweb.net/surrounding/1972/

<?php

namespace Illuminate\Database\Migrations;

abstract class Migration
{
    //データベース接続するために使う名前。
    protected $connection;

    //サポートされている場合、トランザクション内でマイグレーションをラップすることを可能にします。
    public $withinTransaction = true;

    //マイグレーション接続の名前を取得します。
    public function getConnection()
    {
        return $this->connection;
    }
}

Migrationクラス自体は抽象メソッド(abstract)ですが、内部のメソッド・プロパティにはabstractが定義されていないので、これら3つはそのままCreateTestsTableクラスに継承されるわけですね。

up()の中身を記述する - Schemaファサード

さて、ではup()メソッドの中身を改めて確認していきましょう。

2019_03_01_085359_create_tests_table.php
public function up()
{
    Schema::create('tests', function (Blueprint $table) {
        $table->increments('id');
        $table->timestamps();
    });
}

ファサードってなんぞ?となると思いますが、ここでファサードの元々の意味をまた確認してみましょう。

fa·cade
the face of a building, especially the principal front that looks onto a street or open space.

建物の正面、という意味です。詳細はこちらの記事がわかりやすかったです。

Laravelのファサードとは、建築用語での「建築物 の 正面部分」という意味が、プログラミングでの「ライブラリ(建築物)の呼び出し部分(正面部分)」という意味になるわけですね。

ということで、Schemaファサード(ここではcreate)についてみていきましょう

Schema::create('tests', function (Blueprint $table) {
    $table->increments('id');
    $table->timestamps();
});

::はスコープ定義演算子です。クラスをインスタンス化しなくても、クラス内のプロパティを扱うことができます
http://php.net/manual/ja/language.oop5.paamayim-nekudotayim.php

つまり、Schemaのcreateメソッドを実行して、という意味でそのまんま捉えてOKです。

Schema::create([テーブル名], [Blueprintオブジェクトを受け取るクロージャ関数] )

という形式で書かれています。

回り道:createを探す旅

さて、ここでまた回り道をしてcreateがどこに記述されているかを確認してみましょう。

Schemaクラスを確認してみます。

vendor/laravel/framework/src/Illuminate/Support/Facades/Schema.php

Schema.php

<?php

namespace Illuminate\Support\Facades;

/**
 * @method static \Illuminate\Database\Schema\Builder create(string $table, \Closure $callback)
 * ...
 * @see \Illuminate\Database\Schema\Builder
 */
class Schema extends Facade
{
    /**
     * 接続のために Schema Builder インスタンスを取得する。
     * @return \Illuminate\Database\Schema\Builder
     */
    public static function connection($name)
    {
        return static::$app['db']->connection($name)->getSchemaBuilder();
    }

    /**
     * Schema Builder インスタンスを デフォルトの接続として取得する。
     * @return \Illuminate\Database\Schema\Builder
     */
    protected static function getFacadeAccessor()
    {
        return static::$app['db']->connection()->getSchemaBuilder();
    }
}

...あれ?createがない?
ということで、継承元のFacadeクラスを確認してみるが、ない。

あちこち探してみると、こんな場所にいました。

vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php

Builder.php
<?php

namespace Illuminate\Database\Schema;

use Closure;
use LogicException;
use Illuminate\Database\Connection;

class Builder
{
    ...
    /**
     * schema上にテーブルを作成する。
     */
    public function create($table, Closure $callback)
    {
        $this->build(tap($this->createBlueprint($table), function ($blueprint) use ($callback) {
            $blueprint->create();

            $callback($blueprint);
        }));
    }
    ...
}

create($table, Closure $callback)となっており、先ほどの定義と一致しますね。

でもBuilderクラスなんてSchemaクラスにはどこにも出てきません。
では、どこで接続するのか?

...それは、後半のお楽しみ。

閑話休題。
Facadeクラスにもconnection()メソッドはありませんでした。
するとどういうことが起こるのか?

Facadeクラス内の__callStaticに注目してください。

vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php

Facade.php

<?php

namespace Illuminate\Support\Facades;
...

abstract class Facade
{
    ...
    
    public static function __callStatic($method, $args)
    {
        $instance = static::getFacadeRoot();

        if (! $instance) {
            throw new RuntimeException('A facade root has not been set.');
        }

        return $instance->$method(...$args);
    }
}

__callStaticマジックメソッドと呼ばれるPHPの文法の一種で、アンダーライン2つ(__)から始まる関数名は、ある条件下の時に発動するメソッドです。
http://php.net/manual/ja/language.oop5.magic.php

__callStaticは存在しない静的メソッドが呼ばれたら実行されます。

ここで振り返ってみてください。

Schema::createcreateメソッドを呼び出しているのに、Schemaクラスにも、Facadeクラスにもcreateが存在しません。
...そう、つまり、__callStaticが実行されるわけです!

ということで、__callStaticの中身を見ましょう。

すると、

$instance = static::getFacadeRoot();

という処理を行なっていますね。

ちなみにここでstatic::が使用されていますが、これは遅延静的束縛と呼ばれるものです。
ここでは説明を割愛しますが、こちらのサイトがわかりやすいです。
https://ackintosh.github.io/blog/2013/08/25/late-static-binding/

では、getFacadeRootに関連する場所を探してみましょう。

Facade.php

    /**
     * ファサードの裏にあるルートオブジェクトを取得する
     */
    public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }

    /**
     * ファサードのルートのインスタンスをコンテナから解決する
     */
    protected static function resolveFacadeInstance($name)
    {
        if (is_object($name)) {
            return $name;
        }

        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        return static::$resolvedInstance[$name] = static::$app[$name];
    }

まずはstatic::resolveFacadeInstance(static::getFacadeAccessor())という処理をさせたいわけですね。
ここで着目したいのがgetFacadeAccessor()はどこにあるのか、という話。
これは、はるか昔に書いた、Schemeクラスの中に出てきます。

Schema.php

<?php

namespace Illuminate\Support\Facades;

class Schema extends Facade
{
    protected static function getFacadeAccessor()
    {
        return static::$app['db']->connection()->getSchemaBuilder();
    }
}

$appはFacade側で定義されています(ややこしい...)
再び戻って確認します。
すると、こんな関数が。

Facade.php

    public static function setFacadeApplication($app)
    {
        static::$app = $app;
    }

どうやら、bootstrap中にこのsetFacadeApplicationメソッドを使って値を設定するようです。

The first static property contains a reference to the Laravel IoC container. It’s set during the framework bootstrap process using the setFacadeApplication() method found in the full example above.

で、このプロパティはabstract facadeにくっついているから、Facadeクラスを継承する全てのクラスに適用されるらしい。なるほど。

ということで、コメント部にあるように、

Facade.php
    /**
     * The application instance being facaded.
     *
     * @var \Illuminate\Contracts\Foundation\Application
     */
    protected static $app;

という定義で良いようだ。
Illuminate\Contracts\Foundation には Application.phpがある。
そして、これがサービスコンテナの操作をすることができるインスタンスなのだ。
やっとゴールが見えてきた気がするぞ!!

さて、ここで更に寄り道。

        return static::$app['db']->connection()->getSchemaBuilder();

これ、なんで配列になっているか、気になりませんか?
クッソ調べまくって見つけました。
https://stackoverflow.com/questions/50530710/how-laravel-uses-object-as-an-array-in-facade-class?rq=1

PHPにあるArrayAccessインターフェイスを使っているからでした。
http://php.net/manual/ja/class.arrayaccess.php
スッキリした...!

なので、
$app['db']インスタンスを参照ているわけです。

つまり、下記とニアリーイコールです。
DBファサードは Illuminate\Database\DatabaseManager を参照します。

ここのファサードクラス一覧に書いてあります。
https://readouble.com/laravel/5.5/ja/facades.html

ということで、当該のDatabaseManager.phpをみてみましょう

DatabaseManager.php
<?php

namespace Illuminate\Database;

use PDO;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Illuminate\Database\Connectors\ConnectionFactory;

/**
 * @mixin \Illuminate\Database\Connection
 */
class DatabaseManager implements ConnectionResolverInterface
{
    ...

    /**
     * The application instance.
     *
     * @var \Illuminate\Foundation\Application
     */
    protected $app;

    ...

    /**
     * 新規の DataBaaseManagerインスタンスを作成する
     * @param  \Illuminate\Foundation\Application  $app
     * @param  \Illuminate\Database\Connectors\ConnectionFactory  $factory
     * @return void
     */
    public function __construct($app, ConnectionFactory $factory)
    {
        $this->app = $app;
        $this->factory = $factory;
    }

    ...

    /**
     * Database Connection インスタンス を取得する。
     * @param  string  $name
     * @return \Illuminate\Database\Connection
     */
    public function connection($name = null)
    {
        list($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];
    }
    
    ...

}

先ほど言ったように、DBファサードはインスタンス化されているため、__constructメソッドは実行ずみです。

では、connection()メソッドの内部を深掘りしていきましょう。
すると、PHP弱者には初めましての文法がたくさん...

確認していきます。

list($database, $type) = $this->parseConnectionName($name);

これは、List関数です。右辺の配列を順に左側の変数に代入してくれます。
http://php.net/manual/ja/function.list.php

https://www.sejuku.net/blog/24406
こちらの例がわかりやすいですね。引用します。

$fruits_array = ['りんご', 'オレンジ', 'メロン'];
 
//listの複数変数を配列から代入
list($apple, $orange, $melon) = $fruits_array;

今回の例では、parseConnectionName($name)で取得した配列の結果を順に代入している、ということのようですね。
parseってことはもともと繋がって入力されたものを分割するんでしょうね。
$nameは今回入力がないため、connectionメソッドのargsデフォルト値である$name = nullが適用されます。

DatabaseManager.php

    protected function parseConnectionName($name)
    {
        $name = $name ?: $this->getDefaultConnection();

        return Str::endsWith($name, ['::read', '::write'])
                            ? explode('::', $name, 2) : [$name, null];
    }

出たな、三項演算子!

?:と書かれると「ん?」となりますが、怖がることはありません。

$name = $name ?: $this->getDefaultConnection();

$name = $name ? $name : $this->getDefaultConnection();

と同義です。

もっというと、

if ( $name ) {
    $name = $name;
} else {
    $name = $this->getDefaultConnection();
}

です。
ということで、今回は前述のように$namenullなので、getDefaultConnection()メソッドが発動します。

DatabaseManager.php

    public function getDefaultConnection()
    {
        return $this->app['config']['database.default'];
    }

appは$appが代入されている箇所があります。つまり、先ほどと同じ、\Illuminate\Foundation\Applicationのインスタンスです。

では、$this->app['config']['database.default']とはなんでしょうか。

またこちらをreferします。
https://readouble.com/laravel/5.5/ja/facades.html

ConfigIlluminate\Config\Repository らしい。
というところまではわかったのですが、どこの値を参照しているかまでは追えず....
StackOverflowにも投げてみようかと思いまうす。
わかるかた、コメントいただけると嬉しいです。

一度立ち戻って、parseConnectionName($name)はコメント通り

Parse the connection into an array of the name and read / write type.

つまり、名前と読み書き権限の配列を返してくれるメソッドだと理解して次に進みましょう。

随分上になってしまったのでconnection()メソッドを再掲します。

connection()

    /**
     * Database Connection インスタンス を取得する。
     * @param  string  $name
     * @return \Illuminate\Database\Connection
     */ 
    public function connection($name = null)
    {
        list($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];
    }

parseConnectionName($name)のおかげで、名前読み書き権限$database$typeに格納することができました。
そして、もしconnections[$name]が存在しなかったら、configure()メソッドを実行して、設定をしてくれます。

DatabaseManager.php
    /**
     * DatabaseConnectionインスタンスを準備する
     *
     * @param  \Illuminate\Database\Connection  $connection
     * @param  string  $type
     * @return \Illuminate\Database\Connection
     */
    protected function configure(Connection $connection, $type)
    {
        $connection = $this->setPdoForType($connection, $type);

        // First we'll set the fetch mode and a few other dependencies of the database
        // connection. This method basically just configures and prepares it to get
        // used by the application. Once we're finished we'll return it back out.
        if ($this->app->bound('events')) {
            $connection->setEventDispatcher($this->app['events']);
        }

        // Here we'll set a reconnector callback. This reconnector can be any callable
        // so we will set a Closure to reconnect from this manager with the name of
        // the connection, which will allow us to reconnect from the connections.
        $connection->setReconnector(function ($connection) {
            $this->reconnect($connection->getName());
        });

        return $connection;
    }

そして、注目して欲しいのが戻り値の型。

@return \Illuminate\Database\Connection

となっていますね。

Connectionクラスを確認してみましょう。

vendor/laravel/framework/src/Illuminate/Database/Schema/Connection.php

Connection.php
<?php

namespace Illuminate\Database;
...
use Illuminate\Database\Schema\Builder as SchemaBuilder;
...
class Connection implements ConnectionInterface
{
    ....
    /**
     * 接続のために SchemaBuilderインスタンスを作成する
     *
     * @return \Illuminate\Database\Schema\Builder
     */
    public function getSchemaBuilder()
    {
        if (is_null($this->schemaGrammar)) {
            $this->useDefaultSchemaGrammar();
        }

        return new SchemaBuilder($this);
    }
    ...
}

なんか見覚えのある奴がいませんか?

...そう、'Builder'クラスだ!

覚えていない人のためにもう一度。create()メソッドがいる場所でしたね。

vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php

Builder.php
<?php

namespace Illuminate\Database\Schema;

use Closure;
use LogicException;
use Illuminate\Database\Connection;

class Builder
{
    ...
    /**
     * schema上にテーブルを作成する。
     */
    public function create($table, Closure $callback)
    {
        $this->build(tap($this->createBlueprint($table), function ($blueprint) use ($callback) {
            $blueprint->create();

            $callback($blueprint);
        }));
    }
    ...
}

このBuilderクラスがConnectionクラスでuse Illuminate\Database\Schema\Builder as SchemaBuilder;として呼び出されています。つまり、SchemaBuilderとして先ほどのBuilderクラスが使われているわけですね。
そして、getSchemaBuilder()というメソッドでSchemaBuilder, すなわち Builderクラスのインスタンスを作成しているんですね!
(ちなみに名前をわざわざSchemaBuilderとしているのは、 ...\ Query \Builder もuseしているためです。こちらをQueryBuilderとして名前を分けています。)

SchemaBuilder($this)

$thisとは、Connectionクラス自身を指します。

では、改めてSchema/Builder.phpの__constructor(クラスがインスタンス化された時に呼び出されるメソッド)を確認しましょう。

Builder.php
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
        $this->grammar = $connection->getSchemaGrammar();
    }

ここで注目するのはgetSchemaGrammer()です。Builderをインスタンス化するときに、Schemaの文法を取得しろ、と命令しているわけです。
怪しくなってきましたね。

ということで、再びConnectionクラスに戻り、getSchemaGrammer()を確認します。

Connection.php
    public function getSchemaGrammar()
    {
        return $this->schemaGrammar;
    }

このschemaGrammerは setSchemaGrammar()によって定義されます。

Connection.php
    public function setSchemaGrammar(Schema\Grammars\Grammar $grammar)
    {
        $this->schemaGrammar = $grammar;
    }

...頭がごっちゃごちゃになったので一旦ここまでで勘弁してください....

参考

How Laravel Facades Work and How to Use Them Elsewhere
ファサードの概念を掴むのにめっちゃわかりやすい
Laravel Facades
これもファサードの解説
PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応
サービスコンテナの概要はこの本。

21
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?