Issei.M's Techlog

Web/iOS エンジニアの僕が技術関連のメモ等をつらつらと。主に Symfony について書いています。

[Symfony][Form] フィールドタイプはよく考えて決めよう

Symfony2を使い始めて2年くらい経ちましたが、未だ全容をつかみきれない機能の1つがForm。今回はそんなFormのフィールドタイプについて話します。

通常、Symfony2のFormではフィールドタイプを次のようにして定義します。

$builder->add('addressLine', 'text', ['label' => '住所']);
$builder->add('latitude', 'hidden');
$builder->add('longitude', 'hidden');

テンプレートはこんな感じ。

{{ form_start(form) }}
  {{ form_row(form.addressLine) }}
  <button type="submit">送信</button>
{{ form_end(form) }} {# hiddenフィールドはここで自動出力されます #}

この場合、画面上には「住所」を入力するテキストフィールドだけが表示されますが、サブミットの直前に住所の内容からJavaScriptでGoogleMap APIから取得し、hiddenタイプの緯度・経度に位置情報が入る仕様だとします。(実装例は割愛)

サブミットを行うと、データは次のようにマップされます。

$form->handleRequest($request);
var_dump($form->getData());

/*
 * ※DTOは使わないのでデータは配列です。
 * [
 *   'addressLine' => string '東京都渋谷区...',
 *   'longitude'   => string '35.658534',
 *   'latitude'    => string '139.701330',
 * ]
 */

緯度経度がfloatではなくstringとなっています。当然といえば当然なのですが...。
因みにはじめからnumberタイプを定義していれば、適切にfloat型としてマップされます。

$builder->add('latitude', 'number');
$builder->add('longitude', 'number');

※但しこの場合はinput[type=text]として画面上にUIが表示されます。

クライアントサイドの都合で安直にhiddenを使うと今回のような悲劇を生みます。(´・ω・`)
解決法は色々ありますが今回のケースではオーソドックスに DataTransformer を使うのが良さそうです。

$builder->add(
    $builder
        ->create('latitude', 'hidden')
        ->addViewTransformer(new NumberToLocalizedStringTransformer())
);
$builder->add(
    $builder
        ->create('longitude', 'hidden')
        ->addViewTransformer(new NumberToLocalizedStringTransformer())
);

この他にDataTransformerを使わずにnumberタイプを定義しておいて、テンプレート側でhiddenにすると言う方法もあります。

{{ form_start(form) }}
  {{ form_row(form.addressLine) }}
  {{ form_row(form.longitude, {type: 'hidden'}) }}
  {{ form_row(form.latitude, {type: 'hidden'}) }}
  <button type="submit">送信</button>
{{ form_end(form) }}

この場合form_endでのhiddenフィールド自動出力の恩恵が受けられませんが、最終的なUIがテキストボックスであればこちらの方が良いかもしれません。
Symfony2が予め用意しているフィールドタイプは、numberのようにデータを適切に変換してくれる物が多いからです。

何はともあれこれでデータは適切にfloatに変換されました。めでたしめでたし。

[Symfony] Doctrine の Repository をサービスコンテナに登録する

AcmeBlogBundle\Repository\BlogRepositoryをサービスコンテナに追加:

# @AcmeBlogBundle/Resources/config/services.yml
parameters:
    acme_blog.repository.blog.class:  Acme\BlogBundle\Repository\BlogRepository

services:
    acme_blog.repository.blog:
        class:           '%acme_blog.repository.blog.class%'
        factory_service: doctrine.orm.entity_manager
        factory_method:  getRepository
        arguments:       [ 'AcmeBlogBundle:Blog' ]

要するに $doctrine->getRepository('AcmeBlogBundle:Blog'); の処理をサービスとして定義しているだけです。

// returns true
$container->get('acme_blog.repository.blog') === $doctrine->getRepository('AcmeBlogBundle:Blog'); 

[Symfony] Buzz\Browser でお手軽 HTTP リクエスト

HTTP リクエストを送る PHP ライブラリは数多くありますが、中でも Buzz\Browser が使いやすかったので Symfony での使用例をご紹介。

インストール

いつも通り、Composer でインストールします。作者は Assetic で有名な Kirs Wallsmith さんです。

php composer.phar require kriswallsmith/Buzz:dev-master

折角 Symfony で使うのでサービスコンテナに登録しちゃいます。

# services.yml
services:
    buzz.client.curl:
        class:  Buzz\Client\Curl
        public: false
        # 証明書の検証が必要な場合は calls を削除
        calls:
          - [ setVerifyPeer, [false] ]
    buzz.browser:
        class:     Buzz\Browser
        arguments: ["@buzz.client.curl"]

使い方はこんな感じ

適当なコントローラに実装します。

$browser = $this->get('buzz.browser'); # ContainerInterface::get()

# 診断くん
$response = $browser->get('http://taruo.net/e/');
if (!$response->isSuccessful()) {
    throw new HttpException($response->getReasonPhrase(), $response->getStatusCode());
}

$content = $response->getContent(); # 診断くんの HTML ソース

返却された Response オブジェクトの::isSuccessful()でリクエスト結果の成否を調べる事ができます。 エラーの際は::getStatusCode(),::getReasonPhrase()で内容の確認しましょう。
本文の取得は::getContent()です。

またBrowser::get()の第2引数には任意のリクエストヘッダを配列で指定する事ができます。

$browser->get('http://taruo.net/e/', array('User-Agent', 'buzz.browser'));

::get()の他にも::post()::put()等基本的なものが揃っています。

cURL が使えない環境の人は...

file_get_contentsもサポートされているので安心。サービスコンテナの設定を次のように変更すればOKです。

# services.yml
services:
    buzz.client.file_get_contents:
        class:  Buzz\Client\FileGetContents
        public: false
        calls:
          - [ setVerifyPeer, [false] ]
    buzz.browser:
        class:     Buzz\Browser
        arguments: ["@buzz.client.file_get_contents"]

もちろん実装部のソースコードを変える必要はありません。DI コンテナさまさまです♪

[Symfony] LiipThemeBundle でスマホ対応&Assetic を使う際の諸注意

Symfonyスマートフォン対応するにあたって LiipThemeBundle がよさげだったのでご紹介。また当バンドルを使用する際 Assetic でハマる箇所がある為メモ。

Symfony Standard Edition 2.2.1 の AcmeDemoBundle で試してみます。

LiipThemeBundle をインストール

Composer でインストール。

$ php composer.phar require liip/theme-bundle:dev-master

Bundle を登録。

# app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        // ...
        new Liip\ThemeBundle\LiipThemeBundle(),
    );
}

config.yml に追記。themesには使用するテーマを定義します。今回は PC とスマホのみなのでweb,phoneを定義します。 path_patternsについては後述。

# app/config/config.yml
liip_theme:
    themes:           ['web', 'phone']
    active_theme:     'web'
    autodetect_theme: true
    path_patterns:
        app_resource:
            - %%app_path%%/views/themes/%%current_theme%%/%%template%%
        bundle_resource:
            - %%bundle_path%%/Resources/views/themes/%%current_theme%%/%%template%%
    cookie:
        name: site_theme
        lifetime: 31536000 # 1 year in seconds
        path: /
        domain: ~
        secure: false
        http_only: false

続いてテーマ切り替えページ用に routing.yml に追記。

# app/config/routing.yml
liip_theme:
    resource: "@LiipThemeBundle/Resources/config/routing.xml"
    prefix:   /theme

スマホ用テンプレートの追加

準備が整ったので早速スマホ用のテンプレートを作成していきます。 src/Acme/DemoBundle/Resources/views 以下に themes/phone/Welcome とディレクトリを作っていき、その中に次のファイルを作成します。

{# src/Acme/DemoBundle/Resources/views/themes/phone/Welcome/index.html.twig #}

{% extends 'AcmeDemoBundle::layout.html.twig' %}
{% block title %}Symfony - Welcome Smartphone!{% endblock %}
{% block content_header '' %}
{% block content %}
    <h1 class="title">スマホレイアウト!!</h1>
    スマホだよ~ん
{% endblock %}

それでは Web ブラウザで確認してみましょう。まず /theme/phone にアクセスします。トップにリダイレクトされたのち、先ほど追加したテンプレートが適用されているのが確認できます。

f:id:issei_m:20130514204557p:plain

※今回autodetect_themeを有効にしているのでスマホで直接アクセスしてもOKです。また、/theme/web にアクセスすると PC 用レイアウトに戻す事ができます。

次にベースレイアウトをスマホに最適化する為、以下を追加します。

{# src/Acme/DemoBundle/Resources/views/themes/phone/layout.html.twig #}

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>{% block title %}Demo Bundle{% endblock %}</title>
        <meta content="telephone=no" name="format-detection">
        <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
        <meta content="noarchive" name="robots">
        <style>h1{font-size:16pt;}</style>
    </head>
    <body>
        {% block content %}
        {% endblock %}
    </body>
</html>

試しにスマホで表示するとこのような感じになります。(だいぶ質素になりました)

f:id:issei_m:20130514214742p:plain

ちなみに app/Resources/views にも同様のディレクトリ構造を作ることでスマホ対応が可能です。

このライブラリの最も優れている所は、スマホ用にテンプレートが用意されていないページでも通常のテンプレートを使用してフォールバックしてくれる点です。
試しに /demo/hello/World にアクセスすると、PC 用テンプレートではありますがきちんとページが表示されます。

Assetic を併用する場合の注意点

Assetic は非常に便利ですが、LiipThemeBundle との併用にあたってひとつ注意点があります。
README によると本来ディレクトリ構造は Resources/themes/phone/... のようにするのですが、これだと中のテンプレートで定義しているアセットがphp app/console assetic:dumpでダンプできなくなります。(デバッグ環境では Routing Error が発生します)

これを回避する為テーマディレクトリは Resources/views 以下に配置し、前述のとおり config.yml にpath_patternsを指定します。

それでも問題が発生する時は...

その時はphp app/console cache:clear --no-warmupを試してみて下さい。
--no-warmupが重要です。

[Monolog][Symfony] Monolog が神過ぎる件 ~エラーログをメールで送ろう~

Symfony で採用されているロガーライブラリ Monolog は Python の LogBook と言うライブラリに影響を受けており、 柔軟なロギング処理が可能です。
今回は、Symfony の運用環境でERRORレベル以上のログをファイルに書き込みつつ、メールでも通知するように設定してみます。

以下、config.yml記述例。

monolog:
    handlers:
        # ERROR 以上でログファイル書き込み+メール送信
        main:
            type:         fingers_crossed
            action_level: ERROR
            handler:      grouped
        # 以下 fingers_crossed 用
        grouped:
            type:    group
            members: [file, email]
        file:
            type:  stream
            path:  %kernel.logs_dir%/%kernel.environment%.log
            level: DEBUG
        email:
            type:       swift_mailer
            from_email: no-reply@example.com
            to_email:   issei.m7@gmail.com
            subject:    "[example.com] An error occurred!"
            level:      DEBUG

mainにはERRORレベル以上でgroupedが処理され、fileemailが実行されるように設定しています。

このfingers_crossedが非常に優秀でgrouped, file, emailmainの下位ハンドラとなっており、通常は処理されません。この為fileemailはレベルがDEBUGですが、エラー発生時にだけ該当セッション中の全ログが書き込まれます。

これで無駄にログを肥大させる事なく、デバッグ効率が格段上がりまくりんぐです。