PHPのcURLで外部API連携を実装する:HTTPメソッド・認証・エラー処理を共通化
作成日:2026.09.20
PHPのcURL拡張を使い、GET・POST・PUT・PATCH・DELETEのHTTPリクエストを送信する方法を解説します。APIキーやBearerトークンの扱い、タイムアウト、通信エラーとHTTPエラーの分離、JSONレスポンスの解析までを共通関数にまとめます。
目次
PHPから外部APIを呼び出すとき、APIごとにcURLの処理を書いていると、HTTPメソッドや認証、タイムアウト、エラー処理が少しずつ違ってしまいがちです。
今回は、PHPのcURL拡張だけで外部APIへリクエストを送る方法を整理します。GET、POST、PUT、PATCH、DELETEを扱い、APIキーやBearerトークンの送信、通信エラーとHTTPエラーの分離までを共通関数にまとめます。
外部APIのレスポンスはAPIごとに異なるため、この記事では通信処理とレスポンス解析を分けます。
前提
今回の対象環境は以下の通りです。
- Windows 11
- PHP 8.3.15
- PHPのcURL、JSON拡張
- コマンド実行はPowerShell
PHPのcURL拡張が有効であることを確認するには、次のコマンドを実行します。
php -m | Select-String "curl|json"
curlとjsonが表示されれば、今回のサンプルに必要な拡張が有効です。
cURLでHTTPリクエストを送る流れ
PHPのcURLでHTTPリクエストを送るときは、次のような流れになります。
- cURLハンドルを作成する
- URLやHTTPメソッド、ヘッダーなどを設定する
- リクエストを実行する
- レスポンスとHTTPステータスを取得する
- cURLハンドルを閉じる
curl_execがfalseを返す場合は、DNS解決、TLS接続、タイムアウトなど、通信そのものに失敗している可能性があります。一方、HTTP 404や500のようなレスポンスは、通信が完了してAPIから返された結果です。curl_execの戻り値だけで判断せず、curl_getinfoでHTTPステータスも確認します。
GETリクエストを送る
まずは、認証が不要なAPIへGETリクエストを送る形を確認します。URLのクエリパラメータは、文字列を直接連結せず、http_build_queryで組み立てます。
<?php
$query = http_build_query([
'keyword' => 'php',
'limit' => 10,
]);
$url = 'https://api.example.com/items?' . $query;
$handle = curl_init($url);
if ($handle === false) {
throw new RuntimeException('cURLの初期化に失敗しました。');
}
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
],
]);
$body = curl_exec($handle);
$statusCode = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
$errorNumber = curl_errno($handle);
$errorMessage = curl_error($handle);
curl_close($handle);
if ($body === false) {
throw new RuntimeException(
"cURL error {$errorNumber}: {$errorMessage}"
);
}
if ($statusCode < 200 || $statusCode >= 300) {
throw new RuntimeException("HTTP status: {$statusCode}");
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
CURLOPT_RETURNTRANSFERをtrueにすると、レスポンス本文を画面へ直接出力せず、curl_execの戻り値として受け取れます。APIのレスポンスを解析する場合は、この設定を付けておきます。
なお、CURLOPT_CONNECTTIMEOUTは接続確立までの待ち時間、CURLOPT_TIMEOUTはリクエスト全体の制限時間です。いずれも待ち時間は秒単位で指定します。役割が異なるため、両方を設定します。
APIキーをヘッダーへ指定する
APIキーを使うAPIでは、キーをヘッダーへ指定します。APIごとにヘッダー名は異なるため、公式ドキュメントで確認してください。
APIキーはPHPファイルへ直接書かず、環境変数から読み込みます。PowerShellでは、例えば次のように環境変数を設定できます。
$env:EXAMPLE_API_KEY = "ここにAPIキーを設定"
PHPからはgetenvで読み込みます。
<?php
$apiKey = getenv('EXAMPLE_API_KEY');
if ($apiKey === false || $apiKey === '') {
throw new RuntimeException('EXAMPLE_API_KEYが設定されていません。');
}
$headers = [
'Accept: application/json',
'X-API-Key: ' . $apiKey,
];
X-API-Keyは説明用のヘッダー名です。実際には、利用するAPIが指定するヘッダー名に置き換えます。APIキーをURLのクエリパラメータへ含める方式もありますが、アクセスログなどへ残りやすくなるため、仕様が許すならヘッダーを使う方が扱いやすいことが多いです。
Bearerトークンを送信する
Bearerトークンを使うAPIでは、AuthorizationヘッダーへBearerとトークンを指定します。
<?php
$accessToken = getenv('EXAMPLE_ACCESS_TOKEN');
if ($accessToken === false || $accessToken === '') {
throw new RuntimeException('EXAMPLE_ACCESS_TOKENが設定されていません。');
}
$headers = [
'Accept: application/json',
'Authorization: Bearer ' . $accessToken,
];
APIキーとBearerトークンは似ていますが、ヘッダー名や有効期限、更新方法はサービスごとに異なります。認証方式を共通関数へ固定的に埋め込むのではなく、ヘッダーを呼び出し側から渡せるようにしておくと、APIごとの違いに対応しやすくなります。
JSONをPOSTする
JSON形式のデータをPOSTする場合は、リクエストボディをjson_encodeで作成し、Content-Type: application/jsonを指定します。
<?php
$requestData = [
'name' => 'PHP',
'enabled' => true,
];
$requestBody = json_encode(
$requestData,
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
$handle = curl_init('https://api.example.com/items');
if ($handle === false) {
throw new RuntimeException('cURLの初期化に失敗しました。');
}
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $requestBody,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/json',
'X-API-Key: ' . $apiKey,
],
]);
$body = curl_exec($handle);
$statusCode = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
$errorMessage = curl_error($handle);
curl_close($handle);
if ($body === false) {
throw new RuntimeException('cURLの実行に失敗しました: ' . $errorMessage);
}
if ($statusCode < 200 || $statusCode >= 300) {
throw new RuntimeException("HTTP status: {$statusCode}");
}
$responseData = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
JSON_THROW_ON_ERRORを指定すると、JSONのエンコードまたはデコードに失敗したときに例外として扱えます。戻り値がfalseやnullになるだけの実装より、どの段階で失敗したかを切り分けやすくなります。
PUT、PATCH、DELETEを指定する
更新や削除のAPIでは、HTTPメソッドを指定してリクエストを送るケースが多いです。CURLOPT_CUSTOMREQUESTを使うと、POST以外のメソッドも同じ通信処理で扱えます。
// PUT: リソース全体を更新するAPIの例
$response = requestApi(
'PUT',
'https://api.example.com/items/123',
$headers,
json_encode($requestData, JSON_THROW_ON_ERROR),
);
// PATCH: リソースの一部を更新するAPIの例
$response = requestApi(
'PATCH',
'https://api.example.com/items/123',
$headers,
json_encode(['enabled' => false], JSON_THROW_ON_ERROR),
);
// DELETE: リソースを削除するAPIの例
$response = requestApi(
'DELETE',
'https://api.example.com/items/123',
$headers,
);
PUT、PATCH、DELETEの意味や、ボディが必要かどうかはAPIの仕様によって異なります。メソッド名だけでリクエスト形式を決めず、対象APIの仕様に合わせます。
外部API向けの共通関数を作る
ここまでの処理を、URL、メソッド、ヘッダー、ボディを受け取る共通関数へまとめます。共通関数ではAPI固有のJSON項目を解釈せず、HTTPレスポンスを返すところまでを担当させます。
<?php
/**
* @return array{status: int, body: string}
*/
function requestApi(
string $method,
string $url,
array $headers = [],
?string $body = null,
int $connectTimeout = 5,
int $timeout = 15,
): array {
$method = strtoupper($method);
if (!in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], true)) {
throw new InvalidArgumentException(
"対応していないHTTPメソッドです: {$method}"
);
}
$handle = curl_init($url);
if ($handle === false) {
throw new RuntimeException('cURLの初期化に失敗しました。');
}
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_CONNECTTIMEOUT => $connectTimeout,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
];
if ($body !== null) {
$options[CURLOPT_POSTFIELDS] = $body;
}
curl_setopt_array($handle, $options);
$responseBody = curl_exec($handle);
$errorNumber = curl_errno($handle);
$errorMessage = curl_error($handle);
$statusCode = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
if ($responseBody === false) {
throw new RuntimeException(
"cURL error {$errorNumber}: {$errorMessage}"
);
}
return [
'status' => $statusCode,
'body' => $responseBody,
];
}
HTTPステータスが200番台以外の場合も、ここでは例外にしていません。レスポンス本文にはAPIが返したエラーコードやメッセージが含まれていることがあるため、呼び出し側で確認できるようにします。
また、CURLOPT_FAILONERRORは使っていません。このオプションを使うと、HTTPエラーをcurl_execの失敗として扱える一方で、通信エラーとHTTPエラーを同じ分岐で処理しやすくなります。今回のようにエラーの種類を分けたい場合は、HTTPステータスを明示的に確認する方が分かりやすいかと思います。
共通関数を使ってGETする
共通関数を使う側では、APIごとのURLや認証ヘッダーを用意して渡します。
<?php
$apiKey = getenv('EXAMPLE_API_KEY');
if ($apiKey === false || $apiKey === '') {
throw new RuntimeException('EXAMPLE_API_KEYが設定されていません。');
}
$query = http_build_query([
'keyword' => 'php',
'limit' => 10,
]);
$response = requestApi(
'GET',
'https://api.example.com/items?' . $query,
[
'Accept: application/json',
'X-API-Key: ' . $apiKey,
],
);
if ($response['status'] < 200 || $response['status'] >= 300) {
throw new RuntimeException(
"APIがHTTP {$response['status']}を返しました。"
);
}
$data = json_decode(
$response['body'],
true,
512,
JSON_THROW_ON_ERROR
);
呼び出し側にはAPI固有のURL、認証ヘッダー、レスポンス解析だけが残ります。cURLハンドルの作成やタイムアウト設定は共通関数へ集約されるため、APIごとの実装差分を小さくできます。
共通関数を使ってJSONをPOSTする
JSONをPOSTする場合も、リクエストボディを作って共通関数へ渡します。
<?php
$accessToken = getenv('EXAMPLE_ACCESS_TOKEN');
if ($accessToken === false || $accessToken === '') {
throw new RuntimeException('EXAMPLE_ACCESS_TOKENが設定されていません。');
}
$body = json_encode([
'name' => 'PHP',
'enabled' => true,
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
$response = requestApi(
'POST',
'https://api.example.com/items',
[
'Accept: application/json',
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken,
],
$body,
);
if ($response['status'] < 200 || $response['status'] >= 300) {
// 必要に応じて、レスポンス本文をログへ記録する。
throw new RuntimeException(
"APIがHTTP {$response['status']}を返しました。"
);
}
$data = json_decode(
$response['body'],
true,
512,
JSON_THROW_ON_ERROR
);
認証方式がAPIキーからBearerトークンへ変わっても、共通関数の変更は必要ありません。認証情報を含むヘッダーを呼び出し側で組み立てるためです。
エラーを分けて扱う
外部APIのエラーは、少なくとも次の3種類に分けて考えると、対応方針を決めやすくなります。
| 種類 | 例 | 確認する内容 |
|---|---|---|
| 通信エラー | DNS解決失敗、TLSエラー、接続タイムアウト | curl_errno、curl_error |
| HTTPエラー | 401、403、404、429、500 | curl_getinfoのHTTPステータスとレスポンス本文 |
| 解析エラー | JSONとして解釈できないレスポンス | json_decodeの例外 |
例えば、タイムアウトなら一時的な障害として再試行を検討できます。一方、401なら認証情報、404ならURLや対象データ、429なら利用回数制限を確認する必要があります。すべてを「データがありません」として扱うと、原因の切り分けが難しくなります。
APIが返したエラー本文をログへ記録する場合も、APIキー、Bearerトークン、個人情報などが含まれていないか確認します。エラー本文をそのまま利用者へ表示するのではなく、内部ログと画面表示を分けることも重要です。
動作を確認する
実際のAPIを使って確認する場合は、対象APIのURL、認証方式、必要なヘッダー、利用制限を確認してから実行します。APIキーなどの秘密情報は、環境変数へ設定してから使います。
確認する項目は次の通りです。
- GETで200番台のレスポンスを取得できる
- JSONをPOSTし、期待したHTTPステータスとレスポンスを取得できる
- APIキー未設定時に、リクエストを送信せずエラーになる
- 存在しないURLや意図的に遅いエンドポイントで、通信エラーやタイムアウトを確認できる
- HTTP 4xx/5xxを通信エラーとは別に扱える
- JSONではないレスポンスを受け取ったとき、解析エラーとして扱える
外部APIへ接続できない環境では、ローカルのモックサーバーやテスト用の応答を使って、HTTPステータスごとの分岐を確認する方法もあります。外部サービスからレスポンスが返ったことだけで、すべてのエラー処理を検証したことにはならないので注意しましょう。
注意点
CURLOPT_SSL_VERIFYPEERをfalseにして、TLS証明書の検証を無効にしない。- 接続タイムアウトと全体のタイムアウトを設定し、応答を無期限に待たない。
- APIキーやアクセストークンをソースコード、画面、ログへ出力しない。
- APIのレスポンス本文をログへ残す場合は、個人情報や秘密情報をマスキングする。
- リトライする場合は、HTTPメソッドの性質、重複実行の可能性、APIのレート制限を確認する。
- 429や一時的な5xxに対する待機時間、最大試行回数、利用者への案内を決める。
- ファイルアップロード、ストリーミング、プロキシ、Cookieなどは、必要になった段階で個別に設計する。
- 外部APIの利用規約、認証情報の有効期限、レスポンス仕様を執筆時点と運用時に確認する。
まとめ
PHPのcURLでは、URL、HTTPメソッド、ヘッダー、リクエストボディ、タイムアウトを設定して外部APIへリクエストを送信できます。GET、POSTだけでなく、CURLOPT_CUSTOMREQUESTを使うことでPUT、PATCH、DELETEも同じ考え方で扱えます。
APIキーやBearerトークンは環境変数から読み込み、ヘッダーとして渡します。通信処理を共通関数へまとめる場合は、cURLの通信エラー、HTTPエラー、JSON解析エラーを分け、API固有のレスポンス解析とは責務を分離します。
リトライ、レート制限、ファイルアップロード、ストリーミングなどが必要になる場合は、APIの仕様とアプリケーションの要件を確認し、共通関数へ無理に詰め込まないようにしましょう。
参考資料
- PHP: cURL - Manual
- PHP: curl_setopt_array - Manual
- PHP: curl_exec - Manual
- PHP: curl_getinfo - Manual
- PHP: curl_error - Manual
- PHP: curl_errno - Manual
- PHP: json_encode - Manual
- PHP: json_decode - Manual
- GeminiのInteractions APIをPHPから呼び出して会話を継続する
- PHPでISBNから書誌情報を取得する:openBD・Google Books・NDLサーチをcURLで実装する
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
Laravel 13のNamed Rate Limiterでレート制限を実装する:IP・ユーザー・入力値ごとの設定
2026.09.19
LaravelのNamed Rate Limiterを使い、無制限に繰り返されたくない処理へレート制限を追加する方法を解説します。RateLimiter::for()とthrottleミドルウェアの基本から、IPアドレス・ユーザーID・入力値による制限、複数の制限、429レスポンス、ログイン処理への応用、Redis利用時の注意点まで整理します。
Laravel SocialiteでGoogleログインを実装する|OAuth設定から初回登録まで
2026.09.18
Laravel Socialiteを使ったGoogleログインを、OAuthクライアントの設定からコールバック、Laravelユーザーの登録・ログインまで解説します。Googleアカウントは変更されるメールアドレスではなくsubで識別し、初回登録時は検証済みメールアドレスを確認します。既存アカウントへ自動連携しない設計や、セッション管理、Socialite Fakeで確認するテスト項目も紹介します。
Laravel Pintの使い方:PHPコードの書式を整えてCIで検査する
2026.09.15
Laravel Pintを使ってPHPコードの書式を整える方法を紹介します。ローカルでの自動修正、CIで書式違反だけを検査する--test、対象範囲の指定、pint.jsonの設定、Bladeファイルを扱う際の注意点を整理し、Pintを導入する判断基準も説明します。
PHPUnitでPHPのテストを始める:正常系・例外・モックの基本
2026.09.14
PHP 8.3とPHPUnit 12.5を使い、フレームワークやデータベースに依存しないPHPの例で、テストの考え方からComposerによる導入、基本的なアサーション、データプロバイダー、例外、モックまでを解説し、PHPUnitの基本を整理します。