symfony で Web API を実装するときのポイントいくつか

最近はまんぐうん家にいます。 nagata (@handlename) です。

FlashやJSと連携する際には、APIを実装することになりますが、 今回は「こんなことやると実装が楽になるよ!」という小技をいくつか紹介します。

  • ※本記事ではレスポンスの形式としてJSONを使った場合を例として用いています。
  • ※アプリ名を「api」、モジュール名を「kayac」として説明します。
  • symfonyのバージョンは1.4です。

APIのレスポンスを返すための準備

symfonyのレスポンス、そのままではレイアウトが適用されてしまいます。 HTML形式で表示されるわけですね。

APIのレスポンスとして使う場合、こんなんじゃやってられません。 view.yml でレスポンスの形式を設定してしまいましょう。

apps/api/config/view.yml

has_layoutをfalseにすることによって、レイアウトを無効にできます。 具体的には、 /apps/api/templates/layout.php を通さずに、 テンプレートが出力するものをそのまま表示するようになるわけです。

レスポンス用のテンプレートを統一

プロジェクトによってはたくさんの種類のAPIを用意しなければいけなくなることがあります (この前担当した案件では、最終的に60種類ほど用意しました)。

たくさんあるAPIそれぞれにいちいちテンプレートを用意するのは何とも面倒です。 加えて、APIのレスポンス形式はひとつのプロジェクト中で統一されていると思います。

ならばそのテンプレート、ひとつにまとめてしまいましょう!

アクション内でテンプレートを変更する

アクション内でテンプレートを変更する場合、setTemplate メソッドを使えばOKです。

symfony API sfAction Class

apps/api/modules/kayac/actions/actions.class.php

sharedモジュールのjsonSuccessテンプレートで表示されます。

テンプレートの変更も1カ所で

実際には個々のアクション内で定義せずに、すべてのAPIアクションが継承するクラスを定義して、 そのクラスの postExecute でテンプレートの設定をするといいでしょう。

apps/api/lib/myApiActions.class.php

テンプレートにも一工夫

APIレスポンスのチェックは結構しんどいものです。 ばっちりテストを書いていればなにも問題ないですが、 ちょっと実行して目視で確認したいときもあるはず。

レスポンスを読みやすいように整形しておけば万事解決です。 今回はレスポンスをJSON場合の例を紹介します。

JSON整形用のヘルパーを用意する

テンプレート内でごにょごにょやってもいいのですが、 せっかくなのでヘルパーを有効利用してみます。

なお、MyJSONHelper中のjson_format関数はPHP ManualのNotesより引用させていただきました。

lib/helper/MyJSONHelper.php

ヘルパーを使って表示!

テンプレート内ではこんな風に使います。 設定ファイル内で

  • JSONを整形するか (app_json_pretty_print)
  • マルチバイト文字をエスケープしないか (app_json_allow_multibyte)

を設定しています。 後述する environment.yml にこの設定を書くことによって、 テスト環境と本番環境でスムーズに出力を切り替えることができます。

app/api/modules/shared/jsonSuccess.php

これでこんなJSONの出力が、

{"name":"\u682a\u5f0f\u4f1a\u793e\u30ab\u30e4\u30c3\u30af","address":"\u3012248-0006 \u795e\u5948\u5ddd\u770c\u938c\u5009\u5e02\u5c0f\u753a2-14-7 \u304b\u307e\u304f\u3089\u6625\u79cb\u30b9\u30af\u30a8\u30a22\u968e","status":200}

こんな風に整形されて出力されます。見やすい!

{
  "name": "株式会社カヤック",
  "address": "〒248-0006 神奈川県鎌倉市小町2-14-7 かまくら春秋スクエア2階",
}

エラー時のレスポンスを一元化

エラー時のレスポンスもまとめてしまいましょう。

エラー用のレスポンスメソッドを用意

先ほどの myApiActions クラスにこんなメソッドを定義してみました。

  • エラー時のステータスコードと、エラーメッセージを受け取る
  • レスポンスにはステータスコードを返す
  • エラーメッセージをログに残す
  • エラー用のテンプレートに遷移する

メソッド名はsymfonyの forwardfowardIffowardUnless に倣ってます。

apps/api/lib/myApiActions.class.php

エラー用のアクションを用意します。 (これを用意しないですむ方法がありそうなんですが…)

apps/api/modules/shared/actions/actions.class.php

使ってみる

使う場合はこんな感じになります。

apps/api/modules/kayac/actions/actions.class.php

実行環境毎に異なる設定ファイルを使用する

最後は環境毎に用意する設定ファイルについてです。 API実装に限らず、広く使える便利な設定ファイルです。 設定ファイル名は何でもいいのですが、習慣として environment.yml としているようです。

environment.yml を使うための設定

設定箇所は全部で3カ所あります。

apps/api/config/filters.yml

lib/filterにmyEnvironmentConfigFilter.php

このファイルははじめからは用意されていないので、新規作成します。

config/config_handlers.yml

プレフィックスの設定を追記します。

これで config/environment.yml が使えるようになりました。 リポジトリに放り込む際には、 database.yml などのように environment.yml.base をつくり、 environment.yml そのものは各環境毎に用意するようにします。

environment.yml を使う

config/config_handlers.yml でプレフィックスに app_ を設定したので、 app.yml に書かれた設定と同様の書き方で使用することができます。

config/environment.yml

all:
  kayac:
    name: '株式会社カヤック'

設定を読み出す場合。

sfConfig::get('app_kayac_name'); // => 株式会社カヤック

ここで重要なポイント。 environment.ymlで定義された設定は、app.ymlを上書きします

たとえば、 app.ymlenvironment.yml にこんな設定が書かれていた場合、

app.yml

all:
  filename: 'app.yml'

environment.yml

all:
  filename: 'environment.yml'

設定を読み込むと…

echo sfConfige::get('app_filename'); // => environment.yml

テスト環境でだけ変更したい設定項目がある場合には非常に便利です。

おわり

ほかにもいろいろ方法はあると思います。 いろいろ試行錯誤してより便利な方法を編み出していきたいと思います。

カヤックでは便利を追求する技術者も募集しています!

APIのレスポンスを返すための準備

symfonyのレスポンス、そのままではレイアウトが適用されてしまいます。 HTML形式で表示されるわけですね。

APIのレスポンスとして使う場合、こんなんじゃやってられません。 view.yml でレスポンスの形式を設定してしまいましょう。

apps/api/config/view.yml

default:
  # content-type を設定
  http_metas:
    content-type: application/json; charset=UTF-8

# レイアウトを無効に has_layout: false

has_layoutをfalseにすることによって、レイアウトを無効にできます。 具体的には、 /apps/api/templates/layout.php を通さずに、 テンプレートが出力するものをそのまま表示するようになるわけです。

レスポンス用のテンプレートを統一

プロジェクトによってはたくさんの種類のAPIを用意しなければいけなくなることがあります (この前担当した案件では、最終的に60種類ほど用意しました)。

たくさんあるAPIそれぞれにいちいちテンプレートを用意するのは何とも面倒です。 加えて、APIのレスポンス形式はひとつのプロジェクト中で統一されていると思います。

ならばそのテンプレート、ひとつにまとめてしまいましょう!

アクション内でテンプレートを変更する

アクション内でテンプレートを変更する場合、/setTemplate/ メソッドを使えばOKです。

symfony API sfAction Class

apps/api/modules/kayac/actions/actions.class.php

<?php

class kayacActions extends sfActions { public function executeThanks(sfWebRequest $request) { // テンプレートを // apps/appname/modules/shared/templates/jsonSuccess.php // に設定する。 $this->setTemplate('json', 'shared'); } }

sharedモジュールのjsonSuccessテンプレートで表示されます。

テンプレートの変更も1カ所で

実際には個々のアクション内で定義せずに、すべてのAPIアクションが継承するクラスを定義して、 そのクラスの postExecute でテンプレートの設定をするといいでしょう。

apps/api/lib/myApiActions.class.php

<?php

class myApiActions extends sfActions { public function postExecute() { parent::postExecute();

    // テンプレートを
    // apps/appname/modules/shared/templates/jsonSuccess.php
    // に設定する。
    // myApiActionsを継承するすべてのアクションで
    // このテンプレートが有効になる。
    $this-&gt;setTemplate('json', 'shared');
}

}

テンプレートにも一工夫

APIレスポンスのチェックは結構しんどいものです。 ばっちりテストを書いていればなにも問題ないですが、 ちょっと実行して目視で確認したいときもあるはず。

レスポンスを読みやすいように整形しておけば万事解決です。 今回はレスポンスをJSON場合の例を紹介します。

JSON整形用のヘルパーを用意する

テンプレート内でごにょごにょやってもいいのですが、 せっかくなのでヘルパーを有効利用してみます。

lib/helper/MyJSONHelper.php

<?php

function json_format($json) {
$tab = " "; $new_json = ""; $indent_level = 0; $in_string = false;

$json_obj = json_decode($json);

if(!$json_obj)
    return false;

$json = json_encode($json_obj);
$len = strlen($json);

for($c = 0; $c &lt; $len; $c++)
{
    $char = $json[$c];
    switch($char)
    {
        case '{':
        case '[':
            if(!$in_string)
            {
                $new_json .= $char . "\n" . str_repeat($tab, $indent_level+1);
                $indent_level++;
            }
            else
            {
                $new_json .= $char;
            }
            break;
        case '}':
        case ']':
            if(!$in_string)
            {
                $indent_level--;
                $new_json .= "\n" . str_repeat($tab, $indent_level) . $char;
            }
            else
            {
                $new_json .= $char;
            }
            break;
        case ',':
            if(!$in_string)
            {
                $new_json .= ",\n" . str_repeat($tab, $indent_level);
            }
            else
            {
                $new_json .= $char;
            }
            break;
        case ':':
            if(!$in_string)
            {
                $new_json .= ": ";
            }
            else
            {
                $new_json .= $char;
            }
            break;
        case '"':
            $in_string = !$in_string;
        default:
            $new_json .= $char;
            break;
    }
}

return $new_json;

}

// Unicodeエスケープされた文字列をUTF-8文字列に戻す function unicode_encode($str) { return preg_replace_callback("/\\u([0-9a-zA-Z]{4})/", "encode_callback", $str); }

function encode_callback($matches) { $char = mb_convert_encoding(pack("H*", $matches[1]), "UTF-8", "UTF-16"); return $char; }

ヘルパーを使って表示!

テンプレート内ではこんな風に使います。 設定ファイル内で

  • JSONを整形するか (app_json_pretty_print)
  • マルチバイト文字をエスケープしないか (app_json_allow_multibyte)

を設定しています。 後述する environment.yml にこの設定を書くことによって、 テスト環境と本番環境でスムーズに出力を切り替えることができます。

app/appname/modules/shared/jsonSuccess.php

<?php

use_helper('MyJSON');

// アクション内で、配列形式でレスポンス用のデータを入れてます $json = json_encode($sf_data->getRaw('data'));

// デバッグ用にフォーマット if(sfConfig::get('app_json_pretty_print')) { $json = json_format($json); }

// マルチバイト文字をエスケープしない if(sfConfig::get('app_json_allow_multibyte')) { $json = unicode_encode($json); }

echo $json;

これでこんなJSONの出力が、

{"name":"\u682a\u5f0f\u4f1a\u793e\u30ab\u30e4\u30c3\u30af","address":"\u3012248-0006 \u795e\u5948\u5ddd\u770c\u938c\u5009\u5e02\u5c0f\u753a2-14-7 \u304b\u307e\u304f\u3089\u6625\u79cb\u30b9\u30af\u30a8\u30a22\u968e","status":200}

こんな風に整形されて出力されます。見やすい!

{
  "name": "株式会社カヤック",
  "address": "〒248-0006 神奈川県鎌倉市小町2-14-7 かまくら春秋スクエア2階",
}

エラー時のレスポンスを一元化

エラー時のレスポンスもまとめてしまいましょう。

エラー用のレスポンスメソッドを用意

先ほどの myApiActions クラスにこんなメソッドを定義してみました。

  • エラー時のステータスコードと、エラーメッセージを受け取る
  • レスポンスにはステータスコードを返す
  • エラーメッセージをログに残す
  • エラー用のテンプレートに遷移する

メソッド名はsymfonyの forwardfowardIffowardUnless に倣ってます。

apps/api/lib/myApiActions.class.php

<?php

class myApiActions extends sfActions {

// ...(中略)...

/*
 * 条件真のときにエラーレスポンスを返す
 *
 * @param bool $condition 条件。これが真ならエラー
 * @param int $status ステータスコード
 * @param string $message エラーメッセージ
 */
final public function forwardErrorIf($condition, $status = 500, $message = '')
{
    if($condition == true)
    {
        $this-&gt;forwardError($status, $message);
    }
}

/*
 * 条件偽のときにエラーレスポンスを返す
 *
 * @param bool $condition 条件。これが偽ならエラー
 * @param int $status ステータスコード
 * @param string $message エラーメッセージ
 */
final public function forwardErrorUnless($condition, $status = 500, $message = '')
{
    if($condition == false)
    {
        $this-&gt;forwardError($status, $message);
    }
}

/*
 * エラーレスポンスを返す
 *
 * @param int $status ステータスコード
 * @param string $message エラーメッセージ
     */
final public function forwardError($status = 500, $message = '')
{
    $this-&gt;logMessage($message, 'err');
    $this-&gt;getUser()-&gt;setFlash('data', array('status' =&gt; $status));
    $this-&gt;getController()-&gt;forward('shared', 'error');

    throw new sfStopException();
}

}

エラー用のアクションを用意します。 (これを用意しないですむ方法がありそうなんですが…)

<?php

// apps/api/modules/shared/actions/actions.class.php

class sharedActions extends sfActions { / * エラーレスポンス * * @param sfWebRequest $request / public function executeError(sfWebRequest $request) { $this->data = $this->getUser()->getFlash('data'); $this->setTemplate('json'); // jsonSuccess.php } }

使ってみる

使う場合はこんな感じになります。

apps/appname/modules/kayac/actions/actions.class.php

<?php

class kayacActions extends sfActions { public function executeError(sfWebRequest $request) { // 問答無用でエラーレスポンス $this->forwardError('500', 'エラーメッセージ'); }

public function executeErrorIf(sfWebRequest $request)
{
    // 条件が真ならエラーレスポンス
    $this-&gt;forwardError(true, '500', 'エラーメッセージ');
}

public function executeError(sfWebRequest $request)
{
    // 条件が偽ならエラーレスポンス
    $this-&gt;forwardError(false, '500', 'エラーメッセージ');
}

}

実行環境毎に異なる設定ファイルを使用する

最後は環境毎に用意する設定ファイルについてです。 API実装に限らず、広く使える便利な設定ファイルです。 設定ファイル名は何でもいいのですが、習慣として environment.yml としているようです。

environment.yml を使うための設定

設定箇所は全部で3カ所あります。

apps/api/filters.yml

myEnvironmentConfigFilter:
  class: myEnvironmentConfigFilter

lib/filterにmyEnvironmentConfigFilter.php

このファイルははじめからは用意されていないので、新規作成します。

<?php

class myEnvironmentConfigFilter extends sfFilter { public function execute($filterChain) { if ($this->isFirstCall()) { include(sfContext::getInstance()->getConfigCache()->checkConfig(sfConfig::get('sf_config_dir').'/environment.yml')); }

    $filterChain-&gt;execute();
}

}

config/config_handlers.yml

プレフィックスの設定を追記します。

config/environment.yml:
  class:    sfDefineEnvironmentConfigHandler
  param:
    prefix: app_

これで config/environment.yml が使えるようになりました。 リポジトリに放り込む際には、 database.yml などのように environment.yml.base をつくり、 environment.yml そのものは各環境毎に用意するようにします。

environment.yml を使う

config/config_handlers.yml でプレフィックスに app_ を設定したので、 app.yml に書かれた設定と同様の書き方で使用することができます。

config/environment.yml

all:
  kayac:
    name: '株式会社カヤック'

設定を読み出す場合。

sfConfig::get('app_kayac_name'); // => 株式会社カヤック

ここで重要なポイント。 environment.ymlで定義された設定は、app.ymlを上書きします

たとえば、 app.ymlenvironment.yml にこんな設定が書かれていた場合、

app.yml

all:
  filename: 'app.yml'

environment.yml

all:
  filename: 'environment.yml'

設定を読み込むと…

echo sfConfige::get('app_filename'); // => environment.yml

テスト環境でだけ変更したい設定項目がある場合には非常に便利です。

おわり

ほかにもいろいろ方法はあると思います。 いろいろ試行錯誤してより便利な方法を編み出していきたいと思います。

カヤックでは便利を追求する技術者も募集しています!