忍び歩く男 - SLYWALKER

大阪のこっそりPHPer

#CakePHP 爆速でAPIを実装するチュートリアル

f:id:slywalker:20131115100808j:plain

スマートフォンアプリのバックエンドや、JSフレームワークのバックエンドとして、JSONやXMLを返すAPIをサーバサイドで実装する機会は多いと思います。

今回は、ComposerとCakePHP2.4、FriendsOfCake/crudを使って爆速で実装してみます。

できあがりは、これ

slywalker/cakephp-app-api_sample

CakePHPのインストール

まず、プロジェクトのディレクトリにcomposer.jsonをつくります

composer.json

{
    "require": {
        "pear-cakephp/cakephp": "2.4.*"
    },
    "config": {
        "vendor-dir": "Vendor/"
    },
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear.cakephp.org"
        }
    ]
}

つづいてcomposer.pharのダウンロードとパッケージのインストール

$ curl -s http://getcomposer.org/installer | php
$ php composer.phar install

そして、プロジェクトをBake

$ Vendor/bin/cake bake project $PWD --empty

ブラウザでアクセスすると…

f:id:slywalker:20131115102539p:plain

はい。いつもの画面登場!

ただ、このままではCAKE_CORE_INCLUDE_PATH絶対パスになっているので、デプロイした際にダメになるので書き換えます。

webroot/index.php, webroot/test.php

define('CAKE_CORE_INCLUDE_PATH', ROOT . DS . APP_DIR . DS . 'Vendor' . DS . 'pear-pear.cakephp.org' . DS . 'CakePHP');

忘れがちなのがConsole/cake.php

Console/cake.php

25c25
<   $root = dirname(dirname(dirname(__FILE__)));
---
>    $app = dirname(dirname(__FILE__));
29c29
<   ini_set('include_path', $root . PATH_SEPARATOR . __CAKE_PATH__ . PATH_SEPARATOR . ini_get('include_path'));
---
>    ini_set('include_path', $app . $ds . 'Vendor' . $ds . 'pear-pear.cakephp.org' . $ds . 'CakePHP' . PATH_SEPARATOR . ini_get('include_path'));
35c35
< unset($paths, $path, $dispatcher, $root, $ds);
---
> unset($paths, $path, $dispatcher, $app, $ds);

最後に、database.phpをbake

$ Console/cake bake db_config

プラグインのインストール

今回の目玉、FriendsOfCake/crudcakephp/debug_kitを入れておきましょう。composer.jsonに書き足します。

composer.json

{
    "require": {
        "pear-cakephp/cakephp": "2.4.*",
        "cakephp/debug_kit": "~2.2",
        "FriendsOfCake/crud": "3.*"
    },
    "config": {
        "vendor-dir": "Vendor/"
    },
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear.cakephp.org"
        }
    ]
}

Composerでインストールします。

$ php composer.phar update

CakePHPがプラグインを読み込むように設定しときましょう。

Config/bootstrap.php

CakePlugin::loadAll();

Crudプラグインの設定他

さて、Crudプラグインを使えるように設定していきましょう。今回は詳しいとこは省くので、「ん?」と思ったら公式ドキュメントを参照してください。

今回は、.jsonのアクセスでJSONのレスポンス、.xmlのアクセスでXMLのレスポンスを返すようにしましょう。ということでroutes.phpにこれを追加。

Config/routes.php

Router::parseExtensions('json', 'xml');

そして、AppController.phpをゴリゴリ…

Controller/AppController.php

<?php
App::uses('Controller', 'Controller');
App::uses('CrudControllerTrait', 'Crud.Lib'); // これ書いといて

class AppController extends Controller {

    use CrudControllerTrait; // トレイト使うよ!なんてモダン

    public $components = [
        'Session',
        'RequestHandler', // これが拡張子で処理をわけてくれるのさ
        'Paginator' => [
            'paramType' => 'querystring' // APIっぽくクエリ形式
        ],
        'DebugKit.Toolbar' => [
            'panels' => ['Crud.Crud'] // Crud用のパネルがあるのさ 
        ],
        'Crud.Crud' => [
            'actions' => ['index'], // とりあえずindexのアクションだけ
            'listeners' => ['Api'] // ApiListenerを使うよ
        ]
    ];

}

よしっ!準備は整った!

データの準備

今回はMySQL使うということでよろしくお願いします。サンプルデータがgithubのレポジトリ内のConfig/Schema/cakeapi.sql.gzにあるのでインポートしてください。

ちなみに、ちょっと凝ったことをしようと思ったのでデータは東京駅を中心としたランダムの位置情報1万件です。geometry型を使ってます。

Model

さくっと

Model/Geometry.php

<?php
App::uses('AppModel', 'Model');
App::uses('Sanitize', 'Utility');

class Geometry extends AppModel {

    public $virtualFields = [
        'lat' => 'Y(`latlng`)',
        'lng' => 'X(`latlng`)'
    ];

    public function conditionCenter($queryParams) {
        $queryParams = Sanitize::clean($queryParams) + [
            'lat' => null,
            'lng' => null
        ];

        if (
            !is_numeric($queryParams['lat']) ||
            !is_numeric($queryParams['lng'])
        ) {
            return [];
        }

        return ["MBRContains(
          GeomFromText(
              Concat(
                  'LineString(',
                  {$queryParams['lng']} + 1,
                  ' ',
                  {$queryParams['lat']} + 1,
                  ',',
                  {$queryParams['lng']} - 1,
                  ' ',
                  {$queryParams['lat']} - 1,
                  ')'
              )
          ),
          latlng
      )"];
    }

}

緯度経度は、バーチャルフィールドを使います。中心からの範囲検索ようにメソッドを用意してます。

Controller

うりゃっと

Controller/Geometries.php

<?php
App::uses('AppController', 'Controller');

class GeometriesController extends AppController {

    public function beforeFilter() {
        $this->Crud->on('beforePaginate', function(CakeEvent $event) {
            $model = $event->subject->model;
            $request = $event->subject->request;

            $event->subject->paginator->settings += [
                'conditions' => [
                    $model->conditionCenter($request->query)
                ]
            ];
        });

        parent::beforeFilter();
    }

}

はい。これだけです…

「は?」と思ったら公式ドキュメントですからね!

レスポンス

ではでは、アクセスしてみましょう。

http://localhost/geometries.json
{
    "success": true,
    "data": [
        {
            "Geometry": {
                "id": "1",
                "latlng": null,
                "lat": "84.001196",
                "lng": "191.951974"
            }
        },
        {
            "Geometry": {
                "id": "2",
                "latlng": null,
                "lat": "51.617372",
                "lng": "162.921083"
            }
        },
        ....
    ]
}

おおー!20件表示されてますね。これはPaginateのデフォルトだからですね。

ちなみに、

http://localhost/geometries.json?limit=1
{
    "success": true,
    "data": [
        {
            "Geometry": {
                "id": "1",
                "latlng": null,
                "lat": "84.001196",
                "lng": "191.951974"
            }
        }
    ]
}

おおー!効いてる効いてる。

やっぱり、全体の件数とか、何ページ目とか知りたいですよね。そんなときはApiPaginationListenerを追加するんです。

Controller/AppController.php

<?php
class AppController extends Controller {

    use CrudControllerTrait;

    public $components = [
        'Crud.Crud' => [
            'actions' => ['index'],
            'listeners' => [
                'Api', 
                'ApiPagination'  // これ!
            ]
        ]
    ];

}

さて、どうだ?

http://localhost/geometries.json?page=2&limit=3
{
    "success": true,
    "data": [
        {
            "Geometry": {
                "id": "4",
                "latlng": null,
                "lat": "83.012136",
                "lng": "165.754295"
            }
        },
        {
            "Geometry": {
                "id": "5",
                "latlng": null,
                "lat": "123.59616",
                "lng": "201.408059"
            }
        },
        {
            "Geometry": {
                "id": "6",
                "latlng": null,
                "lat": "80.112906",
                "lng": "177.030496"
            }
        }
    ],
    "pagination": {
        "page_count": 3334,
        "current_page": 2,
        "has_next_page": true,
        "has_prev_page": true,
        "count": 10000,
        "limit": 3
    }
}

へー(×3)。これはAPIの受け手にやさしいですよね。

でも、CakePHPが返すデータ配列ってモデル名がついてて、なんかいやですよね。そんなときは、ApiTransformationListenerを使ってください!

Controller/AppController.php

<?php
class AppController extends Controller {

    use CrudControllerTrait;

    public $components = [
        'Crud.Crud' => [
            'actions' => ['index'],
            'listeners' => [
                'Api', 
                'ApiPagination', 
                'ApiTransformation  // これ!
          ]
      ]
  ];

}

どうなるかな?

http://localhost/geometries.json?page=2&limit=3
{
    "success": true,
    "data": [
        {
            "id": 4,
            "latlng": null,
            "lat": 83.012136,
            "lng": 165.754295
        },
        {
            "id": 5,
            "latlng": null,
            "lat": 123.59616,
            "lng": 201.408059
        },
        {
            "id": 6,
            "latlng": null,
            "lat": 80.112906,
            "lng": 177.030496
        }
    ],
    "pagination": {
        "page_count": 3334,
        "current_page": 2,
        "has_next_page": true,
        "has_prev_page": true,
        "count": 10000,
        "limit": 3
    }
}

わぁお!もう最高!

あ、肝心の絞込検索やってなかったですね。

http://localhost/geometries.json?lat=35.67832667&lng=139.77044378
{
    "success": true,
    "data": [
        {
            "id": 2583,
            "latlng": null,
            "lat": 35.790109,
            "lng": 140.713021
        },
        {
            "id": 5111,
            "latlng": null,
            "lat": 35.759589,
            "lng": 140.428571
        },
        {
            "id": 6944,
            "latlng": null,
            "lat": 36.627709,
            "lng": 140.225557
        }
    ],
    "pagination": {
        "page_count": 1,
        "current_page": 1,
        "has_next_page": false,
        "has_prev_page": false,
        "count": 3,
        "limit": 20
    }
}

はい!見事に絞れております!

最後に

Crudプラグインは、GETだけじゃなくてPOSTやPUT、UPDATE、DELETEにも対応してるので、夢が広がりまくりングですよ。

と、こんなエントリーが書けるくらいフリーになって暇しているエンジニアがここにいるので、なにかお仕事ください!

CakePHPの実装指導やパフォーマンス改善、実際のコーディングなどなど、お待ちしております。連絡先はslywalker (Yasuo Harada)にメールアドレスが記載してあります。