【PHP】JSONデータのPOST受け取りで application/x-www-form-urlencoded とapplication/json の両方に対応

#はじめに
Vue.jsではAjax通信でaxiosを使うのがスタンダードということで試してみたところ、[axios公式](https://github.com/axios/axios)のPOSTのExampleの通りに

```js:JavaScript
axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
```
で送信しても、PHPの \$_POST や \$_REQUEST では値を受け取れません。これは、PHPの \$_POST が Content-type: application/x-www-form-urlencoded または multipart/form-data を受け取るのに対し、axiosのデフォルトでは Content-type: application/json で送信しているのが原因です。
参考： [ブラウザの時にaxiosのPOSTの値が送信されない？そんなことはない](https://qiita.com/amamamaou/items/b6cb39739bba4abf4544)

大雑把に言うと、x-www-form-urlencoded では a=1&b=1 のようなクエリパラメータの形（URLエンコードあり）、application/json では {"a":1,"b":2} のようなJSON文字列でデータが送信されます。
参考： [backboneからのajaxがpayloadでPHPの$_POSTで取得できない](http://egapool.hatenablog.com/entry/2015/07/28/225658)

対策としては公式や上記の参考記事にあるように URLSearchParams を用いて application/x-www-form-urlencoded形式で送信する方法と、application/json形式で送信してfile_get_contents('php://input') でbodyのデータを読みに行く方法などがあるようです。

送信側で application/x-www-form-urlencoded に変換してもよいのですが、世の中の流れ的には application/json に傾きつつあるようですし、APIなどではどちらでも受け付けた方が親切だろうということで、受け側のPHPで両方に対応することを考えてみました。

使用したPHPのVersionは 7.1.11 です。

#サンプルコード

まず最初に考えたのが以下のコードです。
目標は戻り値である \$request が application/json の場合と application/x-www-form-urlencoded の場合で同じになることです。
なお、GETリクエストも \$request に入ります。

```php:PHP
function get_request() {
	$content_type = explode(';', trim(strtolower($_SERVER['CONTENT_TYPE'])));
	$media_type = $content_type[0];

	if ($_SERVER['REQUEST_METHOD'] == 'POST' && $media_type == 'application/json') {
		// application/json で送信されてきた場合の処理
		$request = json_decode(file_get_contents('php://input'), true);
	} else {
		// application/x-www-form-urlencoded で送信されてきた場合の処理
		$request = $_REQUEST;
	}

	return $request;
}

```

\$\_SERVER['CONTENT_TYPE']に Content-type が 'application/json; charset=utf-8' のような形で入っているので、セミコロンの前までを切り出して \$media\_type に入れています。また、Content_typeは大文字と小文字を区別しないことになっているようなので、strtolower()で小文字に揃えています。\$media\_type が 'application/json' の場合は

```php
$request = json_decode(file_get_contents('php://input'), true);
```

でbodyの内容を読んで、連想配列にデコードします。json_decodeの第2引数を省略またはfalseにするとオブジェクトにデコードされますが、ここでは \$_POST・\$_REQUEST に合わせるためにtrueにして連想配列にデコードしています。

大抵の場合はこれで問題ないのですが、

```json:JSON
{
	"id": 1,
	"name": {
		"first_name": "John",
		"last_name": "Doe"
	}
}
```

このような階層構造になったJSONデータをPOSTした場合、 application/x-www-form-urlencoded では \$\_REQUEST['name'] の中身が連想配列ではなく {"first\_name": "John","last\_name": "Doe"} という文字列になるので、これを連想配列にデコードする必要があります。


```php:PHP
function get_request() {
	$content_type = explode(';', trim(strtolower($_SERVER['CONTENT_TYPE'])));
	$media_type = $content_type[0];

	if ($_SERVER['REQUEST_METHOD'] == 'POST' && $media_type == 'application/json') {
		// application/json で送信されてきた場合の処理
		$request = json_decode(file_get_contents('php://input'), true);
	} else {
		// application/x-www-form-urlencoded で送信されてきた場合の処理
		// REQUESTのjsonが多層の時はうまく行かない
		$request = $_REQUEST;

		// REQUESTのjsonが多層の場合に対応
		foreach ($_REQUEST as $key => $value) {
			$request[$key] = json_decode($value, true);

			// json_decodeはクォートされていない文字列がnullになるので戻す
			if ($request[$key] == null) {
				$request[$key] = $value;
			}
		}
	}

	return $request;
}
```
\$_REQUEST をforeachで回してjson\_decodeすればOK・・・と思ったらjson\_decode関数はクォートされていない文字列がnullになるという謎仕様らしいので、nullになったら元の値に戻すという処理を入れています。あまりスマートではありませんが・・・。

#終わりに
今まであまりPOSTデータの Content-type やJSONデータの受け取り方をきちんと意識していなかったのでよい勉強になりました。でも将来的にはPHPの標準仕様で \$\_POST や \$\_REQUEST に入るようになってほしいですね。
ご意見や間違いの指摘等があればコメントをよろしくお願いします。