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を英語に翻訳してくれる方募集中です。

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の情報を追っていないのでこういう細かい部分がどうなってるかがさっぱりわかりません。

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()メソッドを知っておくと操作の幅が広がるので、知っておくとよいと思います。

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コンポーネントでいいじゃんって思います。

Command内でy/nの確認ダイアログを表示する

Symfony\Component\Console\Helper\DialogHelper::askConfirmation(OutputInterface $output, $question, $default = true)

<?php

class FooCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if ($this->getHelper('dialog')->askConfirmation(
            $output, '<question>> Are you sure? (y/N)</question>', false)
        ) {
            // y
        } else {
            // n
        }
    }
}

任意のテンプレートファイルを読み込む

Symfony2では "AcmeDemoBundle:Demo:index.html.twig" みたいな形式でテンプレートを指定しますが、ファイルのパスを指定したい場合ってあると思います。そんなときはTemplateReferenceオブジェクトを使います。

<?php

use Symfony\Component\Templating\TemplateReference;

$template = new TemplateReference($path = '/path/to/template', $engine = 'twig');

$templating = $container->get('templating');
$templating->render($template, array('var' => $value));

SymfonyのFormコンポーネントを使いこなすために

どうもこんにちは。小川です。日付変わっちゃいましたが、Symfony2 Advent Calendar JP 2011の5日目です。今回はFormコンポーネントを使いこなす上でぜひ知っておきたいポイントを紹介します。

今回紹介するのは次の3つです。

  • 任意のプロパティをフィールドにマッピングする
  • どのプロパティにもマッピングしないフィールドを定義する
  • どのプロパティにもマッピングしないフィールドをバリデーションする

任意のプロパティをフィールドにマッピングする

Formコンポーネントを通常に使うと、オブジェクトの構造とフォームの構造を合わせるようにすると思います。たとえば次のクラスがあるとします。

<?php

class Foo 
{
    public $prop1;
    public $bar;
}

class Bar 
{
    public $prop2;
}

$foo = new Foo();
$foo->bar = new Bar();

このFooとBarを1つの画面で編集する場合、おそらくFooTypeとBarTypeを作って、FooTypeの中で $buider->add('bar', new BarType()) などとするでしょう。しかしちょっと別のオブジェクトのフィールドにマッピングするためにクラスを1つ用意するのは煩わしいと感じる場合もあります。そんな時は次のように、Property-Pathというオプションを指定します。

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
    
class FooType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options
    {
        $builde->add('prop1', 'text')
            ->add('prop2', 'text', array('property_path' => 'bar.prop2'))
        ;
    }

    public function getName()
    
        return 'foo';
    
}

Property-Path(add()に指定されたproperty_pathオプション)は、指定したフィールドが、オブジェクトのどのプロパティへマッピングするかを記したもので、デフォルトだとadd()の第1引数に指定した値と同一です。FooTypeにはFooオブジェクトが渡されることを前提としています。たとえばProperty-Pathがprop1なら、$foo->prop1となり、Property-Pathがbar.prop2なら、$foo->bar->prop2のようになります。ドットがセパレータになっているのはTwigと同じなので、Twigでどうやってプロパティにアクセスするかというのと同じだと思ってください。つまりProperty-Pathを設定さえすれば、オブジェクトからたどれるすべての値へマッピングができるということです。Validatorの内部でもこのProperty-Pathという概念は使われているので、覚えておくといいでしょう。

追記:
ちなみにProperty-Pathという名前ですが、セッターが定義されている場合はセッターを経由してアクセスします。isXxx()系のメソッドでもOKです。

参考クラス:

  • Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper
  • Symfony\Component\Form\Extension\Core\Type\FieldType
  • Symfony\Component\Form\Util\PropertyPath

どのプロパティにもマッピングしないフィールドを定義する

Property-Pathの応用で、property_pathオプションにfalseもしくは空文字を指定すると、どのプロパティにもマッピングされなくなります。たとえばユーザの登録画面などで、ユーザ情報に加えて規約に同意されたかを確認するチェックボタンを設置するということはよくあると思います。次のRegistrationTypeではデータとはマッピングされないagreementというチェックボックスを追加する例です。

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('email', 'email')
            ->add('password', 'password')
            ->add('agreement', 'checkbox', array('property_path' => false, 'required' => true))
        ;
    }

    public function getName()
    {
        return 'registration';
    }
}

このようにProperty-Pathを使うとわりと思いのままにフォームを組み立てることができるので、覚えておいて損はないと思います。

どのプロパティにもマッピングしないフィールドをバリデーションする

先ほどのRegistrationTypeでagreementフィールドを定義しましたが、このようなオブジェクトにマッピングされていないフィールドをバリデーションしたい場合もあると思います。その場合、CallbackValidatorというものを使ってバリデーション処理を記述します。

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackValidator;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('email', 'email')
            ->add('password', 'password')
            ->add('agreement', 'checkbox', array('property_path' => false))
        ;

        $buider
            ->addValidator(new CallbackValidator(function($form) {
                if (!$form['agreement']->getData()) {
                    $form['agreement']->addError(new FormError('同意したまえ'));
                }
            }))
        ;
    }

    public function getName()
    {
        return 'registration';
    }
}

CallbackValidatorには任意のコールバックを渡すことができ、リクエストデータがバインドされた時に呼び出されます。その際引数にFormオブジェクトが渡されるので、ここからデータを取得してバリデーションを自前で行い、addError()メソッドにエラーを渡します。いくつかクラスをインポートしなきゃいけなかったり少し不便なので、その辺もうちょい簡単にできるように拡張しようかなーと考えてます。

ちなみにCallbackValidatorの他にCallbackTransformerというものもあり、コールバックを用いてデータの変換が可能です。これもまあ知っておくと便利ですが、これらのオブジェクトを意識せずに、思いついたときにぱっとコールバックを書けるといいなーと思います。


こんな感じで、Property-PathとCallbackValidatorを知っておくとよいですよということで、フォームの紹介は終わりです。

Advent Calendarの方ですが、次はuechocoさんです。がんばってください。