Symfonyを使って、Stripeのサブスクリプション申し込みフローを構築する

この記事は、[Symfony Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony)22日目の記事です。

Symfonyを利用してアプリケーションを開発する際の、Stripe.js / Stripe PHP SDKの組み込み方法を簡単に紹介します。

## プロジェクトのセットアップ

Symfonyでのプロジェクトセットアップには、Composerを利用します。

```bash
$ composer create-project symfony/skeleton first-symfony-app
$ cd first-symfony-app
$ composer install
```

今回は、`traditional web application`として構築しますので、次のステップも追加しましょう。

```bash
$ composer require webapp
```

Dockerの設定を更新するか聞かれます。今回は`n`を選びましたが、必要に応じて追加してもらいましょう。

```bash
    This may create/update docker-compose.yml or update Dockerfile (if it exists).

    Do you want to include Docker configuration from recipes?
    [y] Yes
    [n] No
    [p] Yes permanently, never ask again for this project
    [x] No permanently, never ask again for this project
    (defaults to y): n
```

セットアップしたアプリを動かすには、`symfony`コマンドを利用しましょう。

```bash
$ symfony server:start                                                                                                                        
 [OK] Web server listening                                                                                              
      The Web server is using PHP FPM 8.2.0                                                                             
      http://127.0.0.1:8000    
```

https://symfony.com/doc/current/setup.html


## サブスクリプション申し込みのためのページを追加する

http://127.0.0.1:8000/ にアクセスすると、Symfonyで用意されている画面が表示されます。

![スクリーンショット 2022-12-22 8.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/b6cd0833-da88-90c4-544b-0078945e5469.png)

ただしこれはTOPページではなく、ログを見るとHTTP 404が返っています。

```log
[Web Server ] Dec 22 08:51:31 |WARN   | SERVER GET  (404) / ip="127.0.0.1"
```

そのため、コントローラーやビューを追加して、画面を用意しましょう。

### Symfony CLIで、ファイルを作成する

コントローラーとビューは、CLIから作成できます。

```bash
$ symfony console make:controller SubscriptionController

 created: src/Controller/SubscriptionController.php
 created: templates/subscription/index.html.twig

           
  Success! 
           

 Next: Open your new controller class and add some pages!
```

http://127.0.0.1:8000/subscription にアクセスすると、ページが確認できます。

![スクリーンショット 2022-12-22 8.56.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/67062abf-d987-d86c-b453-f662981994f8.png)

`src/Controller/SubscriptionController.php`を次のように変更することで、`http://127.0.0.1:8000/`にページを表示させることもできます。

```diff_php
-    #[Route('/subscription', name: 'app_subscription')]
+    #[Route('/', name: 'app_subscription')]
    public function index(): Response
```

`templates/subscription/index.html.twig`を変更すると、表示内容を更新できます。

```diff_twig
    <h1>Hello {{ controller_name }}! ✅</h1>
    <h2>You can edit this page.</h2>
```


![スクリーンショット 2022-12-22 9.03.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/60ee92de-164c-ca9e-a588-1657c8dee2e9.png)

https://symfony.com/doc/current/the-fast-track/en/6-controller.html

## SymfonyでStripe SDKを利用する

ページの用意ができましたので、Stripe SDKを組み込みましょう。

### Stripe SDKのインストール

まずはSDKをインストールします。

```bash
$ composer require stripe/stripe-php
```

### APIキーを環境変数から読み込む

`.env`にStripeのAPIキーを設定しましょう。

```diff_yaml
+STRIPE_PUBLISHABLE_API_KEY=pk_test_から始まる公開可能キー
+STRIPE_SECRET_API_KEY=sk_test_から始まるシークレットキー
```

`config/services.yaml`を編集して、環境変数を登録します。

```diff_yaml
parameters:
+    app.stripe.publishable_api_key: '%env(resolve:STRIPE_PUBLISHABLE_API_KEY)%'
+    app.stripe.secret_api_key: '%env(resolve:STRIPE_SECRET_API_KEY)%'

services:
    # default config
```

これでコントローラーから環境変数を取得できるようになります。

`src/Controller/SubscriptionController.php`を次のように変更しましょう。

```diff_php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SubscriptionController extends AbstractController
{
    #[Route('/subscription', name: 'app_subscription')]
    public function index(): Response
    {
+        $stripe_pk = $this->getParameter('app.stripe.publishable_api_key');
+        $stripe = new \Stripe\StripeClient( $this->getParameter('app.stripe.secret_api_key') );
        return $this->render('subscription/index.html.twig', [
            'controller_name' => 'SubscriptionController',
+            'stripe_pk' => $stripe_pk,
        ]);
    }
}
```

https://symfony.com/doc/current/configuration.html#configuration-parameters

## Stripe SDKで、サブスクリプションを作成する

Stripe SDKの準備ができましたので、サブスクリプションを作成しましょう。

:::note warn
**Stripe SDKを実行する場所について**
今回は便宜上、コントローラー上に直接実装します。
実際の開発では、Stripe・Stripe Subscription用のサービスを作成することをお勧めします。
:::

`src/Controller/SubscriptionController.php`を次のように変更しましょう。

```diff_php

        $stripe_pk = $this->getParameter('app.stripe.secret_api_key');
        $stripe = new \Stripe\StripeClient( $this->getParameter('app.stripe.secret_api_key') );
+        $customer = $stripe->customers->create();
+        $product = $stripe->products->create([
+            'name' => 'Symfony demo'
+        ]);
+        $subscription = $stripe->subscriptions->create([
+            'customer' => $customer->id,
+            'items' => [
+                [
+                    'price_data' => [
+                        'unit_amount' => 1000,
+                        'currency' => 'jpy',
+                        'recurring' => [
+                            'interval' => 'month',
+                        ],
+                        'product' => $product->id,
+                    ],
+                    'quantity' => 1,
+                ]
+            ],
+            'payment_behavior' => 'default_incomplete',
+            'expand' => [
+                'latest_invoice.payment_intent',
+            ],
+            'payment_settings' => [
+                'save_default_payment_method' => 'on_subscription',
+            ],
+        ]);
        return $this->render('subscription/index.html.twig', [
            'controller_name' => 'SubscriptionController',
            'stripe_pk' => $stripe_pk,
+            'subscription_pi_secret' => $subscription->latest_invoice->payment_intent->client_secret,
        ]);
```

これで`http://localhost:8000/subscription`にアクセスする度に、サブスクリプションなどのデータが作成されます。

`templates/subscription/index.html.twig`を次のように変更すると、作成したサブスクリプションの決済に必要なClient Secretが表示されます。

```diff_twig
    <h1>Hello {{ controller_name }}! ✅</h1>
+    <pre><code>{{ subscription_pi_secret }}</pre></code>

    This friendly message is coming from:
    <ul>
```

![スクリーンショット 2022-12-22 13.47.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/091bf028-c6fd-9173-39b7-6f72aa3eb3a5.png)


### [Tips]作成済みの料金・顧客・商品データを利用する

`$stripe->customers->create`と`$stripe->products->create`を毎回実行しますので、読み込みの度に新しいデータが作成されます。

すでにCustomerやProduct, PriceのIDがある方は作成済みのリソースのIDをお使いください。

```diff_php

-        $customer = $stripe->customers->create();
-        $product = $stripe->products->create([
-            'name' => 'Symfony demo'
-        ]);
        $subscription = $stripe->subscriptions->create([
+            'customer' => 'cus_xxx',
-            'customer' => $customer->id,  
            'items' => [
                [
+                   'price' => 'price_xxx',
-                    'price_data' => [
-                        'unit_amount' => 1000,
-                        'currency' => 'jpy',
-                        'recurring' => [
-                            'interval' => 'month',
-                        ],
-                        'product' => 'prod_xxx',
-                    ],
                    'quantity' => 1,
                ]
            ],
            'payment_behavior' => 'default_incomplete',
            'expand' => [
                'latest_invoice.payment_intent',
            ],
            'payment_settings' => [
                'save_default_payment_method' => 'on_subscription',
            ],
        ]);
```

## サブスクリプションのClient Secretを利用して、決済フォームを実装する

最後に、顧客が決済情報を入力するフォームを追加しましょう。

Encoreを利用して、WebpackでJSファイルをバンドルすることもできますが、今回は便宜上こちらを使わずに実装します。

https://symfony.com/doc/current/frontend/encore/simple-example.html

`templates/subscription/index.html.twig`を次のように変更しましょう。


```diff_twig
...
</div>

+<form id="stripe-subscription-form">
+    <div id="stripe-element"></div>
+    <button type='submit'>Subscrive</button>
+</form>
{% endblock %}

+{% block javascripts %}
+<script>
+    let stripe
+    let elements
+    window.onload = function() {
+        presentPaymentElement();
+        addFormSubmitEvent();
+    }
+
+    function presentPaymentElement() {
+        stripe = new Stripe('{{ stripe_pk }}');
+        elements = stripe.elements({
+            appearance: {
+                theme: 'stripe',
+            },
+            clientSecret: '{{ subscription_pi_secret }}'
+        });
+        const paymentElement = elements.create('payment', {
+            layout: 'accordion'
+        });
+        paymentElement.mount('#stripe-element');
+    }

+    function addFormSubmitEvent() {
+        document
+            .getElementById('stripe-subscription-form')
+            .addEventListener('submit', async function(e) {
+                e.preventDefault();
+                if (!stripe || !elements) return;
+                const { error, paymentIntent } = await stripe.confirmPayment({
+                    elements,
+                    redirect: 'if_required'
+                });
+                if (error) {
+                    window.alert(JSON.stringify(error));
+                } else {
+                    console.log(paymentIntent);
+                    window.alert('done');
+                }
+            });
+    }
+</script>
+{% endblock %}
```

変更を保存すると、決済フォームが表示されます。

![スクリーンショット 2022-12-22 14.16.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/95029d90-5868-798c-309a-094d57b433fe.png)

Stripeが用意している「テストカード番号」を利用して、決済をテストしましょう。

https://stripe.com/docs/testing

成功メッセージが表示されれば、実装完了です。

![スクリーンショット 2022-12-22 14.16.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/0cc92572-d810-0a35-6860-e61769dc3a9f.png)



## Documents

https://symfony.com/doc/current/setup.html

https://symfony.com/doc/current/the-fast-track/en/

## Special Thanks

Twitterにて @ippey_s さんに、環境変数の設定・読み込み方法をアドバイス頂きました。ありがとうございます！

https://twitter.com/ippey_s/status/1605732551393366016

https://twitter.com/ippey_s/status/1605735990865186816