LuftGarden - 熊本のWebサイト・ホームページ制作スタジオ

BLOG技術ブログ

PHP + cURLを使ってAPI を叩く方法

はじめに

CakePHP で Webアプリケーションの開発を行なっている際、外部の API を叩く必要があったので知見をまとめておきます。

PHP + cURL 実装で注意すること

  • POSTパラメータを http_build_query()に通して application/x-www-form-urlencoded 扱いにする
  • 必要に応じて文字エンコーディングの変換を行う

cURL でPOSTするパラメータをhttp_build_query()に通さない場合、multipart/form-data で送信されるため文字化けの可能性が高くなります。また、Webアプリケーション側とAPI側で扱う文字エンコーディングが異なる場合、変換処理が必要になります。

基本形のソースコード

API から返却されるデータの形式はQueryString( 例: name=taro&age=15&country=jpn ) 、Webアプリケーション側の文字エンコーディングはUTF-8、API側の文字エンコーディングはShift-JISと仮定した場合の例です。

<?php
public function request() {
  $ch = curl_init();

  // 必要に応じてPOSTパラメータを設定
  $data = [
    'hoge' => 1,
    'fuga' => 2,
  ];

  mb_convert_variables( 'SJIS-win', 'UTF-8',  $data );

  $options = [
    CURLOPT_URL => 'https://example.com/api/path',
    CURLOPT_POST  => true,
    CURLOPT_TIMEOUT => 60,
    CURLOPT_RETURNTRANSFER  => true,
    CURLOPT_POSTFIELDS  => http_build_query( $data ),
  ];

  curl_setopt_array( $ch,  $options );

  $r = curl_exec( $ch );
  $r = mb_convert_encoding( $r, 'UTF-8', 'SJIS-win' );

  // QueryStringを配列に変換
  $response = [];
  parse_str( $r, $response );

  // parse_str() を使うと「+」が半角スペースに変わってしまうため、
  // 返却値に「+」が含まれるデータの場合は元に戻します。
  $response[ 'key' ] = str_replace( '', '+',  $response['key'] );

  curl_close( $ch );
  return $response;
}

API から返却されるデータ形式がjsonであれば、parse_str( $r, $response )$response = json_decode( curl_exec( $ch ), true );に変更すればOKです。

cURLの使い方やCURLOPT_XXX設定値の意味は公式リファレンスを参照してください。

PHP: curl_exec – Manual
PHP: curl_setopt – Manual

CakePHP でコンポーネント化して使う

API を叩くという独立的かつ明確な役割があるので、 CakePHP で使う場合はコンポーネント化したほうがよいでしょう。例えば、「API を使って何かしらのクーポン情報を取得したい」というようなケースでは、以下のコンポーネントを作成します。

class HogeAPIComponent extends Component {

  // configs
  private $__base_url = null;
  private $__id       = null;
  private $__pass     = null;

  // params
  private $__path     = null;
  private $__data     = null;
  private $__timeout  = null;
  private $__options  = null;


  /**
   * 初期化の際に API に関する設定情報を読み込む
   * (core.php などで定義しておく)
   *
   * @param  [Controller] $controller
   * @return [void]
   */
  public function initialize( Controller $controller ) {
    $this->Controller = $controller;

    $this->__base_url = Configure::read( 'HogeAPI.base_url' );
    $this->__id       = Configure::read( 'HogeAPI.id' );
    $this->__pass     = Configure::read( 'HogeAPI.pass' );

    if ( empty( $this->__base_url ) ||
         empty( $this->__id )       ||
         empty( $this->__pass )     || ) {
      throw new InternalErrorException( 'HogeAPIComponent failed to read configure.' );
    }
  }

  /**
   * クーポン情報を取得する
   * @param  [array] $params
   * @return [array] $response
   */
  public function searchCoupon( $params ) : array {
    $params['path']             = 'coupon/search/';
    $params['data']['api_id']   = $this->__id;
    $params['data']['api_pass'] = $this->__pass;

    $response = $this->request( $params );

    if ( 'APIのエラーコードをもとに判定' ) {
      // 弾くなりログを吐くなり何かしらの処理
    }

    return $response;
  }

  /**
   * API リクエスト送信
   * @param  [array] $params
   * @return [array] $response
   */
  public function request( array $params = [] ) : array {
    $this->__clear();
    $this->__bind( $params );

    $ch = curl_init();
    curl_setopt_array( $ch, $this->__options );

    $r = curl_exec( $ch );
    $r = mb_convert_encoding( $r, 'UTF-8', 'SJIS-win' );

    // QueryStringを配列に変換
    // parse_str() は 「+」 を半角スペースに変換するため注意
    $response = [];
    parse_str( $r, $response );

    curl_close( $ch );
    return $response;
  }

  /**
   * 設定中のパラメータを初期化
   * @return [void]
   */
  private function __clear() {
    $this->__path    = null;
    $this->__data    = null;
    $this->__timeout = null;
    $this->__options = null;
  }

  /**
   * パラメータバインド
   * @param  [array] $params
   * @return [void]
   */
  private function __bind( array $params ) {
    $this->path      = $params['path']    ?? null;
    $this->data      = $params['data']    ?? null;
    $this->timeout   = $params['timeout'] ?? 60;
    $this->__makeOptions();
  }

  /**
   * cURL オプション設定
   * 送信データは文字化け回避のため、http_build_query() を通して
   * Content-Type: application/x-www-form-urlencoded で送る。
   *
   * @return [void]
   */
  private function __makeOptions() {
    mb_convert_variables( 'SJIS-win', 'UTF-8', $this->__data );

    $this->__options = [
      CURLOPT_URL            => $this->__base_url . $this->__path,
      CURLOPT_POST           => true,
      CURLOPT_TIMEOUT        => $this->__timeout,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POSTFIELDS     => http_build_query( $this->__data ),
    ];
  }
}

設定情報は以下のようになっていると仮定します。

Configure::write( 'HogeAPI.base_url', 'https://example.com/hogeapi/' );
Configure::write( 'HogeAPI.id',       'APIアクセス用ID' );
Configure::write( 'HogeAPI.pass',     'APIアクセス用パスワード' );

最後に、このコンポーネントを使用してクーポンNo.100の情報を取得する例です。

class HogeController extends AppController {
  public $components = [
    'HogeAPI',
  ];

  public function search() {
    $coupon = $this->HogeAPI->searchCoupon( [
      'data' => [ 'coupon_id' => 100 ],
    ] );
  }
}

これで、 CakePHP からhttps://example.com/hogeapi/coupon/searchを cURL で叩くことができます。

上記の例では完全な固定値なので問題ありませんが、パラメータにユーザ入力値を含むようなケースでは必ずバリデーション処理を行い、不正なリクエストを弾くようにしましょう(意図しないパラメータを完全に除外するため、ホワイトリスト形式で弾くことをおすすめします)。

PAGETOP