レガシーPHPプロジェクト用のRESTAPIの構築
公開: 2022-03-11REST APIの構築または設計は、特にレガシーPHPプロジェクトで行う必要がある場合は簡単な作業ではありません。 現在、REST APIの実装を容易にするサードパーティのライブラリがたくさんありますが、それらを既存のレガシーコードベースに統合するのはかなり困難な場合があります。 また、LaravelやSymfonyなどの最新のフレームワークを使用する余裕があるとは限りません。 レガシーPHPプロジェクトでは、古いバージョンのPHP上で実行されている、非推奨の社内フレームワークの真ん中にいることがよくあります。
この記事では、REST APIを最初から実装しようとする際の一般的な課題、それらの問題を回避するいくつかの方法、およびレガシープロジェクト用のカスタムPHPベースのAPIサーバーを構築するための全体的な戦略について説明します。
この記事はPHP5.3以降に基づいていますが、コアの概念はバージョン5.0以降のすべてのバージョンのPHPに有効であり、PHP以外のプロジェクトにも適用できます。 ここでは、REST APIの一般的な内容については説明しません。そのため、REST APIに慣れていない場合は、最初に必ず読んでください。
わかりやすくするために、この記事全体で使用されているいくつかの用語とその意味のリストを次に示します。
- APIサーバー:APIを提供するメインのRESTアプリケーション。この場合、PHPで記述されています。
- APIエンドポイント:クライアントがアクションを実行して結果を生成するために通信するバックエンドの「メソッド」。
- APIエンドポイントURL:バックエンドシステムに世界中からアクセスするためのURL。
- APIトークン:ユーザーを識別できるHTTPヘッダーまたはCookieを介して渡される一意の識別子。
- アプリ:APIエンドポイントを介してRESTアプリケーションと通信するクライアントアプリケーション。 この記事では、Webベース(デスクトップまたはモバイル)であると想定しているため、JavaScriptで記述されています。
初期ステップ
パスパターン
最初に決定する必要があることの1つは、APIエンドポイントを使用できるURLパスです。 2つの一般的な方法があります。
- api.example.comなどの新しいサブドメインを作成します。
- example.com/apiなどのパスを作成します。
一見すると、最初のバリアントの方が人気があり魅力的であるように見えるかもしれません。 ただし、実際には、プロジェクト固有のAPIを構築している場合は、2番目のバリアントを選択する方が適切な場合があります。
2番目のアプローチを採用する背後にある最も重要な理由の1つは、これにより、資格情報を転送する手段としてCookieを使用できるようになることです。 ブラウザベースのクライアントは、XHRリクエスト内で適切なCookieを自動的に送信し、追加の認証ヘッダーの必要性を排除します。
もう1つの重要な理由は、一部のプロキシサーバーによってカスタムヘッダーが削除される可能性があるサブドメインの構成や管理の問題に関して何もする必要がないことです。 これは、レガシープロジェクトでは退屈な試練になる可能性があります。
RESTリクエストはステートレスである必要があるため、Cookieの使用は「非RESTful」プラクティスと見なすことができます。 この場合、カスタムヘッダーを介して渡す代わりに、妥協してトークン値をCookieに渡すことができます。 事実上、session_idの代わりにトークン値を直接渡す方法としてCookieを使用しています。 このアプローチはステートレスと見なすことができますが、お客様の好みに任せることができます。
APIエンドポイントURLもバージョン管理できます。 さらに、パス名の拡張子として期待される応答形式を含めることができます。 これらは重要ではありませんが、特にAPI開発の初期段階では、長期的にはこれらの詳細が確実に報われる可能性があります。 特に、新しい機能を実装する必要がある場合。 クライアントが期待しているバージョンを確認し、下位互換性に必要な形式を提供することが最善の解決策になる可能性があります。
APIエンドポイントのURL構造は次のようになります。
example.com/api/${version_code}/${actual_request_path}.${format}
そして、実際の例:
example.com/api/v1.0/records.json
ルーティング
APIエンドポイントのベースURLを選択したら、次に行う必要があるのは、ルーティングシステムについて考えることです。 既存のフレームワークに統合することもできますが、それが面倒な場合は、ドキュメントルートに「api」という名前のフォルダーを作成することを回避できます。 そうすれば、APIは完全に別個のロジックを持つことができます。 APIロジックを次のような独自のファイルに配置することで、このアプローチを拡張できます。
「www/api / Apis / Users.php」は、特定のAPIエンドポイントの個別の「コントローラー」と考えることができます。 データベースと通信するためにプロジェクトにすでに実装されているモデルを再利用するなど、既存のコードベースの実装を再利用するのは素晴らしいことです。
最後に、「/ api/*」から「/api/index.php」へのすべての着信リクエストを必ず指定してください。 これは、Webサーバーの構成を変更することで実行できます。
APIクラス
バージョンとフォーマット
APIエンドポイントが受け入れるバージョンとフォーマット、およびデフォルトのものを常に明確に定義する必要があります。 これにより、古い機能を維持しながら、将来的に新しい機能を構築できるようになります。 APIバージョンは基本的に文字列にすることができますが、理解と比較を容易にするために数値を使用できます。 マイナーバージョン用に予備の数字を用意しておくと、違いがわずかしかないことが明確に示されるため、適切です。
- v1.0は最初のバージョンを意味します。
- いくつかのマイナーな変更を加えたv1.1の最初のバージョン。
- v2.0は完全に新しいバージョンになります。
フォーマットは、JSON、XML、さらにはCSVを含むがこれらに限定されない、クライアントが必要とするものであれば何でもかまいません。 URLを介してファイル拡張子として提供することにより、APIエンドポイントURLは読みやすさを保証し、APIコンシューマーが期待できる形式を簡単に知ることができます。
- 「/api/v1.0/records.json」はレコードのJSON配列を返します
- 「/api/v1.0/records.xml」はレコードのXMLファイルを返します
これらの各形式の応答では、適切なContent-Typeヘッダーも送信する必要があることを指摘しておく価値があります。
着信リクエストを受信したら、最初に行うべきことの1つは、APIサーバーがリクエストされたバージョンとフォーマットをサポートしているかどうかを確認することです。 着信要求を処理するメインメソッドで、 $ _SERVER['PATH_INFO']または$_SERVER['REQUEST_URI']を解析して、要求された形式とバージョンがサポートされているかどうかを確認します。 次に、続行するか、4xx応答を返します(例:406「受け入れられません」)。 ここで最も重要な部分は、クライアントが期待するものを常に返すことです。 これに代わる方法は、URLパス拡張子の代わりにリクエストヘッダー「Accept」をチェックすることです。
許可されたルート
すべてを透過的にAPIコントローラーに転送することもできますが、ホワイトリストに登録された許可されたルートのセットを使用する方がよい場合があります。 これにより柔軟性が少し低下しますが、次にコードに戻ったときにAPIエンドポイントURLがどのように見えるかについて非常に明確な洞察が得られます。
private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );
これらを別々のファイルに移動して、物事をすっきりさせることもできます。 上記の構成は、これらのURLへのリクエストを有効にするために使用されます。
/api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json
PUTデータの処理
PHPは、着信POSTデータを自動的に処理し、それを$_POSTスーパーグローバルの下に配置します。 ただし、PUTリクエストの場合はそうではありません。 すべてのデータはphp://inputに「埋め込まれ」ます。 実際のAPIメソッドを呼び出す前に、それを別の構造体または配列に解析することを忘れないでください。 単純なparse_strで十分かもしれませんが、クライアントがマルチパートリクエストを送信している場合は、フォームの境界を処理するために追加の解析が必要になる場合があります。 マルチパートリクエストの一般的なユースケースには、ファイルのアップロードが含まれます。 マルチパートリクエストの検出と処理は、次のように実行できます。
self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }
ここで、 parse_raw_requestは次のように実装できます。
/** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }
これにより、必要なリクエストペイロードをApi :: $ inputに生の入力として、 Api ::$input_dataに連想配列として含めることができます。

偽のPUT/DELETE
サーバーが標準のGET/POSTHTTPメソッド以外のものをサポートしていない状況に陥ることがあります。 この問題の一般的な解決策は、PUT/DELETEまたはその他のカスタムリクエストメソッドを「偽造」することです。 そのためには、「_method」などの「magic」パラメータを使用できます。 $ _REQUEST配列に表示されている場合は、リクエストが指定されたタイプであると単純に想定してください。 Laravelのような最新のフレームワークには、そのような機能が組み込まれています。 サーバーまたはクライアントに制限がある場合(たとえば、PUT要求を許可しない企業プロキシの背後で仕事のWi-Fiネットワークを使用している場合)に優れた互換性を提供します。
特定のAPIへの転送
既存のプロジェクトオートローダーを再利用する余裕がない場合は、 spl_autoload_register関数を使用して独自のオートローダーを作成できます。 「api/index.php」ページで定義し、「api/Api.php」にあるAPIクラスを呼び出します。 APIクラスはミドルウェアとして機能し、実際のメソッドを呼び出します。 たとえば、「/ api / v1.0 / records / 7.json」へのリクエストは、パラメータ7で「Apis / Records.php」GETメソッドを呼び出すことになります。これにより、関心の分離が確実になり、ロジッククリーナー。 もちろん、これを使用しているフレームワークにさらに深く統合し、その特定のコントローラーまたはルートを再利用できる場合は、その可能性も考慮する必要があります。
プリミティブオートローダーを使用した「api/index.php」の例:
<?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();
これにより、 Apiクラスが読み込まれ、メインプロジェクトとは関係なくサービスが開始されます。
オプションリクエスト
クライアントがカスタムヘッダーを使用して一意のトークンを転送する場合、ブラウザは、サーバーがそのヘッダーをサポートするたびに最初にチェックする必要があります。 そこでOPTIONSリクエストが登場します。その目的は、クライアントとAPIサーバーの両方にとってすべてが正常で安全であることを確認することです。 したがって、OPTIONSリクエストは、クライアントが何かを行おうとするたびに発生する可能性があります。 ただし、クライアントが資格情報にCookieを使用している場合は、ブラウザーがこの追加のOPTIONS要求を送信する必要がなくなります。
クライアントがCookieを使用してPOST/users/8.jsonを要求している場合、その要求はかなり標準的なものになります。
- アプリは/users/8.jsonに対してPOSTリクエストを実行します。
- ブラウザはリクエストを実行し、レスポンスを受け取ります。
ただし、カスタム認証またはトークンヘッダーを使用する場合:
- アプリは/users/8.jsonに対してPOSTリクエストを実行します。
- ブラウザはリクエストの処理を停止し、代わりにOPTIONSリクエストを開始します。
- OPTIONSリクエストは/users/8.jsonに送信されます。
- ブラウザは、APIで定義されているように、使用可能なすべてのメソッドとヘッダーのリストを含む応答を受け取ります。
- 使用可能なヘッダーのリストにカスタムヘッダーが存在する場合にのみ、ブラウザーは元のPOSTリクエストを続行します。
ただし、Cookieを使用している場合でも、PUT / DELETEを使用すると、追加のOPTIONSリクエストを受け取る可能性があることに注意してください。 ですから、それに対応する準備をしてください。
RecordsAPI
基本構造
サンプルのRecordsAPIは非常に単純です。 すべてのリクエストメソッドが含まれ、出力を同じメインAPIクラスに返します。 例えば:
<?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...
したがって、各HTTPメソッドを定義すると、RESTスタイルでAPIをより簡単に構築できるようになります。
出力のフォーマット
データベースから受け取ったすべてのものをクライアントに素朴に応答すると、壊滅的な結果を招く可能性があります。 データが誤って公開されるのを防ぐために、ホワイトリストに登録されたキーのみを返す特定のフォーマットメソッドを作成してください。
ホワイトリストに登録されたキーのもう1つの利点は、これらに基づいてドキュメントを作成し、すべてのタイプチェックを実行できることです。たとえば、user_idは常に整数であり、フラグis_bannedは常にブール値のtrueまたはfalseであり、日時には1つの標準があります。応答形式。
結果の出力
ヘッダー
ヘッダー出力の個別のメソッドにより、ブラウザーに送信されるすべてが正しいことが保証されます。 このメソッドは、カスタム認証ヘッダーを受信する可能性を維持しながら、同じドメインを介してAPIにアクセスできるようにするという利点を利用できます。 同じドメインとサードパーティドメインのどちらを選択するかは、HTTP_ORIGINサーバーヘッダーとHTTP_REFERERサーバーヘッダーを使用して行うことができます。 クライアントがx-authorization(またはその他のカスタムヘッダー)を使用していることをアプリが検出している場合は、すべてのオリジンからのアクセスを許可し、カスタムヘッダーを許可する必要があります。 したがって、次のようになります。
header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);
ただし、クライアントがCookieベースの資格情報を使用している場合、ヘッダーは少し異なる可能性があり、資格情報に対して要求されたホストとCookie関連のヘッダーのみを許可します。
header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }
OPTIONSリクエストはCookieをサポートしていないため、アプリはCookieを一緒に送信しないことに注意してください。 そして最後に、これにより、必要なすべてのHTTPメソッドにアクセス制御の有効期限を設定できます。
header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');
体
本文自体には、クライアントが要求した形式の応答が含まれている必要があります。成功した場合は2xx HTTPステータス、クライアントが原因で失敗した場合は4xxステータス、サーバーが原因で失敗した場合は5xxステータスになります。 応答の構造はさまざまですが、「ステータス」フィールドと「応答」フィールドを指定することも有益です。 たとえば、クライアントが新しいユーザーを登録しようとしていて、ユーザー名がすでに取得されている場合、HTTPステータス200で応答を送信できますが、本文には次のようなJSONが含まれます。
{“status”: “ERROR”, “response”: ”username already taken”}
…HTTP4xxエラーの代わりに直接。
結論
2つのプロジェクトがまったく同じというわけではありません。 この記事で概説されている戦略は、あなたのケースに適している場合とそうでない場合がありますが、それでもコアの概念は類似している必要があります。 すべてのページの背後に最新のトレンドまたは最新のフレームワークが含まれているとは限らないことに注意してください。「RESTSymfonyバンドルがここで機能しない理由」に対する怒りが、何か有用なものを構築する動機に変わることがあります。うまくいくもの。 最終結果は、常にカスタムおよびプロジェクト固有の実装であるため、それほど光沢がない場合がありますが、最終的には、ソリューションは実際に機能するものになります。 このようなシナリオでは、すべてのAPI開発者の目標となるはずです。
ここで説明する概念の実装例は、便宜上GitHubリポジトリにアップロードされています。 これらのサンプルコードをそのまま本番環境で直接使用したくない場合もありますが、これは次のレガシーPHPAPI統合プロジェクトの開始点として簡単に機能します。
最近、いくつかのレガシープロジェクトにREST APIサーバーを実装する必要がありましたか? 以下のコメントセクションで私たちとあなたの経験を共有してください。