2012-03-28

PHP勉強会@東京#58でPhakeの紹介をしました

第58回PHP勉強会@東京 - events.php.gr.jp

久々のPHP勉強会だったのでPhakeについて発表をしてきました。とても久々のPHP勉強会でしたが、懇親会で色々お話もできて楽しかったです。

訂正

Phake::mock()の第2引数以降がうんたら書いてますけど、第2引数以降がコンストラクタの引数になるのはパーシャルモックのときだけでした。


id:sotarokPHP 5.4の話のときにもいってたんですけど、「ゲッターセッター用意するの面倒だからAccessorってトレイトつくったよ!」「おれも!」みたいなことがあったりして、考えることはまったく同じですね。僕が作ってたときのソースさらしておきます。__call()をトレイトで実装しちゃうと、__call()が衝突しちゃうので一応メソッドわけてます。MagicCallMixerトレイトとか作ったけど消しちゃったらしい。

<?php
namespace Fivestar;

trait Accessor
{
    public function __call($method, $args)
    {
        return $this->callMagicAccessor($method, $args);
    }

    public function magicCallAccessor($method, $args)
    {   
        $processMethod = strtolower($method);
        if ('set' === ($verb = substr($processMethod, 0, 3)) 
            || 'get' === ($verb = substr($processMethod, 0, 3)) 
            || 'is' === ($verb = substr($processMethod, 0, 2)) 
        ) { 
            $camelName = lcfirst(substr($method, strlen($verb)));
            $underscoreName = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $camelName));

            $property = null;
            if (property_exists($this, $camelName)) {
                $property = $camelName;
            } elseif (property_exists($this, $underscoreName)) {
                $property = $underscoreName;
            }   

            if (null !== $property) {
                if ('set' === $verb) {
                    $this->{$property} = array_shift($args);

                    return;
                } elseif ('get' === $verb || 'is' === $verb) {
                    return $this->{$property};
                }   
            }   
        }   

        throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s', get_class($this), $method));
    } 
}

最後に、会場を提供していただいたVOYAGE GROUPさん、ありがとうございました。

2012-03-03

CrocosSecurityBundleの新リリースについて

crocos/CrocosSecurityBundle · GitHub

CrocosSecurityBundleは認証管理用のバンドルで、Symfony2標準のSecurityBundleが複雑で使いづらかったのでシンプルに管理できるように作成したものです。

最初のリリース後に何回かアップデートを重ねていまして、公開当初よりも機能が増えたりしています。本日バーション1.4をリリースしたので、これまでリリースとあわせて紹介します。

1.2

Comparing 1.0...1.2 · crocos/CrocosSecurityBundle · GitHub

1.1はhotfixで使ったので2回目のリリースは1.2になります。ここでの主な変更は、AuthException例外の追加とリファクタリングバグフィックスになります。AuthExceptionはどこかでスローされると、ログイン画面へ遷移(forward)するようになっています。

<?php

use Crocos\SecurityBundle\Exception\AuthException;

$security = $this->get('crocos_security.context');

if (!$security->isAuthenticated()) {
    throw new AuthException('Authentication required');
}

1.3

Comparing 1.2...1.3 · crocos/CrocosSecurityBundle · GitHub

1.3から、Twigのテンプレート内でSecurityContextオブジェクトが _security 変数として参照できるようになりました。

{% if _security.isAuthenticated %}
  <p>Logged in as {{ _security.user }}</p>
{% endif %}

1.4

Comparing 1.3...1.4 · crocos/CrocosSecurityBundle · GitHub

本日リリースした1.4では、Basic認証のサポート、エンティティでログインしたい人向けのSessionEntityAuthの追加などを行いました。

Basic認証のサポート

SecureConfigアノテーションにbasic属性を指定するだけでBasic認証が行われるようになります。basic属性には"ユーザ名:パスワード"形式の文字列を渡すだけです。

/**
 * @SecureConfig(domain="admin", basic="admin:adminpass")
 */

現時点ではコンテナパラメータなどは使えないので設定は固定になります。"%basic_user%:%basic_pass%"のような指定ができてもよいかなと思いますが、とりあえず要望があるまでは保留です。

SessionEntityAuth

SessionEntityAuthはデフォルトの認証ロジックであるSessionAuthの拡張です。SessionAuthはログイン状態をセッションで管理する仕組みで、具体的にはセッションにログインフラグとユーザ情報を格納しておくだけなのですが、このうちユーザ情報はログイン時に渡された値をそのままセッションに格納しているため、エンティティを渡したときにあまりよろしくないことが起こっていました。そこで作成したのが今回のSessionEntityAuthです。

SessionAuthだと何が悪いかというと、1つはオブジェクトがそのままシリアライズされることと、2つめがデシリアライズした時にエンティティがDoctrineの管理対象にならない点です。これらを回避するため、SessionEntityAuthではIDとクラス名のみをセッションへ入れて、セッションから復元する際にクラス名を元にリポジトリを特定し、IDを元にデータを取得しています。これによりセッションには2つのスカラ値が格納されるだけで済み、常にDoctrineの管理対象になるため、更新などもスムーズに行えるようになります。

SessionEntityAuthを使う場合はauth属性に"session.entity"を指定します。

/**
 * @SecureConfig(auth="session.entity")
 */

またログインユーザを表すエンティティにはgetId()メソッドが実装されている必要があります。また、ID以外にも何らかの状態(削除フラグなど)をチェックしなければならない場合は、エンティティにisEnabled()メソッドを実装して、その中で状態を判別して、ログイン状態を復元させない場合はfalseを返すようにします。


現時点で僕が必要な機能はある程度そろっており、あとは要望があった時か、バグなどがあったときに拡張していこうかと思います。

あと、READMEを英語に翻訳してくれる方募集中です。

2012-02-20

asset()関数の第2引数に"request"を指定すると絶対URLを返す拡張

fivestar/FivestarAssetsExtraBundle - GitHub

テンプレートで次のように、asset()関数の第2引数にrequestと指定すると、絶対URLとして展開します。

<img src="{{ asset('path/to/image.png', 'request') }}" alt="..." />

スマートな方法があればそれでPull Requestでも送ってたのですが、この方法はどうもいけてない感じがして手元で止めていました。

これを作ったのはもうだいぶ前なのですが、今はasset()関数の戻り値を絶対URLにする方法があるのでしょうか。最近Symfonyの情報を追っていないのでこういう細かい部分がどうなってるかがさっぱりわかりません。

2012-01-16

DIコンテナが生成されたあとに何らかの処理を実行する方法

DIコンテナの定義がすべて読み込まれ使用する準備が整ってから、リクエストの処理が始まるまでの間に、特定のメソッドを呼び出したかったり、手動でDIコンテナの設定をしたい場合ってあると思います。
そんなときはバンドルクラスにboot()メソッドを実装すると大体解決します。

たとえば、EntityRepositoryに何かオブジェクトをDIしたいとします。ですがEntityRepositoryはEntityManagerから取得するようになっており、DIコンテナからだと設定できなくなっています。
そこで次のようにboot()メソッド内部で直接設定しておけば、それ以降オブジェクトがセットされた状態で使えます。

<?php

namespace Acme\DemoBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeDemoBundle extends Bundle
{
    public function boot()
    {
        $userRepository = $this->container->get('doctrine')->getRepository('AcmeDemoBundle:User');
        
        $userRepository->setFooService($this->container->get('acme_demo.foo_service'));
    }
}

上記のように、設定ファイルベースではどうやっていいかわからない場合や、処理が始まる場合に共通で呼び出しておきたい処理などがある場合など、boot()メソッドを知っておくと操作の幅が広がるので、知っておくとよいと思います。

2012-01-04

Google Chartでスケールの違う2つの折れ線グラフをうまいこと出す

2つの折れ線グラフを出そうとして、1つは値の幅が(A)0-200、もう片方は(B)0-4000とかで、これを何も考えず同時にプロットすると、AがBの範囲で、つまり0-4000の中で0-200が表示されるため、結果Aのグラフがほぼ平坦になってしまうわけです。

で、ドキュメントを調べていたらtargetAxisIndexというのをseriesに指定するとYou can define a different scale for different axesだよって書いてあったので、これを指定したらうまいことスケール( = 値の幅)の違うグラフが表示できました。

targetAxisIndex - Which axis to assign this series to, where 0 is the default axis, and 1 is the opposite axis. Default value is 0; set to 1 to define a chart where different series are rendered against different axes. At least one series much be allocated to the default axis. You can define a different scale for different axes.

http://code.google.com/intl/ja/apis/chart/interactive/docs/gallery/linechart.html


ちょっと画像きれてますけど、こんな感じで左右にそれぞれの値の幅が表示されます。

f:id:Fivestar:20120104160838p:plain

コードはこんな感じ。seriesの1ってのが2つ目のグラフで(0始まりなので)、targetAxisIndexを1にすると反対の軸を使うよってことです。

var options = {
  series: {
    1: {targetAxisIndex: 1}
  }
};
var chart = new google.visualization.LineChart(document.getElementById('chart'));
chart.draw(data, options);
2011-12-21

Symfony2のRequestクラスの解説

Symfony Advent Calendar JP 2011 の20日目のエントリーです。今回はSymfony2のRequestクラスについて解説しちゃいます。

RequestクラスはSymfony2のHttpFoundationコンポーネントに含まれており、HTTPリクエストに関する情報(リクエストパラメータやヘッダ、セッションなど)へアクセスするためのAPIを提供します。

Requestオブジェクトの生成


Requestオブジェクトの生成はフロントコントローラ(app.php/app_dev.php)にて行われます。Request::createFromGlobals()メソッドが呼び出されると、内部でスーパーグローバル変数を元にRequestオブジェクトが生成されます。Requestを独自クラスにしたい場合はここを直接差し替えます。

<?php
// ...
$kernel->handle(Request::createFromGlobals())->send();

リクエストパラメータや環境変数などへのアクセス

生PHPでは$_GETや$_POSTといったスーパーグローバル変数へアクセスして環境変数を取得しますが、Requestオブジェクトを用いた場合はParameterBagというオブジェクトを経由して取得します。queryやrequestといったプロパティがそれぞれParameterBagオブジェクトで、get($key)で取得、has($key)で有無の確認、all()で全データの取得など共通のインターフェイスでデータにアクセスできます。

<?php
// $_GET['foo']
$request->query->get('foo');

// $_POST['foo']
$request->request->get('foo');

// ルーティングパラメータ / ex) @Route('/{foo}')
$request->attributes->get('foo');

// $_COOKIE['foo']
$request->cookies->get('foo');

// $_FILES['foo']
$request->files->get('foo');

// $_SERVER['SCRIPT_FILENAME']
$request->server->get('SCRIPT_FILENAME');

// $_SERVER['HTTP_USER_AGENT']
$request->headers->get('User-Agent');

// query > attribute  > request の順で検索
$request->get('foo');

filesに格納されているファイル情報はUploadedFileというオブジェクトに変換されています。これはSplFileInfoを継承しており、同じインターフェイスでファイルを操作できます。

headersは$_SERVERに入っている情報のうち、HTTPリクエストヘッダの情報(HTTP_*)のみが取得できます。User-AgentやAccept-LanguageのようにHTTPヘッダ形式で取得できます。

HTTPメソッドの取得

getMethod()メソッドでHTTPメソッドを取得できます。

<?php
if ('POST' === $request->getMethod()) {
    echo 'post';
}

セッション操作

セッションはSessionオブジェクトを経由してアクセスします。SessionオブジェクトはRequest::getSession()メソッドで取得できます。

<?php
$session = $request->getSession();

$session->has('foo.bar');
$session->set('foo.bar', 'value');
$value = $session->get('foo.bar');
$session->remove('foo.bar');
$session->clear();

$session->setFlash('notice', 'value');
$value = $session->getFlash('notice');

$session->getId();

// IDを変更
$session->migrate();

// 中身をクリアしてIDを変更
$session->invalidate();

URI情報を取得

下記のメソッド参照。getUriForPath()メソッドはよく使います。

getRequestUri()など

<?php
// http://fvstr.jp:8080/mysite/app_dev.php/demo?foo=bar
$request->getUri();

// http
$request->getScheme();

//  fvstr.jp
$request->getHost();

// 8080
$request->getPort();

// fvstr.jp:8080
$request->getHttpHost();

// /mysite
$request->getBasePath();

// /mysite/app_dev.php
$request->getBaseUrl();

// /demo
$request->getPathInfo();

// foo=bar
$request->getQueryString();

// http://fvstr.jp:8080/mysite/app_dev.php/foobar
$request->getUriForPath('/foobar');

HTTPSの判定

isSecure()メソッドを使います。

<?php
if ($request->isSecure()) {
    echo 'https!';
}

リクエストされたコンテンツの拡張子を取得

URLに拡張子をつけたい場合、Symfonyではルーティングに_formatというパラメータを指定します。そうするとgetRequestFormat()メソッドで拡張子が取得でき、さらに@Templateアノテーションでテンプレート呼び出しをしている場合も自動的に拡張子部分に使われます。ちなみにデフォルト値を設定しておくと省略可能になるので、下記の場合は/about/meだけでもアクセス可能です。

<?php
// URI = http://fvstr.jp/about/me.json

/**
 * @Route("/about")
 */
class AboutController
{
    /**
     * @Route("/me.{_format}", defaults={"_format": "html"}, requirements={"_format"="(html|json)"})
     * @Template
     */
    public function meAction(Request $request)
    {
        // json
        $request->getRequestFormat();
    }
}

Ajaxの判定

isXmlHttpRequest()メソッドを使います。

<?php
if ($request->isXmlHttpRequest()) {
    echo 'XHR!!';
}

ユーザのIPアドレスを判定する

getClientIp()を使います。なお引数にtrueを渡すと$_SERVER['HTTP_CLIENT_IP']や$_SERVER['HTTP_X_FORWARDED_FOR']の値があった場合、優先的にそちらを返します。

<?php
// $_SERVER['REMOTE_ADDR'];
$request->getClientIp();

X-Forwarded系のヘッダを有効にする

いくつかのメソッドではX-Forwarded-Forなどのプロキシによって設定されるヘッダを優先的に参照するようになっています(getHost()やgetClientIp()など)。しかしデフォルトでは参照されないようになっており、app/config.ymlで有効にする必要があります。有効にする場合はframeworkの中にtrust_proxy_headers: trueを追加します。

framework:
    # ...
    trust_proxy_headers: true

RequestオブジェクトとDIコンテナ

RequestオブジェクトはrequestというキーでDIコンテナに格納されていますが、扱いが少々特殊です。DIコンテナは1回のアクセスにつき1インスタンスのみ作られます。しかしRequestはforwardした際に複製されるため、呼び出す場所によってインスタンスが変わる可能性があります。それを管理するために「request」というスコープが設定されています。

$container->get('request'); のように自分で取得する場合はあまり意識しませんが、RequestオブジェクトをDIの対象にしたい場合にスコープを意識する必要が出てきます。requestをDIしたい場合、次のようにscopeという属性を追加します。こうすると、Requestが別インスタンスになったあとにacme.foo_serviceを取得しようとすると、別のAcme\DemoBundle\FooServiceインスタンスが作成されるようになります。

services:
    acme.foo_service:
        class: Acme\DemoBundle\FooService
        arguments:
            - @request
        scope: request

また、次のように末尾に=をつけることで、制約を無視するようになります。PHPの@演算子みたいなものです。

services:
    acme.foo_service:
        class: Acme\DemoBundle\FooService
        arguments:
            - @request=

なおCLIの場合、requestがなくてDIできずに怒られる、ということがありますので、まあその辺はうまいことやってください。コンテナごと渡して中で判定するとか、引数として渡すようにするとか。

Requestオブジェクトをアクションの引数として受け取る

下記のようにタイプヒントつきでアクションの引数に書いておくとRequestオブジェクトを受け取れます。

<?php

namespace Acme\DemoBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DemoController extends Controller

    public function indexAction(Request $request)
    {
    }
}

RequestオブジェクトをTwigのテンプレート内で取得

app.requestで取得できます。

{% if not app.request.secure %}
<a href="https://{{ app.request.httpHost ~ app.request.baseUrl ~ app.request.pathInfo }}">HTTPS切り替え</a>
{% endif %}


解説は以上です。アップロードされたファイルをSplFileInfoと同じインターフェイスで扱えるのが結構扱いやすくていいです。Rackみたいなのが欲しいっていってる人いますけど、HttpFoundationコンポーネントでいいじゃんって思います。