JavaScript(jQuery)からPHPのAPIを利用する

# はじめに

類似の記事はありますが、個人的に納得のいくものが無かったので書いてみます。

![SS](https://qiita-image-store.s3.amazonaws.com/0/25060/631e7db0-1d9e-4b2d-da53-9555f46e6397.png)

# 基本形

## HTML

```html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>サンプル</title>
    </head>
    <body>
        <h1>設定</h1>
        <p>
            年齢: <input type="text" id="age" value="24"><br>
            職業: <input type="text" id="job" value="学生"><br>
            <input type="button" id="execute" value="送信"><br>
        </p>
        <h1>結果</h1>
        <p>
            可否: <input type="text" id="result" value=""><br> 
            内容: <input type="text" id="detail" value=""><br>
        </p>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
        <script src="default.js"></script>
    </body>
</html>
```

## JavaScript

### 手抜き

```js:default.js
// DOMを全て読み込んだあとに実行される
$(function () {
    // 「#execute」をクリックしたとき
    $('#execute').click(function () {
        // Ajax通信を開始する
        $.ajax({
            url: 'api.php',
            type: 'post', // getかpostを指定(デフォルトは前者)
            dataType: 'json', // 「json」を指定するとresponseがJSONとしてパースされたオブジェクトになる
            data: { // 送信データを指定(getの場合は自動的にurlの後ろにクエリとして付加される)
                age: $('#age').val(),
                job: $('#job').val(),
            },
        })
        // ・ステータスコードは正常で、dataTypeで定義したようにパース出来たとき
        .done(function (response) {
            $('#result').val('成功');
            $('#detail').val(response.data);
        })
        // ・サーバからステータスコード400以上が返ってきたとき
        // ・ステータスコードは正常だが、dataTypeで定義したようにパース出来なかったとき
        // ・通信に失敗したとき
        .fail(function () {
            $('#result').val('失敗');
            $('#detail').val('');
        });
    });
});
```

### 失敗時の情報も丁寧に表示する

```js:default.js
/* エラー文字列の生成 */
function errorHandler(args) {
    var error;
    // errorThrownはHTTP通信に成功したときだけ空文字列以外の値が定義される
    if (args[2]) {
        try {
            // JSONとしてパースが成功し、且つ {"error":"..."} という構造であったとき
            // (undefinedが代入されるのを防ぐためにtoStringメソッドを使用)
            error = JSON.parse(args[0].responseText).error.toString();
        } catch (e) {
            // パースに失敗した、もしくは期待する構造でなかったとき
            // (PHP側にエラーがあったときにもデバッグしやすいようにレスポンスをテキストとして返す)
            error = 'parsererror(' + args[2] + '): ' + args[0].responseText;
        }
    } else {
        // 通信に失敗したとき
        error = args[1] + '(HTTP request failed)';
    }
    return error;
}

// DOMを全て読み込んだあとに実行される
$(function () {
    // 「#execute」をクリックしたとき
    $('#execute').click(function () {
        // Ajax通信を開始する
        $.ajax({
            url: 'api.php',
            type: 'post', // getかpostを指定(デフォルトは前者)
            dataType: 'json', // 「json」を指定するとresponseがJSONとしてパースされたオブジェクトになる
            data: { // 送信データを指定(getの場合は自動的にurlの後ろにクエリとして付加される)
                age: $('#age').val(),
                job: $('#job').val(),
            },
        })
        // ・ステータスコードは正常で、dataTypeで定義したようにパース出来たとき
        .done(function (response) {
            $('#result').val('成功');
            $('#detail').val(response.data);
        })
        // ・サーバからステータスコード400以上が返ってきたとき
        // ・ステータスコードは正常だが、dataTypeで定義したようにパース出来なかったとき
        // ・通信に失敗したとき
        .fail(function () {
            // jqXHR, textStatus, errorThrown と書くのは長いので、argumentsでまとめて渡す
            // (PHPのfunc_get_args関数の返り値のようなもの)
            $('#result').val('失敗');
            $('#detail').val(errorHandler(arguments));
        });
    });
});
```

## PHP

### 手抜き

```php:api.php
<?php

// Content-TypeをJSONに指定する
header('Content-Type: application/json');

// 「200 OK」 で {"data":"24歳、学生です"} のように返す
$data = "{$_POST['age']}歳、{$_POST['job']}です";
echo json_encode(compact('data'));
```

### エラーの発生を防ぎ、値の整合性チェックも行う

```php:api.php
<?php

// Content-TypeをJSONに指定する
header('Content-Type: application/json');

// $_POST['age']、$_POST['job']をエラーを出さないように文字列として安全に展開する
$age = (string)filter_input(INPUT_POST, 'age');
$job = (string)filter_input(INPUT_POST, 'job');

// 整合性チェック
if ($age === '' || $job === '') {
    $error = '年齢と職業を両方入力してください';
} else if (!ctype_digit($age)) {
    $error = '年齢は正の整数で入力してください';
} else if (($age = (int)$age) > 100) {
    $error = '生きすぎィ！';
}

if (!isset($error)) {
    // 正常時は 「200 OK」 で {"data":"24歳、学生です"} のように返す
    $data = "{$age}歳、{$job}です";
    echo json_encode(compact('data'));
} else {
    // 失敗時は 「400 Bad Request」 で {"error":"..."} のように返す
    http_response_code(400);
    echo json_encode(compact('error'));
}
```

# FAQ

### Q. `done` とか `fail` みたいに繋ぐ書き方なんなの？

私が知ってるのはこういう書き方だったはずなんだけど…

```js
$.ajax({
    url: 'api.php',
    type: 'post',
    dataType: 'json',
    success: function (data) {
        $('#result').val('成功');
        $('#detail').val(response.data);
    },
    error: function () {
        $('#result').val('失敗');
        $('#detail').val(errorTextFromFailArguments(arguments));
    },
});
```

### A. Promise インタフェースを利用したものです

そのような書き方をすると、レスポンスの結果をもとに新たにAjaxリクエストを実行したいときなどに、いわゆる**「コールバックネスト地獄」**に陥ります。そういったシーンのための解決策として、この書き方が**jQuery1.5**以降から推奨されているようです。詳しくは以下を参照してください。

- [jQuery モダンAjaxな書き方を目指して 〜Deferredを使ったAJAX〜](http://blog.toshimaru.net/jquery-ajaxdeferredajax/)
- [jQuery.Deferredを使って楽しい非同期生活を送る方法](http://qiita.com/yuku_t/items/3d1cf51d7ae91305eaaa)
- [結局jQuery.Deferredの何が嬉しいのか分からない、という人向けの小話](http://qiita.com/yuku_t/items/1b8ce6bba133a7eaeb23)
- [jQuery.ajax()](http://api.jquery.com/jquery.ajax/)

[jQuery日本語リファレンス](http://semooh.jp/jquery/) は<ins>jQuery1.4で更新が止まっている</ins>のであまり参考にしない方がいいでしょう。

なお，より標準の[Promise](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise)に近い形で利用する場合，`done` `fail` でバラバラに記述しているものを `then` でまとめたほうがよいでしょう．

```js
.then(
    function (response) {
        $('#result').val('成功');
        $('#detail').val(response.data);
    },
    function () {
        $('#result').val('失敗');
        $('#detail').val('');
    }
);
```

---

### Q. `data` のセットが面倒くさい！

数が増えてた時にどうにかならんの？

```js
var data = {
    age: $('#age').val(),
    job: $('#job').val(),
};
```

### A. `FormData` オブジェクトを利用しましょう

以下のように `name` 属性を使って送信項目を定義しておけば、`FormData` オブジェクトを渡すだけで済みます。

```html
<form id="settings">
    年齢: <input type="text" name="age" value="24"><br>
    職業: <input type="text" name="job" value="学生"><br>
    <input type="button" id="execute" value="送信"><br>
</p>
```

```js
var data = new FormData($('#settings')[0]);
```

後から項目を追加したい場合は `append` メソッドが使えます。

```js
var data = new FormData($('#settings')[0]);
data.append('drink', 'アイスティー');
```

---

### Q. クロスオリジンでやりたい！

このままだと

```
XMLHttpRequest cannot load .... No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin '...' is therefore not allowed access. 
```

みたいなエラーが出る…

### A. `Access-Control-Allow-Origin` か `JSONP` を利用しましょう

#### `Access-Control-Allow-Origin` を利用する場合

PHP側で以下のようにレスポンスヘッダを返します。

```php:example.comからの利用を許可する場合
header('Access-Control-Allow-Origin: http://example.com');
```

```php:全ての利用を許可する場合
header('Access-Control-Allow-Origin: *');
```

#### `JSONP` を利用する場合

PHP側で以下のようにレスポンスボディを返します。

```php
$data = "{$age}歳、{$job}です";
echo 'callback(' . json_encode(compact('data')) . ')';
```

JavaScript側では以下のように書けば、JSONのときと全く同じように処理することが可能です。内部で何やってるか分からなくて気持ち悪いという人は[「jQueryはJSONPの理解の妨げになるか？」](http://blog.mach3.jp/2010/10/19/jquery-jsonp.html)を参照してください。

```js
$.ajax({
    url: 'api.php',
    type: 'post',
    dataType: 'jsonp',
    jsonpCallback: 'callback',
    data: {
        age: $('#age').val(),
        job: $('#job').val(),
    },
})
```

#セキュリティに関する注意点

## XSS対策

ユーザ入力をエレメントに適用する際にはjQueryの `text` メソッドや `val` メソッドを通すことを徹底しましょう。<ins>`html`メソッドは危険</ins>です。

## CSRF対策

通常のフォーム送信と同じように実装しましょう。同一オリジンでは何もしなくてもセッションIDはCookieとして送信されるので、単にPHPから返されてきたトークンを `data` の中に含めて送信するだけで十分です。