【PHP】stripe API で定額課金を実装する

#はじめに stripeとは
stripeは、クレジットカードなどの決算処理を代行してくれるサービスです。
シンプルな実装で決済処理を実現でき、カード情報を自社サーバで持たなくても良いなどの特徴があります。

大まかな流れとして、

1. サービスの請求モデルを作成
2. フロントサイドでカード情報を取得
3. バックエンドで顧客登録、決済処理

と進んでいきます。

#サービスの請求モデルを作成する

##1. stripeに登録する
###1-1.はじめに、[stripeの公式ページ](https://stripe.com/jp)にて、今すぐ始めるをクリック。
<img width="1435" alt="スクリーンショット 2020-01-15 20.24.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/b34d1b49-559e-4f77-7bf9-86e109d3e87b.png">

###1-2メールアドレス、名前、パスワードを入力登録。

なお、テスト用に登録する際には、メールアドレスの存在確認は求められません。
この時点で、登録が完了いたしました。
<img width="1435" alt="スクリーンショット 2020-01-15 20.27.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/dfe78916-4bb6-5eee-fbb8-b0792929857b.png">

###1-3.ダッシュボードが表示される
登録が完了したら、いくつかの質問がされるので答えていきます。
質問が完了したら、次のようなダッシュボードが表示されます。
<img width="1435" alt="スクリーンショット 2020-01-15 20.34.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/18e1be8a-fcd3-83b7-2e04-afb62609582b.png">

##2.テストAPIキーを取得する
ダッシュボードが表示されたら、まずはAPIキーを取得してみましょう。
今回はテスト用のAPIキーを取得します。
ダッシュボードのテストAPIキーの取得をクリックしてください。<img width="1435" alt="スクリーンショット 2020-01-15 20.39.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/c25da7f1-6582-b692-1a2f-c421238ed86d.png">

**公開可能キー**と**シークレットキー**の2つが表示されています。
<img width="1435" alt="スクリーンショット 2020-01-15 20.43.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/330b0247-57c4-1b6d-cdbf-0af1c5cd304e.png">

###公開可能キー
公開可能キーは、不特定多数に表示されてもかまわないキーです。
JavaScriptなど、フロントサイドで使用されるキーです。

###シークレットキー
一方シークレットキーは秘密にしなければなりません。
PHPなど、サーバーサイドで使用されます。

##3.商品(Product)を登録する
###3-1.Billing->商品をクリック
はじめに、提供するサービスである、**商品(Product)**を登録します。
画面左側の`Billing`をクリックしてから、`商品`が表示されるのでもう一度クリックしてください。

<img width="1435" alt="スクリーンショット 2020-01-15 21.24.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/6dd6aeb8-398b-4c2f-1134-abb3a47caa5b.png">

`テスト商品を追加`をクリックします。
<img width="1435" alt="スクリーンショット 2020-01-15 21.31.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/6168fd90-5404-a930-8855-0ef53d8a5149.png">

###3-2.商品を作成
`商品名`を入力して、`商品を作成`をクリックします。
<img width="1436" alt="スクリーンショット 2020-01-15 21.33.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/acb885f3-6f99-5da7-bd2d-e74cb516d066.png">

なお、ここでは商品の価格などの情報は入力しません。
商品の配下に、`プラン(Plan)`を作成して個別に料金体系(価格、通貨、支払い間隔など)を設定します。
一つの商品に対して、複数のプランが設定できます。
例えば、`有料会員`という製品には以下のプランが紐付けられていると考えられます。

- 有料会員
    - ブロンズ会員 月額500円
    - シルバー会員 月額1,000円
    - ゴールド会員 月額3,000円
  
#４.プランを作成する
###4-1.プランの作成
それでは実際にプランを作成してみます。
商品を作成したら、そのままプランを作成するページへ遷移します。
<img width="1436" alt="スクリーンショット 2020-01-15 21.41.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/cf636d07-8d1c-d520-7b68-0c4c24b15a3a.png">
<img width="1436" alt="スクリーンショット 2020-01-15 21.43.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/a33da793-4a62-a4ad-8bc7-62c43036df6d.png">

必要な項目を入力して、`料金プランを追加`をクリックすると、プランが追加されます。

###４-2.プランIDの取得
後ほど会員をプランに追加する際に、プランIDが必要になるのでここで取得します。
**prod_から始まる商品IDではないことに注意してください**
先程作成したプランクリックして詳細を表示します。
<img width="1438" alt="スクリーンショット 2020-01-16 0.17.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/4c215db2-8960-5970-3131-a56eaf949c7e.png">


plan_から始まる英数字がプランIDです。
<img width="1436" alt="スクリーンショット 2020-01-15 22.25.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/9954f376-2b59-2e87-0232-982138956cf7.png">

#コードを実装する。
##5.ライブラリをインストールする
いよいよコードの実装にはいります。
はじめに、`Composer`を利用して、PHPのライブラリを入手します。

[Composerのインストール](https://qiita.com/kakijin/items/02364adacf36410f449e)


```
composer require stripe/stripe-php
```

##6.フロントサイドの実装(JavaScript)
###6-1.カード情報を入手する
カード情報を入手するための入力フォームを、JavaScritpで実装します。
カード情報といっても、実際に得られるのはAPI通信によって得られたトークンの情報です。

```index.html
<form id="form_payment" action="/add.php" method="post">
  Name:<input id="name" type="text" name="name">
  Email:<input id="email" type="text" name="email">
  <!-- ここのdivタブがカード入力フォームに置き換わります。 -->
  <div id="card-element" class="MyCardElement"></div>
  <!-- ここにエラーメッセージが表示されます。 -->
  <div id="card-errors" role="alert"></div>
  <button id="button">Submit</button>
</form>
<!-- stripAPIを読み込みます -->
<script src="https://js.stripe.com/v3/"></script>
<script>
  // 公開可能なAPIキーです。
  const stripe = Stripe('pk_test_aGucPIXrlDAHQuflip6RqErD00CqmOnlKK');
  // 入力フォームを生成します。スタイルを指定することもできます。
  const elements = stripe.elements();
  const cardElement = elements.create("card");

  //　先程のdivタブにマウントします。
  cardElement.mount("#card-element");

  // クレジットカード番号や有効期限の入力に合わせてエラーメッセージを出力します。
  cardElement.addEventListener('change', ({error}) => {
      const displayError = document.getElementById('card-errors');
      if (error) {
        displayError.textContent = error.message;
      } else {
        displayError.textContent = '';
      }
  });

  const submit = document.getElementById('button');
  const name = document.getElementById('name');
  const email = document.getElementById('email');

  // 登録ボタンがクリックされたら、API通信をおこなう
  submit.addEventListener('click', async(e) => {
    e.preventDefault();
    const {paymentMethod, error} = await stripe.createPaymentMethod({
      type: 'card',
      card: cardElement,
        billing_details: {
          // 顧客名emailアドレスはなくてもOK
          name: name.value,
          email: email.value,
      },
    });
    // 通信エラー時
    if (error) {
      console.error(error)
    } else {
        // 成功したらトークンが返されるので、hiddenに埋め込む
        const form = document.getElementById('form_payment');
        const hiddenToken = document.createElement('input');
        hiddenToken.setAttribute('type', 'hidden');
        hiddenToken.setAttribute('value', paymentMethod.id);
        hiddenToken.setAttribute('name', 'token');
        form.appendChild(hiddenToken);
        form.submit();
      }
    });
</script>
```

下記画像のように、カード番号・有効期限・確認番号・郵便番号を入力するフォームが生成されました。
<img width="733" alt="スクリーンショット 2020-01-16 11.09.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/a0d0ca04-2993-08c3-99fd-9e251746d200.png">

これらの項目はすべて必須項目です。
もし、郵便番号の入力をなくしたいなら、入力フォーム生成時に`hidePostalCode`を`true`にします。

```php
	const cardElement = elements.create('card', {hidePostalCode: true});
```

テスト用として、`4242 4242 4242 4242`の番号が用意されているので、そちらを利用します。
その他の値はなんでもかまわないようです。

##7.サーバーサイド(PHP)
###7-1.顧客情報を登録する。
フロントサイドから受け取ったトークンをもとに、顧客情報を登録します。

```php
<?php
require '/vendor/autoload.php';
// シークレットキーをセットする
\Stripe\Stripe::setApiKey(YOUR_SECRET_KEY);

if($_SERVER['REQUEST_METHOD'] == 'POST') {
    // ここではバリデーションは省略しています
    $name = $_POST['name'];
    $email = $_POST['email'];
    $token = $_POST['token'];

    // 顧客情報を登録
    $customer = \Stripe\Customer::create([
        'payment_method' => $token, // 登録する支払い方法
        'name' => $name,
        'email' => $email,
        'invoice_settings' => [
            'default_payment_method' => $token, // デフォルトで使用する支払い方法。必須。
        ],
    ]);
```

###7-2.顧客をプランに登録する
さらに、先程登録した顧客情報をもとに、プランへの登録処理を行います。

```php
    // 顧客をプランに登録する
    $subscription = \Stripe\Subscription::create([
        // 先程登録した顧客情報のID
        'customer' => $customer->id,
        'items' => [
            [
              // 4-2で取得したプランID
              'plan' => 'plan_GYHnkoAjA7PFtG',
            ],
        ],
        'trial_end' => strtotime('+3 month'),
        // トライアル(無料)期間。UNIX秒で指定する。
    ]);
    // subsctiprionIDは、解約時に必要
    $sub_id = $subscription->id; // sub_GYJXuOff81PbZy
   
    
    // DBを準備
    try {
        $db = new PDO('mysql:dbname=php;host=localhost', 'root', '');
    } catch (PDOException $e) {
        print "Coudn't connet to the database: " . $e->getMessage();
        exit();
    }

    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 顧客情報を登録
    try {
        $stmt = $db->prepare('INSERT INTO Users (name, email, sub_id)
                                VAlUES (?,?,?)');
        $stmt->execute($name, $email, $sub_id);

    } catch (PDOException $e) {
        print "Coudn't connet to the database: " . $e->getMessage();
        exit();
    }
}
``` 

`trial_end`を指定すると、トライアル(無料期間)を定めることができます。
音楽ストリーミングサイトによくある初回3ヶ月無料!みたいなやつです。

UNIX秒で指定するので、strtotimeを使用すればある程度簡単に指定することができます。

プラン登録時の返却値の`Subscription`オブジェクトのIDは、解約時に必要になります。
なので、顧客情報とともにDBに登録します。

##8.登録を確認する
ダッシュボードに戻り、左側の`顧客`をクリックすると、先程登録した顧客情報が表示されているかと思います。
<img width="1438" alt="スクリーンショット 2020-01-15 23.34.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/49bbc54a-c042-98f9-ba79-2bf591d1d3c8.png">


無事顧客が登録されていましたね。
さらに、詳細情報をみると顧客の定期支払を確認することができます。
<img width="1438" alt="スクリーンショット 2020-01-15 23.33.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/00ed32fd-c621-8946-97cf-d90685480f96.png">

トライアル期間を設定したので、請求書をみると初回の支払いは0円で、次回請求日は3ヶ月後に設定されていることが確認できます。(今回登録した日付は01/15)
<img width="1438" alt="スクリーンショット 2020-01-15 23.34.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/01b72dba-21c3-1cc5-a1d9-d6c395371b2f.png">

###9.プランを解約する
最後に、顧客が有料会員プランを解約する流れを説明します。

```php
<?php
// 契約を解除するためにログインしている想定
session_start();
require 'vendor/autoload.php';

$name = $_SESSION['name'];
// DBを準備
try {
    $db = new PDO('mysql:dbname=php;host=localhost', 'root', '');
} catch (PDOException $e) {
    print "Coudn't connet to the database: " . $e->getMessage();
    exit();
}

// ログインユーザーからsub_idを取得
$stmt = $db->prepare('SELECT sub_id FROM Users WHERE name = ?');
$stmt->execute($name);
$row = $stmt->fetch('assoc');
if ($row) {
  $sub_id = $row->sub_id;
} else {
  exit();
}
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// シークレットキーをセットする
\Stripe\Stripe::setApiKey(YOUR_API_KEY);

// 解約したあとも有効期限までは、プランに入会したまま
\Stripe\Subscription::update(
  $sub_id,
  [
    'cancel_at_period_end' => true,
  ]
);
```

上記の例のように`cancel_at_period_end`を`true`にしてプランを`Subscription::update`で更新することで、解約後も有効期限内まではプランが有効になります。

もし、解約後はすぐにプランを終了したいときには、以下のようなコードを書きます。

```php
// 解約後はすぐにプランが終了する。
$subscription = \Stripe\Subscription::retrieve('sub_49ty4767H20z6a');
$subscription->cancel();
```

###8-2.解約を確認

ダッシュボードを確認すると、先程の会員が有効期限である`04/15`にプランがキャンセルされることが確認できました。
<img width="1438" alt="スクリーンショット 2020-01-16 0.06.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496565/98f75f91-5599-8330-2197-5e25a62d5699.png">

#おわりに
定額課金の決済の簡単な流れを説明しました。
他にも期間中のアップグレード、ダウングレードや途中解約に対する比例配分、更新時にカードが利用できなかった場合など複雑な状況にも対応できます。

ぜひ、ドキュメントも参照してみてください。

#参考サイト
[公式ドキュメント](https://stripe.com/docs/billing)
[API リファレンス](https://stripe.com/docs/api)
[Stripe Billing 101](https://qiita.com/y_toku/items/235b5e7ee00792edcbbf)
[Stripe.js & Elements を利用して決済フローを理解する](https://qiita.com/y_toku/items/7e51ef7e69d7cbbfb3ca)