PHPで超ミニマムjson-serverもどきを作る

[JSON Server](https://github.com/typicode/json-server)と同等の機能でPHP環境で動く物を探していたけど、結局「使う機能に絞って自分で作っちゃえ」ってなったという記事です。
さくらのレンタルサーバ ライトプラン(PHP7.4)で動作確認しました。

# 対象の読者
- APIでJSONを返すモック環境を手軽に構築したい
- PHPが動くサーバがある
- ComposerやNode.jsを使えない

# JSON Server とは
[JSON Server](https://github.com/typicode/json-server)はAPIのモックです。
モックなので本番利用するものではありません。
公式にも、`(略)who need a quick back-end for prototyping and mocking.` とあります。

https://github.com/typicode/json-server

日本語の記事でざっと特徴を掴みたかったので、こちら↓の記事も参考にしました。

https://www.webprofessional.jp/mock-rest-apis-using-json-server/

# でもPHPで動かしたい
完全に環境の都合なのですが、実はAPIのモックをPHPで動かしたかったのです。
JSON ServerのPHP版といえば、[PHP JSON Server](https://github.com/Zlob/php-json-server)があります。

https://github.com/Zlob/php-json-server

# できればcomposerなしで導入したい
さらにさらに、さくらのレンタルサーバで動かしたい事情があり、できればcomposerなしがいいんです。(その気になれば、さくらのレンタルサーバにcomposerもNode.jsも入るんですが、ここでは割愛します。)
で、いろいろ探してみたのですが「最低限のCRUDだけだし、どうせモックだし作ってみよう」ということになります。

# 今回作るもの
長くなりましたが今回作る**json-serverもどき**を、本家との比較表で以下に示します。
ざっくり言うと、**本当にミニマムだな！**ということです。

###概要

|項目|json-serverもどき|本家JSON Server|
|----|----|----|
|導入方法|phpと.htaccessを置くだけ|npmコマンドでインストール|
|起動方法|(※1)|npmコマンドで起動|
|停止方法|(※1)|プロセスのkillで停止|

※1：リクエストに応じてphpプロセスが立ち上がり、処理の完了とともにphpプロセスが終了するので、サーバ自体の起動・停止はありません。

###Routes(項目は本家目次から抜粋)

|項目|json-serverもどき|本家JSON Server|
|----|:----:|:----:|
|Plural routes|○|○|
|Singular routes|×|○|
|Filter|×|○|
|Paginate|×|○|
|Sort|×|○|
|Slice|×|○|
|Operators|×|○|
|Full-text search|×|○|
|Relationships|×|○|

###Extras(項目は本家目次から抜粋)

|項目|json-serverもどき|本家JSON Server|
|----|----|----|
|Static file server|"db.json"固定|オプションで可変|
|Alternative port|phpが応答するポートのみ|オプションで可変|
|Remote schema|同一サーバのみ|オプションで可変|
|Add custom routes|なし|設定ファイルで拡張可能|
|Add middlewares|なし|自作Node.jsコードを組み込み可能|
|Module|なし|自作Node.jsコードにimport可能|

# 導入方法

サイトのDocumentRootに、後述の .htaccess と、php-json-server/php-json-server.php を配置します。
db.jsonファイルは自動的に生成されます。
(DocumentRoot以外に配置したい方は適宜修正してください。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/94488c51-67c8-d437-f272-fdcaef0b1399.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/dace09f2-d975-2547-561d-08f44681f7ed.png)

配置する2つのファイルは以下の通りです。

### .htaccess

```
RewriteEngine on

RewriteCond %{REQUEST_URI} ^/root(/.*)?$
RewriteRule .* /php-json-server/php-json-server.php?path=$0 [L]
```

※ 上記**root**部分は任意の名前に変更可能です。

### php-json-server.php

```php
<?php
$path = $_GET["path"];
$path = preg_replace('/[ \/]+$/', '', $path);
$path = explode('/', $path);
$root = $path[0];
$id = $path[1] ?? "";

$filename = "db.json";
$stdin = "php://input";
$json = json_decode(file_get_contents($filename)) ?? (object) array($root => array());
$json->{$root} = $json->{$root} ?? array();
$stdin_json = json_decode(file_get_contents($stdin));

function getIndexById($elms, $id) {
  foreach($elms as $i => $elm) {
    if($elm->{"id"} == $id) {
      return $i;
    }
  }
  return -1;
}

function getById($elms, $id) {
  return $elms[getIndexById($elms, $id)] ?? (object) array();
}

function getMaxId($elms) {
  return count($elms)? max(array_map( fn($elm) => $elm->{"id"} , $elms)) : 0;
}

if($_SERVER["REQUEST_METHOD"] == "GET") {
  if($id == "") {
    print(json_encode($json->{$root}));
  } else {
    print(json_encode(getById($json->{$root}, $id)));
  }
}

if($_SERVER["REQUEST_METHOD"] == "POST") {
  $id = getMaxId($json->{$root}) + 1;
  $stdin_json->{"id"} = $id;
  array_push($json->{$root}, $stdin_json);
  file_put_contents($filename, json_encode($json));
  print(json_encode($stdin_json));
}

if($_SERVER["REQUEST_METHOD"] == "PUT") {
  $stdin_json->{"id"} = $id;
  $i = getIndexById($json->{$root}, $id);
  if($i!=-1) {
    $json->{$root}[$i] = $stdin_json;
  } else {
    array_push($json->{$root}, $stdin_json);
  }
  file_put_contents($filename, json_encode($json));
  print(json_encode($stdin_json));
}

if($_SERVER["REQUEST_METHOD"] == "DELETE") {
  $i = getIndexById($json->{$root}, $id);
  if($i!=-1) {
    array_splice($json->{$root},$i,1);
    file_put_contents($filename, json_encode($json));
  }
}
```

# 使い方

###GET
ブラウザで/rootにアクセスしてみます。
まだデータが無いので空の配列が返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/7b9e4837-5765-a8e0-24c9-0c7bc13ae84a.png)

###POST
/rootへPOSTします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/b3124452-8500-3b3e-91d5-d3b84efb09f6.png)
再度/rootにアクセスしてみると、POSTした回数だけJSONデータが返ってきます。
idが採番されていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/089a6416-66e0-48de-a18d-e9e2c34d5068.png)

なお、/root/**ID**にアクセスすると、IDに該当するデータだけが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/53e30b0a-1ebc-7ddf-027b-cfa46faa5b1e.png)

###PUT
/root/**ID**へPUT。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/8cbba999-21c8-b351-1323-a3c25e68c03e.png)
再度/rootにアクセスしてみると、IDに該当するデータが更新されていることが分かります。
ここで、PUTで指定しなかったプロパティ(この例では**b**)は消えますのでご注意ください。(本家JSON Serverも同様の挙動です。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/f7736431-d50b-37dc-240d-2f1a2c1d4bc1.png)

###DELETE
/root/**ID**をDELETE。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/80c52415-20ca-156d-e42c-9a1e85fb80b8.png)
再度/rootにアクセスしてみると、IDに該当するデータが削除されていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1754122/095fd9ee-0463-f706-54d4-e0fa91d7807d.png)

# まとめ
PHPでJSONをGET(全件/1件)、POST(自動採番して追加)、PUT(更新)、DELETE(1件)するAPIモックができました。
もちろん、モックですのでセキュリティも何もないのと、モックとして十分かどうかは要件次第です。

そして、本家が使える環境ならそれに越したことはないです。
(なんというか、モックだろうと名のあるものは細かいところまで本当によくできているなと改めて思いました。。。)

以上です。