技術資料

PHPのcURLで外部API連携を実装する:HTTPメソッド・認証・エラー処理を共通化

作成日:2026.09.20

PHP

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"

curljsonが表示されれば、今回のサンプルに必要な拡張が有効です。

cURLでHTTPリクエストを送る流れ

PHPのcURLでHTTPリクエストを送るときは、次のような流れになります。

  1. cURLハンドルを作成する
  2. URLやHTTPメソッド、ヘッダーなどを設定する
  3. リクエストを実行する
  4. レスポンスとHTTPステータスを取得する
  5. cURLハンドルを閉じる

curl_execfalseを返す場合は、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_RETURNTRANSFERtrueにすると、レスポンス本文を画面へ直接出力せず、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のエンコードまたはデコードに失敗したときに例外として扱えます。戻り値がfalsenullになるだけの実装より、どの段階で失敗したかを切り分けやすくなります。

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_errnocurl_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_VERIFYPEERfalseにして、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の仕様とアプリケーションの要件を確認し、共通関数へ無理に詰め込まないようにしましょう。

参考資料

この記事を書いた人

※上が私です。

奈良市を拠点に、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 PHP

Laravel SocialiteでGoogleログインを実装する|OAuth設定から初回登録まで

2026.09.18

Laravel Socialiteを使ったGoogleログインを、OAuthクライアントの設定からコールバック、Laravelユーザーの登録・ログインまで解説します。Googleアカウントは変更されるメールアドレスではなくsubで識別し、初回登録時は検証済みメールアドレスを確認します。既存アカウントへ自動連携しない設計や、セッション管理、Socialite Fakeで確認するテスト項目も紹介します。

Laravel OAuth PHP

Laravel Pintの使い方:PHPコードの書式を整えてCIで検査する

2026.09.15

Laravel Pintを使ってPHPコードの書式を整える方法を紹介します。ローカルでの自動修正、CIで書式違反だけを検査する--test、対象範囲の指定、pint.jsonの設定、Bladeファイルを扱う際の注意点を整理し、Pintを導入する判断基準も説明します。

CI/CD Laravel PHP

PHPUnitでPHPのテストを始める:正常系・例外・モックの基本

2026.09.14

PHP 8.3とPHPUnit 12.5を使い、フレームワークやデータベースに依存しないPHPの例で、テストの考え方からComposerによる導入、基本的なアサーション、データプロバイダー、例外、モックまでを解説し、PHPUnitの基本を整理します。

PHP PHPUnit

阿部辰也へのお仕事の依頼・お問い合わせ

軽いご相談もお気軽にどうぞ!

個人情報の取り扱いについて *必須 プライバシーポリシーをご確認いただき、同意いただける場合は「同意する」にチェックをしてください。

keyboard_double_arrow_up
TOP