OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

Symfony2のコントローラについてまとめた(後半)

こんにちわ!@mrtryです。 年末年始まであと1月、がんばっていきたいです...!

さて、「Symfony2入門」の5回目の記事です。 前回に引き続き、コントローラについてまとめます。

今回は、実際にコントローラを利用する際に基本となる ルーティングパラメータとHTTPリクエストの利用方法と、Responseオブジェクトの生成方法を紹介します。


以下、今回の記事の目次になります。

  • ルーティングパラメータの利用
  • HTTPリクエストの情報を取得
  • Responseオブジェクトを生成するメソッド

ルーティングパラメータの利用

ルーティングで設定したプレースホルダとdefaultsの値は、同じ変数名でコントローラの引数に定義すると、値をそのまま利用することができます。

以下の例では、 ルーティングで定義されている変数firstNamelastNamecolor_controllerのうち、firstNamelastNameをコントローラの引数に用いています。

# app/config/routing.yml
hello:
    path:      /hello/{firstName}/{lastName}
    defaults:
        _controller: AcmeHelloBundle:Hello:index
        color: green

AcmeHelloBundle:Hello:index

public function indexAction($firstName, $lastName)
{
    ...
}

HTTPリクエストの情報を取得

HTTPリクエストの情報は、Requestオブジェクトを利用すると簡単に取得することができます。 Symfony\Component\HttpFoundation\Requestをタイプヒントで指定し、引数を定義すると、Requestオブジェクトがフレームワークにより自動的に注入されます。

Requestオブジェクトには、スーパーグローバル変数に対応するプロパティが定義されています。

スーパーグローバル変数名 対応するプロパティ名
$_POST $request
$_GET $query
$_SERVER $server, $header
$_FILES $files
$_COOKIE $cookies

また、ルーティングパラメータの値は、$attributesというプロパティに格納されています。

各プロパティの値は、ParameterBagを通して取得します。

use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    // $_POST['hoge']
    $request->request->get('hoge');

    // $_GET['hoge']
    $request->query->get('hoge');

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

    // $_SERVERのインデックスのうち、命名がHTTP_*に該当するもの
    // $_SERVER['HTTP_USER_AGENT']
    $request->header->get('user-agent');

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

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

    // ルーティングパラメータの取得
    $request->attributes->get('_route');
    $request->attributes->get('_controller');

    // プレースホルダ、defaultsで定義した変数も取得できる
    $request->attributes->get('hoge');
    $request->attributes->get('fuga');
}

Responseオブジェクトを生成するメソッド

コントローラは、最終的に、Responseオブジェクトを生成して返す役割があります。 Responseオブジェクトを生成するために用意されているメソッドを紹介します。

リダイレクト

別のページへリダイレクトさせる際は、redirectToRoute()、または、redirect()を利用します。

以下の例では、 ルート名:homepageのページへリンクを作成し、redirectToRoute()redirect()でリダイレクトを設定しています。

public function indexAction()
{
    // このメソッドはSymfony 2.6以上だと利用できる
    // redirectToRoute()の引数は、ルート名
    return $this->redirectToRoute('homepage');

    // redirect()は、引数がURLなので、generateUrl()でURLを生成している
    return $this->redirect($this->generateUrl('homepage'));
}

基本的に、redirectToRoute()redirect()を利用した際のレスポンスコードは302になりますが、 第2引数で、レスポンスコードを任意に指定することもできます。

public function indexAction()
{
    // このメソッドはSymfony 2.6以上だと利用できる
    return $this->redirectToRoute('homepage', 301);

    return $this->redirect($this->generateUrl('homepage'), 301);
}

フォワード

別のコントローラへフォワードする際は、forward()を利用します。 forward() の戻り値は、呼び出したコントローラーが生成するResopnseオブジェクトになります。

以下の例では、indexAction()の処理の過程で、AcmeHelloBundle:Hello:fancyの処理したResponseオブジェクトを利用しています。

public function indexAction($name)
{
    // AcmeHelloBundle:Hello:fancyのResponseオブジェクトが返ってくる
    $response = $this->forward('AcmeHelloBundle:Hello:fancy', [
        'name'  => $name,
        'color' => 'green',
    ]);

    // ... $responseをさらに加工するか、直接返す

    return $response;
}

テンプレートのレンダリング

テンプレートをレンダリングし、HTMLを返すには、render()を利用します

以下の例では、AcmeHelloBundle:Hello:index.html.twigというテンプレートをレンダリングし、HTMLとして返しています。

public function indexAction()
{
    return $this->render(
        'AcmeHelloBundle:Hello:index.html.twig', [
            'name' => $name
    ]);
}

HTTPステータスコードの403、404を返す

HTTPプロトコルに沿ったHTTPステータスコードを返すメソッドとして、 createNotFoundException()と、createAccessDeniedException()が用意されています。 createNotFoundException()は、404(Not Found)createAccessDeniedException()は、403(Forbidden)のHTTPステータスコードを持つ例外を返します。

public function indexAction()

    ...

    // 認証確認
    if (!$isAuthorization) {
        throw $this->createAccessDeniedException('アクセスが禁止されています');
    }

    ...

    // データベースからオブジェクトを取得する
    if (!$product) {
        throw $this->createNotFoundException('製品が存在しません');
    }

    return $this->render(...);
}

また、コントローラ内で例外が発生した時、 $response->headerX-Status-Codeが値を持っているか、発生した例外がHttpExceptionInterfaceを実装している場合、設定されているHTTPステータスコードの値を返します。 どちらも該当しない場合は、500のHTTPステータスコードを返すようになっています。

おわりに

今回は、ルーティングパラメータとHTTPリクエストの利用方法と、Responseオブジェクトの生成方法についてまとめました。 次回は、SymfonyでORMとして利用されているDoctrineについてまとめたいと思います。

また、この記事は、@mrtryの勉強の一環で書いています。 お気づきの点がありましたら、コメント等でご指摘いただければ幸いです!

参考

Symfony Console のコマンド名を自動的に Monolog のログに出そう

この記事は Symfony Advent Calendar 2016 - Qiita 10日目の記事です。

みんな大好き Console のお話です。

CLI から Monolog Logger を使う

Symfony2 or 3 を使っているのであれば Console Component のお世話になっていると思います。

そしてほとんどの場合は便利なサービスを使いたいはずなので、 ContainerAwareCommand を使いますよね。

これを使えばサービスコンテナから Logger を取得し、Web と同じように CLI でもログを吐くことができます。 *1

例えば、下記のようなコマンドを定義し、

<?php

namespace AppBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class AppMyCommandCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('app:my-command')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $logger = $this->getContainer()->get('logger');
        $logger->info('hello otobank');
    }
}

こういうコマンドを叩くと

$ ./bin/console app:my-command

var/logs/dev.log には下記のようなログが出力されます。

[2016-12-08 11:21:34] app.INFO: hello otobank [] []

OK. いいですね。

しかしこの var/logs/dev.log は Web でも使用されますし、別のコマンドを作ればそちらでも使用されることになるので、いろんなログが混ざり合い、とても見づらくなってしまいます。 *2

コマンド名がログに出力されれば、他と見分けがつくのでとても便利だと思いませんか?

といっても、毎回自分で

<?php
$logger->info('[my-command] hello otobank');

なんて書きたくないですよね。 *3

Monolog Processor を使う

そういった場合に有効なのが Monolog Processor です。

app/config/services.yml などに、ちょちょいとこいつを使うように定義してあげれば、

あら不思議、すべてのログに思いのままに付加情報をつけることができます。

デフォルトで用意されているプロセッサーは ここ にある限りだと思うのですが、

URL を出せる WebProcessor

プロセスIDを出せる ProcessIdProcessor はあっても、

コマンド名を出せるプロセッサーはありません。

それもそのはず、Monolog は Symfony Console のことなんて知らないですからね。

そうなってくると Symfony と Monolog を橋渡し(Bridge)する役目の ここ にあるかと思いきや、

残念、ちょっとそれらしきものはないわけです。

というわけで作りましょう。

Monolog Console Processor を作った

作ったものがこちらになります。 otobank/monolog-console-processor - Packagist

ソースコードは monolog-console-processor/ConsoleProcessor.php at 1.0.0 これだけ。

やってることは至極単純で、ConsoleEvents::COMMAND イベントを Subscribe して、コマンド名を保持しておき、ログが吐かれるタイミングで extra フィールドに保持しておいたコマンド名を付加するだけです。

使い方は monolog-console-processor/README.md に書いてあるとおりですが、

composer require otobank/monolog-console-processor

でインストールして、

app/config/services.yml などに

services:
    monolog.processor.console:
        class: Otobank\Monolog\Processor\ConsoleProcessor
        tags:
            - { name: monolog.processor }
            - { name: kernel.event_subscriber }

と書くだけ。

では再度コマンドを叩いてみましょう。

$ ./bin/console app:my-command

var/logs/dev.log には下記のようなログが出力されました。

[2016-12-09 17:55:26] app.INFO: hello otobank [] {"command_name":"app:my-command"}

いいですね。最高ですね。

おわりに

なんてことないプログラムですが、毎回書くことを考えれば便利なんじゃないかと思います。

だるいことはどんどんプログラムに任せよう!

そして空いた時間で龍が如く6をやろう!

はたまたオーディオブックを聴こう!

*1:バッチ処理ではなければわざわざ Logger を使わずに OutputInterface を使って標準出力に出せばいいと思うので、今回の記事はバッチ処理にコマンドを使う場合に有効な話です。とはいえ、サービス内で Logger を使用してロギングしている場合もあると思うので、そういう場合にも有効です。

*2:単純な解決方法として、ファイル名を分けるという方法もありますが、1つのアプリケーションログを構造化されたログとしてそのままFluent経由で送信していたのでログを分けたくなかったという経緯があります。

*3:これを書くことをいとわない人はプログラマーの三大美徳を思い出しましょう

2016年11月のどや会ごはん - 北海道直送 炭焼き酒場 36番倉庫 -

こんにちは! 愛子さまが15歳のお誕生日を迎えたと聞いて驚愕している@perokoです。
この間まで赤ちゃんじゃなかったっけ・・・???

さて、弊社は11月が期末なので、
お陰様を持ちまして、無事13期を迎えることができました。
今後ともオトバンクとオトバンク開発部をどうぞよろしくお願いいたします!

つまり、弊社は愛子さまのお誕生日とともに新しい期が始まるんだなあ。
5年目にして初めて気づきました。(どうでもいい)

ぼやっとしていると夢中で仕事をしていると時間が経つのは早いなあ〜。

f:id:peroko_tokyo:20161201153409j:plain

話は変わってどや会です。
オトバンク開発部では毎月末に月次の締め会として「どや会」を実施しています。

ビール片手に開催するこの会は、開発したものを"ドヤァ"したり、メンバー個人の近況を報告し合ったりして、チームビルディングに役立てようというものですが、参加者の感想としては「ご飯がおいしい」が9割なのではないかという噂もあります。

「どや会」の詳細についてはこちら engineering.otobank.co.jp

11月のどや会ごはん

11月のどや会ごはんは、東京の釧路「北海道直送 炭焼き酒場 36番倉庫」のお料理でした!
36番倉庫は何を食べても美味しい。そして安い。最高のお店です。

retty.me

f:id:peroko_tokyo:20161201144531j:plain 右上に写っている煮込みは世界で一番美味しい牛とうふ煮込み

f:id:peroko_tokyo:20161201144458j:plain 本当に分厚くておいしいハムカツ

f:id:peroko_tokyo:20161201144445j:plain 「弱くて甘いやつ」というオーダーをしたら、部長とデザイナーさんがおいしいやつを買ってきてくれました

f:id:peroko_tokyo:20161201144334j:plain 「他のエンジニアの作業環境が知りたい」という話が出たので、プロジェクターに作業環境を映し出して実演したり

36番倉庫、まじでおすすめです(宣伝)

テイクアウトはやっていない普通の居酒屋さんなんですが、今回はご無理をきいていただきました! 系列店(本郷・五反田・根津)も最高なので気になる方は是非行ってみてください〜😋

というわけで、11月もお疲れ様でした!
お正月休みまでもうちょっと!12月もがんばりましょ〜う!