Home > PHP

PHP Archive

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

PHP 定義されている情報(クラス、関数、変数等)を取得する関数まとめ

  • 2014-06-04 (水)
  • PHP
この記事の所要時間: 124

PHP には、定義されているクラスや関数、変数などの一覧を取得する関数があります。

php-logo

ここでは定義情報を取得できる関数群をまとめてみました。ここでは実行例として、Laravel アプリケーションのビューテンプレート(hoge.blade.php)で実行した内容を記載しています。

定義された情報を取得する関数群

get_defined_constants()

定義されている定数を連想配列として取得します。キーが定数名で、要素がその値となっています。

http://www.php.net/manual/ja/function.get-defined-constants.php

出力してみると、1771 個の定数がありました。内容を見ると、フレームワークやアプリケーションで定義されたものの他に、PHP本体や extension で多数の定数が定義されていることがわかります。

array (size=1771)
  'E_ERROR' => int 1
  'E_RECOVERABLE_ERROR' => int 4096
  'E_WARNING' => int 2
...

get_declared_classes()

定義されているのクラスの名前が配列として取得します。

http://www.php.net/manual/ja/function.get-declared-classes.php

304 個のクラスが定義されていました。

array (size=304)
  0 => string 'stdClass' (length=8)
  1 => string 'Exception' (length=9)
  2 => string 'ErrorException' (length=14)
  3 => string 'Closure' (length=7)
...

get_declared_interfaces()

定義されているインターフェイスを配列として取得します。

http://www.php.net/manual/ja/function.get-declared-interfaces.php

55 個のインターフェイスが定義されていました。

array (size=55)
  0 => string 'Traversable' (length=11)
  1 => string 'IteratorAggregate' (length=17)
  2 => string 'Iterator' (length=8)
  3 => string 'ArrayAccess' (length=11)
...

get_declared_traits()

定義されているトレイトを配列として取得します。

http://www.php.net/manual/ja/function.get-declared-traits.php

トレイトの定義が無かったので、サンプルとして Foo をトレイトとして定義しています。

array (size=1)
  0 => string 'Foo' (length=3)

get_defined_functions()

定義されている関数を連想配列として取得します。

http://www.php.net/manual/ja/function.get-defined-functions.php

連想配列には2つのキーがあり、internalには内部関数、userにはユーザ定義関数が配列として格納されています。

ここでは、1,686個(!)の内部関数と74個のユーザ定義関数がありました。

array (size=2)
  'internal' => 
    array (size=1686)
      0 => string 'zend_version' (length=12)
      1 => string 'func_num_args' (length=13)
      2 => string 'func_get_arg' (length=12)
      3 => string 'func_get_args' (length=13)
...
 'user' => 
    array (size=74)
      0 => string 'composerrequirea978da05ae47fd0758967870dd5a04f2' (length=47)
      1 => string 'composerautoloadincludefile' (length=29)
      2 => string 'crypt_random_string' (length=19)
      3 => string '_swiftmailer_init' (length=17)

get_defined_vars()

全ての定義済の変数を連想配列で取得します。

http://www.php.net/manual/ja/function.get-defined-vars.php

連想配列のキーに変数名、要素が変数値となっています。

array (size=6)
  '__path' => string '/share/app/config/../views/login.php' (length=36)
  '__data' => 
    array (size=4)
      '__env' => 
        object(IlluminateViewEnvironment)[208]
          protected 'engines' => 
            object(IlluminateViewEnginesEngineResolver)[202]
              ...
          protected 'finder' => 
            object(IlluminateViewFileViewFinder)[207]
              ...
...

get_included_files() / get_required_files()

include または require で読み込まれたファイル名を配列として取得します。

http://www.php.net/manual/ja/function.get-included-files.php

255 個のファイルが読み込まれていました。get_required_files()は、get_included_files()のエイリアスとなっているので、実行結果はどちらでも同じです。

array (size=225)
  0 => string '/share/public/index.php' (length=23)
  1 => string '/share/bootstrap/autoload.php' (length=29)
  2 => string '/share/vendor/autoload.php' (length=26)
  3 => string '/share/vendor/composer/autoload_real.php' (length=40)
...

get_loaded_extensions()

コンパイル/ロードされているモジュールを配列として取得します。

http://www.php.net/manual/ja/function.get-loaded-extensions.php

58 個のモジュールが有効になっていました。

array (size=58)
  0 => string 'Core' (length=4)
  1 => string 'date' (length=4)
  2 => string 'ereg' (length=4)
  3 => string 'libxml' (length=6)

なお、get_loaded_extensions()では、引数に真偽値を指定することができ、trueを渡すと、Zend 拡張モジュールのみを取得します。

実行してみると、下記の 2 つだけでした。

array (size=2)
  0 => string 'Zend OPcache' (length=12)
  1 => string 'Xdebug' (length=6)

ini_get_all()

設定値を連想配列として取得します。

http://www.php.net/manual/ja/function.ini-get-all.php

下記エントリで実行結果を記載していますので、ご参考まで。

http://qiita.com/shin1x1/items/f469bfb73c396007f911

使いどころ

色々と使いどころはあるのですが、便利な使い方を一つ。

フレームワークのビューテンプレートで、定義(バインド)されている変数をリストアップします。CakePHPやLaravelなどのフレームワークでは、ビューテンプレートをビュークラスのメソッドで読み込んで実行するので、ビューテンプレート内はこのメソッドのコンテキストで実行されることになります。

get_defined_vars()を使うことで、実際にはどのような変数がビューテンプレートで定義されているかを確認できます。

下記が、Laravel ビューテンプレートでget_defined_vars()を実行した例です。このビューテンプレートでは 6 つの変数が定義されており、$__path$__data$__env$app$errors$loginUserとして利用できることが分かります。

このように、マニュアルなどのドキュメントを見ずとも、コードで利用できる変数を知ることができます。

<?php var_dump(array_keys(get_defined_vars())) ?>

array (size=6)
  0 => string '__path' (length=6)
  1 => string '__data' (length=6)
  2 => string '__env' (length=5)
  3 => string 'app' (length=3)
  4 => string 'errors' (length=6)
  5 => string 'loginUser' (length=9)

他には、クロージャ内でget_defined_vars()を実行して、スコープにある変数がどれかを確認したり、PHPの挙動を知るのにも使えます。

さいごに

こういった定義を確認する関数群は日々使うというわけではありません。ただ、知っておくと、アプリケーションのデバッグや、フレームワークの挙動を確認したりする際に役立ちます。

日々開発しているPHPコードの定義情報を見てみると、新たな発見があるかもしれませんよ。

このエントリで紹介した関数群を書いたコード Gist に置いてます。参考まで。

追記

このエントリでは、配列で取得する関数を取り上げましたが、コマンドラインでも取得できます。extension で定義されているクラスや設定を見るなら、コマンドの方が手軽ですね。

$ php -h
...
  --rf       Show information about function .
  --rc       Show information about class .
  --re       Show information about extension .
  --rz       Show information about Zend extension .
  --ri       Show configuration for extension .
  • コメント (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

composer install をどこで実行するか

  • 2014-03-06 (木)
  • PHP
この記事の所要時間: 32

最近の PHPer が集まれば、一度は話題に上がるのが、この composer install をどこで実行するのか問題。

Composer

これまで聞いた話をまとめると、大きく分けて、以下の2パターンになります。どちらの方法を取っているか教えて下さい 😀

0. 前提

前提ですが、以下のような方法で、Composer 関連のファイルは管理しているとします。おそらく多くはこのような形になっていると思います。

  • PHP コードは、Git などの VCS で管理する。
  • composer.json, composer.lock は、VCS で管理する。
  • composer.phar, vendor/ は、VCS で管理しない。

また、今回対象としているのはアプリケーションで、Packagist に登録して、配布するようなフレームワークやライブラリは対象外です。

1. 本番サーバで実行

本番PHPサーバ上で composer install を実行するパターンです。

本番PHPサーバに何かしらの方法で、PHP コードと共にcomposer.jsoncomposer.lockを設置し、composer installを実行します。

複数台サーバがある場合、それぞれのサーバで実行します。

コマンドの実行は、SSH でログインして手でする人もいるでしょうし、CapistranoやFabricなどで、自動化している人もいます。

順番としては、PHPコードをデプロイしてから、依存解決(composer install)になります。

2. 別サーバで実行して、実行結果を本番サーバに設置

本番PHPサーバ以外の場所で、composer installを実行して、生成されたvendor/を本番サーバに設置する方法です。

composer installを実行する場所は、ビルドサーバの場合もありますし、開発者の PC の場合もあります。

本番サーバではcomposer installを実行しないのがポイントです。

こちらは、依存解決してからデプロイの順になります。

私の場合

私自身は、本番環境については、2. の方法で行っています。

依存解決は、デプロイの前に行っておき、すでに依存解決されたものを本番環境にデプロイします。

The Twelve-Factor App で言うところのビルドステージで行うものという認識です。

ビルドステージ は、コードリポジトリを ビルド と呼ばれる実行可能な塊へと変える変換である。デプロイプロセスで指定したコミットのコードで指定されたバージョンを使って、ビルドステージは依存関係を取得してローカル環境に配置し、バイナリやアセットファイルをコンパイルする。

ビルドステージで可変部分は全て処理しておき、本番環境には、イミュータブルなパッケージとしてデプロイします。(実際は、パーミッションの設定など多少の操作は必要ですが)

複数台の場合、あちこちでcomposer installを実行するのが単純に無駄というのもありますし、大きな可変部分を各サーバで実行するより、1箇所で生成して、全く同じものを各サーバに配布した方が安全かと。

ただ、1. の方が手軽なので、こちらを採用しているという意見も良く聞きますね。

さいごに

Composer が普及してきたからこそ出てくる議論だと思うので、すっかり利用するのが当たり前になりましたね。今後は、こうした運用面に関するノウハウも共有していきたいです。

上記案に限らず、こうしてるよ、などあれば教えて下さい。

  • コメント (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 5.6 に採用されるデバッガ phpdbg を使ってみた

この記事の所要時間: 952

Shin x blog Advent Calendar 2013 の 21 日目です。

phpdbg_php_debugger

第 12 回関西 PHP 勉強会 にて、PHP 5.6 に採用予定の phpdbg をひと足先に PHP 5.5.7 で触ってみました。

phpdbg

phpdbg は、gdb ライクな PHP 用のデバッガです。ブレークポイントを設定して、その時点のコンテキストを確認したり、ステップ実行などができます。

phpdbg | php debugger

インストール

PHP 5.6 から同梱される予定の phpdbg ですが、これ自体はすでにリリースされており、PHP 5.4 から利用することが可能です。インストールには、PHP のソースコードが必要になるので、PHP も ソースからインストールします。

$ sudo yum -y groupinstall "Development Tools"
$ sudo yum -y install libxml2-devel
$ curl -L -o php-5.5.7.tar.bz2 http://jp1.php.net/get/php-5.5.7.tar.bz2/from/this/mirror
$ tar xvf php-5.5.7.tar.bz2
$ cd php-5.5.7
$ ./configure --with-readline
$ make
$ sudo make install

次に phpdbg をインストールします。ソースコードを git clone して、ビルドします。

$ cd /path/to/php-5.5.7/sapi
$ git clone https://github.com/krakjoe/phpdbg
$ cd ../
$ ./buildconf --force
$ ./config.nice
$ make -j8
$ sudo make install-phpdbg

Vagrantfile を gist をアップしたので、試してみたい方はどうぞ。

インストールすると phpdbg コマンドが実行できるようになります。phpdbg コマンドを実行すると、phpdbg のコンソールが表示されます。quit で phpdbg を終了できます。

phpdbg

help コマンドでコマンドのヘルプが表示されます。help run のように、help の後にコマンドを指定すると、そのコマンドのヘルプが表示されます。

phpdbg> help
[Welcome to phpdbg, the interactive PHP debugger, v0.3.0]
To get help regarding a specific command type "help command"
[Commands]
       exec     set execution context
    compile     attempt compilation
       step     step through execution
       next     continue execution
        run     attempt execution
       eval     evaluate some code
(snip)

phpdbg> help run
[Welcome to phpdbg, the interactive PHP debugger, v0.3.0]
Execute the current context inside the phpdbg vm

[Examples]
        phpdbg> run
        phpdbg> r
        Will cause execution of the context, if it is set.

Note: The execution context must be set, but not necessarily compiled before execution occurs
[Please report bugs to ]

簡単なサンプル

簡単な動作サンプルとして、下記の PHP ソースを phpdbg で実行してみます。

phpdbg コマンドに -e オプションを付けることで、デバッガで実行する PHP スクリプトを指定します。

$ phpdbg -e sample.php

まず、この PHP スクリプトのオペコードを表示してみます。compile コマンドで PHP コードをコンパイルします。

phpdbg> compile
[Attempting compilation of /vagrant/sample.php]
[Success]

print exec コマンドを実行するとオペコードが表示されます。exec オプション以外にも classmethod などを指定することができます。

phpdbg> print exec
[Context /vagrant/sample.php]
    L0-0 {main}() /vagrant/sample.php
        L2      0x7fb5dfd24c68 ZEND_ADD                       C0                   C1                   @0
        L2      0x7fb5dfd24c98 ZEND_ASSIGN                    $a                   @0                   @1
        L3      0x7fb5dfd24cc8 ZEND_ECHO                      $a                                

次にブレイクポイントを設定してみます。break line コマンドを使って、3 行目にブレイクポイントを設定します。ブレイクポイントには、line 以外にも、funcmethod 、また特定のオペコードが呼ばれたタイミングでブレイクする op などを指定することができます。

phpdbg> break line 3
[Breakpoint #0 added at line 3]

コードを実行して、ブレイクポイントで停止するか確認します。コードを実行するには、run コマンドを実行します。ちゃんと設定した 3 行目で実行が停止しました。

phpdbg> run
[Attempting compilation of /vagrant/sample.php]
[Success]
[Breakpoint #0 at /vagrant/sample.php:3, hits: 1]
 00002: $a = 1 + 2;
 >00003: e

停止した時点での変数やリテラルを確認することができます。

phpdbg> info vars
[Variables in /vagrant/sample.php (1)]
Address         Refs    Type            Variable
0x7fc36cac3060  1       (integer)       $a

phpdbg> info literal
[Literal Constants in /vagrant/sample.php (2)]
|-------- C0 -------> [1]
|-------- C1 -------> [2]
|-------- C2 -------> [1]

next コマンドを実行するとコードの実行が再開されます。実行はオペコード単位になっているようなので、next コマンドを 2 回実行するとコードが終了します。

phpdbg> next
[Breakpoint #0 at /vagrant/sample.php:3, hits: 2]
 00002: $a = 1 + 2;
 >00003: echo $a . PHP_EOL;
  00004:
phpdbg> next
3

ジェネレータの動きを見る

次にジェネレータの動きを phpdbg で見てみます。PHP コードは下記です。

phpdbg コマンドを実行します。ブレイクポイントには、yield 文のオペコードである ZEND_YIELD を設定します。phpdbg のコマンドは、省略系で指定することができ、下記では、break op コマンドを省略して、b O としています。

$ phpdbg -e generator.php
(snip)

phpdbg> b O ZEND_YIELD
[Breakpoint #0 added at ZEND_YIELD]

run で実行します。設定したブレイクポイントで処理が停止します。

phpdbg> run
[Attempting compilation of /vagrant/generator.php]
[Success]
[Breakpoint #0 in ZEND_YIELD at /vagrant/generator.php:4, hits: 1]
 00003:     for ($i = $min ; $i <= $max ; $i++) {
>00004:         yield $i;
 00005:     }

next コマンドを実行するとステップ実行を行います。コマンドを入力せずにエンターだけを押すと、前回実行したコマンドがそのまま実行されます。この状態で、エンターを繰り返すと next コマンドが実行されます。

phpdbg> n
1
[Breakpoint #0 in ZEND_YIELD at /vagrant/generator.php:4, hits: 2]
 00003:     for ($i = $min ; $i <= $max ; $i++) {
>00004:         yield $i;
 00005:     }
phpdbg>
2
[Breakpoint #0 in ZEND_YIELD at /vagrant/generator.php:4, hits: 3]
 00003:     for ($i = $min ; $i <= $max ; $i++) {
>00004:         yield $i;
 00005:     }
phpdbg>
3
phpdbg>

さいごに

これまでも gdb や xdebug でデバッグを行うことができましたが、別途インストールや設定が必要でした。

PHP 5.6 から、phpdbg が同梱されることで、PHP が入っている環境では、手軽にこのデバッガを利用できるようになります。リモートデバッグにも対応しており、いずれ PhpStorm のような IDE でも対応されるかもしれません。

phpdbg 自体は 5.4 からインストール可能ですので、ちょっと試してみようという方はインストールしてみてください。

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

ホーム > PHP

検索
フィード
メタ情報

Return to page top