[FuelPHP] Rest コントローラを使って WebAPI を作ってみる

この記事は FuelPHP Advent Calendar 2015 の23日目です。

FuelPHP の Rest コントローラを使って WebAPI を作成したいと思います。

#環境

開発環境は以下の通りです。

  • CentOS 7
  • PHP 7.0
  • FuelPHP 1.8-dev
  • Nginx 1.9
  • MariaDB 10.1.9

特殊な環境ではないので、大きく環境差は出ないと思います。

#仕様

#概要

アニメ作品の放送話リストを返すAPIを想定しています。
更新頻度の高くないデータだということをご留意ください。

#構成

単純な構成で、DBから取得した値をRESTコントローラでjson形式で返すようにします。

ちなみにDBから取得する値は以下の想定です。

ID
タイトル
放送日
アニメオリジナル作品

それぞれ、以下の様な呼び出しにしようと思います。
原作ストーリーの場合:
http://localhost/api/anime/comics
アニメオリジナルストーリーの場合:
http://localhost/api/anime/original

今回、データの挿入や更新を行うような管理画面の作成は行いません。

#Restコントローラの使い方

ドキュメントからの引用ですが、以下の様に使用します。

class Controller_Test extends Controller_Rest
{

    public function get_list()
    {
        return $this->response(array(
            'foo' => Input::get('foo'),
            'baz' => array(
                1, 50, 219
            ),
            'empty' => null
        ));
    }
}

この場合、http://localhost/test/list.json?foo=barで呼び出すと、Input::get('foo')に”bar”という値が入り、以下の様な出力になります。

{
    "foo":"bar",
    "baz":[1,50,219],
    "empty":null
}

ちなみに以下のフォーマットが使用可能です。

  • xml
  • json
  • csv
  • html
  • php (eval() 可能な PHP コードでの表現)
  • serialize (PHP でアンシリアライズ可能な、シリアライズされたデータ)

以上の説明で解説すべき本筋が殆ど終わっていますが、実用的な形で実装例を紹介したいと思います。

参照: ドキュメント: Restコントローラ

#実装

#定数

以下の様な定数クラスを作成します。
/app/common/constant.php

class Constant {
    const TIME_HOUR = 3600;   // 1 hour
    const TIME_DAY  = 86400;  // 24 hours
    const TIME_WEEK = 604800; // 1 week
    
    const ANIME_COMIC = 0;
    const ANIME_ORIGN = 1;
}

以下に作成したクラスを追加します。
/app/bootstrap.php

Autoloader::add_classes(array(
    'Constant' => APPPATH.'common/constant.php',
));

Constant::TIME_DAYのような形で呼び出す事ができます。
(他に良い方法があれば教えて下さい)

#データの取得

まず、データがないと始まりません。
今回の仕様はデータはすべてDBに格納してありますのでDBへアクセスをしてデータを取得します。

$result = DB::select()->from('users')->execute();

データが取得出来さえすれば良いので、お好きなデータの管理・取得方法で問題はありません。

#キャッシュ

呼び出される度にDB通信を行っていては時間がかかるかと思います。その場合、クエリキャッシュを利用します。

$result = DB::select()->from('users')->cached(3600)->execute();

上記の使用例だと3600秒キャッシュが有効になります。
cached()の詳しいフォーマットに関してはドキュメントを参照ください。

指定する秒数は定数にしておくと良いです。

#ベースコントローラを作成

継承用のコントローラを作成します。

今回は anime に関する API を作成しますが、今後 music や movie など新しい API を作成した際、同じ仕様であれば同じようなコーディングをするかと思います。それをバラバラに記述すると保守が非常に面倒になります。予め共通の処理を実装したベースのコントローラを作成しておくことで、 API 用のコントローラはそれを継承するだけで無駄なコーディングをする必要がなくなります。

参照: コントローラ:: ベースコントローラを生成する


ベースになるコントローラを以下に配置します。
/app/classes/controller/base/rest.php

class Controller_Base_Rest extends Controller_Rest {
    protected $format = "json";
}

今回は特に共通処理を考えていないので、$formatだけ指定した例になります。
共通でjson形式のデータを返したいので上記のような指定を入れています。

この場合、継承は以下の様な指定になります。

class Controller_Api_Anime extends Controller_Base_Rest {
    //
}

#Restコントローラ

コントローラにはデータ取得関数だけ記述したいので、処理はModelなどに記述します。

public function get_original() {
    return Model_Api::get_data(Constant::ANIME_ORIGN_NAME, Constant::ANIME_ORIGN_TYPE);
}

#Cacheクラスを使用する

クエリキャッシュと重複しますが、動的に変更がない場合はCacheクラスを使うと良いでしょう。

詳しいインターフェースなどは割愛しますが、使用例は以下です。

public static function get_data($data_name = null, $data_type = 0) {
    $data = null;

    try {
        // キャッシュからデータを取得する
        $data = Cache::get($data_name);
        
    } catch (CacheNotFoundException $e) {
        // キャッシュがない場合
        // DBからデータを取得する
        $data = Model_Api::get_anime($data_type);
        
        if ($data && $data_name) {
            // 改めてキャッシュにデータをセットする
            Cache::set($data_name, $data, Constant::TIME_WEEK);
        }
    }

    return $data;
}

処理の流れとしては以下です。

  1. キャッシュからデータを取得
    1. キャッシュが無い場合はCacheNotFoundExceptionが発生
  2. DBからデータを取得し、キャッシュを作成する
  3. データを返却する

簡単に説明すると、「キャッシュがある場合はキャッシュからデータを取得」「キャッシュがない場合はDBから取得」というような感じです。

指定した時間キャッシュが残りますので、データ変更時や任意のタイミングでキャッシュが出来るような処理も作成した方が良いです。

参照: ドキュメント::Cacheクラス

#完成

割愛している箇所もありますが、概ね以下の様な感じです。

#Controller

class Controller_Api_Anime extends Controller_Base_Rest {

    public function get_comics() {
        return Model_Api::get_data(Constant::ANIME_COMIC_NAME, Constant::ANIME_COMIC_TYPE);
    }

    public function get_original() {
        return Model_Api::get_data(Constant::ANIME_ORIGN_NAME, Constant::ANIME_ORIGN_TYPE);
    }

}

それぞれが以下に対応しています。

get_comics() http://localhost/api/anime/comics
get_original() http://localhost/api/anime/original

#Model

get_anime()get_data()からしか呼ばないのでprivateにしてます。

class Model_Api extends Model {
    
    public static function get_data($data_name = null, $data_type = 0) {
        $data = null;

        try {
            // キャッシュからデータを取得する
            $data = Cache::get($data_name);
            
        } catch (CacheNotFoundException $e) {
            // キャッシュがない場合
            // DBからデータを取得する
            $data = self::get_anime($data_type);
            
            if ($data && $data_name) {
                // 改めてキャッシュにデータをセットする
                Cache::set($data_name, $data, Constant::TIME_WEEK);
            }
        }

        return $data;
    }
    
    private static function get_anime($original = null) {
        $query = DB::select("hoge1", "hoge2", "hoge3")->from('table');
        
        if (!is_null($original)) {
            $query = $query->where('hoge4', $original);
        }
        
        return $query->cached(Constant::TIME_WEEK)->execute()->as_array();
    }

}

#おわり

無駄な処理も少々ありますが、概ねこんな実装で良いのではないでしょうか。