tohokuaikiのチラシの裏

技術的ネタとか。

LaravelのメソッドのTypeHintがどうやってパラメータをうまい具合にセットしているのか?

Routingクロージャや、Controllerメソッドで

<?php
Route::get('/', function (Authenticatable $user) {

とか

<?php
public function index(Authenticatable $user)

とした時に、どうやってこの$userを良い感じで\App\Userにしてくれるのかを追ってみた。

タイプヒンティングの取り方

Reflectionを使うところから知らなかったのでまずそこから。

Reflection使ってMethodやConstructorの情報を得るのは下記のようにする。参照

<?php
class Foo {
    public function __construct(Bar $bar, Foo $foo) {
    }
    public function methodA(Bar $bar, Foo $foo){
    }
}

class Bar {
    public function __construct() {
    }
    public function methodA(Bar $bar){
    }
}

getRef('Foo');
getRef('Bar');

function getRef($class)
{
    $reflection = new ReflectionClass($class);
    $params = $reflection->getConstructor()->getParameters();
    echo "Constructor params\n";
    foreach ($params AS $param) {
        echo "    ". $param->getClass()->name . "\n";
    }
    echo "---\n";
    
    $params = $reflection->getMethod('methodA')->getParameters();
    echo "methodA params\n";
    foreach ($params AS $param) {
        echo "    ". $param->getClass()->name . "\n";
    }
    echo "---\n";
}

というので、Reflectionを使うことでMethodが何のクラスを欲しがっているかを解決できる。

Laravelでは、Illuminate/Routing/RouteDependencyResolverTrait.phpのReflectionでどうこうやってるみたい。

Routingのクロージャを使った場合

<?php
Route::get('/', function (Authenticatable $user) {

この場合、Illuminate/Routing/Route.php のrunCallable()がコールされる。

<?php
    /**
     * Run the route action and return the response.
     *
     * @return mixed
     */
    protected function runCallable()
    {
        $parameters = $this->resolveMethodDependencies(
            $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses'])
        );

        $callable = $this->action['uses'];

        return $callable(...array_values($parameters));
    }

このresolveMethodDependenciesにより、$this->action['uses']はタダのタイプヒントからObjectになっている。

RouteDependencyResolverTrait::resolveMethodDependencies()メソッドから・・・

  1. RouteDependencyResolverTrait::resolveMethodDependencies()
  2. RouteDependencyResolverTrait::transformDependency()
  3. Illuminate/Foundation/Application::make("Illuminate\Contracts\Auth\Authenticatable")
  4. Illuminate/Container/Container.php::make("Illuminate\Contracts\Auth\Authenticatable")
  5. "Illuminate\Contracts\Auth\Authenticatable"は、"Illuminate\Auth\AuthServiceProvider"のインスタンスが当該のObjectを作ってくれると判定。このAuthServiceProviderインスタンスにこのAuthenticatableクラスのインスタンスを作らせる。(GoFのBuilderパタン)

では、どうやってAuthenticatableはAuthServiceProviderが作ってくれると判明(設定)したのか?

Illuminate/Foundation/Application の$bindingプロパティに依る。

この$bindingにこのauthの対応を登録しているのは、Illuminate/Auth/AuthServiceProvider.php

<?php
    protected function registerUserResolver()
    {
        $this->app->bind(
            AuthenticatableContract::class, function ($app) {
                return call_user_func($app['auth']->userResolver());
            }
        );
    }

になる。

ここでようやく、$app['auth']がregisterCoreContainerAliases()でヒモづけられた'Illuminate\Auth\AuthManager'であり、そのAuthManagerがconfig/auth.phpのproviders/users/modelを参照しているということになる。

ここまで抽象化されるとわからんな。。。

Laravel リファレンス 4-1 認証

さて、だいたい慣れてきた。折角なのでここからは与えられたサンプルコードを使わずに一から作っていこうと思う。
もちろんサンプルコードは参考にする。

とりあえず、プロジェクトの開始

すっかり忘れてしまったのだけど、.bashrcに

export COMPOSER_HOME=~/.composer
export PATH=$PATH:~/bin:$COMPOSER_HOME/vendor/bin

と書き、~/.composer/composer.json

{
    "require": {
        "laravel/installer": "~1.1"
    }
}

としているので、~/.composerにて $composer updateする。もちろん、~/bin/にcomposer.pharを入れている。すると、Laravel Installerによるlaravelコマンドが任意の場所で使える。

プロジェクトの作成

$ laravel new ~/laravel/auth_sample

でとりあえず、雛形作成。キー生成も済んでいる。

データベースの作成

作るテーブルの情報はP.158の表4.2と表4.3の通り。

マイグレーションの作成

usersテーブルを作るMigration

$ ./artisan make:migration create_user_table --create=users
Created Migration: 2016_12_29_124332_create_user_table

password_resetsテーブルを作るMigration

$ ./artisan make:migration create_password_resets_table --create=password_resets


  [InvalidArgumentException]
  A CreatePasswordResetsTable migration already exists.

???あれ、既にある。

$ ls database/migrations/
2014_10_12_000000_create_users_table.php  2014_10_12_100000_create_password_resets_table.php  2016_12_29_124332_create_user_table.php
$ ./artisan make:migration create_users_table --create=users


  [InvalidArgumentException]
  A CreateUsersTable migration already exists.

あるじゃん・・・というか、usersテーブルもあるんだ。

ということで、さっき作った2016_12_29_124332_create_user_table.phpを削除して、composer dump-autoloadする。

マイグレーションの実行

$ ./artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
$ mysql -u **** -p**** laravel_authsample -e "show tables"
+------------------------------+
| Tables_in_laravel_authsample |
+------------------------------+
| migrations                   |
| password_resets              |
| users                        |
+------------------------------+

作成完了。

Modelクラスを作る

$ ./artisan make:model User
Model already exists!

・・・あれ、もうできてる。

認証機能の設定

コントローラの配置

書籍によると、

  • app/Http/Controllers/Auth/AuthController.php
  • app/Http/Controllers/Auth/PasswordController.php

の2つが既にあるらしい。

5.3だとそれらは無いのだが、

  • app/Http/Controllers/Auth/RegisterController.php
  • app/Http/Controllers/Auth/ResetPasswordController.php

の2つがある。見た感じそれぞれが対応しそうである。

認証の設定

config/auth.php が本の5.1とは随分と異なっている。

肝心な部分はここだと思われる。

<?php
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

このままでおそらく本書と同じであろうからそのまま。

メッセージファイル

resources/lang/ja/passwords.php を作成。面倒なので、本書のサンプルコードをそのままコピー。

ルーティング

Routingを行う。

routes/web.php

<?php
Route::resource('/register', 'Auth\RegisterController');

を追加。

実装開始

とりあえず・・・

雛形のAuth\RegisterControllerは、Illuminate/Foundation/Auth/RegistersUsers をtraitしているのでこのクラスを使ってみる。

GET/indexを追加

<?php
    protected function index() {
        return $this->showRegistrationForm();
    }

いきなりLaravel5.1と5.3での違いに戸惑う

で、次はpost周りを・・・と思ったけど Illuminate/Foundation/Auth/RegistersUsers.phpが5.1と5.3では大きく変わってる。

あれー、と思って調べるとHow to register a user in Laravel 5.3?で「Route::auth()」使うんやでっていうコメントが。

Illuminate/Routing/Router.php のauth()を見てみると、あー。。。なんか色々とRoutingしてくれている。

ということで、 routes/web.php

<?php
// Route::resource('/register', 'Auth\RegisterController');
Route::auth();

と変更。Auth\RegisterControllerに付け加えた protected function index()も削除。

findすると vendor/laravel/framework/src/Illuminate/Auth/Console/stubs/make/views/auth/register.stub なるスタブもある。

んんん~~。と思い、調べると、artisanにmake:authコマンドがある。

結局、make:authコマンドの実行がベスト

./resources/views/layouts/app.blade.php
./resources/views/auth/register.blade.php
./resources/views/auth/passwords/reset.blade.php
./resources/views/auth/passwords/email.blade.php
./resources/views/auth/login.blade.php
./resources/views/home.blade.php
./app/Http/Controllers/HomeController.php

が追加されていた。

routes/web.phpには以下の2行が追記されていた。もはや、Route::auth()メソッドですらない。

<?php
Auth::routes();

Route::get('/home', 'HomeController@index');

Authの機能について

Authファサードでよく使いそうな機能について

Auth::attempt()

普通のHTTPの場合なので、実態は、Illuminate/Auth/SessionGuard.php

  • 第1引数:usersテーブルに合致する情報の配列。ただし、passwordをキーに持つ値の場合はhasherによりHash化される。
  • 第2引数:trueにすると自動ログ引用トークンを発行。そのトークンを持っているだけでログインできる。
  • 第3引数:trueにすると認証が正当な場合にログイン処理を行う。

実際には、Illuminate/Foundation/Auth/AuthenticatesUsers.php のlogin()にて使われている。 これを直接使うのはあまり無いかも知れない。

Auth::check()

ログイン状態のチェックをする。guest()メソッドはこれの反転。Bladeテンプレートでも @if (Auth::guest()) とかで簡単に使える。

Auth::viaRemember()

Tokenによるログインを行ったかどうか。

セッションが切れてCookieのRememberトークンによるログインになった場合はこれがtrueになる。

Auth::user()

App\Userオブジェクトを返してくれる。

Controllerメソッド(あるいはRoutingの際のClosure)のTypeHinting

これ、どうやって実装してるんだろう・・・・。

Middlewareではなく、config/auth.phpで設定される何かから引っ張ってきているんだろうとは思うんだけど。

何となくこんな感じというのを調べてみた。 LaravelのメソッドのTypeHintがどうやってパラメータをうまい具合にセットしているのか? - tohokuaikiのチラシの裏

Illuminate\Http\RequestをTypeHintingに使うと、$request->user()でユーザー情報が取れる。

RoutingのClosureで使う場合

<?php
use Illuminate\Http\Request;

Route::get('/', function (Request $request) {
    dd($request->user());
    return view('welcome');
});

Controllerメソッドで使う場合。

<?php
use Illuminate\Http\Request;

class HomeController extends Controller
{
    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        dd($request->user());
        return view('home');
    }
Illuminate\Contracts\Auth\Authenticatable をTypeHintingに使うとApp\Userオブジェクトがそのまま入ってくる。

ただし、この場合は認証を通過していないと引数にnullが入ってしまい

Controller::index() must implement interface Illuminate\Contracts\Auth\Authenticatable, none given

のエラーが出てしまう。

従って、

<?php
    public function __construct()
    {
        $this->middleware('auth');
    }

として、middlewareにてAuthチェックを行うなどする必要がある。

Auth::login() / Auth::loginUsingId()

どちらも任意のログインユーザーでログインする。テストを書く時に使う。

<?php
Auth::login(new \App\User::find(1));

Auth::loginUsingId(1);

Auth::once()

1リクエスト内で有効なログイン状態を作る。それ以外はAuth::attempt()と同じ。

Laravel5でDBのQueryログを出すEvent Listenerを作る

app/(Handlers/)Events/QueryLogTracker.php

雛形は、

$ ./artisan make:event QueryLogTracker

で作る。

<?php

namespace App\Handlers\Events;

use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class QueryLogTracker
{
    /**
     * Create the event handler.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  illuminate.query  $event
     * @return void
     */
    public function handle($query, $bindings, $time, $name)
    {
        Log::debug(sprintf("Query: %s | Bindings: %s",
                           $query,
                           json_encode($bindings)
                           ));
    }
}

app/Providers/EventServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use DB;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'App\Events\SomeEvent' => [
            'App\Listeners\EventListener',
            ],
    ];
    
    
    
    /**
     * @brief 
     * @param 
     * @retval
     */
    public function __construct($app)
    {
        parent::__construct($app);
        
        if (env('APP_DEBUG')){
            $this->listen['illuminate.query'] = [
                'App\Handlers\Events\QueryLogTracker',
                ];
        }
    }

    /**
     * Register any other events for your application.
     *
     * @param  \Illuminate\Contracts\Events\Dispatcher  $events
     * @return void
     */
    public function boot(DispatcherContract $events)
    {
        parent::boot($events);
    }
}

「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クラスのインスタンスになる。