PHPで簡単なAPIを作った話という記事を読みまして、自分ならどう改善していくかを考えました。
Step 1. 整形する
=
+
.
などの演算子の周りを空白で空けてあげます。これだけで何か見ためが綺麗めになります。
<?php
$front = $_GET['front'];
$back = $_GET['back'];
$result = $front + $back;
$response['result'] = $result;
$response['status'] = "true";
$response['formula'] = $front . '+' . $back;
print json_encode($response, JSON_UNESCAPED_UNICODE);
私はPHPを以下のように起動しました。
% php -S localhost:3939 easyapi.php
この状態で http://localhost:3939/ にアクセスするとAPIが動作するという寸法です。
Step 2. 入力値を安全に受け取る
さて、この状態で何のクエリパラメータも渡さずにAPIにアクセスすると、このような表示1になってしまいます。
さあ、このようなエラー表示が出力に混じってしまうようではAPIとして役目を果たせません。実際にJSONとしてデコードできませんからね。
このような場合にどうするかは2通りの考えがあります。
- A: 値が渡されなかった場合は、0が渡されたということにして計算を続ける
- B: 値が渡されなかった場合は、計算できないのでエラーを返す
Aパターンの路線でいくならば、以下のように ?? 0
というものを付け足すだけです。
<?php
$front = $_GET['front'] ?? 0;
$back = $_GET['back'] ?? 0;
// 以下は同じ
これはnull合体演算子という、なんだかかっちょいい名前のPHPの機能です。
// ↓ わざと長く書くと
if (isset($_GET['front'])) {
$front = $_GET['front'];
} else {
$front = 0;
}
// ↓ ちょっと短く書くと
$front = isset($_GET['front']) ? $_GET['front'] : 0;
ということで、 $front = $_GET['front'] ?? 0
と書くことで、すっきり書けるようになるのでした。
これでめでたしなのでしょうか。……いいえ、 もっと悪いこと ができます。
-
?front=1&back=2
: 正常なパターン -
?front=&back=
: 空文字列を入力 = 異常 -
?front=a&back=b
: アルファベットの文字を入力 = 異常 -
?front[]=1&back=2
: 整数が入った配列と整数を入力 = 異常 -
?front[]=1&back[]=2
: 整数が入った配列と整数を入力 = 異常? 正常?
以上のようなクエリパラメータを入力すると、たとえば以下のような表示になります。
これではやはりAPIとしての仕事をまっとうできるとは言えませんよね。
APIは「値が渡されない」だけではなく、「意図しない形式の値が渡される」ことを必ず考えなければいけないのです。
それは特別なことではなく、靴を脱いだら揃えたり、用を足したら手を洗うくらい当たり前のことです。 別に洗わなくたって氏にはしない? あ、そう…
というわけで、今後はBパターンで入力値検証に合格しなければ計算を進めない方針で進めます。
Step 3. 入力値を型安全に受け取る
@mpywの$_GET, $_POSTなどを受け取る際の処理 - Qiitaを読んできてください。読みましたね? おめでとうございます。これであなたもPHPマスターです。
復習しましょう。filter_input()
またはfilter_var()
と検証フィルタを活用するのです。
<?php
$front = filter_input(INPUT_GET, 'front', FILTER_VALIDATE_INT);
$back = filter_input(INPUT_GET, 'back', FILTER_VALIDATE_INT);
if ($front === false || $front === null || $back === false || $back === null) {
$response = [
'result' => null,
'status' => false,
'formula' => '',
];
} else {
$result = $front + $back;
$response['result'] = $result;
$response['status'] = "true";
$response['formula'] = $front . '+' . $back;
}
print json_encode($response, JSON_UNESCAPED_UNICODE);
FILTER_VALIDATE_INT
は入力値が整数かどうかを検証するフィルタです。もし小数として受け取りたければFILTER_VALIDATE_FLOAT
を使います。
$_GET['front']
のように値を取り出したときは文字列型(string)なのでしたが、filter_input(INPUT_GET, 'front', FILTER_VALIDATE_INT)
のように取り出すと整数型(int)に、filter_input(INPUT_GET, 'front', FILTER_VALIDATE_FLOAT)
のように取り出すと浮動小数点型(float)に値がキャストされるのでした。とても便利ですね。
不正な値が渡された場合はfalse
に、そもそも値が渡されなかった場合はnull
になるのでした。よって、このような判定になるのです。$front === false || $front === null || $back === false || $back === null
は冗長ですが、empty($front) || empty($back)
などと書くと0
も弾かれてしまいます。
$is_unexpected_value = fn($v) => in_array($v, [false, null], true);
if ($is_unexpected_value($front) || $is_unexpected_value($back)) {
// ...
もっとも、今回の場合はint
, false
, null
の3バターンしかないので、以下のように書いた方が簡単でした。
if (!is_int($front) || !is_int($back)) {
// ...
この条件は以下のように書き換えても構いません。
if (!(is_int($front) && is_int($back))) {
これはド・モルガンの法則 - Wikipediaによるものです。私は算数がとても苦手ですが名前がおもしろいのでよく覚えています。
Step 4. 適切なHTTPヘッダを設定する
PHPはデフォルトではContent-type: text/html; charset=UTF-8
として取り扱うので、HTML以外のデータ形式を出力する場合はContent-Type
を適切にセットする必要があります。
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
echo json_encode($response, JSON_UNESCAPED_UNICODE);
- X-Content-Type-Options - HTTP | MDN
- 機密情報を含むJSONには X-Content-Type-Options: nosniff をつけるべき - 葉っぱ日記
- X-Content-Type-Options: nosniff はIE以外にも必要 – yohgaki's blog
これはWebブラウザの推測による予期しない振舞いを避けるためのものです。
細かいようですが、これらはWebからアクセスされうるAPIでは基本的に設定すべきものです。なぜ付ける必要があるのかわからなければ、必ず付けてください。
Step 5. 細かいところもこだわっていこう
神は細部に宿ります。
<?php
declare(strict_types=1);
error_reporting(E_ALL);
$front = filter_input(INPUT_GET, 'front', FILTER_VALIDATE_INT);
$back = filter_input(INPUT_GET, 'back', FILTER_VALIDATE_INT);
$response = [
'result' => null,
'status' => false,
'formula' => '',
];
if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD'])) {
http_response_code(405);
} elseif (!is_int($front) || !is_int($back)) {
http_response_code(400);
} else {
$response = [
'result' => $front + $back,
'status' => true,
'formula' => "{$front} + {$back}",
];
}
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
echo json_encode($response, JSON_UNESCAPED_UNICODE);
-
declare(strict_types=1);
- これを付けると関数呼び出し時に引数の型が間違ってると判定が厳しくなります
- ただし今回はPHPの内部関数しか使ってないので、あまり意味がないんですけど
-
error_reporting(E_ALL)
- これを付けるとNoticeなどのエラーも見逃されなくなります
- 今回はエラーが発生しないように作ってありますが、細かいエラーを握り潰さないという気概を示しておきます
- HTTPメソッドは
GET
とHEAD
以外は許可せず405
を返すことにします- 405 Method Not Allowed - HTTP | MDN
- 一般的なWeb APIはHTTPメソッドが固定されています
- フレームワークを使わないPHPではHTTPメソッドの判定が甘くなりがちです
-
HEAD
メソッドをサポートする必要があるかといえばないのですが、気分です
- 意図したパラメータが渡されなかった場合は
400
を返すことにします
まとめ
今回は外部ライブラリやフレームワークなどを使わずにPHPで安全なAPIを実装するために気にするべきことをまとめました。これらは普段フレームワークを使って開発していたとしても、PHPを使っていれば基本的に気にするべき事柄です。残念なことにフレームワークを使うだけで即座に安全になるといった性格のものではありません。(フレームワークの機能をきちんと選択すれば避けられるかもしれませんが)
以下は読者への課題とします。
- 好きな個数の数を計算するAPIを実装する
- 好きな演算の種類を選んで実行時エラーを起こさずに計算できるAPIを実装する
- 使い慣れているフレームワークでAPIを実装してみて不正な入力にどう対応できるか確認する
私からは以上です。
おまけ: 内部関数と厳密な型検査
今回はpow()
を再実装することで検証してみましょう。
<?php
declare(strict_types=1);
var_dump(pow('2', '10'));
var_dump(mypow('2', '10'));
function mypow(int|float $base, int|float $exp): int|float
{
return $base ** $exp;
}
動作確認してみましょう。
pow()
とmypow()
はdeclare(strict_types=1);
を外すと同じになります。
-
PHPに詳しいかたはすぐに気付くでしょうが、最新のPHP 8.0ではなく7.4とかいう古臭いバージョンを使っています ↩