読者です 読者をやめる 読者になる 読者になる

tohokuaikiのチラシの裏

技術的ネタとか。

Confluenceのプラグイン開発を承ります。ご連絡はこちらのホームページからお願いいたします。

「Laravelリファレンス」3 データベース

マイグレーション

つらつらっと読んで気になったところだけメモ

artisanのmake:migrationコマンドで作るClass名の重複に注意

コマンド引数の実行内容(下記の場合はcreate_authors_table)は同じものが被るとClass名として被ってしまい、Fatalになるので気を付ける。

./artisan make:migration create_authors_table --create=authors

migrationファイルが不要になった時は composer dump-autoloadしてautoloadファイルを作り直す

マイグレーションファイルを削除したのに亡霊のように出てきてオカシイ

$ ./artisan migrate:refresh


  [ErrorException]
  include(/home/vagrant/laravel/sample/3/vendor/composer/../../database/migrations/2016_12_26_105
  300_create_authors_table.php): failed to open stream: No such file or directory

何かと思ったら、 ./vendor/composer/autoload_static.php ./vendor/composer/autoload_classmap.php でこの作ったファイルをローディングしていた。

$ composer dump-autoload

して解決。

migrateコマンドの--pretendオプションが効かないっポイ

SQLを見せてくれるはずのオプションなんだけど

$ ./artisan migrate --pretend


  [Doctrine\DBAL\Schema\SchemaException]
  There is no column with name 'name' on table 'authors'.

ちゃんと動かない・・・。

多分、ALTERが入っている時にはそのTableが既に作られて無いとダメなんだと思う。

同じように、

<?php
    public function up()
    {
        if (Schema::hasTable('authors')) {

ってやったらちゃんと判定してくれなかった。

実migrate実行時には正しく判定してたのでおそらくConnectionをしてない状態でやってるからだと思う。

スキーマビルダ

これもメモ程度で。

  • columnの削除はBlueprint $table->dropColumn(カラム名)で。*1
  • columnの作成とプロパティの設定は、$table->string('name')->nullable()のようにチェインでColumnオブジェクトを使う。
  • indexの作成は、$table->index('name') あるいは、$table->string('name', 255)->index() とする。どちらも追加引数としてインデックス名を指定できる。
  • indexの作成はインデックス名を省略できるけど、削除の際はできない。カラム名を指定しても勿論ダメ。
  • columnの変更の場合は、取得したcolumnに->change()メソッドを掛けてやることで変更の指示が加わる。
<?php
    public function up()
    {
        Schema::table('authors', function (Blueprint $table) {
            $table->index('name');
            $table->string('katakana')->index();
            $table->string('furigana', 10)->change()->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('authors', function (Blueprint $table) {
            $table->dropIndex('authors_name_index');
            $table->dropIndex('authors_katakana_index');
        });
    }

これ、実際には何度かmigrate → rollbackを繰り返してチェックしないときれいなMigrationは作れないな。

シーダー(あるいは初期投入テスト用データ)

  • 最初のLaravelプロジェクトの作成時に作られるdatabase/seeds/DatabaseSeeder.php のrunメソッドが実行される。
  • run()実行の際にEloquentのMass Assignment制約を解除するためにModel::unguard();を実行する。
  • 各テーブルのデータ投入については、 ./artisan make:seeder Hoges でHogesテーブルのSeedを作り、できたファイルのrunメソッドを実装する。
  • ↑のrunメソッドでは、各カラムを要素に持った配列を作ってDB::table('authors')->insert($author)
  • ↑のrunメソッドでは、Eloquentを利用したデータの挿入も行える。\App\Author::create([...])
  • ↑のクラスにtruncateTable($table)をつくり、各Seedのrun内の$this->call()の前にそのテーブルのtruncateを実行させる。
  •  ↑truncateTableの際に外部キー制約を解除するためDB::statement('SET foreign_key_checks = 0');を実行する。(MySQLの場合)

シーダーの実行

./artisan db:seed

でシード投入

./artisan db:seed --class=AuthorTableSeeder

でAuthorTableSeederのみ実行。

./artisan migrate:refresh --seed

マイグレーションの再実行(rollback&migrate)からseedの投入まで一発

DBファサード

select

<?php
$result = DB::select(select文);
$result = DB::select(プレースホルダ付きselect文, [param1 => "foo" , param2 => "bar"]);
foreach ($result as $r){
    echo $r->name;
}

プレースホルダは、

select from authors where name=:name and email=:email

というSQLに対して、第二引数を

<?php
["name" => "ITOH", "email" => "itoh@example.com"]

という感じで与える。

あるいは、

select from authors where name=? and email=?

に引数を

<?php
["ITOH", "itoh@example.com"]

いずれもプレースホルダはクオートで囲む必要は無い。

返り値は、config/database.phpの'fetch' => PDO::FETCH_CLASSで設定したものになる。

selectの繋ぎ先

read/writeでDBを分けている場合、readの方に繋ぐ。強制的にwriteの方に繋ぎたい場合は、DB::selectFromWriteConnection()を使う。

INSERT

DB::insert(INSERT文); でBooleanが返ってくる。

UPDATE

DB::update(UPDATE文); で影響を受けた行数が返ってくる。

DELETE

DB::delete(DELETE文); で影響を受けた行数が返ってくる。

ステートメントの変更

DB::statement(SET文やDROP文など); でBooleanが返ってくる。

接続先の変更

普通にDB::select()などをした場合、config/database.phpの「'default' => env('DB_CONNECTION', 'mysql'),」で指定されているものに繋ぎに行く。この場合、.envにDB_CONNECTIONが無かったらmysqlに行くようになる。

config/database.phpのconnectionsキーの別のものに繋ぎにいきたい場合、DB::connection()を使う。

<?php
DB::connection('mysql2')->select(...)

トランザクションの指定

DB::transaction()とクロージャを使う

<?php
DB::transaction(function(){
    /// ここにさまざまなSQL処理を行う。
});

特にRollbackやBeginを行う必要は無い。一応手動で、DB::commit()/ DB::rollback()/ DB::begin()はある。

クエリログをとりたい場合

<?php
DB::enableQueryLog(); // これでクエリログが残る

////// ここにさまざまなSQL処理を行う。

$logs = DB::getQueryLog(); // クエリログを取得
dd($logs); // ddはブラウザにきれいに出力してくれるvar_dump()

実行SQLをとりたい場合

<?php
$sql = DB::pretend(function(){
/// ここにさまざまなSQL処理を行う。
});
Log::debug($sql); // ログファイルにデバッグ出力

クエリビルダ

プレースホルダを使わなくてもいいっていう所が若干楽かも。

Eloquent ORM

やっぱりクエリビルダよりもORM欲しいですよね。

LaravelはFacadeとフォーム周り(特にValidateorとエラー処理)が肌感覚にあうので、あとはこのORMがしっかりしてればもう言うこと無し。

デフォルト設定

テーブルや主キーについて、以下の設定で作られている。

  • テーブル名はClass名と同じ。table_name => TableNameという感じの対応。変更する場合は、$tablesプロパティで指定。
  • 主キーは、idフィールド。変更する場合は、$primaryKeyプロパティで指定。
  • タイムスタンプが必ず付く。created_atとupdated_at。$timestampsプロパティをfalseにするとタイムスタンプ無し
  • タイムスタンプのフォーマットは、Y-m-d H:i:sだがUnixtimestampにしたい場合は $dateFormatを"U"にする。
  • $connection => database.phpの設定値。必ずどこのDBに取りにいくか決まっている場合
  • incrementing 主キーが自動増加か。デフォルトはtrue

select系

  • all()メソッドで全件取得
  • where('name', "=", "itoh")などのクエリビルダが使える。

insert/update系

  • newして、propertyをセットしてsave()メソッド。(new User())->save()
  • あるいは、updateメソッドの引数に新しいプロパティ配列を渡して実行(save()の際の引数にこれを渡しても無効)
  • updateメソッドの便利なところはEloquent::where()->で取ってきたものに対してそのまま->update()をチェインできるので複数のデータに対して処理が一括でできること。

MassAssignmentエラー

サンプルのUser.phpは既に対策済みなのでこのエラーが出なかった。

要は、update()やsave()においてHTTPからの入力を許可して良いものと悪いものがあるということか。

正確には、HTTPからというより $model->propertyA みたいに直接プロパティを変更しようとするとダメってことね。

$fillable/$guardedのどちらかを使うとこのエラーは出なくなる。

ホワイトリスト形式で混入を防ぐ

Model継承クラスに $fillable プロパティを設定し、このプロパティの中に直接プロパティを変更してもよいものを羅列する。 これ以外のプロパティを変更しようとしても変更はできない。

ブラックリスト形式で購入を防ぐ

$fillableとどうように$guardedを作り、この中のプロパティは設定しようとしても変更できない。

$guardedと$fillableは同時には使えない。エラーとかにはならない。$guardedが優先されるようだ。

変更を制限されたプロパティを変更したい場合は

Modelに独自にSetterなどのメソッドを用意してやればいい。

削除

<?php
Author::find(1)->delete();
Author::destroy(1); // これは上と全く同じ
論理削除

Illuminate\Database\Eloquent\SoftDeletesを使う

  1. tableにdeleted_atフィールドをtimestamp型で作成する
  2. ModelクラスにSoftDeletesをtraitさせる。
  3. Modelクラスの$datesプロパティ配列にdeleted_atを加える。*2

すると、destroy()やdelete()が論理削除になる。

論理削除すると、Modelのメソッドのfindやallではselectの対象にならない。その他、以下のような特徴がある。

  • $model->trashed() で論理削除済みかどうかが分かる。
  • Model::withTrashed()->find()を使うと論理削除済みのものも取得できる。
  • restore()で復帰できる
  • forceDelete()で物理削除

usersテーブルを例にとってやってみる。

migrationを作る
./artisan make:migration  alter_table_user_add_softdeletes --table=users

できたClassファイルに

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AlterTableUserAddSoftdeletes extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->softDeletes()->after('id'); // add (afterは無くても大丈夫)
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropSoftDeletes(); // add
        });
    }
}

とする。2行加えただけ。 ./artisan migrate で実行。

Modelクラスを変更

NamespaceでuseしてTraitでuse。$datesプロパティ設置

<?php
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model 
{
    use SoftDeletes;

    $dates = ['deleted_at'];

}

アクセサとミューテータ(getterとsetterの仲介者)

get(set)プロパティ名Attributeがそれにあたる。これは、Modelのset/getマジックメソッドで使っているものでその媒介に使われるもの。

getの場合はAttributeの生値$valueを受けてそれをどう変形するかを記載する。

setの方は、Model::setAttributeを上書きする形で$this->attributesに値を納めるような処理をする。

日付の扱い

$datesプロパティを配列にしてにフィールドを入れると、$model->date_at がCarbonクラスのンスタンスになる。$model->created_at,$model->updated_atと同じ扱い。

シリアライズ あるいはModelのプロパティを全て一気に表示したい場合の話

toArray/toJsonメソッドを使う。1つのModelでも複数のModelでも同じ。

<?php
$u = User::find(1)->toArray();
$a = Author::all()->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
dd($u, $a);

toJsonの引数は表示オプション。

シリアライズに含めたくないパラメータ

ブラックリスト方式 → $hidden にそのProperty名を入れる。

ホワイトリスト方式 → $visibleにそのProperty名を入れる。

リレーション

リレーションを作りたい場合は、Modelクラスに対応するTableの名称のメソッドを作る。

1対1の場合は、メソッド名が単数形(にすると分かりやすいというだけで必須ではない)+hasOne

<?php
class Author extends Model {

    public function phone()
    {
        // コード規程に則ったものであれば、author_id, idの2つの引数は不要。
        return $this->hasOne(Phone::class, 'author_id', 'id');
    }

1対多の指定はメソッド名が複数形(にすると分かりやすいというだけで必須ではない) + hasMany

<?php
class Author extends Model {

    public function books()
    {
        // コード規程に則ったものであれば、author_id, idの2つの引数は不要。
        return $this->hasMany(Book::class, 'author_id', 'id');
    }
books()をクエリビルダとしてコールする

メソッドとしてコールした場合、クエリビルダが返ってくるので、こういうこともできる。

<?php
Author::find(1)->books()->where('title', 'LIKE', 'Laravel%');

(Properyコールのマジックメソッドとして呼ばれた場合はModelのCollectionが返ってくる。)

逆向きのリレーション

phoneテーブルにauthor_idがあり、PhoneオブジェクトからAuthorを取得しようとする場合。

<?php
class Phone extends Model
{
    public function author()
    {
        // コード規程に則ったものであれば、author_id, idの2つの引数は不要。
        return $this->belongsTo(Author::class, 'author_id', 'id');

ところで何気に複数形ってどうやって決めてるの?

何故EloquentModelはクラス名を複数形にしたテーブルを自動で見に行けるのか - QiitaでPluralizerクラスがやってるっていうのでそこだけ抜き出して調べてみた。

<?php
use Illuminate\Support\Pluralizer;

$r = ['fish', 'carp', 'salmon', 'trout', 'sheep', 'cattle', 'deer', 'Japanese', 'Chinese', 'Korean' ];
foreach ($r as $_r){
    printf("%s => %s<br>", $_r, Pluralizer::plural($_r)); 
}
fish => fish
carp => carp
salmon => salmon
trout => trout
sheep => sheep
cattle => cattles
deer => deer
Japanese => Japanese
Chinesse => Chinesses
Korean => Koreans

実態は、vendor/doctrine/inflector/lib/Doctrine/Common/Inflector/Inflector.php らしい。これ見たら沢山単語があった。

ということで、テーブルの複数形が気になったらこのクラスを使って実際に複数形を見てみることが大切なような気がする。

中間テーブルのあるリレーション

中間テーブルの規約
  • テーブル名は「Aテーブル名(単数形)_Bテーブル(単数形)」である。
  • 中間テーブルには、Aテーブルの主キーとBテーブルの主キーのフィールドとIDとタイムスタンプのみ
SELECT

このリレーション先を取得するメソッドを作成し、belongsToManyの第一引数に中間テーブル越しのリレーション先のClassを指定する。

以下は、authors => author_author_type => author_types とRDBMSの存在する際の例。

<?php
class Author extends Model
{
    public function types()
    {
        return $this->belongsToMany(AuthorType::class);
    }

中間テーブルにある別のフィールドが欲しい場合は

<?php
class Author extends Model
{
    public function types()
    {
        return $this->belongsToMany(AuthorType::class)->withPivot('id');
    }

とすると、

<?php
foreach (Author::find(1)->types as $type){
     echo $type->pivot->id ;
}

とすることで、中間テーブルのIDが取得できる。

タイムスタンプが欲しい場合は

<?php
class Author extends Model
{
    public function types()
    {
        return $this->belongsToMany(AuthorType::class)->withTimestamps();
    }

とすると、$type->pivot->created_atが使えたりする。

ちなみに、types()メソッドはBuilderを返すので、

<?php
Author::find(1)->types()->where('name', 'like', '%i%')->get();

とすることで、AuthorTypesクラスのインスタンスをそのまま配列で受け取れる。

->typesも->types()もどちらもAuthorTypesインスタンスのコレクションを返すようになっている。全く同じであり、typesはtypes()の省略形(最後の->get()が不要)である。

簡単にAuthorTypesの単一フィールドnameだけを一覧で欲しい場合は、

<?php
Author::find(1)->types()->where('name', 'like', '%i%')->lists('name');

とするとStringの配列で取得できる。

hasメソッドでデータのあるもののみを取得
<?php
$book_authors = Author::has('books')->get();
// 3つ以上あるauthorを取得
$book_authors = Author::has('books', '>=', 3)->get();
whereHasメソッドでリレーション先のデータのフィールドのWhere句を作る
<?php
$php_book_authors = Author::whereHas('books', function($query){
    $query->where('title', 'like', '%PHP%');
})->get();

これで作られるSQL

select * from `authors` 
    where (select count(*) from `books`
              where `books`.`author_id` = `authors`.`id` and
                    `title` like "%18%")
           >= 1
Eager Loading

使う時はその後foreachでまわすような場合。

<?php
Book::with('author')->all();
複数のプロパティをEager Loadingする場合(引数を2つ)。更に一段向こうのテーブルからも含める場合(ドット区切りで示す)。
<?php
$pref = Prefecture::with('author', 'author.phone')->where('id', 13)->get();
// これだと動作しない。Builderからwith()は効かない。
// $pref = Prefecture::find(13)->with('author', 'author.phone');
foreach ($pref as $p){
    foreach ($p->author as $a){
        if ($a->phone){
            echo $a->phone->phone_number."<br>";

これで使われるSQL

select * from `prefectures` where `id` = 13;
select * from `authors` where `authors`.`deleted_at` is null and `authors`.`prefecture_id` in (13);
select * from `phones` where `phones`.`author_id`
    in ("1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21");

の3本。author.phoneが無くても動作するが、SQLの発行数が多くなる。

Eager Loadingの対象から更に絞込み

対象となるテーブルの指定を配列にして、'table' => クロージャで指定。

<?php
$pref = 
  Prefecture::with(['author' => function($query){
                                    $query->where('name', 'like', '%3');
                                }],
                    'author.phone')->where('id', 13)->get();
元となるデータを取得したあとにEager Loading

取得結果に対してloadメソッドを使う。

<?php
$pref = Prefecture::where("name", 'LIKE', '%県')->get();
$pref->load('author');
1対N × 1対Nの場合(Has Many Through)

1つのModelのIDに複数のModelがからみ、さらにそのModelに複数のModelが絡んでいる場合。

「(著者の)出身県」→「著者一覧」→「(著者の)本一覧」

というリレーションの場合に、出身県別の本を取得したい場合。

<?php
class Prefecture extends Model
{
    public function books()
    {
        return $this->hasManyThrough(Book::class, Author::class);
//      引数を全て書く場合は下記のとおり。中間テーブルが持つ自分の主キー・最終テーブルが持つ中間テーブルの主キー・自分の主キー
//      return $this->hasManyThrough(Book::class, Author::class, 'prefecture_id', 'author_id', 'id');

Preferctureから直接Bookを取るメソッドをはやす。このメソッドでは、hasManyThroughを使う。引数の順番は「最終的に取るもの」が最初。

主キーが規則どおりで無い場合は逐一指定する。

CREATE/UPDATE
リレーション先への更新

親となるIDを含んだまま更新して欲しいということで。

こんな感じ。

<?php
$author = new Author();
$author->fill([
            'name' => '伊藤貴史',
            'prefecture_id' => 14,
            'furigana' => 'フリガナ',
            'romaji' => 'itoh takashi',
            ])->save();

$phone = new Phone();
$phone->fill([
            'phone_number' => '09000000000'
            ]);
$author->phone()->save($phone);

あるいは $author=Author::create(['name'...])で得てもかまわない。

ただ、$phoneは自ら$phone->save()しようとすると、外部キー制約に引っかかりINSERTできない。必ず、$author->phone()からsaveしてやる必要がある。

リレーションしている親を更新する場合

自分がbelongsしている親を変える場合は、associate()メソッドを使う。

<?php
$phone = Phone::find(28);
echo $phone->author_id; // 36
$phone->author()->associate(Author::find(35));
echo $phone->author_id; // 35
N対Nの場合の更新

中間テーブルがある場合、どちら側からにしても

<?php
$author = Author::find(1);
$type_ids = $author->types()->lists('name'); // 0,1,2,3,4
$author->types()->detach(6);
$type_ids = $author->types()->lists('name'); // 0,1,2,3,4,6 

逆側から

<?php
$type = AuthorType::find(1);
$type->authors()->attach(20);

子モデルが変わった際に、親モデルのupdated_atを変更する

自分がbelongsTo()しているテーブルに対してその通知を記録したい場合

<?php
class Phone extends Model
{
    protected $touches = ["author"];

    public function author()
    {
        $this->belongsTo(Author::class);
    }

$touchesプロパティに対象(単数形)を入れることで自動的に記録される。

ちょっと思ったこと・・・プロパティ呼び出しとメソッド呼び出しのどちらを使うのか判定

リレーションがある場合、

$model->relation か $model->relation() かどっちを使う?っていう場合。

単純にインスタンスとしてのリレーションが欲しかったら、$model->relationで、relationの先に何かをつなぎたい場合。。。。relationをwhere句で狭めたり、relationのAssosiationIDを変更したり、という場合は $model->relation()を使うって言う感じかな。

*1:何故かこの本ではdropIndex()は説明してるのにこれが無かった。まぁ想像できるけどさ。。。

*2:これは論理削除の実行に特に要らないみたいだが、日付をCarbonインスタンスとして扱うのにやっておくと便利

「Laravelリファレンス」 2-3 基本コンポーネント

環境設定

.envファイル

このファイルは、.gitignoreに含める。

APP_ENVで本番環境(production)、ユニットテスト環境(testing)、それ以外の開発環境(任意)を設定できる。

値は、env()ヘルパ関数で取得できる。配列は設定できない。

config/*.phpファイル

アプリケーションの設定値を記述。

値は、Config::get('foo.bar')で、config/foo.phpのbarキーを取得する。.を更につなげてハッシュの更に奥まで指定できる。

動作環境の設定

config/*php内で.envを使って設定する。

$GET/$POST/$COOKIE/$SERVER

Requestファサードで取得。

<?php
$foo = Request::input('foo'); // $_GET['foo']/$_POST['foo']
// デフォルト値付き
$foo = Request::input('foo', 'default value');
// 全取得
$vars = Request::all();
// foo barのみ取得
$foobar = Request::only(['foo', 'bar']);
// foo bar以外取得
$foobar = Request::except(['foo', 'bar']);
// ファイルインスタンス(UploadedFile)を取得
$file = Request::file('foo');
// cookie値を取得
$cookie_foo = Request::cookie('foo');
// $_SERVERを取得
$server = Request::server();
// getallheaders
$headers = Request::header('Accept-Language');

ブラウザに返却するResponseを組み立てるResponseインスタンス

ControllerやRouteの返却値にResponseインスタンスを返すといい感じにしてくれる。

<?php
// Responseの作り方
$response = Response::make('hello world');
$response = response('hello world'); // 上と同じ

// Viewにつながる渡し方
$response = Response::view(View::make('template.file'));
$response = Response::view(view('template.file')); // 上と同じ

// Jsonを出力
$response = Response::json(['foo' => 'bar']);

// ファイルダウンロード
$response = Response::download('file-path');

// Locationによるリダイレクト
$response = Response::redirectTo('/foo/bar');
$response = redirect(action('FooController@getIndex')); 

// Cookieを追加してリダイレクト
$response = Response::redirectTo('/foo/bar')->withCookie('foo', 'bar');
$response = Response::redirectTo('/foo/bar')->withCookie(cookie()->forever('foo', 'bar'));

ルーティング

routes/web.php にマッチするものがあればその時点で評価は終了し、次には回らない。

名前付きルーティング、便利。

ルーティング自体にパラメータをつけてController(Closure)でそれを取得したい場合、

<?php
Route::get('/home', [
  'param1' => 'foo/bar',
  function(){
    $actions = Route::current()->getAction();
    return $actions['param1']; // foo/barが表示される。 
  }
]);

パラメータをパスに含んでいる場合

<?php
// スラッシュによる区切り
Route::get('/foo/{id}/{param}', function($id, $param){ 

// {id}はある正規表現によって制約をつけることができる。ただし、これは{id}を使ったRoute設定の前におかないとダメ
Route::pattern('id', '^[a-z]+$'); // ^と$は省略可能

// このルートの{id}だけ制約をつけたい場合
Route::get('/foo/{id}/{param}', function($id, $param){ 
})->where(['id' => '0[a-z]+', 'param' => 'a[a-z]+']);

暗黙のController

この機能は5.3でなくなったので省略

RESTfulコントローラ

<?php
Route::resource('resource', 'ResourceController', [オプション]);

オプションには下記がある。 * names => メソッド名とルート名の対応の変更ができる。通常は、ルート名は{resource名}.{Controllerメソッド} となる。 * only => RESTメソッドの制約ができる * except => RESTメソッドの除外ができる

ルート名というのは何なんだろう?

・・・あぁ、わかった。RESTのルート名とは関係なくて単にLaravelでルートを識別するための名称のことか。Route::getとかだと単一のルーティングを指定するのに対して、RESTfulにルーティングを決めると複数のルート名をいっぺんに決めるので個々の設定をReoute::get()->name('foo')でできないってことね。URIを自在に変えられるのかと思った。

あと、RESTfulであってRESTを提供しているわけでは無い。

ミドルウェア

よくある感じのフィルタ機能。フィルタというのはLaravelでもその名称であるらしいがミドルウェアに変わっていった。

3段階の設定場所があり、それぞれに実行に入る前の処理(Before)と実行後の処理(After)が行える。登録の方法は、App/Http/Kernel.phpに記述する。

AfterとBeforeの指定の方法

こっちだとAfter

<?php
class AfterHoge
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        // ここに処理


        
        return $response;
    }
}

これだとBefore

<?php
class BeforeHoge
{
    public function handle($request, Closure $next)
    {
        // ここに処理
        
        
        
        return $next($request);
    }
}
グローバルなミドルウェア
php artisan make:middleware Hogehoge

で雛形を作れる。これをKernel.phpに登録して完了。

Routerに仕込むミドルウェア
// get/post
<?php
Route::get('user/{id}',[
    'middleware' => ['hoge', 'hoge.b','hoge.a', ],
    function($userId){
    }
]);

// resourceは以下のようにはできないのでControllerに仕込む
Route::resource('user', 'UserController', [
    'middleware' => 'hoge'
]);
Controllerに仕込むミドルウェア

constructで実行させる。

<?php
class UserController extends Controller {
    public function __construct() {
        $this->middleware('hoge.b');
        $this->middleware('hoge.a');
    }

もちろん、これでもBefore/Afterの区別をつけて処理してくれる。

バリデーション

基本的なシンタックス

<?php
$inputs = ['name' => 'Tanaka']; // GET/POSTで与えられる配列
$rules = ['name' => ['required', 'min:4']];
$validator = Validator::make($inputs, $rules);
if ($validator->fail() === true) {
    abort(404);
}

という感じ。バリデーションルールは、「ルール名:パラメータ」という感じ。

バリデーションルールの拡張

簡単にできる。

<?php
use Validator;
Validator::extend(新ルール名, function($attr, $value, $params){
    return false ; // => エラー
});

$attrには、$inputsのキーが入る。上記の場合"name"が出てくる。

・・・・と、これ、どこに仕込ませればいいのだろう?と疑問に思って調べてみると

あたりが見つかった。extend使うよりクラスを作った方がいいよね・・・。こういうところ、この本読んでて思うことがしばしば・・・。なんか、こう、詰めが甘いというかサンプルコードがあって動くしコメント見てな的な・・・。

で、このextendは app/Providers/AppServiceProvider.php のbootメソッド内に書けば使える。

条件付きバリデーションルール

Validator::sometimeメソッドを使う。

<?php
$val = Validator::make($inputs, $rules);
$val->sometimes('追加したいINPUT名', [追加したいルール], function($入力値) use ($他の入力値) {
    // ここに判定条件。trueをリターンすると追加したいルールが適用される。
});
エラーメッセージの変更

Validator::makeの第三引数に配列として入れてやる。

[
'required' => ':attributeは必要です。'
]

この際、:attirbuteは自動的に「name」や「email」などの入力項目に変換される。

あるいは、resources/lang/ja/validation.php

<?php
    'custom' => [
        'name' => [
            'required' => ':attribute は入れなくても良いけど入れないと登録できません。',
            ],
    ],

をいうように指定する。また、このvalidation.phpのrequiredキーに書くとrequiredの際のエラーメッセージを一括で変更できる。

また、nameを「名前」にするのも、このvalidation.phpで指定している。

Controllerにおけるvalidateの簡略化

Validator::makeしてfailsをチェックして元のURLに戻るのは定型なので、Illuminate/Foundation/Validation/ValidatesRequests.php にvalidateメソッドがある。BaseControllerとなるapp/Http/Controllers/Controller.phpはこれをtraitしているのでvalidateメソッド一発で失敗した際に前の画面に戻るなどの処理も行ってくれる。

この「前のURL」を取る処理は、Illuminate/Routing/UrlGenerator->previous()であり、Refererがあればそれを、なければSessionから取り出す。てか、いまさら気づいたけどLaravelってセッションスタート前提なのか。

なので、アクションメソッドの範囲で

<?php
$this->validate($request, $rules, $custom_message);
// バリデーション通過後

とするだけで構わない。

フォームリクエストでこれらの設定をControllerからRequestクラスへ

$GET/$POSTはRequestクラスで受け取るのだけど、ControllerにFormの情報であるnameの羅列やValidationルールを書きたくない。そんな感じでロジックをRequestクラスに詰め込んでしまう。

$ ./artisan make:request NameFormRequest

でひな形作成。

<?php
    public function authorize()
    {
        // このフォームへのアクセス権限。とりあえずtrueにしておかないと403 Forbiddenが返る
        return true;
    }

    public function rules()
    {
//        $userId = $this->route()->parameters();
        $userId = $this->route('one');
        $user = User::findOrFail($userId);
        
        return [
            'name' => 'required|max:255',
            // 更新時、ユーザー自身のメールアドレスとの重複は許す
            'email' => 'required|email|max:255|unique:users,email,'.$user->id,
            'password' => 'min:6',
        ];
    }

    /**
     * カスタムエラーメッセージはここに入れる。
     *
     * @return array
     */
    public function messages()
    {
        return [
            'required' => ':attributeが足りません。',
            'name.required' => ':attributeを正しく入れてください。',
            ];
    }

    /**
     * 要素名はここに入れる。
     *
     * @return array
     */
    public function attributes()
    {
        return [
            'name' => 'お名前'
            ];
    }
    

ルーティングによるパラメータは、$this->route('one')で取得できる。2つ目はtwo... これは vendor/laravel/framework/src/Illuminate/Routing/ControllerInspector.php で設定されている模様。

このフォームクラスを使うには、Controllerのアクションメソッドで

<?php
    public function postEdit(NameFormRequest $request, $userId) {

としてしまえば自動的に$requestにはNameFormRequestクラスのインスタンスになる。

LaravelのClassのオートロードを読んでみて、Facadeがどうやって短い名前でCallされているかを考える

本丸まで

とりあえず、読み込み順としては

  1. public/index.php
  2. bootstrap/autoload.php
  3. vendor/autoload.php
  4. composer/autoload_real.php
  5. vendor/composer/ClassLoader.php
  6. vendor/composer/autoload_classmap.php とくる。

Composer\Autoload\ClassLoaderで実行されていること

このClassで、ClassMapを持ってそのクラスとファイルの対応表が vendor/composer/autoload_classmap.php にあるのでその一覧からClassを読み込んでいる。

とりあえず今手元のautoload_classmap.phpを見てみたら、3000個近くのClassが列挙してあった。

この3000個の中にあるClassであれば、 Symfony\Component\CssSelector\XPath\Extension\FunctionExtension というのも、

new Symfony\Component\CssSelector\XPath\Extension\FunctionExtension

だけで使えるようになる。

Facadesは何故いきなりuseだけで使えるようになるのか?

これ、結構わかりづらい。

  1. vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterFacades.php で config/app.php のClassエイリアスを登録
  2. vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php のloadによりautoloadの一番優先度の高いautoloadとしてregisterされる
  3. autoloadされた時点で、class_aliasによりClassのエイリアスを作る

これにより、例えばMiddlewareファイルで

<?php
namespace App\Http\Middleware;

use Auth;
error_log(class_exists('Auth') ? 'Auth exists': 'none');

とすると、

  • use Authの時点ではまだAuthというクラスに対する実装は決まっていない
  • class_existsを実行した時点でautoloadが働く
  • Authクラスのエイリアスは、config/appでIlluminate\Support\Facades\Authとなっている

ことから、Illuminate\Support\Facades\Authクラスがautoloadされるようになっている。

ついでに、このIlluminate\Support\Facades\Authクラスから実際の$authインスタンスが決められる手順

結論から言うと、

vendor/laravel/framework/src/Illuminate/Foundation/Application.php

のregisterCoreContainerAliases()により決定されている。

このregisterCoreContainerAliases()により、authファサードだと'Illuminate\Auth\AuthManager', 'Illuminate\Contracts\Auth\Factory'の2つの・routeファサードだと、'Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar'の2つのクラスがその対象になるのである。

エイリアスの一覧は、そのregisterCoreContainerAliasesメソッドに20個ほど記述がある。

aliasからのインスタンス生成

このファサードエイリアス名と関連付けられたClassのインスタンスがFacadeの実体として実行されるのだが、この候補のうちからどれを選択するか?なのか?

候補が詰まった、Facade::$app(Illuminate/Foundation/Applicationのインスタンス)は、Illuminate/Container/Container.phpを継承しておりこれはArrayAccessをインプリメントしている。なので、このFacade::$app[$name]した時の挙動はContainerの実装に委ねられている。

・・・と考えてまでコード読み切れなかったので挫折。

なんだけど、これ実は候補じゃなくって単に1つ目のClassがインスタンスになっているだけという

このregisterCoreContainerAliases()内のローカル変数$aliasesって、キーの値の配列が

2つの場合 (router, auth) =>
* 1つ目が普通のClass * 2つ目がinterface

3つの場合 (cookie)=>
* 1つ目が普通のClass * 2つ目がinterfaceの親クラス * 3つ目が普通のClassがimplementしているinterface

3つの場合 (queue)=>
* 1つ目が普通のClass * 2つ目がinterface * 3つ目もinterface(2個目)

となっている。ということは、Facadeで呼ばれるのは1つ目のClassになる。

これに意味があるんだろうか?

と思って、authの2つ目のinterfaceクラスを削除したら

Illuminate \ Contracts \ Container \ BindingResolutionException
Target [Illuminate\Contracts\Auth\Factory] is not instantiable while building [Illuminate\Auth\Middleware\Authenticate].

ってエラーが出た。

なんじゃこりゃ・・・。

デバッグトレースを見てみると(filp/whoopsが無かったらとてもできる代物ではない)、middlewareの"auth"呼び出しの際にそのインスタンス解決に対して

array:1 [▼
  0 => ReflectionParameter {#169 ▼
    +name: "auth"
    position: 0
    typeHint: "Illuminate\Contracts\Auth\Factory"
  }
]

となっていた。

おそらくであるが、middlewareのauthに対して「"Illuminate\Contracts\Auth\Factory"」を実装したものであればFacadeで登録されていたものであっても使えるようにするという方針があるのではないか?

ということで、それっぽいところがMiddleware使ってるところで無いかと調べてみると

vendor/laravel/framework/src/Illuminate/Session/Middleware/AuthenticateSession.php

のコンストラクタでタイプヒントに使っていた。

ということは、AuthenticateSessionをnewする際の引数として

  • Illuminate\Contracts\Auth\Factory がTypeHintとして与えられる
  • そのTypeHintから実装可能なClassを探す
  • 探す手段として、FacadeのApplicationインスタンスで登録されているものを探す
  • 無かった場合、そのTypeHintを直接Newしてみる。
  • TypeHintがinterfaceだった場合、Fatalで終了

ということなんだな。

ところで、PHPって関数やクラスをNewする際にその関数名やクラス名からタイプヒントのClassを取れるの?

ReflectionParameterクラスを使うことでできるっぽい。

んだけど、Reflection使うのは本当に大変そうなのでちょっと実際にどうやってるかはあとで。

LaravelのFacadeがどうやってメソッドを決定しているかコード読んだのでメモ

例えばAuthの場合。

Auth::guest()のメソッドはどのインスタンスが作られるのか?

  1. Authクラスは直接的には、Illuminate/Support/Facades/Auth.php のStaticメソッドがコールされる。
  2. Authクラスは、getFacadeAccessor() return 'auth'; のみだが、このauthは Illuminate/Foundation/Application.php のregisterCoreContainerAliases()により、'Illuminate\Auth\AuthManager'に結び付けられている。
  3. AuthManagerは、create***Driver()がコールされる。このメソッドでドライバが作られる。このドライバはGuardクラスのObjectを返す
  4. この場合、***の部分は、AuthManager->getDefaultDriver()で取得でき、この値は、applicationのconfig/auth.phpで設定される。EloquentはLaravelのデフォルトORM
  5. Illuminate/Auth/Guard.phpインスタンスのメソッドがFacadeで実行されるメソッドである。
Illuminate\Support\Facades\Auth 
Illuminate\Support\Facades\Facade 
Illuminate\Auth\AuthManager
Illuminate\Auth\Guard

んー、つか。これGuardクラスが完全に外部になっているのか。

DatabaseとEloquentと2個あって、それぞれがUserテーブルに対する実装になっていてこれはサンプルとしてなかなか参考になりそう。

PHPのself::とstatic::の違い

PHP: static キーワード - Manualのコメント読んでわかったのでメモ。

<?php
class a {

    static protected $test="class a\n";

    public function static_test(){

        echo static::$test;
        echo self::$test;

    }

}

class b extends a {

    static protected $test="class b\n";

}

$obj = new a();
$obj->static_test();

echo "\n";

$obj = new b();
$obj->static_test();
?>

$ php static_test.php
class a
class a

class b
class a
  • static::$foo は、上書きされる可能性がある
  • self::$foo は、上書きされない基底クラスのstaticを読みに行く

なんつーか、感覚的には逆な感じのするキーワードだな。