tohokuaikiのチラシの裏

技術的ネタとか。

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

MySQLのForeignキー指定時の自動命名則

今更だけど、MySQLのForeignキーの自動命名則。

たとえば、こんなテーブルを作った時。

CREATE TABLE `author_author_type` ( 
  `id` int (10) unsigned NOT NULL AUTO_INCREMENT,
  `author_id` int (10) unsigned NOT NULL,
  `author_type_id` int (10) unsigned NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`),
  FOREIGN KEY (`author_type_id`) REFERENCES `author_types` (`id`)
) ENGINE = InnoDB; 

一旦作って、show create tableすると

CREATE TABLE `author_author_type` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `author_id` int(10) unsigned NOT NULL,
  `author_type_id` int(10) unsigned NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  KEY `author_id` (`author_id`),
  KEY `author_type_id` (`author_type_id`),
  CONSTRAINT `author_author_type_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`),
  CONSTRAINT `author_author_type_ibfk_2` FOREIGN KEY (`author_type_id`) REFERENCES `author_types` (`id`)
) ENGINE=InnoDB;

というようになる。

自動で作成されるもの

INDEXが張られる。

自前のフィールド名でINDEX名が付けられる。

CONSTRAINTが付けられる

FOREIGNキーを作った順番にibfk_1,ibfk_2…というように自動的に制約名が付けられる。

この制約名とかINDEX名がダサイという場合は、きちんと自分で指定しろってことですね。

Atlassian JIRAとConfluenceのバージョン情報が分かるRSSを作った

ConfluenceとJIRAがいつの間にかバージョンアップしてるので、把握できるようなRSSを配信できるスクリプト組んだ。

httpが面倒だったのでwget前提。

以下の3つのファイルを置いて atlassian-products-jira.php atlassian-products-confluence.php 二アクセスすればOK

FeedのネタにしてるJSONが変わったら適当に変更しなければならない。

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);
    }
}