[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 にアクセスします。トップにリダイレクトされたのち、先ほど追加したテンプレートが適用されているのが確認できます。
※今回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>
試しにスマホで表示するとこのような感じになります。(だいぶ質素になりました)
ちなみに 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
が処理され、file
とemail
が実行されるように設定しています。
このfingers_crossed
が非常に優秀でgrouped
, file
, email
はmain
の下位ハンドラとなっており、通常は処理されません。この為file
やemail
はレベルがDEBUG
ですが、エラー発生時にだけ該当セッション中の全ログが書き込まれます。
これで無駄にログを肥大させる事なく、デバッグ効率が格段上がりまくりんぐです。