WordPressで、Stripe SDKを使わずにStripeのWebhookイベントを受け付けるAPIを作る方法

Stripeでは、決済や顧客・サブスクリプションの「状態変化」をシステムに通知するために、Webhook APIを登録できます。

![スクリーンショット 2022-10-28 14.48.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/018ffed5-b841-28fa-320a-54c387a64b9b.png)

WordPressにStripeを組み込む際にも、このWebhook APIを実装する必要があります。

ですが、容量削減やComposerの導入が難しい環境などの問題から、StripeのPHP SDKを利用したくないケースも少なからず存在します。

この記事では、WP APIを利用してStripe Webhookのリクエストを受け付ける方法を紹介します。

:::note warn
注意点
本記事で紹介するコードは、GPLv3でライセンスしています。

これは、コードの一部に「[WooCommerce Stripe Payment Gateway](https://github.com/woocommerce/woocommerce-gateway-stripe)」のコードおよび「WordPressコアのコード」が含まれているためです。

WordPress内で利用する場合、ほとんどのケースではGPLを継承して配布されると思いますが、念の為告知します。
:::

## Webhook APIをWordPressで作る 3Step

### Step1: `register_rest_route`でAPIエンドポイントを追加する

まず必要なのは、APIエンドポイントです。

WordPressの場合、以下のコードでAPIエンドポイントを追加できます。

```php
<?php
/*
 * Plugin Name: Stripe Custom code plugin
 * Description: Demo code plugin for integrating Stripe and WordPress.
 * Version: 100
 * License: GPLv3
 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
*/

add_action( "rest_api_init", function () {
    register_rest_route(
        "stripe/v0",
        "webhook",
        [
            "methods" => "POST",
            "callback" => function () {
                return [
                    "message" => "hello"
                ];
            }
        ]
    );
} );
```

`rest_api_init`フックで、`register_rest_route`を実行し、`POST wp-json/stripe/v0/webhook`APIを登録しています。

WordPressをローカルで起動している場合、以下のようなコマンドでレスポンスを受け取ることができます。

```bash
% http://localhost:8888/wp-json/stripe/v0/webhook -XPOST  | jq .
{
  "message": "hello"
}
```

`method`で`POST`のみを指定しているため、`GETなどのリクエストはエラーになります。

```bash
% http://localhost:8888/wp-json/stripe/v0/webhook | jq .
{
  "code": "rest_no_route",
  "message": "No route was found matching the URL and request method.",
  "data": {
    "status": 404
  }
}
```

### Step2: Webhookの署名検証を実装する

APIエンドポイントの追加はできました。

が、このままではエンドポイントを知っている人は誰でもAPIを呼び出すことができます。

Stripe以外からのリクエストでもシステムが動作すると、システムが予期しない動きをすることがあります。

そのため、Stripeが発行する署名シークレットを利用した検証ステップを追加しましょう。

```php
function validate_stripe_webhook_request() {
    $un_authorized_error = new WP_Error( "401", esc_html( "Not Authorized", "text_domain" ), [ "status" => 401 ] );

    // ご自身のアカウントの署名シークレットを設定
    $endpoint_secret = 'whsec_xxxx';
    $payload = @file_get_contents('php://input');

	// Check for a valid signature.
    $signature_format = '/^t=(?P<timestamp>\d+)(?P<signatures>(,v\d+=[a-z0-9]+){1,2})$/';
    if ( empty( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) || ! preg_match( $signature_format, $_SERVER['HTTP_STRIPE_SIGNATURE'], $matches ) ) {
        return $un_authorized_error;
    }

    // Verify the timestamp.
    $timestamp = intval( $matches['timestamp'] );
    if ( abs( $timestamp - time() ) > 5 * MINUTE_IN_SECONDS ) {
        return $un_authorized_error;
    }

    // Generate the expected signature.
    $signed_payload     = $timestamp . '.' . $payload;
    $expected_signature = hash_hmac( 'sha256', $signed_payload, $endpoint_secret );

    // Check if the expected signature is present.
    if ( ! preg_match( '/,v\d+=' . preg_quote( $expected_signature, '/' ) . '/', $matches['signatures'] ) ) {
        return $un_authorized_error;
    }

    return "";
}
```

:::note info
実装参考
- WooCommerce Stripe Payment Gateway: https://github.com/woocommerce/woocommerce-gateway-stripe/blob/bf229bfcf3d025dc743a7cf2300212759c5ef493/includes/class-wc-stripe-webhook-handler.php#L90-L133
- Stripe PHP SDK: https://github.com/stripe/stripe-php/blob/master/lib/WebhookSignature.php
:::

#### Webhookの署名シークレットを取得する方法

上記サンプルコードの、`'whsec_xxxx'`にご自身のアカウントの署名シークレットを設定します。

簡単な署名シークレットの取得方法は２つあります。

一つは、Stripe CLIで`stripe listen --forward-to http://localhost:8888/wp-json/stripe/v0/webhook`のようなローカルデバッグ用のシークレットを利用する方法です。

この方法では、コマンド実行時に、署名シークレットが発行されますので、これを利用します。

```bash
$ stripe listen --forward-to http://localhost:8888/wp-json/stripe/v0/webhook
⣽ Checking for new versions...
> Ready! Your webhook signing secret is whsec_yyyyyyy (^C to quit)
```

もう１つは、StripeダッシュボードでWebhookのエンドポイントを登録後に取得する方法です。

![スクリーンショット 2022-10-28 15.34.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/b159dc0f-e441-4de7-5f79-d8db627db3d3.png)

こちらは、ローカル開発では利用できませんのでご注意ください。

#### `permission_callback`で、追加したエンドポイントに権限チェック処理を追加する

`register_rest_route`で追加したエンドポイントには、`permission_callback`パラメータで権限チェック処理を追加できます。

先ほど実装した関数を設定して、署名検証処理をAPIに追加しましょう。

```diff_php
add_action( "rest_api_init", function () {
    register_rest_route(
        "stripe/v0",
        "webhook",
        [
            "methods" => "POST",
            "callback" => function () {
                return [
                    "message" => "hello"
                ];
            }
+            "permission_callback" => "validate_stripe_webhook_request",
        ]
    );
} );
```

APIを直接呼び出してみて、以下のようにHTTP401エラーが返って来ればOKです。

```bash
$ curl http://localhost:8888/wp-json/stripe/v0/webhook -XPOST   | jq .
{
  "code": 401,
  "message": "Not Authorized",
  "data": {
    "status": 401
  }
}
```

ダッシュボードからWebhookエンドポイントを登録するか、Stripe CLIの`stripe listen`コマンドでローカルのAPIと接続した状態で、顧客データの作成などを実行すると、APIが呼び出されます。

### Step3: Stripeのイベントによって処理を分岐する

最後にStripeから来たイベントデータを、イベントの種類によって分岐させます。

リクエストBodyを取得して、JSONのデコード処理を実施したのちに、`if`や`switch`でイベントの種類に応じた分岐を作りましょう。

```php
function handle_stripe_webhook_request () {
    $payload = @file_get_contents( "php://input" );
    $data = json_decode( $payload, true );
    switch ( $data[ "type" ] ) {
        case "payment_intent.created": {
            error_log("_____");
            break;
        }
        default:
            break;
    }
    return "";
}
```

作成した関数は、`register_rest_route`の`callback`パラメータを変更して適用します。

```diff_php
add_action( "rest_api_init", function () {
    register_rest_route(
        "stripe/v0",
        "webhook",
        [
            "methods" => "POST",
-            "callback" => function () {
-                return [
-                    "message" => "hello"
-                ];
-            }
+            "callback" => "handle_stripe_webhook_request",
            "permission_callback" => "validate_stripe_webhook_request",
        ]
    );
} );
```

これでStripe内で発生した「状態変化」と、WordPress内のデータを連携させる準備が完了しました。

あとは、`handle_stripe_webhook_request`関数に処理を追加して、カスタマーポータルとの連携やサブスクリプションとWordPressユーザーの紐付けなどを実装すればOKです。

## Appendix: Stripe SDKを利用する場合

Stripe SDKを利用する場合、署名検証部分の処理がとても短くなります。

```php
\Stripe\Stripe::setApiKey( 'sk_test_xxxx' );

function validate_stripe_webhook_request() {
    $un_authorized_error = new WP_Error( "401", esc_html( "Not Authorized", "text_domain" ), [ "status" => 401 ] );
    $endpoint_secret = 'whsec_xxxxx';

    $payload = @file_get_contents('php://input');


    try {
        $event = \Stripe\Webhook::constructEvent(
            $payload, $_SERVER[ 'HTTP_STRIPE_SIGNATURE' ], $endpoint_secret
        );
    } catch( \UnexpectedValueException $e ) {
        // Invalid payload
        return $un_authorized_error;
    } catch( \Stripe\Exception\SignatureVerificationException $e ) {
        // Invalid signature
        return $un_authorized_error;
    }
    return "";
}
```

Stripe APIを複数種類利用する場合や、実装をなるべくシンプルに済ませたい場合は、Stripe PHP SDKを利用することをお勧めします。

もしできるだけ容量を減らしたい場合は、SDKを省略してある程度自前で実装することも可能ですので、要件や環境に応じて適宜判断いただけると幸いです。


## [告知] 東京WordPress Meetup 2022 Fallにて、Stripe <> WordPress / WooCommerceなセッションをします

WordPress Meetupコミュニティの方々のご厚意で、StripeとWordPress / WooCommerceについてのセッションを持たせていただけることになりました。

2022年11月6日日曜日に、東京新宿区のCASE Shinjikuにて14時から開催です。

https://www.meetup.com/tokyo-wordpress-meetup/events/288173891/

WooCommerceの日本ローカライズプラグイン作者のShohei Tanakaさんも参加されますので、ぜひご参加ください。