PHPでShopifyアプリを作成する(1回目)
中学生の時に趣味でZ80マシン語やFortran等を始めてから、現在まで数多くのプログラミング言語を経験。ShopifyによるECサイト構築では主にカスタマイズを担当。
はじめに
本記事は、ローカル開発環境を使用せず(Node.js・Shopify CLI・ngrok等無し)に、「手軽にカスタムアプリを作る方法」のご紹介になります。
例として、ショップから商品データを取得するアプリを作成してみます。
ここでは、国内の大抵のレンタルサーバー(ホスティングサービス)で使用できるPHPでの作成例をご紹介します。
従って本記事でのチュートリアルではPHPが動作するWEBサーバーが必要になります。
また、Shopifyカスタムアプリの認証で使用するOAuth2.0の認可サーバーは、URIが偽装されていないことをSSL証明書で確認しているのでhttpsは必須です。
Shopifyのアプリの種類
Shopifyで使用できるアプリは2022年現在、以下の3種類があります。
アプリ登録 | Shopifyパートナーダッシュボード | ストアオーナー(マーチャント)のストア管理画面 | |
---|---|---|---|
種類 | 公開アプリ | カスタムアプリ | カスタムアプリ |
対象 | 世界中のストア | 単一ストア | 単一ストア |
認証 | OAuth2.0 | OAuth2.0 | 管理画面からアクセストークンを取得できる |
管理画面への埋込み | ○ | ○ | × |
Shopifyパートナーダッシュボードから作成できるのは「公開アプリ」と「カスタムアプリ」の2種類があり、アプリで出来ることは同じです。「カスタムアプリ」は単一のストアをカスタマイズするために使用します。
ストア管理画面からも「カスタムアプリ」の作成ができるのですが、Shopify埋め込みアプリの作成は出来ません。しかしもしAPIを叩くだけの場合はこれでも十分でして、管理画面上でアクセストークンが取得できます。
実はこの後に紹介する「カスタムアプリ」はストア管理画面から作るカスタムアプリでも十分なのですが、公開アプリの作成方法のことも知ってもらいたく、この記事ではShopifyパートナーダッシュボードからカスタムアプリを作成する方法をご紹介します。
Shopifyパートナーダッシュボードから開発ストアを作成する
Shopifyパートナーアカウントを持っていない場合は、無料ですのでぜひともアカウントを取得してください。開発ストア作成やアプリのテスト等ができるようになります。
Shopifyパートナーダッシュボードにログインし、「ストア管理」から「ストア追加」ボタンをクリックしてください。あとは「開発ストア」を選択して「ストア名」「ストアURL」「パスワード」等を設定して「保存」をクリックすれば開発ストアが出来ます。
開発ストアで商品情報を登録する
Shopifyパートナーダッシュボードの「ストア管理」から、先ほど作成した開発ストアにログインすると、別タブで開発ストアのShopify管理画面に入れます。
商品情報・顧客情報・コレクション・注文情報等のサンプルデータの準備ですが、Shopifyアプリ「Simple Sample Data」を使用するのが手軽です。公開ストアで使用するには0.99ドルかかりますが、開発ストアでは無料で使用できます。サンプルデータの一括追加も一括削除もこのアプリでできるので便利です。
開発ストアでカスタムアプリの開発を許可する
開発ストアでカスタムアプリのインストールを許可しておきます。
開発ストア管理画面の「アプリ」>「アプリと販売チャンネルの設定」をクリックし、右上の「アプリを開発」あるいは「ストア用のカスタムアプリを管理する」をクリックします。
「カスタムアプリ開発を許可」をクリックします。
Shopifyパートナーダッシュボードからカスタムアプリを登録する
Shopifyパートナーダッシュボードにログインし、「アプリ管理」から「アプリを作成する」をクリックします。
左の「Shopify CLIを使用する」は、あらかじめNode.jsインストールとngrok(エングロック)登録・インストールが必要ですが、Reactベース(Polaris・App Brigeモジュール入り)の雛形が簡単に作成でき、バックエンドのテンプレートもNode.js・Laravel・Ruby on Railsから選択でき、そして開発ストアへのアプリインストールまであっという間にできます。
今回は「Shopify CLI 3」を使用しないので、右側の「アプリを手動で作成する」をクリックしてアプリ名を入力して「作成」ボタンをクリックしてください。
アプリ名は後から変えられますので、ここでは仮に「商品情報取得」とでも入力しておいてください。
Shopifyのアプリ認証用のコードを作成する
APIキーやスコープ等をOAuth認証サーバーへGET送信するコードを作成します。とりあえずここでは「install.php」という名前にしています。
<?php $api_key = 'クライアントIDをここにセットします'; $shop_domein = $_GET['shop']; $scopes = "write_content,read_product_listings,read_customers,read_orders,write_products,write_inventory,read_themes"; $redirect_uri = "認証サーバーからのリダイレクト先のURLをここにセットします"; $install_url = "https://" . $shop_domein . "/admin/oauth/authorize?client_id=" . $api_key . "&scope=" . $scopes . "&redirect_uri=" . urlencode($redirect_uri); //OAuth2.0認証サーバーにリダイレクトする header("Location: " . $install_url); die();
変数$api_keyについてですが、Shopifyパートナーダッシュボードの「アプリ管理」から先ほど作成したアプリ「商品情報取得」をクリックし、「クライアントの資格情報」の「クライアントID」をコピーして貼り付けてください。
※その下にあるクライアントシークレット(秘密鍵)は後ほどアクセストークン生成用に使用します。この時点でコピーしてメモ帳とかに貼り付けておいても良いかもしれません。
変数$scopesについてですが、ここでアプリのAPIへのアクセス権限を設定します。ShopifyのAPIアプセススコープは大量にあるのでここでは紹介しきれませんが、書き込み権限を付与した場合は自動的に読み込み権限も付与されます。
(今回は商品情報取得なのでread_productsだけで十分ですが、テストなので権限を沢山付与してしまいました)
変数$redirect_uriについてですが、ここはリダイレクト先のURLが決定してから設定します。
Shopify Admin APIアクセス用のアクセストークンを取得する
今度はOAuth認証サーバーのリダイレクト先のコードを作成します。とりあえずここでは「getproductdata.php」という名前にしています。
※cURLを使用しているので、PHP拡張モジュール「php-curl」が有効になっていない場合はphp.iniを編集して有効にし、そもそもインストールされていない場合はパッケージをインストールしておいてください。
<?php $api_key = 'クライアントIDをここにセットします'; $secret_key = 'クライアントシークレット(秘密鍵)をここにセットします'; $params = $_GET; $hmac = $_GET['hmac']; // HMACを取得 $shop_domein = $params['shop']; preg_match('/^.*?(?=\.)/', $shop_domein, $s); $shop = $s[0]; $params = array_diff_key($params, array('hmac' => '')); ksort($params); $computed_hmac = hash_hmac('sha256', http_build_query($params), $secret_key); if (hash_equals($hmac, $computed_hmac)) { $query = array( "client_id" => $api_key, "client_secret" => $secret_key, "code" => $params['code'] ); // アクセストークン生成URL $access_token_url = "https://" . $params['shop'] . "/admin/oauth/access_token"; $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, $access_token_url); curl_setopt($ch, CURLOPT_POST, count($query)); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($query)); $result = curl_exec($ch); curl_close($ch); $result = json_decode($result, true); $token = $result['access_token']; } else { die('This request is NOT from Shopify!'); } // アクセストークン等を表示 (アクセストークンは運用時は絶対に表示させないでください) echo $shop_domein . '<br>'; echo $shop . '<br>'; echo $token . '<br>';
$api_keyと$secret_keyには、先程のShopifyパートナーダッシュボード「アプリ管理」の「クライアントの資格情報」の「クライアントID」と「クライアントシークレット」をそれぞれセットしてください。
この「getproductdata.php」をサーバーにプットし、今度は「install.php」の$redirect_uriに「getproductdata.php」のURLをセットしてこれもサーバーにプットしてください。
そして、今度はShopifyパートナーダッシュボード「アプリ管理」の「アプリ設定」をクリックします。
「アプリURL」にはinstall.phpのURLを設定し、「リダイレクトURLの許可」にはgetproductdata.phpのURLを設定して、右上の「保存する」ボタンをクリックしてください。
Shopifyパートナーダッシュボード「アプリ管理」の「概要」に戻って、「アプリをテストする」の「ストアを選択する」ボタンをクリックして最初に作った開発スアを選択し、右上の「アプリをインストール」ボタンをクリックします。
この画面が表示されずにエラーが出る場合は、install.phpとgetproductdata.phpの$api_keyと$secret_keyに間違いはないか、install.phpの$redirect_uriに間違いはないかを確認してください。
ブラウザで以下のような表示が出れば、カスタムアプリのインストールは成功です!
一番下に表示されているのがShopify Admin APIのアクセストークンになります。一度作成されたアクセストークンは永続的に使用できストアにアクセスできてしまうので、秘密にしておいてください。
今度は開発ストアの管理画面を確認してみてください。アプリ「商品取得情報」がインストールされていると思いますので、クリックしてみてください。
ここで気づかれたかと思いますが、「getproductdata.php」での表示がここで表示されていると思います。実際にはiframeで埋め込まれています。
つまり、アプリのフロントエンドを開発すると、その出力がここに表示されることになります。
アプリのインストールおよびアクセストークンが取得が確認できたところで、「getproductdata.php」の以下表示部分をコメントアウトするか削除するかしておいてください。
echo $shop_domein . '<br>'; echo $shop . '<br>'; echo $token . '<br>';
Shopify Admin APIにアクセスする関数を作成する
Shopifyアプリを作成し始めた当初は情報が少なく、情報源はGithubのサンプルコードを解読するくらいしか有りませんでした。しかしこれはREST Admin API用だったので、それをGraphQL用に改造したものが以下のコードになります。
「functions.php」として保存しておいてください。
<?php //GraphQL Admin API function shopify_gql_call($token, $shop, $query) { $url = "https://" . $shop . ".myshopify.com" . "/admin/api/2022-10/graphql.json"; $curl = curl_init($url); curl_setopt($curl, CURLOPT_HEADER, TRUE); curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, TRUE); curl_setopt($curl, CURLOPT_MAXREDIRS, 3); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); $request_headers[] = ""; $request_headers[] = "Content-Type: application/graphql"; if (!is_null($token)) $request_headers[] = "X-Shopify-Access-Token: " . $token; curl_setopt($curl, CURLOPT_HTTPHEADER, $request_headers); curl_setopt($curl, CURLOPT_POSTFIELDS, $query); curl_setopt($curl, CURLOPT_POST, true); $response = curl_exec($curl); $error_number = curl_errno($curl); $error_message = curl_error($curl); curl_close($curl); if ($error_number) { return $error_message; } else { $response = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2); $headers = array(); $header_data = explode("\n",$response[0]); $headers['status'] = $header_data[0]; array_shift($header_data); foreach($header_data as $part) { $h = preg_split('/:/', $part, 2); $headers[trim($h[0])] = trim($h[1]); } return array('headers' => $headers, 'response' => $response[1]); } }
幸いにも現在はShopify公式のPHP用APIライブラリがあるのでそちらを試しても良いのかもしれませんが、上記コードでも十分実用に耐えるのでここまま使い続けてます。
shopify_gql_call([アクセストークン], [ショップ名], [GraphQLクエリ])
と3つの変数をセットすればレスポンスを返します。
RESTに対してGraphQLのメリットは、エンドポイントが一つしか無い上に最小限のデータのみを取得できるので、レート制限に引っかかりにくいということでしょう。
デメリットは、やはり「GraphQLを覚えなければならない」というところです。しかしStorefront APIの方はGraphQLでしかアクセスできないのでぜひ覚えるべきです。
学習方法はShopify公式アプリ「Shopify GraphiQL app(グラフィクル)」をインストールしてひたすらいじるのがGraphQLを覚える近道でしょう。
Bulk Operationsで大量のデータを取得する
Shopifyで大量データ、例えば数万件の注文データとかの取得をするにはGraphQL Admin APIのBulk Operations(バルクオペレーション)を使用します。
GraphQLには1回のリクエストが1,000ポイント(Shopify Plusの場合は2,000ポイント)を超えてはならないというレート制限があるのですが、Bulk Operationsを使えばレート制限を気にする必要がなく大量データを一括処理するのに向いており、放置していても Shopify 側で完了するまでひたすら処理し続けてくれます。
補足しますがREST Admin APIの方にはBulk Operationsが用意されていません。
まずは商品データ取得用のGraphQLクエリを準備します。「Shopify GraphiQL App」を立ち上げます。
右側のドキュメントを参考にしながら左側でクエリを作成し、クエリ実行ボタンを押すと、JSON形式のレスポンスが中央ペインに表示されます。
今回は以下のようなクエリを準備しました。
query { products(first:5, query: "status:active") { edges { node { id handle title description vendor featuredImage { url } totalVariants variants(first:10) { edges { node { price compareAtPrice } } } } } pageInfo { hasNextPage endCursor } } }
※冒頭の「query」は省略可能
上記クエリをバルクオペレーションのクエリに変換するには、bulkOperationRunQueryミューテーションを使用します。書き方は以下のとおりです。
mutation { bulkOperationRunQuery ( query: """ 「ここにGraphQLクエリを書きます」 """ ) { bulkOperation { id status } userErrors { field message } } }
三重引用符(“””)の間にクエリを書きます。GraphQLでは三重引用符を使用すると複数行の文字列を書くことが出来ます。
クエリを組み込んだコード例は以下のとおりです。getproductdata.phpに追加してください。
※first:5やpageInfo等はバルクオペレーションでは無視されるのでこのまま残しておいても構わないのですが、余計な文字列なのでこの例では削除しています。
//~~~~~ (省略) ~~~~~~~ require_once(dirname(__FILE__) . '/functions.php'); $query = <<<EOF mutation { bulkOperationRunQuery ( query: """ query { products(query: "status:active") { edges { node { id handle title description vendor featuredImage { url } totalVariants variants { edges { node { price compareAtPrice } } } } } } } """ ) { bulkOperation { id status } userErrors { field message } } } EOF; //bulk Operationのミューテーションが成功してステータスがCREATEDになるまでリトライする $c = 0; do{ $res = shopify_gql_call($token, $shop, $query); $c++; if($c >= 3){ die('Bulk Operationのミューテーションに失敗しました'); } sleep(1); } while(is_null( json_decode($res['response'], true)['data']['bulkOperationRunQuery']['bulkOperation'] ) && json_decode($res['response'], true)['data']['bulkOperationRunQuery']['bulkOperation']['status'] != 'CREATED' ); //バルクオペレーションIDを取得する $bulk_id = json_decode($res['response'], true)['data']['bulkOperationRunQuery']['bulkOperation']['id']; $query = <<<EOF { node(id: "$bulk_id") { ... on BulkOperation { id status errorCode createdAt completedAt objectCount fileSize url partialDataUrl } } } EOF; //ステータスがCOMPLETEDになるまでポーリングする do { sleep(10); $res = shopify_gql_call($token, $shop, $query); $status = json_decode($res['response'], true)['data']['node']['status']; if($status == 'CANCELED' || $status == 'EXPIRED' || $status == 'FAILD') { die('問題が発生しました。(' . $status . ')'); } } while($status != 'COMPLETED'); //JSONL形式のデータURLが返ってくるので、一行ずつ読み出して表示する $resfile = fopen(json_decode($res['response'], true)['data']['node']['url'], 'r'); if($resfile){ while($line = fgets($resfile)){ echo $line . '<br>'; } } fclose($resfile);
バルクオペレーションのミューテーションが成功するとCREATEDが返るのですが、たまに失敗する事があるのでここではリクエストを最大3回リトライしています。
リクエストをすると瞬時にレスポンスが返ってくるわけではありません。バルクオペレーションの処理が完了しデータが準備できるまでポーリング(定期的に問い合わせ)をする必要があります。
ステータスがCREATEDになっているのを確認したら、今度はオペレーションIDを取得して、バルクオペレーションの処理が完了するまでポーリングをしながらステータスがCOMPLETEDになるまで待ちます。この例では10秒ごとに問い合わせをしています。
バルクオペレーションのステータスの意味は以下のとおりです。
ステータス名 | 説明 |
---|---|
CANCELED | バルクオペレーションがキャンセルされた |
CANCELING | バルクオペレーションをキャンセル処理中 |
COMPLETED | バルクオペレーションが完了した |
CREATED | バルクオペレーションが作成された |
EXPIRED | バルクオペレーションの有効期限が切れた (URLが生成されてから1週間以上経過した) |
FAILED | バルクオペレーションが失敗した |
RUNNING | バルクオペレーションは実行中 |
ステータスがCOMPLETEDになると、urlフィールドにデータがダウンロードできるURLが返ってきます。このデータはJSONL(JSON Lines)形式で返されます。
JSONL形式とは改行で区切られたJSON形式なので、1行ずつ読み出して処理します。file_get_contentsで一気に取得してJSONL形式をJSON形式に変換して処理するやり方もあるかもしれませんが、バルクオペレーションで取得するようなデータは巨大な場合がありリソース不足に陥る危険性があるのでおすすめしません。
この例ではfgetsで1行ずつ読み出して表示しています。
開発ストアの管理画面を再読込してみてください。JSON形式の商品情報が改行区切りでずらっと並んで表示されているのが分かると思います。
データの取得が確認出来れば、今度はデータの保存です。保存先はMySQLやSQLite等のデータベースかCSV形式で保存する場合が多いと思いますが、ここではCSV形式で保存する例を載せます。
//~~~~~ (省略) ~~~~~~~ $file = new SplFileObject('productdata.csv', 'w'); $file->fputcsv(['id', 'handle', 'vendor', 'title', 'description', 'price', 'prices', 'list_price', 'list_prices', 'image']); $resfile = fopen(json_decode($res['response'], true)['data']['node']['url'], 'r'); if(is_array($resfile) && empty($resfile)) die('データを取得できませんでした'); while(!feof($resfile)){ $data = json_decode(fgets($resfile) ,true); $line = array(); $line['id'] = $data['id'] ?? ''; $line['handle'] = $data['handle'] ?? ''; $line['vendor'] = $data['vendor'] ?? ''; $line['title'] = $data['title'] ?? ''; $line['description'] = $data['description'] ?? ''; $line['image'] = $data['featuredImage']['url'] ?? ''; $v = $data['totalVariants'] ?? 0; $prices = $list_prices = array(); for($i = 0; $i < $v; $i++){ $data = json_decode(fgets($resfile) ,true); if($i == 0){ $line['price'] = $data['price']; $line['list_price'] = $data['compareAtPrice']; } $prices[] = $data['price']; $list_prices[] = $data['compareAtPrice']; } $line['prices'] = implode('|', $prices); $line['list_prices'] = implode('|', $list_prices); $file->fputcsv([ $line['id'], $line['handle'], $line['vendor'], $line['title'], $line['description'], $line['price'], $line['prices'], $line['list_price'], $line['list_prices'], $line['image'] ]); } fclose($resfile); unset($file);
※Null合体演算子を使ってしまったので、実行にはPHP7以上を使用してください。PHP5を使用している方はisset()と三項演算子で書き換えてください。ちなみに私の環境はPHP8.1です。
開発ストアの管理画面を再読込してみてください。今度はサーバーに「productdata.csv」が保存されていると思います。
後は、クリックボタンでダウンロードできるようにするかとかcrontabで定期実行して保存するかとかのカスタマイズはお任せします。
アプリを開発ストアでのテストを繰り返して完成すれば、今度は「配信」の流れになり、公開アプリにするか単一マーチャント専用のカスタムアプリにするかを選択する、という流れになります。
Shopify PolarisをCDNで使用する
補足説明にはなりますが、Shopifyアプリ用のUIライブラリPolarisはCDNでもCSSファイルが配布されているので、React環境ではなくても利用可能です。使用するにはHTMLのheadタグ内に以下のコードを貼り付けてください。
<link rel="stylesheet" href="https://unpkg.com/@shopify/polaris@10.13.0/build/esm/styles.css">
使い方についてはpolaris.shopify.comに使い方が大量のサンプルと共に詳しく載っています。そのサンプルにあるボタングループを配置してみます。
<div class="Polaris-ButtonGroup"> <div class="Polaris-ButtonGroup__Item"> <button class="Polaris-Button" type="button"> <span class="Polaris-Button__Content"> <span class="Polaris-Button__Text">Cancel</span> </span> </button> </div> <div class="Polaris-ButtonGroup__Item"> <button class="Polaris-Button Polaris-Button--primary" type="button"> <span class="Polaris-Button__Content"> <span class="Polaris-Button__Text">Save</span> </span> </button> </div> </div>
実はReactの場合はもっと簡単で、以下のコードで上と同じデザインのボタングループの関数コンポーネントが出来てしまいます。
import {ButtonGroup, Button} from '@shopify/polaris'; import React from 'react'; function ButtonGroupExample() { return ( <ButtonGroup> <Button>Cancel</Button> <Button primary>Save</Button> </ButtonGroup> ); }
さいごに
上記は手軽にShopifyアプリを作る方法のご紹介でした。Shopifyのアプリ開発では、APIにアクセスできるサーバーサイド言語なら何でも使用できるのがお分かりいただけたと思います。
PHPはバージョンが上がるにつれて処理速度がどんどん上がってきており、インタプリタ言語のPHPでも実用に耐えるShopifyアプリは公開アプリも含めて十分に作成可能です。もちろんRuby(Ruby on Rails)の方を使用しても良いと思っています。
しかしフロントエンドの方はPHPだけでは開発効率は良くないと思っています。
そのため、次回は今年(2022年)にリリースされたShopify CLI 3を使ってReact+PHP(Laravel)のアプリ開発環境を試してみた記事をご紹介しようと思います。