Home > PHP > Laravel

Laravel Archive

Laravel の Queue で非同期処理を実装する(beanstalkd / IronMQ / SQS)

この記事の所要時間: 1654

Laravel で実装されている Queue について見てみました。

laravel

Laravel では Queue を使うことで、時間がかかる処理や、時間差で実行したい処理を非同期で実行することができます。

Laravel 4.2 の Queue では、以下の 5 つのキュードライバをサポートしています。

  • sync
  • Beanstalkd
  • Amazon SQS
  • IronMQ
  • Redis

ここでは、sync、Beanstalkd、IronMQ、Amazon SQS について試してみました。

Laravel での設定

Laravel で Queue を使うには、app/config/queue.phpにて、利用するキューエンジンの選択、設定を行います。

もちろん他の設定と同じく、app/config/local/queue.phpapp/config/production/queue.phpなど、環境に応じて設定を切り替えることも可能です。

下記が設定例です。defaultでは利用するキュードライバを指定します。指定できる値は、sync, beanstalkd, sqs, iron, redis の 5 種類です。

connectionsキーでは、それぞれのドライバについて接続情報を指定します。下記では、beanstalkdの接続情報を指定しています。

return [
    'default' => 'beanstalkd',
    'connections' => [
        'beanstalkd' => [
            'driver' => 'beanstalkd',
            'host' => 'localhost',
            'queue' => 'default',
            'ttr' => 60,
        ],
    ],
];

キューへジョブを登録

キューへのジョブを登録するには、Queueクラスのpushメソッドを使います。

第一引数にはジョブを実行するワーカーのクラス名を、第二引数にはワーカーに渡すパラメータを指定します。

use CarbonCarbon;

Queue::push('MyWorker', ['message' => 'Hello!', 'date' => Carbon::now()]);

ここでは、ワーカーのクラス名のみを指定しているため、ジョブを実行する際はfireメソッドが呼ばれます。他にもMyWorker@doJobとすることで任意のメソッドを実行したり、クロージャを渡して、そのクロージャをワーカーとして実行したりできます。

詳しくはドキュメントを参照してください。

http://laravel.com/docs/queues
http://laravel4.kore1server.com/docs/42/queues

ワーカーの実装

ジョブを処理するワーカーとしてMyWorkerクラスを実装します。ワーカーは特定のクラスを継承する必要はなく、 POPO(Plain Old PHP Object)で良いです。

MyWorkerクラスにはfireメソッドを実装します。ジョブがキューに登録されると、リスナーからこのメソッドが呼ばれます。

第一引数にはジョブインスタンス、第二引数では、Queue::push()の第二引数で指定したパラメータが渡されます。ただ、このパラメータはジョブに入る時にjson_encode()されるので、オブジェクトのインスタンスはプロパティの連想配列となります。

下記の実装では、渡されたパラメータを元に文字列を生成して、echoで標準出力へ出力しているだけです。

ジョブが正常に完了したら、delete()メソッドで、ジョブを削除しておきます。これを行わないと同じジョブが何度も実行されています。

<?php
use CarbonCarbon;
use IlluminateQueueJobsJob;

class MyWorker
{
    /**
     * @param Job $job
     * @param array $data
     */
    public function fire(Job $job, array $data)
    {
        echo sprintf('[%s] %s at %s', Carbon::now(), $data['message'], $data['date']['date']) . PHP_EOL;
        $job->delete();
    }
}

キューの監視

キューを監視してワーカーを起動するリスナーを起動します。

Laravel では artisan コマンドですでに用意されているのでこれを利用します。

php artisan queue:listenコマンドを実行すると、キューを監視状態になります。この状態でキューにジョブが登録されていると、自動でワーカーが起動して処理が実行されます。

$ php artisan queue:listen

この状態でジョブが登録されると下記のように出力されます。

$ php artisan queue:listen
[2014-08-14 16:52:35] Hello! at 2014-08-14 16:52:34.000000
Processed: MyWorker

このプロセスが終了しているとキューにジョブが登録されても実行されないので、supervisor や monit などで常時起動するように設定しておくと良いでしょう。

一定時間後に実行するジョブ

Queue::push()で登録したジョブは、リスナーが感知するとワーカーが起動して実行されます。

それとは別に、Queue::later()というメソッドを使うと、一定時間経過後に実行するジョブを登録することができます。

第一引数には、ジョブの実行を開始する時間を指定します。数値を渡すと秒数として認識され、その秒数が経過した際にジョブが実行されます。Carbonクラスのインスタンスを渡すとCarbonクラスで指定された日時に実行されるジョブとして登録されます。

第二引数と第三引数は、Queue::push()の第一引数と第二引数と同じです。

下記の例では、10秒後に実行されるジョブを登録しています。

Queue::later(10, 'MyWorker', ['message' => 'Delayed', 'date' => Carbon::now()]);

リスナー側では下記のような出力になります。[]内の日時と、atの後ろの日時で 10 秒ずれていることが分かります。

$ php artisan queue:listen
[2014-08-14 16:52:45] Delayed at 2014-08-14 16:52:34.000000
Processed: MyWorker

これはローカルの beanstalkd での実行なのでずれが無いですが、IronMQ を利用した際は、数秒のずれがありました。多少のずれは発生するので、それを認識した上で利用すると良いでしょう。

サポートしているキュードライバ

Laravel でサポートしているキュードライバ(Redisを除く)を見てみましょう。

sync

デフォルトで指定されているドライバです。

仕組みとしてはキュー、ワーカーの流れを通るのですが、ジョブが登録されると、即時にワーカーが実行されます。

非同期処理にはならないので、実際にキューを使う場面では、他のドライバを利用する必要があります。

Beanstalkd

Beanstalkd は、オープンソースのキューイングシステムです。

利用するには、Beanstalkd 自体のインストールが必要になります。

Beanstalkd は、yum などパッケージでも公開されているので、利用するプラットフォームごとに選択してインストールすると良いです。

ここでは OSX 環境を想定して、Homebrew でインストールします。

$ brew install beanstalkd

beanstalkd を起動します。デフォルトではポート11300で待ち受けます。

$ beanstalkd

なお、beanstalkd は、デフォルトではキューの情報を永続化しません。このままだと beanstalkd が落ちるとキューの情報が消えてしまうので、本番環境などで運用する際は、永続化するように設定を行うのが良いです。

http://kr.github.io/beanstalkd/

Laravel で利用する際は、composer で下記のパッケージを追加しておきます。pda/pheanstalk は、3.x がリリースされていますが、Laravel 4.2 は、2.x 系にしか対応していないので、バージョン番号に注意して下さい。

"require": {
  "pda/pheanstalk": "~2.1.0"
}

IronMQ

Iron.io が提供しているメッセージキューサービスです。

ブラウザから登録するだけで利用することができます。基本は有償サービスなのですが、無料プランが用意されており、1,000,000APIリクエスト/月まで利用することができます。

グラフィカルな管理画面が用意されており、キューの状況などが分かりやすいのが良いです。

Heroku の Addon としても提供されているので、Heroku へデプロイするなら簡単に連携することができます。

Laravel で利用する際は、composer で下記のパッケージを追加しておきます。

"require": {
  "iron-io/iron_mq": "~1.5.1"
}

下記が Heroku 環境で IronMQ を利用する際の設定例です。Heroku では、接続情報が環境変数で提供されてるので、それらをtokenprojectに設定しています。

return [
    'default' => 'iron',
    'connections' => [
        'iron' => [
            'driver' => 'iron',
            'host' => 'mq-aws-us-east-1.iron.io',
            'token' =>  getenv('IRON_MQ_TOKEN'),
            'project' => getenv('IRON_MQ_PROJECT_ID'),
            'queue' => 'sample',
            'encrypt' => true,
        ],
    ],
];

AWS US-EastAWS EU-WestRackspace ORDRackspace LONのインスタンスを利用することができます。日本国内のインスタンスは存在しないので、国内のアプリケーションからキューを登録する場合はレイテンシが気になるかもしれません。

http://www.iron.io/

SQS

AWS のメッセージキューサービスです。

こちらも管理画面からキューを作成するだけで利用することができます。有償サービスですが、無料枠が用意されており、100万件キューイングリクエスト/月まで無料で利用することができます。

東京リージョンを利用できるので、アプリケーションサーバが国内にあるなら利用しやすいですね。

Laravel で利用する際は、composer で下記のパッケージを追加しておきます。

"require": {
  "aws/aws-sdk-php-laravel": "1.*"
}

設定例は下記です。ここでは、アクセスキーやシークレット、エンドポイントURL を環境変数で渡しています。AWS ではおなじみですが、regionにキューを作成したリージョンを指定するのを忘れないようにしましょう。

    'default' => 'sqs',
    'connections' => [
        'sqs' => [
            'driver' => 'sqs',
            'key' => getenv('AWS_ACCESS_KEY_ID'),
            'secret' => getenv('AWS_SECRET_ACCESS_KEY'),
            'queue' => getenv('AWS_SQS_URL'),
            'region' => 'ap-northeast-1',
        ],
    ],

http://aws.amazon.com/jp/sqs/

ユニットテスト

キューにジョブを登録する側のテストについてです。

QueueクラスをshouldReceiveメソッドでモック化して、テストするのが手軽です。

<?php
use CarbonCarbon;

/**
 * Class QueueTest
 */
class QueueTest extends TestCase
{
    /**
     * @test
     */
    public function queuePush()
    {
        $now = Carbon::create(2014, 8, 13, 12, 34, 56);
        Carbon::setTestNow($now);

        Queue::shouldReceive('connected')->once();
        Queue::shouldReceive('push')->once()->with('MyWorker', ['message' => 'Hello!', 'date' => $now]);
        Queue::shouldReceive('later')->once()->with(10, 'MyWorker', ['message' => 'Delayed', 'date' => $now]);

        $this->client->request('GET', '/queue/push');

        $this->assertTrue($this->client->getResponse()->isOk());
    }
}

サンプルアプリケーション(Heroku)

このエントリの内容を実装したサンプルアプリケーションを github に公開しています。

コード自体はシンプルで、http://localhost/quque/push にアクセスするとジョブがキューに登録されます。あとは php artisan queue:listen コマンドでジョブが実行されます。

Heroku で試せるように、heroku_create というシェルスクリプトで、heroku 関連のコマンドを記述しています。このコマンドを流せば、Heroku アプリケーションが構築されます。

Heroku でのポイントは、`Procfile`で、ワーカープロセスとしてリスナーを起動するという点です。

web: vendor/bin/heroku-php-apache2 public/
worker: php artisan queue:listen

このアプリケーションを Heroku にデプロイすると、web(ジョブをキューに登録する側)、worker(リスナー)の Dyno が構築されます。

あとは Heroku の管理画面で worker の Dyno 数を 1 にすると、web 経由で登録したジョブが worker によって実行されるようになります。

https://github.com/shin1x1/laravel-queue-sample

さいごに

Laravel の Queue について見てきました。

最後に、どのドライバを使うかについてですが、開発環境では beanstalkd を利用するのが手軽で良いでしょう。ローカルにインストールするので動作も速いです。

本番環境では、要件によりけりですが、国内にアプリケーションサーバがあるなら SQS、Heroku など US リージョンを利用するなら IronMQ が良さそうです。

もちろん、自前で beantalkd や Redis を立てるのも良いですが、利用できるなら、ありものを利用するのが楽ですね。

このように、開発環境と本番環境とでドライバを変えても、コード側は一切変更する必要が無いというのは、よく出来ていますね。

参考

blog.ISHINAO.net | Laravel 4でキューを使ってみる
laravel – キュードライバにbeanstalkdを使用する – Qiita

  • コメント (Close): 0
  • トラックバック (Close): 0

Laravel DB テーブルの簡易メンテ画面を作る Laravel-Table-Admin を作りました

この記事の所要時間: 532

laravel

マスタテーブルのメンテナン画面は、単純な CRUD 画面なのですが、テーブル数が多いと、いちいち作るのも手間がかかります。

そこで、Laravel のパッケージとして、Laravel-Table-Admin を作りました。

https://github.com/shin1x1/laravel-table-admin

Laravel の対象バージョンは、4.1 以降としています。

何をするものか

マスタテーブルのメンテナンス画面など、ごく単純な CRUD 画面だけを作るパッケージです。

作るといっても、コードジェネレータではなく、設定を行えば、自動でテーブルスキーマから画面を動的に構築します。CakePHP の Scaffold と似た発想です。

いわば、機能がごくシンプルになった phpMyAdmin / phpPgAdmin を Laravel アプリケーションに組み込めるというものです。

百聞は一見に一見に如かずなので、画面キャプチャを。

一覧画面です。

classes-1

編集画面です。

classes_edit-1

外部キーがある場合は、参照先テーブルの内容がプルダウンで選択できます。

riders-1

デモアプリケーションを公開しています。こういった画面を簡単に作ることができます。

http://laravel-table-admin.herokuapp.com/crud/classes

インストール

Composer でインストールします。

composer.json に以下を追加して、composer install or update して下さい。

{
    "require": {
        "shin1x1/laravel-table-admin": "~0.1.0"
    }
}

app/config/app.phpに ServiceProvider と Facade を追加します。

 'providers' => [
      // ....
      'Shin1x1\LaravelTableAdmin\TAbleAdminServiceProvider`
  ],
 'aliases' => [
      // ....
      'TableAdmin' => 'Shin1x1\LaravelTableAdmin\TableAdminFacade',
  ],

あとは、app/routes.php で、このパッケージで CRUD 画面を表示する対象のテーブルを指定するだけです。下記では、classes, nationalities, riders という3つのテーブルについて CRUD 画面を表示します。

TableAdmin::route([
    'classes',
    'nationalities',
    'riders',
]);

URL は、http://localhost/crud/{TABLE} になります。{table}の部分が、上記で指定したテーブル名になります。

これで、URL にブラウザからアクセスすると CRUD 画面が表示されます。

デモアプリケーションのコードも公開していますので、参考にして下さい。

https://github.com/shin1x1/laravel-table-admin-example

対象データベース

MySQL と PostgreSQL を対象としています。

対象テーブル

今のところ、プライマリキーがidで、一覧やプルダウンメニューでの表示カラムはnameに固定しています。

多くの場合、これで問題無いと思いますが、ニーズがあれば、設定で変更できるような対応も考えています。

実用する際のポイント

実際のところ、この画面は管理者用機能になるでしょう。TableAdmin::route()での指定では、Route::group()などを使って、認証をかけると良いでしょう。

// admin フィルターがあるとして
Route::group(['before' => 'admin'], function() {
    TableAdmin::route([
        'classes',
        'nationalities',
        'riders',
    ]);
});

Laravel-4-Generators

同様のことを行う Laravel パッケージとしては、Laravel-4-Generators が有名です。

これはとても便利なパッケージで、私も migration file のジェネレータとして良く利用しています。

大きく異なるのは、Laravel-4-Generators は、コードジェネレータですが、Laravel-Table-Admin はコードの生成は行なわず、動的にテーブルスキーマを読んで、画面を組み立てています。

双方とも良し悪しはあるのですが、動的に画面を生成した方が、画面デザインの修正などを一箇所で行うことができるので、後であれこれ変更する際は楽だと判断しました。

Laravel-4-Generators は、バージョン2 以降には、ビューテンプレートのジェネレートで、一覧画面やフォーム画面の詳細部分が生成されなくなりました。これにより、単純にジェネレートしても、CRUD画面が作れなくなっています。これも本パッケージを作ろうと思った動機の一つです。

さいごに

自分が欲しかったので、作ってみました。今のところ、必要最低限を実装したという状態です。

あとは、付加機能(一覧での検索や並び替え、テーブルカラムの対応等)を足していこうと思っています。コントリビュートもお待ちしています:D

https://github.com/shin1x1/laravel-table-admin

  • コメント (Close): 0
  • トラックバック (Close): 0

Heroku で作るスケーラブルな PHP アプリケーション

この記事の所要時間: 221

第16回関西PHP勉強会で、「Heroku で作るスケーラブルな PHP アプリケーション」という発表をしてきました。

heroku-logo-light

発表資料

Heroku でちゃんと動く PHP アプリケーションを作ると、自然とスケーラブルな構成になりますよ、という内容です。

会場でも、Heroku 自体は知っているが、まだ使ってはいないという人が多かったので、細かな Tips は省いて、こういった構成でやりますよというイメージをお話しました。

実際に構築する上での Tips などは、また別の機会に話してみたいです。

サンプルアプリケーション

サンプルアプリケーションとして、簡単な画像アップロードサイトを Laravel 4.2 で作りました。

https://github.com/shin1x1/laravel-on-heroku

アプリケーションデータは、以下のアドオンへ保存するようにしています。画像ファイルは、アドオンではなく、S3 に保存しています。

  • データベース = Heroku Postgres
  • ログ = Papertrail
  • セッションストレージ = Redis To Go
  • 画像ファイル = S3(AWS)

使い方は、README.md に記載しているのですが、Heroku 関係は、heroku_create というシェルスクリプトにまとめています。

これを流せば、Heroku アプリケーションの作成、環境変数追加、アドオン追加などをひと息で行うことができます。

#!/bin/sh

heroku create -r heroku
heroku config:set LARAVEL_ENV=heroku

heroku addons:add heroku-postgresql
heroku addons:add newrelic:stark
heroku addons:add scheduler
heroku addons:add sendgrid
heroku addons:add redistogo
heroku addons:add papertrail
heroku addons:add librato

デモ用に Heroku にデプロイしています。

http://infinite-caverns-8536.herokuapp.com/

さいごに

Heroku は、無料から使えるのが良いですね。アドオンも機能制限(保存レコード数等)版ながら無料で使えるものが多いので、アプリケーションからの連携を試すことができます。

スケーラブルな PHP アプリケーションを作る練習場として、Heroku を触ってみるというのも面白いですよ。

  • コメント (Close): 0
  • トラックバック (Close): 0

PHP で配列を走査して処理するのは、for / foreach だけじゃない

この記事の所要時間: 918

PHP で配列の要素にアクセスして、処理を行うには、for や foreach を使うのがおなじみです。

php-logo

この方法でも良いのですが、PHPには、それ以外にも配列を走査する関数やライブラリがあります。ここでは、配列を走査して処理を行う方法を見てみましょう。

サンプル仕様

このエントリで以下の配列を処理対象とします。array.phpで保存されている想定です。

<?php
return [
    [
        'id' => 1,
        'year' => 1993,
        'name' => 'Harada',
    ],
    [
        'id' => 2,
        'year' => 2001,
        'name' => 'Kato',
    ],
    [
        'id' => 3,
        'year' => 2009,
        'name' => 'Aoyama',
    ]
];

この配列について処理を行います。

  • 配列内に連想配列が格納されており、nameyearというキーを持つ
  • yearが、2000以上の要素のみ、結果配列に格納する
  • 結果配列には、nameyearを連結した文字列を格納する

求める結果は、以下になります。

array(2) {
  [0] =>
  string(8) "2001Kato"
  [1] =>
  string(10) "2009Aoyama"
}

foreach を使う

まずは、foreach を使う方法です。よくある手続き的なPHPコードですね。foreachで配列を回して、yearが2000以上の場合だけ、結果配列に値を入れています。

<?php
$array = include('array.php');

$result = [];

foreach ($array as $v) {
    if ($v['year'] <  2000) {
        continue;
    }

    $result[] = $v['year'] . $v['name'];
}

var_dump($result);

array系関数を使う

次に、filter と map を使って、実装します。PHP には、array_maparray_filter関数があるので、これを使います。

実装は下記になります。array_filterarray_mapを使うので、それぞれ配列の要素をフィルタリングする、要素に処理を行い、結果配列を格納するといった意図がより明確になります。

ただ、array_fileterarray_mapで引数の順序が異なるのと、2 行に分かれており、中間の結果を保持する一時変数が必要になるのが難点です。

<?php
$array = include('array.php');

$tmp = array_filter($array, function($v) {
    return $v['year'] >= 2000;
});

$result = array_map(function($v) {
    return $v['year'] . $v['name'] ;
}, $tmp);

var_dump($result);

試しに 1 行にまとめると下記になります。一見良さそうですが、このコードをぱっと見て、array_filterが先に適用されるのと認識できるでしょうか。

$result = array_map(function($v) {
    return $v['year'] . $v['name'] ;
}, array_filter($array, function($v) {
    return $v['year'] >= 2000;
}));

Laravel(Illuminate\Supportパッケージ)を使う

filter / map を使う別の例として、Illuminate\Supportパッケージ のIlluminate\Support\Collectionクラスを使います。

https://github.com/illuminate/support

Illuminate\Support パッケージは、Laravel を構成しているパッケージの一つで、フレームワークを使わずとも、このパッケージ単体でも利用することができます。

インストールするには、composer.jsonに以下のように指定して、composer installもしくはcomposer updateを実行します。

{
    "require": {
        "illuminate/support": "~4.2"
    }
}

Illuminate\Support\Collectionを使うことで、メソッドチェインで配列を操作することができます。

実装すると下記のようになります。filterメソッドでフィルタリングを行い、その結果配列に対してmapメソッドを実行して、結果配列の要素を作成していることが分かります。

<?php
use IlluminateSupportCollection;

require_once __DIR__ . '/vendor/autoload.php';

$array = include('array.php');

$result = Collection::make($array)->filter(function ($v) {
    return $v['year'] >= 2000;      // filter

})->map(function ($v) {
    return $v['year'] . $v['name']; // map

})->toArray();

var_dump($result);

Ginq を使う

Illuminate\Support\Collectionと似た機能を持つライブラリに、Ginq があります。こちらもメソッドチェインで配列への操作を行うことができます。

https://github.com/akanehara/ginq

インストールするには、composer.jsonに以下のように指定して、composer installもしくはcomposer updateを実行します。

{
    "require": {
        "ginq/ginq": "~0.1"
    }
}

Ginq を使って、実装すると、下記のようになります。配列を取り込むところ以外は、Illuminate\Support\Collectionと全く一緒になりました。

<?php
require_once __DIR__ . '/vendor/autoload.php';

$array = include('array.php');

$result = Ginq::from($array)->filter(function ($v) {
    return $v['year'] >= 2000;      // filter

})->map(function ($v) {
    return $v['year'] . $v['name']; // map

})->toArray();

var_dump($result);

さいごに

4 つのパターンで配列を走査して、結果配列を求めるという処理を書いてみました。

foreach は、配列の要素を走査していくという汎用的な役割なので、そのループの中で様々な処理を書くことができます。一方、array系関数やライブラリは、それぞれのメソッドで用途や目的が決まっているので、どのような処理を行い、結果、どのような解を求めているのが分かりやすいです。

また、filter や map という概念は、多くのプログラミング言語で利用されており、こうした概念をおさえておくと、別の言語でコードを書いたり、読んだりする際に、意図を汲むことができ理解しやすくなります。

foreach で書くことがダメだというわけではなく、それ以外の書き方が、PHPにもあるということを知っておくということが大事ですね。

Illuminate\Support\Collection と Ginq

Illuminate\Support\Collection と Ginq は、Linq to Object ライクなインターフェースを持ち、実際に使い方も似ています。

ただ、この2つで大きく違うのが、評価のタイミングです。

Illuminate\Support\Collection は、mapメソッドを実行したタイミングで即時に評価され、処理が行われます。

一方、Ginqは、遅延評価となっており、mapメソッドを実行してもすぐに map 処理が行われません。このエントリの例では、toArray()が実行されたタイミングで、はじめて map 処理が行われます。

実際に利用する際は、この評価タイミングの違いは、意識しておく必要があります。

参考

迫り来る「forおじさん」と呼ばれる時代

  • コメント (Close): 0
  • トラックバック (Close): 0

Heroku で Composer を使う時に気を付けたいこと

この記事の所要時間: 745

Heroku が PHP をサポートしたので、テストがてら Laravel アプリケーションをデプロイしてみました。

heroku-logo-light

デプロイしたのは、Doctrine を利用するアプリケーションだったのですが、ローカルでは composer でインストールできるのですが、Heroku にデプロイするとインストールされないという現象が起こりました。

Laravel での Doctrine 使用

今回のアプリケーションでは、DBのテーブルスキーマ情報を読み込んで、動的に画面を作るという処理があり、そこで Doctrine の SchemaManager を使っていました。

Laravel で、Doctrine の SchemaManager のインスタンスを取得するのは簡単で、下記のメソッドを実行するだけです。

$manager = DB::connection()->getDoctrineSchemaManager();

こんなあっさりなので、laravel/frameworkパッケージをインストールすると、Doctrine も入るものだと思ってました。

Heroku での Composer

Heroku へデプロイするコードのルートディレクトリにcomposer.jsonが含まれていると、PHPアプリケショーンとしてセットアップが行われます。(依存解決に Composer を使わない場合でも、空のcomposer.jsonが必要です。)

このcomposer.jsonでは、通常の依存関係を指定するだけでなく、実行するPHPバージョンや拡張の指定ができます。例えば、下記のようにするとPHP 5.5.12をランタイムにして、memcached拡張が利用できます。

{
  "require": {
    "php": "5.5.12"
    "ext-memcached": "*",
  }
}

この記法は、Composer としてはサポートしているのですが、Composer 自身がランタイムの変更などを行うわけではありません。

そこで、Heroku の Composer は独自拡張しているのだろうと考えました。まあ、この思い込みが惑わすわけですが。

あと、Heroku での PHP サポートは、まだベータなので、そのせいもあるのかなと思ってました。

https://getcomposer.org/doc/02-libraries.md#platform-packages

開発環境、別 PaaS では正常に動作

もちろん開発環境(OSX, Linux VM)では、Heroku にデプロイしたものと同じコード(composer.(json|lock))で問題無く動いていました。

別のPaaSを試してみようと思い、Pagodabox に設置すると、これも正常に動作しました。

Heroku と 別 PaaS で、vendor/ 以下を比べると、10個近くのパッケージがHerokuでは入っていませんでした。

なぜ Heroku だけ入らない。やっぱり、独自拡張のせい?ベータ版だから?

–no-dev!

この作業していたのが、夜中でした。Pagodabox にデプロイできたので、これで良しとして、その日は寝ました。

しかし、気になるのが、同じ現象をググっても、Laravel アプリケショーンが Heroku で動かない,
Doctrine が入らないなんて出てこないんですね。動かしてみた系の記事はあるのに。

翌日、もう一度、Heroku の公式のドキュメントを読み直すことにしました。

すると、ありました。ちゃんと書いてありました。実行する composer コマンドが。

Heroku では、下記のオプション付きで実行するようです。そう、--no-devオプション付きでね!

$ composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction

早速、手元で同じオプションで実行してみると、ちゃんとDoctrineがインストールされない。

ああ、これか。。。

Heroku PHP Support | Heroku Dev Center

–no-dev オプション

composer コマンドでは、いくつかオプションを指定することができ、--no-devはその一つです。

Composer公式サイトでは、下記の記述があります。--no-devオプションを付けると、依存解決する際にcomposer.jsonrequire-devセクションの内容は解決されません。

--no-dev: Skip installing packages listed in require-dev.

デフォルトでは、--devが付いているのと同じ状態になり、require-devセクションの内容も依存解決の対象となります。

https://getcomposer.org/doc/03-cli.md#install

依存パッケージのcomposer.jsonをチェック

アプリケーションが依存しているパッケージのcomposer.jsonを洗い出してみると、require-devにDoctrineを指定しているものはいくつかあれど、requireに指定しているものは見事にありませんでした。

つまり、開発環境やPagodaboxでは、--no-dev無しだったため、require-devセクションを含んで依存解決したため、Doctrine が入っていました。

かたや、Heroku では、--no-devありだったので、Doctrine が入りませんでした。おそらく Heroku だけに入らなかった他のパッケージも同様でしょう。

[解決] require に doctrine を指定

はい、原因は分かったので、解決方法です。

composer.jsonrequireに、Doctrine を追加しました。

    "require": {
        "php": ">=5.4.0",
        "laravel/framework": "4.1.*",
        "doctrine/dbal": "~2.3" // <--- 追加!
    },

Heroku にデプロイすると、バッチリ動きました!Herokuさん、疑ってゴメン。

実際にデプロイしたものが下記です。

http://laravel-table-admin.herokuapp.com/crud/classes

コードは、Github で公開しています。Heroku でも Pagodabox でもデプロイできているので、両 PaaS にPHPアプリケーションをデプロイする際は参考にどうぞ。

https://github.com/shin1x1/laravel-table-admin-example

さいごに

まあ分かってしまえば、単純というよくある話です。

Heroku が Composer を拡張しているかどうかは分かりませんが、ドキュメントには、composer 実行前に self-update しているという記述もあるので、Composer は標準のもので、別途compoer.jsonを見てランタイムを決定するシステムがあるのかもしれません。

あと、夜中に躓いたら、さっさと寝るということですね。睡眠大事。

追記

ラインタイムや拡張を実際に指定する処理は、PHP の buildpack に記述がありました。@iakio さん、ありがとうございました!

  • コメント (Close): 0
  • トラックバック (Close): 0

Laravel ユーザなら知っておくべきAuthオートログインのこと

この記事の所要時間: 425

Authフィルタによるオートログインについてです。

laravel

「ひとり Laravel Japan ツアー 2014」と称して、Laravel 福岡Laravel Meetup Tokyo vol.3 に参加してきました。

どちらでも発表を行ったのですが、ここでは、Laravel Meetup Tokyo で発表した Auth オートログインの資料を公開します。

知っておくべきAuthオートログイン

Laravel では Auth という認証を行う機能があるのですが、標準でオートログイン機能が実装されています。

Login::attempt() というログイン処理を行うメソッドの第二引数にtrueを渡すだけで、オートログイン用のクッキーが発行され、もしログインセッションが切れても、自動でオートログインが行われます。

とても簡単に使えるのは良いのですが、暗号化したクッキーでのみ認証を行うので、利用には注意が必要です。

詳細は資料を確認してみてください。

私は、オートログイン処理を自作して、カスタムドライバとして組み込むことで対応しています。
Laravel でカスタムドライバを使って Remember Me(オートログイン)を実装する

Auth フィルタによるオートログイン

このオートログインは、Auth フィルタを使っていると常に有効となっているので、アプリケーションでオートログインを使っているか否かに関わらず、影響があります。

該当のコードは以下です。

まず、Auth フィルタの定義です。app/filters.php で定義されており、Auth::guest() が認証が行われます。

Route::filter('auth', function () {
    if (Auth::guest()) {
        return Redirect::guest('login');
    }
});

Auth::guest() はファサードクラスで、実体は\Illuminate\Auth\Guard::guest()です。check()メソッドが呼ばれており、さらにその中でuser()メソッドが呼ばれます。

    public function guest()
    {
        return ! $this->check();
    }

user()メソッドが、認証の中核になります。前半では、セッションからログインユーザ情報を取得しています。もしセッションにユーザ情報が無ければ、後半でオートログイン処理を行います。

user()メソッドは、Authフィルタを呼べば、常に実行されるので、アプリケーションでオートログインを利用しているか否かに関わらず、オートログイン処理が実行されることが分かります。(セッションにログインユーザ情報が無く、オートログインクッキーの値があれば常に実行される)

    public function user()
    {
        if ($this->loggedOut) return;

        if ( ! is_null($this->user))
        {
            return $this->user;
        }

        $id = $this->session->get($this->getName());

        $user = null;

        if ( ! is_null($id))
        {
            $user = $this->provider->retrieveByID($id);
        }

        $recaller = $this->getRecaller(); // <--- オートログインクッキー値取得

        if (is_null($user) && ! is_null($recaller))
        {
            $user = $this->getUserByRecaller($recaller); // <--- オートログイン処理
        }

        return $this->user = $user;
    }

さいごに

このオートログイン仕様を受け入れるかどうかは使う人次第ですが、その場合、暗号鍵( app/config/app.php の key )は絶対に漏洩しないように扱う必要があります。

なお、Laravel ツアーは、福岡(+大分)と東京の Laravel ユーザと色々なアツい話ができて楽しかったです:D

日本では、まだ知っている人が使っているという感じですが、コミュニティとしての動きも出てきて、今後は盛り上がっていきそうですね。

  • コメント (Close): 0
  • トラックバック (Close): 0

Laravel コードで見るファサードクラスの仕組み

この記事の所要時間: 839

Laravel の特徴として良く挙げられるファサードクラスの仕組みをコードで見てみました。

laravel

Laravel のファサードクラス

Laravel では、Input::get()Route::get()など、クラスメソッドでフレームワークの機能を利用する場面が多くあります。

これは一見すると、InputやRouteクラスで提供されているクラスメソッドを実行しているだけに見えますが、これらのクラスにメソッドの実装があるわけではなく、実際はIoCコンテナに格納されているインスタンスのメソッドを実行しています。

例えば、InputであればIlluminate\Http\Requestクラス、RouteであればIlluminate\Routing\Routerのインスタンスメソッドが実行されます。

これらのインスタンスは、IoCコンテナにて管理されており、ファサードクラスのクラスメソッドが実行されると、IoCコンテナから定められたインスタンスを取得して、そのインスタンスメソッドを実行する仕組みになっています。

ファサードクラスの利用により、コードの記述がシンプルになります(ここは意見が分かれるところですが)。また、ファサードクラスでのメソッド呼び出しでは、自身では処理を行わずに、IoCコンテナに格納されたインスタンスなどに委譲します。つまり、ファサードクラスが呼び出すインスタンスを差し替えることで実際に処理を行うクラスを変えることができます。

Laravel のファサードクラスでは、shouldReceive()メソッドにて、テスト時に実行するインスタンスをモックオブジェクトと差し替える機能が標準で用意されています。

ファサードクラス実行の流れ

ではRoute::get()を例にして、ファサードクラスがどのように実行されるか見て行きましょう。

  • 1) ファサードクラスの解決
  • 2) ファサードクラスのクラスメソッド実行
  • 3) IoCコンテナからインスタンス取得
  • 4) インスタンスメソッドの実行

1. ファサードクラスの解決

まずRouteクラスの解決を行います。実は、Routeクラスは、フレームワークでは定義されていません。

では、なぜRouteクラスのクラスメソッドが実行できるのかというと、class_alias()関数にて、存在するクラスの別名として、Routeを定義するためです。

この処理はIlluminate\Foundation\AliasLoaderにて行います。

Routeクラスへのアクセスがあると、クラス定義を探すためにオートローダが起動します。オートローダには、Illuminate\Foundation\AliasLoaderとComposerのものが登録されているのですが、はじめにIlluminate\Foundation\AliasLoaderが実行されます。

Illuminate\Foundation\AliasLoaderでオートロードを行う箇所が以下です。

$this->aliasesに対象のクラス名(ここではRoute)があれば、class_alias()で、クラス別名を設定します。

	public function load($alias)
	{
		if (isset($this->aliases[$alias]))
		{
			return class_alias($this->aliases[$alias], $alias);
		}
	}

$this->aliasesは、app/config/app.phpで定義しているaliasesキーの内容がセットされています。

Routeは、下記のように設定されているため、Illuminate\Support\Facades\Routeへの別名として設定されます。

		&#039;Route&#039;           => &#039;IlluminateSupportFacadesRoute&#039;,

次にIlluminate\Support\Facades\Routeについて、オートロードによる読み込みが行われるので、RouteIlluminate\Support\Facades\Routeの別名として利用できるようになります。

2. Routeクラスのクラスメソッドを実行

次に、Routeクラスのget()メソッドを実行します。

Routeクラス(Illuminate\Support\Facades\Route)を見ると、get()というメソッドは存在しません。

Illuminate\Support\Facades\Routeの基底クラスであるIlluminate\Support\Facade\Facadeには、__callStatic()メソッドが定義されているため、このメソッドが呼ばれます。

__callStatic()メソッドが下記です。

	public static function __callStatic($method, $args)
	{
		$instance = static::resolveFacadeInstance(static::getFacadeAccessor());

		switch (count($args))
		{
			case 0:
				return $instance->$method();

			case 1:
				return $instance->$method($args[0]);

			case 2:
				return $instance->$method($args[0], $args[1]);

			case 3:
				return $instance->$method($args[0], $args[1], $args[2]);

			case 4:
				return $instance->$method($args[0], $args[1], $args[2], $args[3]);

			default:
				return call_user_func_array(array($instance, $method), $args);
		}
	}

}

3. IoC コンテナからインスタンス取得

__callStatic()メソッドの先頭では、resolveFacadeInstance()メソッドを実行して、IoC コンテナから処理対象のインスタンスを取得します。

まず、static::getFacadeAccessor()を実行して、どのインスタンスを取得するかを決定します。このメソッドは各ファサードクラスで定義されており、Routeクラスの場合は下記のようになっています。ここでは、routerという文字列を返しています。

このようにファサードクラスではgetFacadeAccessor()の戻り値で、実行するインスタンスを指定します。

	protected static function getFacadeAccessor() { return &#039;router&#039;; }

このrouterを引数にresolveFacadeInstance()メソッドを実行して、インスタンスを取得します。

resolveFacadeInstance()メソッドの実装は以下です。

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

引数で与えられた$name(ここではrouter)がオブジェクトであれば、そのまま返します。もし、すでにファサードクラスで解決済ならそのインスタンスを返します。そうでなければ、IoCコンテナからインスタンスを取得して返します。

IoCコンテナからインスタンスを取得した場合は、Fasadeクラスのクラス定数$resolvedInstanceにキャッシュされる仕組みになっているので、常に同じインスタンスが利用されます。

もし実行インスタンスを変えたい場合はswap()clearResolvedInstance()を使うと良いでしょう。

4. インスタンスメソッドの実行

  1. で取得したインスタンスについて、指定されたインスタンスメソッド(この場合get())を実行します。

さいごに

フレームワークのソースから Laravel のファサードクラスの仕組みを見てきました。

ファサードクラスの作り方として、ServiceProvider の構築が良く手順に含まれていますが、実はファサードクラスを作る上ではこれは必須ではありません。ファサードクラスが IoC コンテナからインスタンスを取得するため、そのインスタンスを事前にコンテナに設定するために ServiceProvider を利用することが多いだけです。

ファサードクラスを自作する際もこうした動きを知っていると作りやすいですね。

なお、Laravel のファサードクラスは、GoF のファサードパターンとは異なり、IoCコンテナにあるインスタンスを透過的に呼び出す仕組みとなっており、便利なサービスロケータと言った方がイメージしやすいかもしれません。( ちなみに、Laravel のファサードと GoF のファサードパターンは違うものだそうです。)

  • コメント (Close): 0
  • トラックバック (Close): 0

Laravel コードからフレームワークの起動から終了までの流れを追う

この記事の所要時間: 1633

Laravel フレームワークが起動してから終了するまでの流れについて、コードを読んでみました。

laravel_code

今回読んだフレームワークのバージョンは、4.1.21 です。

エントリポイント

エントリポイントは、public/index.php です。

このファイルではコメントが多数ありますが、実際に処理を行っている行は、下記の 3 行だけです。

このコードから想像できるように、オートローダの設定、フレームワークの設定、そして実行という流れです。

require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/start.php';
$app->run();

それぞれについて見ていきます。

1. bootstrap/autoload.php

ここでは、オートローダの設定を行います。Composerのオートローダもここで読み込みます。

先頭で現在時刻が定数として定義されているので、$_SERVER['REQUEST_TIME']の代わりに使えるかもしれません。

define('LARAVEL_START', microtime(true));

2. bootstrap/start.php

次にフレームワークとアプリケーションの起動を行います。

フレームワークの中核を成すIlluminate\Foundation\Applicationクラスのインスタンスを生成して、実行環境の設定、ファイルパスの設定を行って、フレームワークの起動ファイルを読み込みます。

最後に、Illuminate\Foundation\Applicationクラスのインスタンスを返します。

$app = new IlluminateFoundationApplication;

$env = $app->detectEnvironment(function() {
    return getenv('ENV_STAGE') ?: 'local';
});

$app->bindInstallPaths(require __DIR__.'/paths.php');

$framework = $app['path.base'].'/vendor/laravel/framework/src';
require $framework.'/Illuminate/Foundation/start.php';

return $app;

下記は、require文で読み込むファイルについてです。

2-1. bootstrap/paths.php

アプリケーション関連のパスが連想配列で定義されています。必要があれば、このファイルを編集します。

2-2. vendor/laravel/framework/src/Illuminate/Foundation/start.php

フレームワークの起動処理を行います。

Laravel をはじめる際につまづくことの多いmcrypt拡張チェックはここで行われています。

  • error_reporting(-1)
  • mcrypt 拡張チェック
  • IoC コンテナに$appを入れる
  • ユニットテストなら、$app['env']に’testing`をセット
  • ファサードクラスの初期処理
  • コアクラスを短縮名で呼べるように IoC コンテナに別名をセット
    => Application#registerCoreContainerAliases()
  • 環境変数をセット
    => .env.php or .env.${enviroment}.php の値を $_ENV / $_SERVER / putenv() にセット
  • Configクラス(Illuminate\Config\Repository)を$app['config']にセット
  • 例外ハンドリングの開始
  • display_errors を Off にする
  • タイムゾーン設定(date_default_timezone_set()
  • AliasLoader()の設定
  • HTTP メソッドのオーバーライド有効化
  • コアのServiceProvider有効化
  • bootedハンドラの登録(アプリケーション起動時に実行)
    => start/global.php の読み込み
    => start/{$env}.php の読み込み
    => routes.php の読み込み

3. $app->run()

アプリケーションを実行します。run()メソッドは、わずか 4 行です。

まず、Symfony\Component\HttpFoundation\Requestクラスのインスタンスを取得します。index.phpでは、引数をセットしていないので、IoCコンテナからインスタンスを取得します。

次に、実行するミドルウェアのツリーを構築して、handle()メソッドでアプリケーションの実行を行います。この行がアプリケーション実行の中心となります。

Symfony\Component\HttpFoundation\Responseクラスのインスタンスが戻り値として返るので、send()メソッドでレスポンスを出力します。

そして、最後に先ほど構築したミドルウェアツリーのterminate()メソッドを実行して、終了処理を行います。

これでアプリケーションが終了します。

	public function run(SymfonyRequest $request = null)
	{
		$request = $request ?: $this['request'];

		$response = with($stack = $this->getStackedClient())->handle($request); // (1)

		$response->send();

		$stack->terminate($request, $response);
	}

下記では、(1) の行について見ていきます。

3-1. $this->getStackedClient()

getStackedClient()メソッドでは、Stack/Builder を使って、実行するミドルウェアを組み合わせていきます。

ミドルウェアは、Illuminate\Foundation\Applicationクラス(正確には、`Symfony\Component\HttpKernel\HttpKernelInterface を implement したクラス)の Decorator となっており、それぞれの処理をアプリケーション実行の前後に挟むことができます。

デフォルトでは、以下の 4 クラスがミドルウェアとして登録されます。(handle()メソッド呼び出し順)

  • Illuminate\Http\FrameGuard
  • Illuminate\Session\Middleware
  • Illuminate\Cookie\Queue
  • Illuminate\Cookie\Guard

ミドルウェアはApp::middleware()で任意のものを追加することもできます。ただ、追加する場合は、Application#run()が呼ばれる前に追加しておく必要があります。

	protected function getStackedClient()
	{
		$sessionReject = $this->bound('session.reject') ? $this['session.reject'] : null;

		$client = with(new StackBuilder)
						->push('IlluminateCookieGuard', $this['encrypter'])
						->push('IlluminateCookieQueue', $this['cookie'])
						->push('IlluminateSessionMiddleware', $this['session'], $sessionReject);

		$this->mergeCustomMiddlewares($client);

		return $client->resolve($this);
	}

3-2. Illuminate\Cookie\Guard

直接Illuminate\Foundation\Applicationクラスのhandle()メソッドを呼び出すミドルウェアがこのクラスです。

実行するhandle()メソッドは、下記です。

	public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
	{
		return $this->encrypt($this->app->handle($this->decrypt($request), $type, $catch));
	}

1行で一気に書いているので、個々の文をばらしてみます。

	public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
	{
		$request = $this->decrypt($request);
		$response = $this->app->handle($request, $type, $catch);
		$response = $this->encrypt($response);		
		return $response;
	}

まず、decrypt()メソッドでリクエストされたクッキーの複合を行います。

次に、自身が保持しているミドルウェアのhandle()メソッドを実行します。ここで保持しているミドルウェアは、Applicationクラスなので、アプリケーションが実行されます。

そして、最後にレスポンスのクッキーを暗号化して、レスポンスを返します。

つまり、アプリケーションが実行している間は、クッキーの内容は平文ですが、その前後で複合、暗号化を行っているので、この範囲外では、クッキーの内容は暗号化されている状態になります。

3-3. Illuminate\Foundation\Application#handle()

前途のとおり、Illuminate\Cookie\Guard#handle()から呼び出されます。

boot()メソッドでアプリケーションの起動処理を行い、dispatch()メソッドでアプリケーションを実行します。

この処理で、もし例外が発生した場合、IoC コンテナにある例外ハンドラで処理します。ユニットテスト中であれば、そのままスローします。

	public function handle(SymfonyRequest $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
	{
		try
		{
			$this->refreshRequest($request = Request::createFromBase($request));

			$this->boot();

			return $this->dispatch($request);
		}
		catch (Exception $e)
		{
			if ($this->runningUnitTests()) throw $e;

			return $this['exception']->handleException($e);
		}
	}

3-4. Illuminate\Foundation\Application#boot()

Illuminate\Foundation\Application#handle()から呼び出されます。

アプリケーションのServiceProviderのboot()メソッドを実行していきます。

また、2-2. で登録したbootedハンドラは、ここから呼ばれるbootApplication()メソッドで実行されます。

	public function boot()
	{
		if ($this->booted) return;

		array_walk($this->serviceProviders, function($p) { $p->boot(); });

		$this->bootApplication();
	}

3-5. Illuminate\Foundation\Application#dispatch()

Illuminate\Foundation\Application#handle()から呼び出されます。

まず、メンテナンス中かを確認して、もしそうならilluminate.app.downイベントを発火してレスポンスを返します。

次に、ユニットテスト中かつセッション開始前なら、セッションを開始します。

そして、Illuminate\Routing\Routerクラスのdispatch()メソッドを実行します。

	public function dispatch(Request $request)
	{
		if ($this->isDownForMaintenance())
		{
			$response = $this['events']->until('illuminate.app.down');

			if ( ! is_null($response)) return $this->prepareResponse($response, $request);
		}

		if ($this->runningUnitTests() && ! $this['session']->isStarted())
		{
			$this['session']->start();
		}

		return $this['router']->dispatch($this->prepareRequest($request));
	}

3-6. Illuminate\Routing\Router#dispatch()

Illuminate\Foundation\Application#dispatch()から呼び出されます。

ここで、リクエストをアプリケーションに渡して実行します。dispatchRoute()メソッドがアプリケーション実行のメインです。

その前後では、beforeafterフィルタが呼ばれます。それぞれrouter.befiorerouter.afterイベンドが発火されます。


	public function dispatch(Request $request)
	{
		$this->currentRequest = $request;

		$response = $this->callFilter('before', $request);

		if (is_null($response))
		{
			$response = $this->dispatchToRoute($request);
		}

		$response = $this->prepareResponse($request, $response);

		$this->callFilter('after', $request, $response);

		return $response;
	}

3-7. Illuminate\Routing\Router#dispatchRoute()

Illuminate\Routing\Router#dispatch()から呼び出されます。

リクエスト内容から実行するRouteクラスを取得して、run()メソッドを実行します。これにより、app/routes.phpで定義したルーティングの内容が実行され、アプリケーションの処理が行われます。

run()メソッドの戻り値は、最終的にクライアントへのレスポンスとなります。

callRouteBefore()callRouteAfter()では、Routeクラスで定義したbeforeafterフィルタが実行されます。(ex. Route::get(‘/’, [‘before’ => ‘auth’,… ))

callRouterBefore()メソッドの戻り値がnull以外なら、run()メソッドは実行されないので、authフィルタなど、アプリケーション実行前にチェックをかける場合はこれを利用することになります。

	public function dispatchToRoute(Request $request)
	{
		$route = $this->findRoute($request);

        $this->events->fire('router.matched', array($route, $request));

		$response = $this->callRouteBefore($route, $request);

		if (is_null($response))
		{
			$response = $route->run($request);
		}

		$response = $this->prepareResponse($request, $response);

		$this->callRouteAfter($route, $request, $response);

		return $response;
	}

さいごに

Laravel フレームワークの起動から終了までの全体の流れを見てみました。

流れとしてはそれほど複雑ではないのですが、Stack/Builder によるミドルウェアやイベント、フィルタなど、多くの箇所でアプリケーション固有の処理を差し込めるような仕組みになっています。

拡張できるポイントを分かっておくと、いつどこでどのように処理を追加すれば良いかが判断しやすくなります。

こうしたフレームワークのコードを読む勉強会なんかもやってみたいですね。

  • コメント (Close): 0
  • トラックバック (Close): 0

Laravel でカスタムドライバを使って Remember Me(オートログイン)を実装する

この記事の所要時間: 743

Laravel には remeber me 機能があるのですが、これは暗号化した Cookie の情報だけで認証を行うので、やや心許ない実装です。

rememberme

そこで、Laravel フレームワークを拡張して、独自の認証処理ドライバを実装してみました。

ログイントークンテーブル

認証用ログイントークンを保存するテーブルを作成します。

まず、artisanコマンドでマイグレーションクラスを作成します。

$ php artisan migrate:make make_login_tokens

生成されたマイグレーションクラスにテーブル定義を書いていきます。

<?php

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

class MakeLoginTokens extends Migration {
	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
        Schema::create('login_tokens', function($table) {
            $table->increments('id');
            $table->text('token')->unique();
            $table->integer('user_id')
                ->references('id')->on('users')
                ->onDelete('cascade')->onUpdate('cascade');
            $table->timestamps();
        });
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
        Schema::drop('login_tokens');
	}
}

マイグレーションを実行して、login_tokensテーブルを生成します。

$ php artisan migrate

認証カスタムドライバの作成

ログイン認証処理は、Authクラスが担っています。ただこれは只のファサードでしかなく、実体はIlluminate\Auth\AuthManagerです。Illuminate\Auth\AuthManagerクラスは、認証処理をドライバに委譲しているので、このドライバを差し替えることで、独自の認証処理を行うことができます。

フレームワークにはIlluminate\Auth\Guardというドライバが用意されているので、これを継承してカスタムドライバを実装します。

今回、実装(オーバーライド)したのはgetUserByRecaller()メソッドとqueueRecallerCookie()メソッドです。

getUserByRecaller()メソッドは、オートログイン時に呼ばれるので、ブラウザから送信された Cookie の値を使って自動ログインを行うようにしています。

queueRecallerCookie ()メソッドは、ログイン時に呼ばれるので、ログイントークンを生成してDBに登録し、Cookie に埋め込むようにしました。

<?php
namespace Acme;

use Carbon\Carbon;
use Config;
use DB;
use Illuminate\Auth\Guard;
use LoginToken;

/**
 * Class AuthLoginGuard
 * @package Acme
 */
class AutoLoginGuard extends Guard
{
    /**
     * @param mixed $id
     * @return mixed
     */
    protected function getUserByRecaller($id)
    {
        $limit = Carbon::now()->subDays(Config::get('auth.login_token_limit_day'));

        // gc
        DB::table(with(new LoginToken)->getTable())->where('updated_at', '<=', $limit)->delete();

        $token = LoginToken::where('token', $id)->where('updated_at', '>', $limit)->first();
        if (empty($token)) {
            return null;
        }

        $user = parent::getUserByRecaller($token->user_id);
        if (empty($user)) {
            return null;
        }

        $token->touch();
        $this->updateSession($user->id);

        return $user;
    }

    /**
     * @param string $id
     */
    protected function queueRecallerCookie($id)
    {
        $bytes = Config::get('auth.login_token_length') / 2;
        $token = bin2hex(openssl_random_pseudo_bytes($bytes));

        $loginToken = new LoginToken();
        $loginToken->token = $token;
        $loginToken->user_id = $id;
        $loginToken->save();

        parent::queueRecallerCookie($token);
    }
}

カスタムドライバを有効にする

作成したカスタムドライバをフレームワークで有効にします。

Auth::extend()

Auth::extend()で、Acme\AutologinGuardクラスをインスタンス化するクロージャを定義します。ドライバ名にはautologinを指定しています。

Auth::extend('autologin', function($app) {
    $provider = new EloquentUserProvider($app['hash'], $app['config']['auth.model']);
    return new AutoLoginGuard($provider, $app['session.store']);
});

app/config/auth.php

app/config/auth.php に、このautologinドライバを認証ドライバとして利用するように設定します。

'driver' => 'autologin',

さらに、Acme\AutologinGuardクラスに必要なログイントークンに関する設定も追加します。

  'login_token_limit_day' => 30, // ログイントークン有効期間
  'login_token_length' => 60, // ログイントークンの長さ

まとめ

独自のカスタムドライバを作成して、Authクラスの認証処理を差し替えてみました。

このように Laravel では、フレームワークの処理を置き換えることができるような機構が用意されているので、要件に合わない部分に関しては、好きに変更することができます。

<

p>こうした柔軟性の高さも Laravel の面白いところですね。

  • コメント (Close): 0
  • トラックバック (Close): 0

Laravel IoC コンテナの使い方

この記事の所要時間: 1149

laravel

最近は Laravel + AngularJS で Web アプリケーションを開発する毎日です。

少し時間が取れたので Laravel フレームワークのソースを読んでいます。その中から Laravel の肝でもある IoC コンテナの使い方をまとめてみました。

Laravel の IoC コンテナ

Laravel で使われている IoC コンテナは、Illiminate\Foundation\Applicationクラスです。これはIlliminate\Container\Containerクラスを継承したもので、コンテナとしての基本機能はIlliminate\Container\Containerクラスが担っています。

Laravel アプリケーションで、この IoC コンテナを利用する際は、AppクラスというIlliminate\Foundation\Applicationのファサードクラスが用意されているので、こちらを利用することが多いです。

// Foo インスタンスを取得
$foo = App::make('Foo');

AppクラスのgetFacadeAccessor()メソッドを使うと、Illiminate\Foundation\Applicationクラスのインスタンスを取得することができるので、直接メソッドを実行できます。

// Foo インスタンスを取得
$foo = App::getFacadeAccessor()->make('Foo');

下準備

ここでは、IoC コンテナの動きを見ていくので、Laravel フレームワークを起動せずにシンプルなコードを書きます。

ファサードクラスは定義されていないので、Illiminate\Foundation\Applicationクラスのインスタンスを直接操作します。(Appクラスを使う場合は、メソッド呼び出しをクラスメソッドとして読み替えて下さい。)

<?php
require __DIR__.'/vendor/autoload.php';

class Foo
{
    /**
     * @var string
     */
    protected $message;

    /**
     * @var string $message
     */
    public function __construct($message = '')
    {
        $this->message = $message;
    }

    /**
     * @return string
     */
    public function getMessage()
    {
        return $this->message;
    }
}


$app = new \Illuminate\Foundation\Application();

コンテナからインスタンス取得

IoC コンテナからインスタンスを取得するには、make()メソッドを使います。引数には、コンテナにバインドした際の名前を指定します。コンテナに指定した名前がバインドされていない場合、クラスとしてインスタンスを生成します。下記では、’Foo’という名前のバインドは行っていませんが、Fooクラスのインスタンスを返します。make()メソッドの第2引数に連想配列を渡すと、生成するインスタンスのコンストラクタに引数として渡されます。

$foo = $app->make('Foo'); // message = ''
$foo = $app->make('Foo', ['Hello']); // message = 'Hello'

IoC コンテナは、AraryAccessインターフェースを実装しているので、配列としてアクセスすると、内部でmake()メソッドを実行して、該当インスタンスを返します。

$foo = $app['Foo']; // message = ''

コンテナにファクトリをバインド

コンテナにインスタンスを生成するファクトリをバインドしてみます。バインドを行うメソッドはいくつかあり、それぞれ用途に応じて使い分けます。

bind()

インスタンスを生成するファクトリをバインドします。make()メソッド実行毎にファクトリが実行され、インスタンスが生成されます。

$app->bind('foo_bind', 'Foo');
$foo = $app->make('foo_bind');
$app->bind('foo_bind', function($app) {
    return new Foo('foo_bind');
});
$foo = $app->make('foo_bind');

bindShared()

bind()と同じですが、make()実行時に生成されたインスタンスをコンテナが保持します。再度make()を実行した際は同じインスタンスを返します。下記コードでは、$foo1$foo2は同じインスタンスです。

$app->bindShared('foo_bind_shared', function($app) {
    return new Foo('foo_bind_shared');
});
$foo1 = $app->make('foo_bind_shared');
$foo2 = $app->make('foo_bind_shared');

bindIf()

コンテナに同じ名前のバインドが無ければ、バインドします。すでにあれば何も行いません。

$app->bindIf('foo_bind', function($app) {
    return new Foo('foo_bind_if');
});
$foo = $app->make('foo_bind'); // 変わっていない

instance()

生成したインスタンスをバインドします。make()メソッド実行時は、常にこのインスタンスを返します。

$app->instance('foo_instance', new Foo('instance'));
$foo1 = $app->make('foo_instance'); // message == 'instance'
$foo2 = $app->make('foo_instance'); // message == 'instance'

singleton()

bindShared()と同じくmake()実行時に生成したインスタンスをコンテナが保持するので、以後同じインスタンスを返します。

$app->singleton('foo_singleton', 'Foo');
$foo1 = $app->make('foo_singleton'); // message == 'foo_singleton'
$foo2 = $app->make('foo_singleton'); // message == 'foo_singleton'
assert($foo1 === $foo2);
$app->singleton('foo_singleton_closure', function($app) {
    return new Foo('foo_singleton_closure');
});
$foo1 = $app->make('foo_singleton_clusure'); // message == 'foo_singleton_closure'
$foo2 = $app->make('foo_singleton_closure'); // message == 'foo_singleton_closure'
assert($foo1 === $foo2);

alias()

コンテナにバインドした名前に別名を付けます。

$app->share('bar', 'foo');
$bar = $app->make('bar'); // message == 'foo'

extend()

コンテナにバインドされたファクトリを拡張します。クロージャの第1引数に元にバインドされたファクトリが返すインスタンスが渡されるので、追加処理を記述して、そのインスタンスを返すようにします。

$app->extend('foo_bind', function($foo, $app) {
    $foo->extend = 'ex';
    return $foo;
});
$foo = $app->make('foo_bind'); // message == 'foo_bind', extend = 'ex'

連想配列として代入

make()メソッドと同じく連想配列として代入することでバインドすることができます。この場合、bind()メソッドの実行と同じ扱いになります。

$app['foo_set'] = function() {
  return Foo('foo_set');
};
$foo = $app->make('foo_set');

具象クラスをインジェクトするDI

では IoC コンテナを使った DI を行ってみます。サンプルのソースは以下です。

このソースでは、HelloクラスとBarクラスがあり、Barクラスではコンストラクタで、Helloクラスのインスタンスを必要としています。

IoC コンテナのmake()メソッドを使って Bar クラスのインスタンスを取得しようとすると、コンテナがリフレクションを使って、コンストラクタの引数を判別して、Helloクラスのインスタンを生成し、Barクラスのコンストラクタに渡してくれます。

結果として、Helloクラスのインスタンスを保持したBarクラスのインスタンスを取得することができます。

このように具象クラスであればコンテナへのバインドを行わなくても、よしなにインスタンスをインジェクトしてくれます。

インターフェースをインジェクトするDI

次は、インスタンスを生成するファクトリをコンテナにバインドしてDIを行います。

サンプルソースを変更しました。Greetableインターフェースを定義して、Helloクラスはこのインターフェースをimplementしています。Barクラスのコンストラクタでは、Helloクラスではなく、Greetableインターフェースのインスタンスを引数として取るように変更しました。

Barクラスを$app->make('bar')で取得しようとするとIlluminate\Container\BindingResolutionExceptionという例外が発生します。これはコンストラクタで必要としているGreetableはインターフェースのため、インスタンス化ができないためです。

これを解決するにはいくつかの方法があります。

Greetableという名前で取得できるインスタンスをコンテナにバインドしておきます。ここではHelloクラスを指定しています。これにより、BarクラスのコンストラクタにはHelloクラスのインスタンスがセットされるようになります。

$app->bind('Greetable', 'Hello');
$app->make('Bar')->say();

Greetableという名前にHelloクラスのインスタンスを生成するファクトリを指定することもできます。

$app->bind('Greetable', function($app) {
    return new Hello();
});
$app->make('Bar')->say();

もちろん、Barという名前でBarクラスのファクトリをバインドする方法もokです。

$app->bind('Bar', function($app) {
    return new Bar($app->make('Hello'));
});
$app->make('Bar')->say();

インジェクトするクラスを変える

IoC コンテナを使って、DI できるようにしておくと、インジェクトするクラスを簡単に変更することができます。

Helloクラスの代わりにGreetableを実装したByeクラスをインジェクトするには、下記のようにコンテナのバインドを変更するだけです。Barクラスの実装は変更する必要がありません。

class Bye implements Greetable
{
    /**
     *
     */
    public function greet()
    {
        echo 'Bye';
    }
}

$app->bind('Greetable', 'Bye');
$app->make('Bar')->say(); // Bye

おわりに

Laravel に実装されている IoC コンテナの使い方を見てみました。

IoC コンテナは、Laravelを構成する重要な機能なので、使い方を理解しておくとフレームワークへの理解も深まります。

コンテナを連想配列ライクに利用すると Pimple と似ているので、Pimple を使ったことがある人には馴染みやすいかもしれませんね。

(Laravel 作者Taylor Otwell氏の著書(Laravel: From Apprentice To Artisanでは「Laravel の IoC コンテナは、のPimple IoC コンテナとそのまま置き 換えることができます。」と書かれていたりします:D)

  • コメント (Close): 0
  • トラックバック (Close): 0

ホーム > PHP > Laravel

検索
フィード
メタ情報

Return to page top