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

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

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さんです。がんばってください。