codeigniter3でREST API

PHPでRESTなAPIを作ることになり、codeigniterはパフォーマンスが良いという話をよく聞くので試してみた。

ただし、デフォルトではrestに対応していないようで、ググっても皆さん独自で対応されている模様。
今回は [hanischit/codeigniter-restserver](https://github.com/chriskacerguis/codeigniter-restserver) を使ってrest対応した。

# 動作環境

* CentOS6.7
* nginx
* MySQL5.6
* php7.0

vagrantとansibleを使って構築した
⇒ [github](https://github.com/teratsyk/lnmp7) に置いてあります

* AmazonLinux使うかもしれないのでCentOSは6系にしておいた
* APIバージョン毎にディレクトリを分ける想定だが、~~その辺りの作り込みは適当~~  
  ⇒ 中途半端過ぎたので一旦やめた


# codeigniter3のインストール

## composer.json

プロジェクトのディレクトリ(APPPATH)直下に composer.json を作成。
codeigniter本体とrest拡張、ローカライズ言語ファイルも追加。

```
{
    "repositories": [
      {
        "type":"package",
        "package": {
          "name": "bcit-ci/codeigniter3-translations",
          "version":"dev",
          "source": {
            "url": "https://github.com/bcit-ci/codeigniter3-translations.git",
            "type": "git",
            "reference":"develop"
          }
        }
      }
    ],
    "require": {
        "php": ">=5.2.4",
        "codeigniter/framework": ">=3.0",
        "hanischit/codeigniter-restserver": "^1.0",
        "bcit-ci/codeigniter3-translations": "dev"
    },
    "require-dev": {
        "mikey179/vfsStream": "1.1.*"
    }
}
```

## composer本体のインストールと実行

```
# curl -sS https://getcomposer.org/installer | php
# php composer.phar install
```

# codeigniterの構成

## ディレクトリ構成を変更

* `APPPATH/public_html`ディレクトリを作成して、`APPPATH/vendor/codeigniter/index.php`をコピー
    * 確認用のphpinfoとかもここに置く
* `APPPATH/vendor/codeigniter/application`ディレクトリを`APPPATH `にコピー
    * ここにアプリを作成する
* 環境依存ファイルを準備（とりあえずDBのみ）
    * APPPATH/application/config/{development|testing|production}/database.php

## 変更したディレクトリ構成に合わせて設定変更

### APPPATH/public_html/index.php

* パスの変更

```
    $application_folder = '../application';

    $system_path = '../vendor/codeigniter/framework/system';
```

### APPPATH/application/config/config.php

* autoloadの設定を修正

```
//$config['composer_autoload'] = FALSE;
$config['composer_autoload'] = TRUE;
$config['composer_autoload'] = FCPATH . '/vendor/autoload.php';
```

* indexの設定を修正

```
//$config['index_page'] = 'index.php';
$config['index_page'] = '';
```

## 日本語ファイルを取得

[bcit-ci/codeigniter3-translations](https://github.com/bcit-ci/codeigniter3-translations)を使用。

* `vendor/bctit-ci/codeigniter3-translations/language/japanese`を`APPPATH/application/language`にコピー

* APPPATH/application/config/config.php の設定変更

```
//$config['language']	= 'english';
$config['language']	= 'japanese';
```

※言語ファイルは`APPPATH/language` > `vendor/codeigniter/framework/system/language`の優先順位で参照している模様

# REST対応

下記のファイルをコピーするだけ

* vendor/hanischit/codeigniter-restserver/libraries/REST_Controller.php
  ⇒ application/libraries/REST_Controller.php

* vendor/hanischit/codeigniter-restserver/libraries/Format.php
  ⇒ application/libraries/Format.php

* vendor/hanischit/codeigniter-restserver/config/rest.php
  ⇒ application/config/rest.php

* vendor/hanischit/codeigniter-restserver/language/english/rest_controller_lang.php
  ⇒ application/language/english/rest_controller_lang.php

# APIサンプル

## GET */users*

```shell-session:APPPATH/application/config/route.php
#routeを追加
$route['users'] = 'users/list';
#メソッドを厳密にしたい場合は下記
$route['users']['get'] = 'users/list';
```

```shell-session:APPPATH/application/controller/Users.php（新規作成）
<?php
require APPPATH . '/libraries/REST_Controller.php';
class Users extends REST_Controller {

    function __construct()
    {
        // Construct the parent class
        parent::__construct();
    }

    public function list_get()
    {
        // 1. クエリパラメータのバリデーション
        // (array)$this->query()をバリデーションする

        // 2. データ取得した事にする
        $users = [
            ['id' => 1, 'name' => 'John', 'email' => 'john@example.com', 'fact' => 'Loves sax'],
            ['id' => 2, 'name' => 'Jim', 'email' => 'jim@example.com', 'fact' => 'Loves guitar'],
            ['id' => 3, 'name' => 'Miles', 'email' => 'miles@example.com', 'fact' => 'Loves trumpet'],
        ];

        if (!empty($users))
        {
            $this->set_response($users, REST_Controller::HTTP_OK);
        }
        else
        {
            $this->set_response([
                'status' => FALSE,
                'message' => 'No users were found',
            ], REST_Controller::HTTP_NOT_FOUND);
        }
    }
```

REST_Controllerを継承したコントローラを書くだけでjsonで返ってくる。
`http://localhost/users?format=xml`とすればXMLで取得できるが、`http://localhost/users.xml`だと正常にルーティングされなくなってしまう模様。
（ドキュメント見ると出来るっぽいけど。。ルーティングに正規表現が必要？？）

本来のサンプルでは「各エンドポイントに対応するクラスは、厳密にはコントローラではない」という考え方なのか、`controller/api`というディレクトリの下にコントローラを作成している。
とはいえ、ブラウザでアクセスするURIと混在させないので、その辺は気にしなくていいと思う。

## PUT */users/{id}*

リクエストボディは `POST` の時と同じ階層にした方がバリデーションしやすいと思う

```json
[
    {
        "name": "Elvin",
        "email": "elvin@example.com",
        "fact": "Loves drums"
    }
]
```



```shell-session:APPPATH/application/config/route.php
$route['users/(:num)'] = 'users/detail/$1';
```

```shell-session:APPPATH/application/controller/Users.php
    public function detail_put(Int $id)
    {
        // 1. クエリパラメータのバリデーション
        // $this->query()をバリデーションする

        // 2. リクエストボディのバリデーション
        $request_body = json_decode($this->put()[0], true)
        // $request_bodyをバリデーションする

        // 3. データ更新したことにする
        $updated_user = [
            'id' => $id,
            'name' => $request_body[0]['name'],
            'email' => $request_body[0]['email'],
            'fact' => $request_body[0]['fact']
        ];

        if (!empty($updated_user))
        {
            $this->set_response($updated_user, REST_Controller::HTTP_OK);
        }
        else
        {
            $this->set_response([
                'status' => FALSE,
                'message' => 'error',
            ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR);
        }
    }
```

`$this->put()`が文字列のまま渡ってきた。。。

# とりあえず動いた

まぁだいたい動きそう。
後は細かいところがどうかという感じで、ダメならFuelかな。。

作ったサンプルは[github](https://github.com/teratsyk/ci3_rest_sample) に置いてあります