自作Amazonアソシエイトのリンク生成ツール

自作Amazonアソシエイトのリンク生成ツール

前記事「AmazonアソシエイトPA-API経由の商品リンクを作る」で PA-API のおおよその仕組みがわかりましたので WordPress 用のリンク生成ツールを自作してみようと思います。

01スクラッチパッドで PHP のサンプルコードを取得する

PA-API スクラッチパッドは、ブラウザ上で Amazon アソシエイト PA-API にリクエストを送りますとその結果とサンプルコードを返してくれるツールです。とにかくやってみればどんなものかよくわかります。

利用には認証キーが必要です。実際の手順は前記事の「PA-API から商品データを取得する」に画像付きで書いています。

リクエストを送りますと、その結果と下の方に JAVA, PHP, curl のサンプルコードを返してくれます。PHP のコードは次のものです。もちろんシークレットキーを入れないと動きません。

<?php

/* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
/* Licensed under the Apache License, Version 2.0. */

// Put your Secret Key in place of **********
$serviceName="ProductAdvertisingAPI";
$region="us-west-2";
$accessKey="AKIAJHXO543CMYXSGX3Q";
$secretKey="**********";
$payload="{"
        ." \"ItemIds\": ["
        ."  \"B07XV8VSZT\""
        ." ],"
        ." \"Resources\": ["
        ."  \"Images.Primary.Large\""
        ." ],"
        ." \"PartnerTag\": \"imuza-22\","
        ." \"PartnerType\": \"Associates\","
        ." \"Marketplace\": \"www.amazon.co.jp\""
        ."}";
$host="webservices.amazon.co.jp";
$uriPath="/paapi5/getitems";
$awsv4 = new AwsV4 ($accessKey, $secretKey);
$awsv4->setRegionName($region);
$awsv4->setServiceName($serviceName);
$awsv4->setPath ($uriPath);
$awsv4->setPayload ($payload);
$awsv4->setRequestMethod ("POST");
$awsv4->addHeader ('content-encoding', 'amz-1.0');
$awsv4->addHeader ('content-type', 'application/json; charset=utf-8');
$awsv4->addHeader ('host', $host);
$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems');
$headers = $awsv4->getHeaders ();
$headerString = "";
foreach ( $headers as $key => $value ) {
    $headerString .= $key . ': ' . $value . "\r\n";
}
$params = array (
        'http' => array (
            'header' => $headerString,
            'method' => 'POST',
            'content' => $payload
        )
    );
$stream = stream_context_create ( $params );

$fp = @fopen ( 'https://'.$host.$uriPath, 'rb', false, $stream );

if (! $fp) {
    throw new Exception ( "Exception Occured" );
}
$response = @stream_get_contents ( $fp );
if ($response === false) {
    throw new Exception ( "Exception Occured" );
}
echo $response;

class AwsV4 {

    private $accessKey = null;
    private $secretKey = null;
    private $path = null;
    private $regionName = null;
    private $serviceName = null;
    private $httpMethodName = null;
    private $queryParametes = array ();
    private $awsHeaders = array ();
    private $payload = "";

    private $HMACAlgorithm = "AWS4-HMAC-SHA256";
    private $aws4Request = "aws4_request";
    private $strSignedHeader = null;
    private $xAmzDate = null;
    private $currentDate = null;

    public function __construct($accessKey, $secretKey) {
        $this->accessKey = $accessKey;
        $this->secretKey = $secretKey;
        $this->xAmzDate = $this->getTimeStamp ();
        $this->currentDate = $this->getDate ();
    }

    function setPath($path) {
        $this->path = $path;
    }

    function setServiceName($serviceName) {
        $this->serviceName = $serviceName;
    }

    function setRegionName($regionName) {
        $this->regionName = $regionName;
    }

    function setPayload($payload) {
        $this->payload = $payload;
    }

    function setRequestMethod($method) {
        $this->httpMethodName = $method;
    }

    function addHeader($headerName, $headerValue) {
        $this->awsHeaders [$headerName] = $headerValue;
    }

    private function prepareCanonicalRequest() {
        $canonicalURL = "";
        $canonicalURL .= $this->httpMethodName . "\n";
        $canonicalURL .= $this->path . "\n" . "\n";
        $signedHeaders = '';
        foreach ( $this->awsHeaders as $key => $value ) {
            $signedHeaders .= $key . ";";
            $canonicalURL .= $key . ":" . $value . "\n";
        }
        $canonicalURL .= "\n";
        $this->strSignedHeader = substr ( $signedHeaders, 0, - 1 );
        $canonicalURL .= $this->strSignedHeader . "\n";
        $canonicalURL .= $this->generateHex ( $this->payload );
        return $canonicalURL;
    }

    private function prepareStringToSign($canonicalURL) {
        $stringToSign = '';
        $stringToSign .= $this->HMACAlgorithm . "\n";
        $stringToSign .= $this->xAmzDate . "\n";
        $stringToSign .= $this->currentDate . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "\n";
        $stringToSign .= $this->generateHex ( $canonicalURL );
        return $stringToSign;
    }

    private function calculateSignature($stringToSign) {
        $signatureKey = $this->getSignatureKey ( $this->secretKey, $this->currentDate, $this->regionName, $this->serviceName );
        $signature = hash_hmac ( "sha256", $stringToSign, $signatureKey, true );
        $strHexSignature = strtolower ( bin2hex ( $signature ) );
        return $strHexSignature;
    }

    public function getHeaders() {
        $this->awsHeaders ['x-amz-date'] = $this->xAmzDate;
        ksort ( $this->awsHeaders );

        // Step 1: CREATE A CANONICAL REQUEST
        $canonicalURL = $this->prepareCanonicalRequest ();

        // Step 2: CREATE THE STRING TO SIGN
        $stringToSign = $this->prepareStringToSign ( $canonicalURL );

        // Step 3: CALCULATE THE SIGNATURE
        $signature = $this->calculateSignature ( $stringToSign );

        // Step 4: CALCULATE AUTHORIZATION HEADER
        if ($signature) {
            $this->awsHeaders ['Authorization'] = $this->buildAuthorizationString ( $signature );
            return $this->awsHeaders;
        }
    }

    private function buildAuthorizationString($strSignature) {
        return $this->HMACAlgorithm . " " . "Credential=" . $this->accessKey . "/" . $this->getDate () . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "," . "SignedHeaders=" . $this->strSignedHeader . "," . "Signature=" . $strSignature;
    }

    private function generateHex($data) {
        return strtolower ( bin2hex ( hash ( "sha256", $data, true ) ) );
    }

    private function getSignatureKey($key, $date, $regionName, $serviceName) {
        $kSecret = "AWS4" . $key;
        $kDate = hash_hmac ( "sha256", $date, $kSecret, true );
        $kRegion = hash_hmac ( "sha256", $regionName, $kDate, true );
        $kService = hash_hmac ( "sha256", $serviceName, $kRegion, true );
        $kSigning = hash_hmac ( "sha256", $this->aws4Request, $kService, true );

        return $kSigning;
    }

    private function getTimeStamp() {
        return gmdate ( "Ymd\THis\Z" );
    }

    private function getDate() {
        return gmdate ( "Ymd" );
    }
}
?>

やっていることは、アクセスキー、シークレットキーを設定し、$payload に商品ID(ItemIds)、必要データ(Resources)、トラッキングID(PartnerTag) をセットし、PA-API に接続するための認証用クラス AwsV4 を使ってリクエスト用のヘッダーを作成してホストにリクエストしています。

ですので、このコードにアクセスキー、シークレットキー、トラッキングIDを設定してコマンドラインから実行すれば結果が JSON データで返ってきます。

02自作ツールの構想

最終的にどうするかは別にして、まずは functions.php に組み込むために次の構想でやってみようと思います。

  1. 記事からはショートコードで呼び出す
  2. 商品ID は引数で渡す
    (例) [amazon B0CQ7GXR6N]
  3. functions.php にサンプルコードの AwsV4 クラスを分離して書く
  4. functions.php に関数 add_shortcode を使ってリクエスト用のコールバック関数を書く
  5. 戻ってきた JSON データから HTML を生成して出力する

これでやってみましょう。

03自作Amazonアソシエイトのリンク生成ツール

まず、上のサンプルコードの class AwsV4{ から末尾の } までを抜き出してそのままfunctions.php に書き込みます。

次に、次のショートコードのコールバック関数を functions.php に書き込みます。まずは HTML 整形前にどんなデータが返ってくるか見てみましょう。

アクセスキー、シークレットキー、トラッキングID は自分のものを入れてください。

function imz_amazon_associates($atts){
    $data = array();

    $serviceName="ProductAdvertisingAPI";
    $region="us-west-2";
    $accessKey="アクセスキー";
    $secretKey="シークレットキー";
    $payload="{"
        ." \"ItemIds\": ["
        ."  \"$atts[0]\""
        ." ],"
        ." \"Resources\": ["
        ."  \"Images.Primary.Large\","
        ."  \"ItemInfo.ByLineInfo\","
        ."  \"ItemInfo.Classifications\","
        ."  \"ItemInfo.ContentInfo\","
        ."  \"ItemInfo.Title\""
        ." ],"
        ." \"PartnerTag\": \"トラッキングID\","
        ." \"PartnerType\": \"Associates\","
        ." \"Marketplace\": \"www.amazon.co.jp\""
        ."}";
    $host="webservices.amazon.co.jp";
    $uriPath="/paapi5/getitems";
    $awsv4 = new AwsV4 ($accessKey, $secretKey);
    $awsv4->setRegionName($region);
    $awsv4->setServiceName($serviceName);
    $awsv4->setPath ($uriPath);
    $awsv4->setPayload ($payload);
    $awsv4->setRequestMethod ("POST");
    $awsv4->addHeader ('content-encoding', 'amz-1.0');
    $awsv4->addHeader ('content-type', 'application/json; charset=utf-8');
    $awsv4->addHeader ('host', $host);
    $awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems');
    $headers = $awsv4->getHeaders ();
    $headerString = "";
    foreach ( $headers as $key => $value ) {
        $headerString .= $key . ': ' . $value . "\r\n";
    }
    $params = array (
        'http' => array (
        'header' => $headerString,
        'method' => 'POST',
        'content' => $payload
        )
    );
    $stream = stream_context_create ( $params );
        
    $fp = @fopen ( 'https://'.$host.$uriPath, 'rb', false, $stream );
        
    if (! $fp) {
        throw new Exception ( "Exception Occured" );
    }
    $response = @stream_get_contents ( $fp );
    if ($response === false) {
        throw new Exception ( "Exception Occured" );
    }

    $object = json_decode( $response );
    var_dump($object);
}
add_shortcode('amazon', 'imz_amazon_associates');

これをショートコード[amazon B0CQ7GXR6N]で呼んでみます。下の商品の ASIN です。

だらだらとした文字列が返ってきますが、ブラウザでソースを見れば階層化されたものが表示されます。

object(stdClass)#1097 (1) {
  ["ItemsResult"]=>
  object(stdClass)#1096 (1) {
    ["Items"]=>
    array(1) {
      [0]=>
      object(stdClass)#1078 (4) {
        ["ASIN"]=>
        string(10) "B0CQ7GXR6N"
        ["DetailPageURL"]=>
        string(75) "https://www.amazon.co.jp/dp/B0CQ7GXR6N?tag=imuza-22&linkCode=ogi&th=1&psc=1"
        ["Images"]=>
        object(stdClass)#1082 (1) {
          ["Primary"]=>
          object(stdClass)#1081 (1) {
            ["Large"]=>
            object(stdClass)#1080 (3) {
              ["Height"]=>
              int(500)
              ["URL"]=>
              string(59) "https://m.media-amazon.com/images/I/411Xe1qEvYL._SL500_.jpg"
              ["Width"]=>
              int(338)
            }
          }
        }
        ["ItemInfo"]=>
        object(stdClass)#1086 (4) {
          ["ByLineInfo"]=>
          object(stdClass)#1084 (2) {
            ["Contributors"]=>
            array(1) {
              [0]=>
              object(stdClass)#1083 (4) {
                ["Locale"]=>
                string(5) "ja_JP"
                ["Name"]=>
                string(12) "九段理江"
                ["Role"]=>
                string(3) "著"
                ["RoleType"]=>
                string(6) "author"
              }
            }
            ["Manufacturer"]=>
            object(stdClass)#1085 (3) {
              ["DisplayValue"]=>
              string(9) "新潮社"
              ["Label"]=>
              string(12) "Manufacturer"
              ["Locale"]=>
              string(5) "ja_JP"
            }
          }
          ["Classifications"]=>
          object(stdClass)#1088 (2) {
            ["Binding"]=>
            object(stdClass)#1087 (3) {
              ["DisplayValue"]=>
              string(9) "Kindle版"
              ["Label"]=>
              string(7) "Binding"
              ["Locale"]=>
              string(5) "ja_JP"
            }
            ["ProductGroup"]=>
            object(stdClass)#1089 (3) {
              ["DisplayValue"]=>
              string(21) "Digital Ebook Purchas"
              ["Label"]=>
              string(12) "ProductGroup"
              ["Locale"]=>
              string(5) "ja_JP"
            }
          }
          ["ContentInfo"]=>
          object(stdClass)#1092 (3) {
            ["Languages"]=>
            object(stdClass)#1091 (3) {
              ["DisplayValues"]=>
              array(1) {
                [0]=>
                object(stdClass)#1090 (2) {
                  ["DisplayValue"]=>
                  string(9) "日本語"
                  ["Type"]=>
                  string(6) "発行"
                }
              }
              ["Label"]=>
              string(8) "Language"
              ["Locale"]=>
              string(5) "ja_JP"
            }
            ["PagesCount"]=>
            object(stdClass)#1093 (3) {
              ["DisplayValue"]=>
              int(123)
              ["Label"]=>
              string(13) "NumberOfPages"
              ["Locale"]=>
              string(5) "en_US"
            }
            ["PublicationDate"]=>
            object(stdClass)#1094 (3) {
              ["DisplayValue"]=>
              string(24) "2024-01-17T00:00:00.000Z"
              ["Label"]=>
              string(15) "PublicationDate"
              ["Locale"]=>
              string(5) "en_US"
            }
          }
          ["Title"]=>
          object(stdClass)#1095 (3) {
            ["DisplayValue"]=>
            string(18) "東京都同情塔"
            ["Label"]=>
            string(5) "Title"
            ["Locale"]=>
            string(5) "ja_JP"
          }
        }
      }
    }
  }
}

HTML 整形のための値を取り出すのが面倒くさそうですね。とりあえずタイトルと画像を表示してみます。

    $object = json_decode( $response );

    $items    = $object->ItemsResult->Items;
    foreach ( $items as $item ) {
        echo '<p>' . $item->ItemInfo->Title->DisplayValue . '</p>';
        echo '<img src="' . $item->Images->Primary->Large->URL . '" width=100>';
    }

コールバック関数の $object 以降を書き換えますと、

確かにタイトルと画像が表示されます。これでなんとか自作ツールができそうです。

とりあえず今日はここまです。