サブスクリプションという言葉を聞き始めてから随分時間が経ちますが皆さんサブスクリプションのサービスを契約しています?私の場合はMicrosoft, Adobe, Amazon, Netflixなど複数のサブスクリプションの契約を行い定額の料金を毎月支払っています。私と同じように契約しているほとんどがアメリカのグローバル企業という人も多いのではないでしょうか。

サブスクリプションサービスが蔓延している中でLaravelアプリケーション上でサブリスクリプションのサービスを提供したいと考える人もいるかと思います。そんな時に利用できるLaravelパッケージがLaravel Cashierです。

Laravel Cashierとは

Laravel CashierはStipe(またはPaddle)を経由したオンライン決済を行う際に利用することができるLaravel公式のパッケージです。Laravel Cashierを利用することで一定期間毎に定額料金を自動で徴収するサブスクリプションサービスをLaravelアプリケーション上で簡単に構築することができます。サブスクリプションだけではなく一度だけの支払いに利用できるSingle Chargesにも使えます。Laravel Cashier上で直接支払い処理を行うのではなくStripe側で支払い/クレジットカードの管理を行ってくれるのでLaravel側でユーザのクレジットカード情報に触れることもないので安心してオンライン決済に利用することができます。

Laravel CashierではPaddleなどの他のサービスも利用できますが本書ではStripeのみ扱っています。

Stripeの組み込み方法

Stripeを利用した組み込み方法にはStripe Payment Links, Stripe Checkout, Stripe Elementsの3つがStripeから提供されています。Laravel CashierではStripe CheckoutとStripe Elementsを利用することができます。Stripe Checkoutは支払いページはStripeが提供するページを利用(Laravelの自サイトからリダイレクトを行う)し、Stripe Elementsでは自サイトにStripeが提供する支払いフォームを組み込む(iframe利用)という大きな違いがあります。カスタマイズを行えるのはStripe Elementsなので学習者であればこちらを利用することになります。

Stripe Payment LinksやPayment Intents APIを利用したStripeの利用方法については別の文書で公開しています。

さまざまな支払い方法

Stripeでは支払いを行うための設定方法が複数あります。Laravel Cashierを理解する際にCashierで利用されている支払い方法がStripeのどの方法に対応しているのか混乱する可能性があります。Laravel CashierではSubscriptionとSingle Chargesの機能を持っており、繰り返し支払いを行うサブスクリプションの場合にはSetup Intents APIを利用しており一回限りの支払い時に利用するSingle ChargesではPayment Methods APIを利用しています。

支払いの設定方法が複数あると説明しましたがStripeではどのような支払い設定を行えるかはStripeのドキュメントにアクセスして”その他の支払いシナリオ”をみるのが参考になります。

その他の支払いシナリオ
その他の支払いシナリオ

Stripeのドキュメントではオンライン支払いの組み込みサンプルとしてPayment Intentsの詳しい手順が公開されていますPayment Intentsの組み込みサンプルを見てLaravel Cashierの設定方法を読んでも理解はするのは難しいかもしれません。サブスクリプションのSetup Intents APIを確認したい場合は”将来の支払いの設定”を確認してみてください。Single ChargesのPayment Methods APIの場合”はサーバで支払いを確定する”を確認してみてください。Laravel Cashierを理解するのに参考になるはずです。

Stripeのアカウント登録

Laravel CashierでStripeを利用する前にStripeのアカウントの作成を行います。 アカウントの作成を行うためにStipeのサイトにアクセスします。テストは無料で行うことができます。

 Stripeのトップページ
Stripeのトップページ

画面中央にある”今すぐ始める”をクリックします。入力フォームが表示されてるので入力を行ってください。

アカウント作成画面
アカウント作成画面

アカウントの作成を行うと入力したメールアドレスに確認メールが届き、認証が完了するとダッシュボードが表示されます。ダッシュボードでは公開可能キーとシークレットキーを確認することができます。この2つのキーはLaravel Cashierを設定する際に必須となります。

ダッシュボードの表示
ダッシュボードの表示

Laravel環境の構築

laravel newコマンドを使ってLaravelプロジェクトの作成を行います。laravel_cashierという名前をつけていますが任意の名前をつけてください。


 % laravel new laravel_cashier

laravelプロジェクトを作成後にユーザ認証を行うためにBreezeパッケージをインストールします。JetStreamでも利用することは可能ですがシンプルなBreezeを本文書では利用しています。


 % cd laravel_cashier
 % composer require laravel/breeze --dev

breezeのインストールを行います。実行すると認証のためのルーティングやbreezeの認証で利用するbladeファイルなどが作成されます。


 % php artisan breeze:install

npm install && npm run devを実行してJavaScriptのライブラリのインストールとビルドを行います。


 % npm install && npm run dev

データベースには簡易的に利用できるSQLiteを利用します。


 % touch database/database.sqlite

.envファイルを開いてDB_CONNECTIONをデフォルト値のmysqlからsqliteに変更し、それ以外のDB_が先頭につく環境変数を削除してください。

テーブルを作成するためにphp artisan migrateコマンドを実行します。


 % php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (3.50ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (1.91ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (1.60ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (2.52ms)

Laravel Cashierのインストール

laravel Cashierパッケージのインストールを行います。


 % composer require laravel/cashier

Laravel Cashierで利用するテーブルを作成するためにphp artisan migrateコマンドを実行します。


 % php artisan migrate
Migrating: 2019_05_03_000001_create_customer_columns
Migrated:  2019_05_03_000001_create_customer_columns (10.40ms)
Migrating: 2019_05_03_000002_create_subscriptions_table
Migrated:  2019_05_03_000002_create_subscriptions_table (3.39ms)
Migrating: 2019_05_03_000003_create_subscription_items_table
Migrated:  2019_05_03_000003_create_subscription_items_table (3.25ms)

新たにsubscriptions, subscription_itemsテーブルが作成されusersテーブルにstripe_id, pm_type, pm_last_four, traial_ends_atの4つの列が追加されます。

Userモデル(User.php)にBillableトレイトを追加します。Billableトレイトの中にこれから紹介していくCashierに関するメソッドが含まれています。


use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, Billable;
}

.envファイルにStripeのダッシュボードに表示されていた公開可能なキーとシークレットキーを設定します。各環境に合わせて設定を行ってください。


STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret

Laravel側のCashierの初期設定は完了です。

ルーティングの確認

Cashierのインストールが完了すると新たにルーティングが登録されるのでphp artisan route:listコマンドで確認を行います。

実行するとstripe/payment/{id}とstripe/webhookの2つのルーティングが追加されており、Laravel\Cashier\Http\Controllers\PaymentController@show, Laravel\Cashier\Http\Controllers\WebhookController@handleWebhookがそれぞれ対応します。

追加されたRoutingの確認
追加されたRoutingの確認

商品の設定

Laravel Cashierはサブスクリプションのサービスを構築するための機能が備わっているためECサイトなどのショッピングカードからの商品の購入ではなくNetflixやHuluなどの毎月定額料金の請求が行われるサービスを想像して下記の文書を読み進めてください。

Laravel上でコードを記述する前にサブスクリプションで選択できる商品の登録を行います。ダッシュボードの上部にある商品タブをクリックします。商品ページが表示されたら右上にある”商品を追加”ボタンをクリックします。

商品の追加画面
商品の追加画面

任意の名前をつけることができますがここではNetflixという名前をつけています。

商品の登録
商品の登録

商品はNetflixのプランを参考にベーシック、スタンダード、プレミアムの3つの金額を登録します。ベーシックは990円なので価格に990円を入れて継続を選択し請求期間は月次としています。月次に設定すると1ヶ月毎に請求が行われます。次の金額を設定するの”別の料金を追加”ボタンをクリックしてください。1,490円と1980円を追加してください。追加したら右上の”商品を保存”をクリックしてください。

3つの料金を追加
3つの料金を追加

商品の追加が完了すると下記の画面が表示されます。サブスクリプションのプランを選択する場合は料金のAPP IDに表示されているprice_XXXXXを利用することになります。後ほどこの値を利用します。サブスクリプションの契約があるとサブスクリプション列の数が増えていきます。

追加した商品の情報
追加した商品の情報
価格の設定の際にその他のオプションで価格の説明を任意で登録できるのでベーシック、スタンダード、プレミアムなどの説明を入れると上記の1ヶ月ごとに1のグループごとの下に入力した説明が表示されます。

ユーザの作成

User.phpファイルにtraitを追加したことからもLaravelに登録したユーザのみサブスクリプションの購入を行うことができます。php artisan serveコマンドでLaravelの開発サーバを起動してユーザの登録を行ってください。

ユーザの登録を行うと/dashboardにリダイレクトされます。

ダッシュボード画面
ダッシュボード画面

サブスクリプションの設定

ページの作成

新たにサブスクリプションの契約ができるsubscriptionページの作成を行います。/dashboardを元に/subscriptionのルーティングを追加します。


Route::get('/subscription', function () {
    return view('subscription');
})->middleware(['auth'])->name('subscription');

resouces¥viewの下のdashboard.blade.phpファイルをコピーしてsubscription.blade.phpファイルを作成して更新を行います。


<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Subscription') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 bg-white border-b border-gray-200">
                    サブスクリプション
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

/subscriptionにアクセスすると下記の画面が表示されます。

サブスクリプションページの作成
サブスクリプションページの作成

ページ上部に表示されているナビゲーションのメニューにSubscriptionのリンクも追加します。navigation.blade.phpファイルのNavigation Linksに追加します。


<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
        {{ __('Dashboard') }}
    </x-nav-link>
</div>
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
    <x-nav-link :href="route('subscription')" :active="request()->routeIs('subscription')">
        {{ __('Subscription') }}
    </x-nav-link>
</div>

メニューが表示されDashBoardとSubscription間のページの移動が可能になります。

Subscriptionリンクの追加
Subscriptionリンクの追加

JavaScriptの設定

サブスクリプションの契約を行いユーザが閲覧するフロントエンドのページでは支払いフォームの作成やStripeサーバとのデータの送受信にJavaScript(Stripe.js)を利用します。subscription.blade.phpファイル上でJavaScriptが実行できるように設定を行います。

レイアウトファイルであるresouces¥views¥layous¥app.blade.phpファイルのbodyの閉じタグの手前に@stack(‘scripts’)を追加します。


            <!-- Page Content -->
            <main>
                {{ $slot }}
            </main>
        </div>
        @stack('scripts')
    </body>
</html>

subscription.blade.phpファイルに@pushディレクティブでscriptタグを追加します。Stripe.jsを利用するためにhttps://js.stripe.com/v3/を読み込む必要があります。読み込むとStripeを利用することができるのでStripeの引数にStripeのダッシュボードで確認したpublic key(公開可能キー)を設定してください。


<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Subscription') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 bg-white border-b border-gray-200">
                    サブスクリプション
                </div>
            </div>
        </div>
    </div>
    @push('scripts')
      <script src="https://js.stripe.com/v3/"></script>
      <script>
        const stripe = Stripe('pk_test_your_public_key');
        console.log(stripe)
      </script>
    @endpush
</x-app-layout>

ブラウザから/subscriptionのページにアクセスしてブラウザのデベロッパーツールのコンソールを確認するとconsole.logで設定した値が表示されます。

コンソールに表示される内容
コンソールに表示される内容

フォームの表示

ここからの設定はLaravelドキュメントとStripeのドキュメントを参考に入力フォームでどのようなことを設定しているのか完全に理解していくため一括設定ではなくStep By Stepで説明を行なっていきます。どちらのドキュメントもLaravel Cashierでサブスクリプションを設定するまでの一連の流れを完全に説明していないので両方を活用します。

Stripeの組み込み方法の一つであるStripe Elementsを利用してカード番号入力フォームを作成します。Stripe ElementsはStripeから提供されるJavaScriptを利用してページ上にフォームを作成します。

まず入力フォームをマウントする場所を追加します。マウント場所にはiframeにより入力フォームが追加されることになります。入力フォームの中身はStripe側で作成されます。


<div class="p-6 bg-white border-b border-gray-200">
  <h2>サブスクリプション</h2>
  <div id="card-element"></div>
</div>

JavaScript側でcard-elementにマウントする要素を作成します。


const stripe = Stripe('your_private_key');

const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');

上記の設定でフォームの作成はStripeが行ってくれるのでブラウザにアクセスするとカード番号、有効期限、CVCを持つフォームが表示されます。クレジット決済に必須となるカード番号、有効期限、CVCのinput要素の追加などはこちらで行う必要ありません。

入力フォームの表示
入力フォームの表示

フォームの外側にborderをつけます。CSSはStripeのドキュメントカスタムの決済フローの中のglobal.cssで利用されているものを使います。CSSは自由に設定することができます。resources¥cssのapp.cssファイルにcssを記述することができますがビルドが必要になるのでapp.blade.phpファイルにstyleタグを追加してそこに記述していきます。


<!-- Styles -->
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<style>
    #card-element {
        border-radius: 4px 4px 0 0 ;
        padding: 12px;
        border: 1px solid rgba(50, 50, 93, 0.1);
        height: 44px;
        width: 100%;
        background: white;
    }
</style>

CSS適用後はフォームにボーダーが表示されます。

フォームのborder
フォームのborder

Laravelのドキュメントの中ではcard-holder-nameのinput要素が追加されているのでここでも同じように追加します。カード名義人などのフォームはStripeからは提供されていません。Stripeではクレジット支払いをする際はカード名義人は必須ではありません。


<h2>サブスクリプション</h2>
<input id="card-holder-name" type="text" placeholder="カード名義人">
<div id="card-element"></div>
</div>

追加した要素(id=”card-holder-name”)にもcard-elementと同じCSSを適用しておきます。


#card-element,#card-holder-name {
    border-radius: 4px 4px 0 0 ;
    padding: 12px;
    border: 1px solid rgba(50, 50, 93, 0.1);
    height: 44px;
    width: 100%;
    background: white;
}

input要素とCSSの適用により下記のように表示されます。文字の大きさが異なりますがここではそのまま進めます。

input要素追加
input要素追加
フォームの外側のcard-elementにCSSを適用することでborderを表示させましたがフォーム内部の要素にCSSを適用したい場合は、elements.create(‘card’)の第二引数にオプションとしてstyle、classを設定することでできます。

formタグで囲んでボタンを追加します。ボタンにはサブスクリプションという名前をつけています。ボタンを押すとサブスクリプションの商品(Stripe上で登録)を購入(契約、購読, ..)することになります。


<form id="setup-form">
  <input id="card-holder-name" type="text" placeholder="カード名義人">
  <div id="card-element"></div>
  <button id="card-button">
    サブスクリプション
  </button>
</form>

ボタンに対してCSSを設定します。これもStripeのドキュメントのglobal.cssから持ってきています。好きなCSSを適用してください。


button#card-button {
    background: #5469d4;
    color: #ffffff;
    font-family: Arial, sans-serif;
    border-radius: 0 0 4px 4px;
    border: 0;
    padding: 12px 16px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    display: block;
    box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
    width: 100%;
}
サブスクリプションボタンの追加
サブスクリプションボタンの追加

ここまででサブスクリプションを契約するためのフォームが完成しました。

イベントの設定

フォームの作成の後は入力した情報をStripeに送信する必要があります。JavaScriptのイベントを利用してStripeへの送信を実装していきます。

サブスクリプションボタンをクリックするとカード名義人の情報が取得できるようにボタンに対してイベントリスナーを追加します。


const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');

const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');

cardButton.addEventListener('click', async (e) => {
  e.preventDefault();
  console.log(cardHolderName.value)
});

clickイベントが正常に動作するか確認するためにカード名義人の項目の名前を入力して”サブスクリプション”ボタンをクリックすると入力した名前が表示されることを確認してください。

カード情報の送信

入力したカードの情報をStripeに送信するためにstipe.confirmCardSetupメソッドを実行します。confirmCardSetupメソッドではStripeでカードの確認などが行われます。

stipe.confirmCardSetupメソッドの構文はStripeのドキュメントを確認すると以下であることがわかります。


stripe
  .confirmCardSetup('{SETUP_INTENT_CLIENT_SECRET}', {
    payment_method: {
      card: cardElement,
      billing_details: {
        name: 'Jenny Rosen',
      },
    },
  })
  .then(function(result) {
    // Handle result.error or result.setupIntent
  });

paymenet_methodのプロパティのcardはcardElement(入力フォームに表示されていたカード番号、有効期限、CVC)とbilling_detalsのnameが必要になります。nameには設定する値はイベントの設定でカード名義人のinput要素から取得できることを先ほど確認したものです。重要なのがSETUP_INTENET_CLIENT_SECRETです。将来の支払いに備えてカード情報をStripe上に保存したい場合はSetupIntentsというオブジェクトを作成する必要があります。将来の支払いというのは例えばサブスクリプションを契約したが請求までに2週間トライアル期間がありその期間が完了すると請求が行われるといったものです。

SetupIntentsはカード情報を保存する際に必ず支払いを行う必要がありません。事前にカード情報を保存して後ほど登録したカード情報で支払いといったことができます。SetupIntentsの他にPayIntentsがありこちらは支払い時にカード情報を保存する時に利用します。CashierのSubscriptionではSeupIntentsを利用しています。

SETUP_INTENET_CLIENT_SECRETはSeuptIntentsオブジェクトのの中に含まれています。SetupIntentsはサーバ側で作成してBladeファイルに渡す必要があります。


Route::get('/subscription', function () {
    return view('subscription', [
        'intent' => auth()->user()->createSetupIntent()
    ]);
})->middleware(['auth'])->name('subscription');

JavaScript側で渡したintentを取得できるようにdata属性を利用します。


<button id="card-button" data-secret="{{ $intent->client_secret }}">
  サブスクリプション
</button>

JavaScript側では下記のようにdata属性に設定したclient_secretを取得します。


const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;

stripe.confirmCartSetupをJavaScriptのコードに追加します。


const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;

cardButton.addEventListener('click', async (e) => {
        e.preventDefault();
    const { setupIntent, error } = await stripe.confirmCardSetup(
        clientSecret, {
            payment_method: {
                card: cardElement,
                billing_details: { name: cardHolderName.value }
            }
        }
    );

    if (error) {
        // Display "error.message" to the user...
        console.log(error);
    } else {
        // The card has been verified successfully...
        console.log(setupIntent)
    }
});
Laravelのドキュメントに沿ってasync/awaitを利用していますがstirp.confirmCardSetupはPromiseを返すのでthenを利用することもできます。

テスト用のカード番号(4242 4242 4242 4242)を利用して処理が正常に動作するか確認します。テスト用のカードはこちらから確認できます。

使用期限は未来の日付を設定しCVC、その後に郵便番号は任意の数字を入れてください。テスト用のカードはアメリカのカードなので郵便番号を入力する必要があります。日本のカードの場合は郵便番号の項目は表示されません。

テスト用の番号を使って動作確認
テスト用の番号を使って動作確認

カード情報に問題がない場合はコンソールにはStripeから戻されたsetIntentのオブジェクトが表示されます。

戻されたsetupIntentの中身
戻されたsetupIntentの中身
setIntentが戻されたからといったLaravel側のテーブルに何か情報が追加されたりStripeのダッシュボード側に何か情報が追加されているわけではありません。Stripe CLIというツールを利用してイベントをリッスンしておけばここまでの処理でsetup_intent.createdとsetup_intent.succededという2つのイベントの通知を受け取ります。

ここから先の処理についてはLaravelのドキュメントには”After the card has been verified by Stripe, you may pass the resulting setupIntent.payment_method identifier to your Laravel application”と記述されており詳細なコードはありません。次はStripeから受け取ったsetupIntet.payment_methodをLaravelサーバに送信する処理が必要になります。

コンソールログのオブジェクトsetupIntentを見るとpayment_methodが含まれているのは確認できます。

送信する処理の前にLaravelのドキュメントに記載されているsetupIntet.payment_methodを受け取った後の処理を先に確認します。

Laravelサーバ側では受け取ったsetupIntet.payment_methodを利用してnewSubscriptionメソッドを利用してサブスクリプションを作成します。これでユーザに対してサブスクリプションの支払いを設定するこになります。Laravelのドキュメントに記載されている以下のコードを利用します。


use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription(
        'default', 'price_monthly'
    )->create($request->paymentMethodId);

    // ...
});

web.phpファイルにルーティングを追加します。newSubscriptionの第一引数はLaravel内でサブスクリプションを識別するための名前でdefaultを設定しています。設定した名前は後ほどmiddlewareで利用する際のアクセス制限を行う際に利用することができます。defaultという名前はsubscritionsテーブル内に保存されます。第二引数はStripe上の下価格のIDで本文書では3つの金額を作成しましたが設定しているのは990円のIDです。処理が完了したらダッシュボードにリダイレクトする設定にしています。


use Illuminate\Http\Request;
//略
Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription(
        'default', 'prod_KOSz0iTsubm5pq'
    )->create($request->paymentMethodId);

    return redirect('/dashboard');

})->middleware(['auth'])->name('subscribe.post');

サーバにpaymentMethodIdを送信する

setupIntet.payment_methodをPOSTリクエストで/user/subscribeに送信します。送信するためのコードについてはStripeのドキュメントの以前のAPIの”Stripe Elements を使用して支払いを受け付ける”を参考にしています。Charge APIを利用したコードでサーバにTokenを送信する箇所を利用しています。

Charge APIは以前に利用されていた方APIで現在も利用することができますが現在は推奨されていません。補足ですがStripeの設定を説明しているネット上の古い文書ではChange APIを使ったものが多いです。

formタグにactionとmethodを追加し、/user/subscribeにPOSTリクエストを送信できるように@csrfを追加します。csrfトークンがないとPOSTリクエストを受け取ってもらえません。actionに設定するリクエストの送信先はweb.phpファイルに追加した/user/subscribeです。


<form id="setup-form" action="{{ route('subscribe.post') }}" method="post">
  @csrf
  <input id="card-holder-name" type="text" placeholder="カード名義人" name="card-holder-name">
  <div id="card-element"></div>
  <button id="card-button" data-secret="{{ $intent->client_secret }}">
    サブスクリプション
  </button>
</form>

confirmCardSetupメソッドが成功した場合に先ほどはconsole.log(setupIntent)を実行していましたがここにstripePaymentIdHandler関数を追加します。引数にはsetupIntet.payment_methodを設定しています。またボタンをクリックするとformのPOSTリクエストを実行しないようにe.preventDefault()を追加しています。


cardButton.addEventListener('click', async (e) => {
  e.preventDefault()
    const { setupIntent, error } = await stripe.confirmCardSetup(
        clientSecret, {
            payment_method: {
                card: cardElement,
                billing_details: { name: cardHolderName.value }
            }
        }
    );

    if (error) {
        // Display "error.message" to the user...
        console.log(error);
    } else {
        // The card has been verified successfully...
        stripePaymentIdHandler(setupIntent.payment_method);
    }
});

stripePaymentIdHandler関数を追加します。関数の中ではinput要素をtype属性hiddenで作成しnameにpaymentMethodId、値にpaymentMethodIdを設定してフォームに追加し、submitメソッドでPOSTリクエストを/user/subscriptに送信しています。


function stripePaymentIdHandler(paymentMethodId) {
  // Insert the paymentMethodId into the form so it gets submitted to the server
  const form = document.getElementById('setup-form');

  const hiddenInput = document.createElement('input');
  hiddenInput.setAttribute('type', 'hidden');
  hiddenInput.setAttribute('name', 'paymentMethodId');
  hiddenInput.setAttribute('value', paymentMethodId);
  form.appendChild(hiddenInput);

  // Submit the form
  form.submit();
}

ここまでの設定が完了したら動作するか確認してみましょう。サブスクリプションボタンを押すと処理が終了までに数秒かかりますが処理が完了後ダッシュボードにリダイレクトされたら設定はうまくいっています。

処理まで時間がかかるのでボタンを無効にしたりスピナーをつけたりエラーが発生した場合にユーザにエラーがわかるように処理を追加する必要があります。Stripe上のサンプルのコードなどを参考に実装してください。

サブスクリプション完了後の確認

サブスクリプションが完了するとStripeのダッシュボードでどのような登録が行われているのか確認します。支払いタブを見ると支払いが成功していることがわかります。

支払いの確認
支払いの確認

顧客を確認するとサブスクリプションでNetfilxと表示されており、次回の請求日と金額も表示されています。またemailなどの情報はフォームに入力していませんがLaravelで登録したメールアドレスも設定されています。Laravelと連携できていることも理解できます。

顧客の確認
顧客の確認

Stripe上ではなくLaravelのテーブルを確認します。ここではデータベース管理ソフトのTablePlusを利用してデータベースの中身を確認しています。usersテーブルを確認していますがstripe_idはStripeの顧客のIDと一致します。pm_typeは利用したクレジット会社、pm_last_fourはカード番号の終わりの4桁でトライアルは設定していないのでNULLです。Stripe上の顧客情報に紐づいています。

usersテーブルの確認
usersテーブルの確認

subscriptionsテーブルも確認します。nameにはnewSubscriptionの第一引数に設定した値、stripe_idはStripe上のSubscriptionのIDが設定されています。stripe_priceにはnewSubscriptionの第二引数に設定した価格のIDが設定されています。

subscriptionsテーブル
subscriptionsテーブル

subscription_itemsテーブルではstripe_idにはサブスクリプションアイテムid、stripe_productには商品のidが設定されています。ここでは登録したNetflixです。

subscription itemsテーブル
subscription itemsテーブル

LaravelのテーブルにはStripe上に登録されている情報の各種IDが登録されていることがわかります。

入力フォームにはカード名義人のみinput要素を追加しましたがselect要素やradio要素などを利用してプランの選択肢を追加しLaravelサーバに送信してnewSubscriptionの第二引数に対応する価格IDを設定すれば他のプランも選択することができます。

サブスクリプションの支払いについての機能をCashierを利用してLaravel上に実装することができるようになりました。

Single Chargesの設定

Single Chargesは一回限りの支払いに利用できる方法です。支払いが完了するとダッシュボードに支払い情報は表示されませんまた支払いを行った顧客が登録されることはありません。またサブスクリプションの場合はテーブルのusers, subscriptions, subscriptions_itemsに情報が追加されましたがSingle Chargeでは何も追加は行われません。

金額もサブスクリプションの時のように決まった価格ではなくStriep上に商品を登録する必要もありません。価格は自由に設定することができます。

サブスクリプションではサーバ側でSetupIntentを作成してbladeファイルに渡していましたがSingle Chargeの場合は必要ありません。利用するAPIも異なりサブスクリプションではSetupIntent APIを利用していましたがSingle ChargesではPayment Methods APIを利用しています。利用するAPIが異なるので処理で実行するメソッドは変わりますが流れはほとんど同じなのでサブスクリプションで作成した情報を元に作成を行なっていきます。

ページの作成

購入を行うことができるpurchaseページの作成を行います。/subscritionを元に/purchaseのルーティングを追加します。Single ChargesではSetupIntentの作成は必要ではありません。


Route::get('/purchase', function () {
    return view('purchase');
})->middleware(['auth'])->name('purchase');

resouces¥viewの下のsubscription.blade.phpファイルをコピーしてpurchase.blade.phpファイルを作成します。サブスクリプションの文字列を購入に変更しています。POSTリクエストの送信先をsubscription.postからpurchase.postに変更しています。purchase.postに対応するルーティングは後ほど追加します。ボタンにdata属性をつけてclient_secretを設定していましたが必要なので削除しています。


<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Purchase') }}
    </h2>
</x-slot>

<div class="py-12">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
            <div class="p-6 bg-white border-b border-gray-200">
              <h2>購入</h2>
              <form id="setup-form" action="{{ route('purchase.post') }}" method="post">
                @csrf
                <input id="card-holder-name" type="text" placeholder="カード名義人" name="card-holder-name">
                <div id="card-element"></div>
                <button id="card-button">
                  購入
                </button>
              </form>
            </div>
        </div>
    </div>
</div>

JavaScript部分は以下となります。大きな変更点はclientSecretを削除してStripeにカード情報を送信するメソッドをconfirmCardSetupからcreatePaymentMethodに変更になっていることです。戻り値はSetupIntentではなくPaymentMethodのオブジェクトになります。Laravelサーバ側にはPaymehtMethodに含まれるidをPOSTリクエストで送信します。


const stripe = Stripe('your_key');

const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');

const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');

cardButton.addEventListener('click', async (e) => {
  e.preventDefault()
  const { paymentMethod, error } = await stripe.createPaymentMethod(
            'card', cardElement, {
                billing_details: { name: cardHolderName.value }
            }
        );

    if (error) {
        // Display "error.message" to the user...
        console.log(error);
    } else {
        // The card has been verified successfully...
        stripePaymentIdHandler(paymentMethod.id);
    }
});

function stripePaymentIdHandler(paymentMethodId) {
  // Insert the paymentMethodId into the form so it gets submitted to the server
  const form = document.getElementById('setup-form');

  const hiddenInput = document.createElement('input');
  hiddenInput.setAttribute('type', 'hidden');
  hiddenInput.setAttribute('name', 'paymentMethodId');
  hiddenInput.setAttribute('value', paymentMethodId);
  form.appendChild(hiddenInput);

  // Submit the form
  form.submit();
}

POSTリクエスト先のルーティングを追加します。サブスクリプションの時とは異なりchargeメソッドを利用して支払い処理を実行します。実行時には価格と受け取ったpaymentMethodIdを引数に指定しています。


Route::post('/purchase', function (Request $request) {
    $request->user()->charge(
        100, $request->paymentMethodId
    );

    return redirect('/dashboard');

})->middleware(['auth'])->name('purchase.post');

デフォルトでは価格の単価がusdなので.envファイルにCASHIER_CURRENCYを設定してjpyを設定すると円になります。


CASHIER_CURRENCY=jpy

ここまでの設定ができたらpurchaseページから購入ボタンをクリックします。処理が成功したらダッシュボードのページにリダイレクトされます。

Sigle Chargesを実行
Sigle Chargesを実行

処理が完了したらStripeのダッシュボードを確認します。支払い情報には設定した100円を確認することができます。顧客には購入者の情報は登録されていません。

支払い情報
支払い情報

ここまでの設定でSubsctiptionとSingle Chargesの違いと支払いに関する設定方法を理解することができました。

トライアル期間の設定

サブスクリプションのサービスでは契約後1ヶ月の無料トライアル期間がありそのトライアル期間が完了すると課金が行われるものもあります。Laravel Cashierではトライアル期間の設定を行うことができます。

サブスクリプション設定時にトライアル期間10日を設定し通常のサブスクリプションとの違いを確認します。traialDaysのメソッドを追加に期間を設定しています。


Route::post('/user/subscribe', function (Request $request) {
    $request->user()->newSubscription('default', 'price_1JjzyoC94s8gJpHed2AJy9tk')
                    ->trialDays(10)
                    ->create($request->paymentMethodId);

    return redirect('/dashboard');

})->middleware(['auth'])->name('subscribe.post');

ユーザを新たに登録しsubscriptionページからサブスクリプションを行います。ダッシュボードを確認するとトライアル期間が10日あるので支払いには情報は追加されていません。顧客の情報を確認するとサブスクリプションのトライアル終了日と次回の請求を確認することができます。

トライアル期間
トライアル期間

subscriptionsテーブルを見るとトライアル期間が設定されている場合にはtraials_end_atに日付が入っていることが確認できます。1行目がトライアル期間の設定がないもの2行目がトライアル期間が設定されているサブスクリプションです。

subscriptionsテーブル
subscriptionsテーブル

トライアル期間かどうか確認するためのメソッドもありトライアル期間のユーザのみにトライアル終了日を表示させるということも可能です。トライアル期間でないユーザには表示されません。


@if(auth()->user()->onTrial())
<p>{{ auth()->user()->trialEndsAt()->format('Y-m-d')}}までトライアル期間中です。</p>
@endif
トライアル期間の終了日の表示
トライアル期間の終了日の表示

サブスクリプションによるアクセス制限

Netflixなどの動画のサブスクリプションでは契約するかしないかで視聴できるかどうかが決まりますがサブスクリプションの契約をすると閲覧できるページが増えるといったサービスもあります。Cashierを利用してサブスクリプションの契約をしているかどうかでページへのアクセスを制限する方法を確認します。

トライアル期間かどうか確認できるメソッドがあったようにサブスクリプションを契約しているかどうか確認するメソッドも存在します。引数のdefaultはnewSubscriptionメソッドの引数に設定したサブスクリプションを識別する名前です。


$user->subscribed('default')

新たにサブスクリプションの契約者のみアクセスできるページbasicを追加するためルーティング/basicを追加します。


Route::get('/basic', function () {
    return view('basic');
})->middleware(['auth'])->name('basic');

dashboard.blade.phpファイルをコピーしてbasic.blade.phpファイルを作成します。


<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Basic') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 bg-white border-b border-gray-200">
                    Subscription契約者のみアクセス可能なページです。
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

navigation.blade.phpファイルにもリンクを追加します。


<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
    <x-nav-link :href="route('purchase')" :active="request()->routeIs('purchase')">
        {{ __('Purchase') }}
    </x-nav-link>
</div>
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
    <x-nav-link :href="route('basic')" :active="request()->routeIs('basic')">
        {{ __('Basic') }}
    </x-nav-link>
</div>

追加したbasicページは以下のように表示されます。今の状態では登録したユーザならだれでもアクセスすることができます。

basicページ
basicページ

ルーティングに対してアクセス制限を行いたい時に利用できるものにmiddlewareがあります。アクセス制限であれば他にも方法はあると思いますがLaravelのドキュメントにもこういった場合にmiddlewareを利用するのはgreate candidateと記述されています。middlewareではLaravelのアプリケーションに対して外側から入ってくるRequestの中身をチェックすることができます。

middlewareをphp artisanコマンドで作成します。app¥Http¥middlewareの下にEnsureUseIsSubscribed.phpファイルが作成されます。


 % php artisan make:middleware EnsureUserIsSubscribed
Middleware created successfully.

middlewareにはhandleメソッドが記述されており、handleメソッドが実行されます。


class EnsureUserIsSubscribed
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        return $next($request);
    }
}

追加する行はユーザはログインが完了しており、サブスクリプションを契約している場合にはそのまま次の処理に進みそうでない場合はsubscriptionのページにリダイレクトするといった内容です。


public function handle(Request $request, Closure $next)
{

    if ($request->user() && ! $request->user()->subscribed('default')) {
        // This user is not a paying customer...
        return redirect('subscription');
    }

    return $next($request);
}

middlewareを作成後は忘れずにapp¥Http¥Kerenel.phpファイルに追加を行う必要があります。最後にbasicという名前でmiddlewareを登録しています。


protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    'basic' => \App\Http\Middleware\EnsureUserIsSubscribed::class, //追加
];

kernel.phpファイルへのmiddlewareの登録が完了したらルーティングに追加したmiddlewareのbasicを実行するように追加します。


Route::get('/basic', function () {
    return view('basic');
})->middleware(['auth','basic'])->name('basic');

サブスクリプションで契約を行なっているユーザでアクセスを行うとページは先ほどと同様に閲覧することができますがサブスクリプションの契約を行なっていないユーザでアクセスするとSubscriptionにリダイレクトされます。トライアルのユーザもアクセス可能です。

本文書では商品の登録を行なった際に3つのプランを設定していました。次はプランによってもアクセスできるページを制限できるかどうか確認するために一人のユーザのプランをメソッドを利用して変更し動作確認を行います。

メソッドによるプランの変更

プランの変更はswapメソッドを利用して実行することができます。


$user->subscription('default')->swap('price_1JjzyoC94s8gJpHeOcm81a2d');

通常はプランを変更する画面が必要となりますがここでは/dashboardにアクセスしてきたユーザのプランを変更します。swapの引数にはStripeのダッシュボードの商品から価格のIDを取得して設定を行います。


Route::get('/dashboard', function () {
    auth()->user()->subscription('default')->swap('price_1JjzyoC94s8gJpHeOcm81a2d');
    return view('dashboard');
})->middleware(['auth'])->name('dashboard');

サブスクリプションを契約しているユーザで/dashboardにアクセス後にStripeのダッシュボードでそのユーザのサブリスクリプションの確認を行います。/dashboardにアクセス後は追加したswapの行は削除してください。

Stripeのダッシュボード上では次回の請求書の金額が変更されていることが確認できます。請求の詳細は次回のインボイスで確認できます。

プランの変更
プランの変更

次回のインボイスの内容を確認すると請求の詳細がわかります。

請求額の更新
請求額の更新

プランによるアクセス制限

プランをbasicからstandardに変更したのでstandardのみアクセスできる場所を追加します。basicと行った方法と同じ方法で行ってください。

starndard用に別のmiddlewareを作成します。


 % php artisan make:middleware EnsureUserIsSubscribedStandard
Middleware created successfully.

先ほどのmiddlewareとは異なり、subscribedメソッドの第二引数に商品の価格のIDを設定しています。この設定でこの価格で契約しているユーザのみアクセスが可能となります。


public function handle(Request $request, Closure $next)
{
    if ($request->user() && ! $request->user()->subscribed('default','price_1JjzyoC94s8gJpHeOcm81a2d')) {
        // This user is not a paying customer...
        return redirect('basic');
    }
    return $next($request);
}

public function handle(Request $request, Closure $next)
{
    if ($request->user() && ! $request->user()->subscribed('default','price_1JjzyoC94s8gJpHeOcm81a2d')) {
        // This user is not a paying customer...
        return redirect('basic');
    }
    return $next($request);
}

Kernel.phpファイルに作成したmiddlewareを追加します。basicのmiddlewareも残した状態でstandardを追加します。


protected $routeMiddleware = [
//略
    'basic' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
    'standard' => \App\Http\Middleware\EnsureUserIsSubscribedStandard::class,
];

ルーティングのmiddlewareにもstandardを追加します。


Route::get('/standard', function () {
    return view('standard');
})->middleware(['auth','standard'])->name('standard');

サブスクリプションのstandardを契約しているユーザのみstandardページにアクセスすることができます。basicとstandardを契約しているユーザはどちらもbasicページにアクセスすることができます。サブスクリプションの契約があるかどうかだけではなくプランによってアクセスできるページの制限を行うことができました。

逆にsubscriptionページのようなサブスクリプションの設定が完了しているユーザがアクセスできないようにする時もmiddlewareを利用することができます。$request->user()->subscribedがtrueの場合にbasicにリダイレクトするように設定を行います。リダイレクトする場所は任意です。


if ($request->user() && $request->user()->subscribed('default')) {
      return redirect('basic');
}

Laravel Cashierを利用してサブスクリプションのサービスの設定方法について確認を行ってきました。Cashierにはサブスクリプションに関連するメソッドが備わっていることも理解してもらえたのではないでしょうか。