82
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

簡単なAPIをちょっとした工夫で完成度を高める話

Last updated at Posted at 2021-03-02

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になってしまいます。

スクリーンショット 2021-03-02 21.53.57.png

さあ、このようなエラー表示が出力に混じってしまうようでは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: 整数が入った配列と整数を入力 = 異常? 正常?

以上のようなクエリパラメータを入力すると、たとえば以下のような表示になります。

スクリーンショット 2021-03-02 22.07.53.png

これではやはり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);

これは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メソッドはGETHEAD以外は許可せず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);を外すと同じになります。

  1. PHPに詳しいかたはすぐに気付くでしょうが、最新のPHP 8.0ではなく7.4とかいう古臭いバージョンを使っています

82
72
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
82
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?