Amazon SP-API(セリングパートナーAPI)をPHP ノンフレームワークで実装してみたので使い方を説明します

Amazon「SP-API」は利用開始まで非常に難易度が高く、参考になるドキュメント自体が難解で使い方がわからず非常に困ってたので、何かの役に立てばなと思い投稿しました。

##SP-APIって？
Amazonは、これまで出品用時に利用できるAPIとしてMWS-APIを提供していましたが廃止がきまり
後継として「セリングパートナーAPI(SP-API)」が登場しました、MWS-APIから移行？（作り変えかな、、、）する必要があります。

https://developer.amazonservices.jp/

ただ、大きな問題があります。
公式からドキュメントは提供されているのですが、GitHubをそのまま見てね状態です。

https://github.com/amzn/selling-partner-api-docs

英語直訳の日本語、サンプルソースもほぼ皆無。

2022年6月25日追記
少しだけマシな公式のページが公開されました。英語なので、翻訳するしかないですが、少しばかり、サンプルソースも載っています。
https://developer-docs.amazon.com/sp-api

C#とかのサンプルは見つけたけど、こっちはPHP使ってるし。。。。
素人にはもちろんのこと、AmazonのAPI関連扱ったことがない人は、ハードル高すぎます！

そこで今回、必要にかられてAPIの移行が必要となったため「SP-APIを使った商品データ取得」の概要を説明します。
（概要なので、ソースはピンポイントで載せますね）

##環境
通常のLAMP環境です。
PHP 7.4以上が動いているであれば問題ないと思います。
フレームワークは使っていませんので、ソースを修正すれば単体で使えます

##ポイント
大きな壁が2箇所あります。
（API利用のために開発者として登録する必要ありますがここは省略します）
###1.AWS認証
これは、探せば日本語で画像つきのサンプルがよくみられますので、AWS経験者はわかると思います。
簡単に説明すると、API専用にIAMユーザーを作成して、適切に権限を与えてキーとかトークンとか必要な情報準備してねーってことです。

###2.署名バージョン4
これが非常に難解です。
APIを利用するためには必須になるのですが、ドキュメント読んだだけでは全然わからない！
SP-APIは関係ないのですが、AWSの署名として作成されているところを見つけたので参考にさせていただきました
https://memo.abridge-lab.com/?p=173

##ご説明
(このままじゃ動かないので、参考程度にみてください)

###1.Login with Amazonアクセストークンをリクエスト
いきなりポイントですね。AWS経由でアクセストークンをもらいます。

```php
$header = [
    'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
    'Host: api.amazon.com',
    'POST /auth/o2/token HTTP/l.l'
];
$url   = "https://api.amazon.com/auth/o2/token";
$param = array(
    'grant_type' => 'refresh_token',
    'refresh_token' => リフレッシュトークンを記入,
    'client_id'=> クライアントIDを記入,
    'client_secret'=> シークレットを記入
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_CUSTOMREQUEST,  'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER,     $header);
curl_setopt($ch, CURLOPT_URL,            $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST,           true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_POSTFIELDS,     http_build_query($param));

$response = curl_exec($ch);
curl_close($ch);

$json_response =  json_decode($response);
//アクセストークン
$access_token = $json_response->access_token;
```

###2.リクエストするURIを作成
署名に必要なので先に作っておきます。
今回利用するのは商品情報取得用のAPI
https://github.com/amzn/selling-partner-api-docs/blob/main/references/catalog-items-api/catalogItemsV0.md
**GET /catalog/v0/items/{asin}
Operation: getCatalogItem**
って書いている辺りです。
もちろん対象は日本のAmazonマーケットプレイスです

```php
$asin = "B079QRQTCR"; //Fire TV Stickです
$request = "/catalog/v0/items/".$asin;
$queryString = "Asins=".$asin."&MarketplaceId=A1VC38T7YXB528";
$request_url = "https://sellingpartnerapi-fe.amazon.com" .$request ."?" . $queryString;
```

###3.署名作成の流れ
最難関の署名作成です。
SP-APIを利用するには必須の署名バージョン4ですが、初見だとさっぱりわからないので、先に何をやるか説明します。
####1.正規リクエスト作成
「今からこのURLで、このヘッダ情報つけて、このパラメーターでアクセスしますねー」っていう正規化されたURLをHashで変換した文字列をつくります。
####2.署名を計算
呪文ですね。
日時だったり、接続先のリージョン。どんなサービスを利用するかとかを文字列にして、Hashで変換します。
####3.署名作成に必要となる文字列作成
これも呪文ですね。
2と同様に、サービスや日時、あと1や2で事前に作成したhash文字列を利用します。
####4.署名作成
ここでようやく署名作成
1、3で作ったhash文字列と、またまた同じ日時、サービスなどを使って署名文字列を作成します。

署名作成の流れとしては以上です。
今からアクセスするURLを宣言して、Hash化
別でHash文字を作って
出来上がった文字列を元に、別の文字列作って、更にHash化して、
それをつかって署名文字を作る感じですね。（もう、訳わんないです）

正式のドキュメントでは、同じ様に手順が紹介されているのですが、2と3が逆の順番で記載されています。
3の作成には「2の署名計算」が必要なのに、
順番が逆に書いてるから、なにも知らない人は混乱してしまうのです。

###4.署名作成実装
では以下参考ソースです。手順に合わせて記載します。（絶対このままでは動かないと思うので参考程度にどうぞ）
####1.正規リクエスト作成

```php
$gmtDate = gmdate("Ymd\THis\Z");//リクエストごとの先頭で予め取得しとくといいです。
/**
  * 署名バージョン 4 の正規リクエストを作成
  */
function CanonicalRequest($canonicalRequest) {
        $str = $canonicalRequest['method'] . "\n" . $canonicalRequest['uri'] ."\n". $canonicalRequest['query'] . "\n";
        if (count($canonicalRequest['headers']) > 0) {
            foreach ($canonicalRequest['headers'] as $item) {
                $str .= $item . "\n";
            }
        } else { 
            $str .= "\n";
        }
        $str .= "\n";
        $str .=$canonicalRequest['signedheaders'];
        $str .= "\n";
        $str .=$canonicalRequest['payload'];

        return hash('sha256', $str, true);
    }

$headers = [
        'host:sellingpartnerapi-fe.amazon.com',
        'user-agent:System Tool/2.0 (Language=PHP/7.4;Platform=Windows/10)',
        'x-amz-access-token:'.事前取得のAWSアクセストークン,
        'x-amz-date:' . $gmtDate
];

$canonicalRequest = [
        'method' => 'GET',
        'uri' => $request_url, //署名作成のまえに作ったリクエストURL
        'query' => $queryString, //署名作成のまえに作ったリクエストパラメーター
        'headers' => $headers,
        'signedheaders' => 'host;user-agent;x-amz-access-token;x-amz-date',
        'payload' => hash('sha256',''),
    ];
//リクエスト生成
$canonical_request = CanonicalRequest($canonicalRequest);

```

####2.署名を計算

```php
/**
  * AWS 署名バージョン 4 の署名を計算する
  */
function signatureKey($key, $dateStamp, $regionName, $serviceName,$termination) {
        $kDate = hash_hmac('sha256', $dateStamp, 'AWS4'.$key, true);
        $kRegion = hash_hmac('sha256', $regionName, $kDate, true);
        $kService = hash_hmac('sha256', $serviceName, $kRegion, true);
        $kSigning = hash_hmac('sha256', $termination, $kService, true);

        return $kSigning;
    }
//署名を計算
$signing_key = signatureKey(開発者用シークレットアクセスキー, $gmtDate, "us-west-2", "execute-api", "aws4_request");
```

####3.署名作成に必要となる文字列作成

```php
/**
  * 署名バージョン 4 の署名文字列を作成
  */
function signature($param, $canonical_request,$algorithm, $signing_key, $aws_regions, $service,$termination) {
        $credential_scope = $param['datestamp'] . "/". $aws_regions . "/". $service . "/". $termination;

        $string_to_sign = $algorithm . "\n"
                        . $param['amzdate'] . "\n"
                        . $credential_scope . "\n"
                        . bin2hex($canonical_request);

        return hash_hmac('sha256', $string_to_sign, $signing_key, true);
    }
//署名文字列を作成
//$canonical_requestは1で取得したもの
//$signing_keyは2で取得したもの
$algorithm = 'AWS4-HMAC-SHA256';
$param['amzdate'] = $gmtDate;
$param['datestamp'] = substr($gmtDate, 0, 8);
$signature = signature($param, $canonical_request, $algorithm, $signing_key, "us-west-2", "execute-api", "aws4_request");
```

####4.署名作成

```php
/**
  * 署名情報をリクエストに追加する
  */
function createAuthorization($param, $canonicalRequest, $signature, $algorithm, $access_key_id, $aws_regions, $service, $termination) {
        $credential_scope = $param['datestamp'] . "/". $aws_regions . "/". $service . "/". $termination;
        $credential = $access_key_id . "/". $credential_scope;

        $signedHeaders = $canonicalRequest['signedheaders'];

        $auth = $algorithm . ' '. 'Credential=' . $credential . ', ' . 'SignedHeaders=' . $signedHeaders . ', ' . 'Signature=' . bin2hex($signature);

        return $auth;
    }

//署名情報作成
//$algorithm、$param 3で作ったやつと同じでOK
//$canonical_requestは1で取得したもの
//$signatureは3で取得したもの

$auth = createAuthorization($param, $canonicalRequest, $signature, $algorithm,開発者用アクセスキーID , "us-west-2", "execute-api", "aws4_request");

$AuthorizationHeader = 'Authorization: ' . $auth; //これがバージョン4の署名で必要になる文字列
```

↓一応「署名バージョン 4」の正式ドキュメントです↓
https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4_signing.html

###5.APIリクエスト
ようやくリクエストです。
ここはcurlつかった一般的な呼び出しで大丈夫です！

```php
/**
 * 商品データを取得してみる
 */

//[注意]このヘッダは、「署名部分」以外 上で作ったヘッダと同じにしてないとエラーになるので注意してください！
$request_header = [
    $AuthorizationHeader, //これが苦労して作った署名です。ヘッダ情報にいれるんですね！
    'host:sellingpartnerapi-fe.amazon.com',
    'user-agent:System Tool/2.0 (Language=PHP/7.4;Platform=Windows/10)',
    'x-amz-access-token:'.事前取得のAWSアクセストークン,
    'x-amz-date:' . $gmtDate
];
// 必要に応じてオプションを追加。
$ch = curl_init();
curl_setopt($ch, CURLOPT_CUSTOMREQUEST,  'GET');
curl_setopt($ch, CURLOPT_HTTPHEADER,     $request_header);
curl_setopt($ch, CURLOPT_URL,            $request_url); //最初の方に作ったURL
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST,           false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HEADER, true);

$response = curl_exec($ch);
curl_close($ch);

$json_response =  json_decode($response);
```

ソースの説明は以上です。

整理したら、1つのPHPファイルで、全部完了できるはずなので、お試しください。
エラーになるとすれば、署名部分だと思います。
正式ドキュメントと私のソースを参考にしてもらえれば通ると思いますので
お困りの方、頑張ってください！

これじゃだめじゃない！？みたいなのあればコメントいただければチェックしますのでよろしくです。