Laravelで課金する方法(Laravel Cashier + Stripe)【ダウンロード可】

さてさて、前回の意外と簡単!Laravel で全文検索をつくるでは、Laravelが提供する公式パッケージ「Laravel Scout」で全文検索する機能をつくってみました。

しかし、実はLaravelには公式パッケージがいくつか他にも公開されていて、私達がウェブサイト開発する上でたまに「うーん、ホントはほしいけど絶対失敗できなし、自前でつくるのはちょっと怖いかも・・・」と考える機能も提供してくれています。

それは何かと言うと、

課金システム(サブスクリプション)

です。

つまり、毎月自動的に利用料を支払うシステムのことで、身近な例で言うと「アマゾン・プライム」だとか、有料メルマガを思い浮かべるとわかりやすいかもしれません。

ということで、今回は「Laravel Cashier + Strip」で月額課金システムをつくってみたいと思います。

最後に今回つくったプログラムをダウンロードできますよ!😊✨

開発環境: Laravel 5.8 + Vue 2.6

やりたいこと

まずはゴールとして以下の6つを実装することにします。

  • 月額課金ができる
  • 課金のプランは「プレミアム(1,000円/月)」と「ベーシック(500円/月)」の2種類
  • 課金のキャンセルができる
  • キャンセルしたものを元に戻すことができる
  • プランの変更ができる
  • クレジットカードの変更ができる

前提として

Laravelの「php artisan make:auth」でログイン機能が有効になっていて、テストユーザーが登録されているものとします。(詳しくは、【Laravel5.6】インストール直後にやること3点をご覧ください)

また、決済サービスのStripeにアカウントを作成しておいてください。(←私はすでに昔作ってしまったのでスクリーンショットはありません。ごめんなさい😅 Stripeは日本語対応してますのでそれほど難しくないです)

※ ちなみに、ローカル環境でテストする場合でもstripeが提供するjsライブラリにはhttpsで接続している必要が有ります。もしローカル環境にhttpsを導入する場合は、コピペでOK!ローカル環境にHTTPSを導入する(nginx編)を参考にしてみてください。

Stripeの設定をする

それでは、ここからStripeにログインして設定をしていきます。
今回は説明なので、テスト・バージョンの設定になりますが、本番環境の場合はそれぞれ置き換えて実行してください。

APIキーを取得する

画面左側にある「開発者」というリンクをクリックします。

さらに「APIキー」をクリックします。

すると、Standard Keysの項目に「公開可能キー」「シークレットキー」が表示されます。

この2つのキーを以下のように.envに登録しておきましょう。

STRIPE_KEY=************************
STRIPE_SECRET=***************************

ウェブフックのキーを取得する

続いてウェブフックのキーを取得します。
ウェブフックは課金やキャンセルが実行された直後にPOSTメソッドでLaravel側にアクセスしてくるもので、課金の失敗や各種データの変更を管理してくれることになります。

では、メニューから「Webhook」をクリックします。

画面右上にある「+ エンドポイントを追加」をクリック。

すると、入力フォームがポップアップされるので、

  • エンドポイントURL
  • 送信イベント(すべてのイベントを受信をクリック。お好みで選んでもOK)

を入力して「エンドポイントを追加」ボタンをクリックしてください。

クリックすると登録した情報が表示されますので、その中から署名シークレットにあるウェブフックをよりセキュリティに強くするキー「署名シークレット」をコピーしてください。

署名シークレットは.envに以下のように登録しておきましょう。

STRIPE_WEBHOOK_SECRET=****************************

プランを追加する

課金のプラン「プレミアム」と「ベーシック」を追加します。
メニューの「Billing > 商品」をクリックしてください。

そして、「+ 新規」というボタンをクリック。

すると入力フォームが現れるので、商品名を入力して「商品を作成」ボタンをクリックしてください。

そして、次は料金プランを追加です(まずは「プレミアム」から登録します)

以下のように、必須入力になっている

  • プランのニックネーム
  • 料金体系
  • 通貨
  • 単価
  • 請求間隔

を入力&選択して「料金プランを追加」ボタンをクリックしてください。

ボタンをクリックすると詳細が表示されるので、「料金プラン」にある「+ 料金プランを追加」をクリックして「ベーシック」プランも同様に追加してください。

2つのプランが登録できたら以下のように表示されているはずです。

「ベーシック」もしくは「プレミアム」をクリックすると詳細が表示されますので、「詳細」項目のIDをそれぞれ.envconfig/services.phpに登録しておいてください。(後でプランを選択する部分で使います)

(.env)

STRIPE_BASIC_ID=plan_**************
STRIPE_PREMIUM_ID=plan_**************

(config/services.php)

'stripe' => [
    'model' => App\User::class,
    'key' => env('STRIPE_KEY'),
    'secret' => env('STRIPE_SECRET'),
    'webhook' => [
        'secret' => env('STRIPE_WEBHOOK_SECRET'),
        'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300),
    ],
    'plans' => [
        env('STRIPE_BASIC_ID') => 'ベーシック',
        env('STRIPE_PREMIUM_ID') => 'プレミアム'
    ]
],

インストール&準備をする

パッケージのインストール

では、続いてLaravelで課金機能を開発していきましょう。
まず以下のLaravel Cashierの専用パッケージをインストールします。

composer require laravel/cashier

パッケージがインストールされたら、app/User.phpで課金機能が使えるようにBillableトレイトを追加します。

<?php

namespace App;

// 省略

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Notifiable, Billable;

    // 省略

これで、課金するためのメソッドが利用できるようになりました。

DBテーブルの準備

パッケージのインストールが完了したら、DBのテーブル環境を整えます。

まずはusersテーブルにクレジットカード情報(の一部)が保存できるように項目を追加します。
以下のコマンドでマイグレーション・ファイルを作成してください。

php artisan make:migration add_stripe_to_users_table

すると、database/migrations/****_**_**_******_add_stripe_to_users_table.phpが作成されるので中身を以下のように変更します(太字が変更した部分です)

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddStripeToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('stripe_id')
                ->nullable()
                ->collation('utf8mb4_bin')
                ->after('remember_token');
            $table->string('card_brand')
                ->nullable()
                ->after('stripe_id');
            $table->string('card_last_four', 4)
                ->nullable()
                ->after('card_brand');
            $table->timestamp('trial_ends_at')
                ->nullable()
                ->after('card_last_four');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('stripe_id');
            $table->dropColumn('card_brand');
            $table->dropColumn('card_last_four');
            $table->dropColumn('trial_ends_at');
        });
    }
}

次に課金情報を保存しておくsubscriptionsテーブルを作成します。同じく以下のコマンドでマイグレーション・ファイルを作成してください。

php artisan make:migration create_subscriptions_table

そして、database/migrations/****_**_**_******_create_subscriptions_table.phpを開いて以下のようにします。

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateSubscriptionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('subscriptions', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id');
            $table->string('name');
            $table->string('stripe_id')->collation('utf8mb4_bin');
            $table->string('stripe_plan');
            $table->integer('quantity');
            $table->timestamp('trial_ends_at')->nullable();
            $table->timestamp('ends_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('subscriptions');
    }
}

これで必要なマイグレーションを2つ作成できました。
以下のコマンドでマイグレーションを実行しておきましょう。

php artisan migrate

コマンドを実行したらテーブルは以下のようになります。

課金ページをつくる

では、ここからは課金を実行するページを作っていきます。
ページ構成としては、ページは1つだけで状態によってVueで表示項目を切り替えていきます。

また、データの保存や課金の実行はすべてAjax通信で行います。

ルーティングをつくる

では、routes/web.phpに課金ページのためのルーティングを追加してください。

Route::prefix('user')->middleware(['auth'])->group(function() {

    // 課金
    Route::get('subscription', 'User\SubscriptionController@index');
    Route::get('ajax/subscription', 'User\Ajax\SubscriptionController@index');

});

// Stripeのウェブフック
Route::post(
    'stripe/webhook',
    '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);

※ ログイン後のみ表示させるのでauthミドルウェアを有効にしています。

なお、ウェブフックはこのままでは「CSRFブロック」が働いてアクセス拒否されてしまいますので、app/Http/Middleware/VerifyCsrfToken.phpを開いて解除しておいてください。

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    // 省略

    protected $except = [
        'stripe/*',
    ];
}

※ ちなみに、stripeのウェブサイトからテストのウェブフック送信もできますので開発が完了したらテストしてみるといいでしょう。

コントローラーをつくる

続いて、コントローラーです。
まず、ブラウザで表示するページのコントローラーを作ります。
以下のコマンドを実行してください。

php artisan make:controller User\\SubscriptionController

すると、app/Http/Controllers/User/SubscriptionController.phpが作成されるので、以下のように変更します。

<?php

namespace App\Http\Controllers\User;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class SubscriptionController extends Controller
{
    public function index() {

        return view('user.subscription.index');

    }
}

続いて以下のコマンドでAjax用のコントローラーを作成してください。

php artisan make:controller User\\Ajax\\SubscriptionController

app/Http/Controllers/User/Ajax/SubscriptionController.phpの中身を以下のように変更します。

<?php

namespace App\Http\Controllers\User\Ajax;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class SubscriptionController extends Controller
{
    // 課金を実行
    public function subscribe(Request $request) {

        $user = $request->user();

        if(!$user->subscribed('main')) {

            $token = $request->token;
            $plan = $request->plan;
            $user->newSubscription('main', $plan)->create($token);
            $user->load('subscriptions');

        }

        return $this->status();

    }

    // 課金をキャンセル
    public function cancel(Request $request) {

        $request->user()
            ->subscription('main')
            ->cancel();
        return $this->status();

    }

    // キャンセルしたものをもとに戻す
    public function resume(Request $request) {

        $request->user()
            ->subscription('main')
            ->resume();
        return $this->status();

    }

    // プランを変更する
    public function change_plan(Request $request) {

        $plan = $request->plan;
        $request->user()
            ->subscription('main')
            ->swap($plan);
        return $this->status();

    }

    // カードを変更する
    public function update_card(Request $request) {

        $token = $request->token;
        $request->user()
            ->updateCard($token);
        return $this->status();

    }

    // 課金状態を返す
    public function status() {

        $status = 'unsubscribed';
        $user = auth()->user();
        $details = [];

        if($user->subscribed('main')) { // 課金履歴あり

            if($user->subscription('main')->cancelled()) {  // キャンセル済み

                $status = 'cancelled';

            } else {    // 課金中

                $status = 'subscribed';

            }

            $subscription = $user->subscriptions->first(function($value){

                return ($value->name === 'main');

            })->only('ends_at', 'stripe_plan');

            $details = [
                'end_date' => ($subscription['ends_at']) ? $subscription['ends_at']->format('Y-m-d') : null,
                'plan' => \Arr::get(config('services.stripe.plans'), $subscription['stripe_plan']),
                'card_last_four' => $user->card_last_four
            ];

        }

        return [
            'status' => $status,
            'details' => $details
        ];

    }
}

やっていることは、上から

  • subscribe() ・・・ 新しい課金を登録
  • cancel() ・・・ すでに課金されているものをキャンセル
  • resume() ・・・ キャンセルされた課金を元に戻す
  • change_plan() ・・・ プラン変更
  • update_card() ・・・ クレジットカード変更

となります。

ここで重要なのが、subscribe()内の以下の部分です。

$user->load('subscriptions');

この行がないと、課金状態の更新がされないため常にunsubscribedステータスが返されることになってしまいます。(←ちょっとハマりました😅)

ちなみに、コードの中で「main」という課金名が出てきますが、今回課金システムはひとつだけを想定しているのでこの名前にしています。(もしくはprimarydefaultなどでもいいでしょう)

ビューをつくる

では、最後にビューです。
resources/views/user/subscription/index.blade.phpを作成して中身を以下のようにしてください。

<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
    <style>

        body {

            padding: 15px;

        }

        #new-card, #update-card {

            border: 1px solid #ccc;
            padding: 8px;

        }

    </style>
</head>
<body>
    <div id="app" class="container">
        <div class="card">
            <div class="card-body">
                <div v-if="!isSubscribed">
                    <div class="row">
                        <div class="col-md-4">
                            <select class="form-control" v-model="plan">
                                <option v-for="(value,key) in planOptions" :value="key" v-text="value"></option>
                            </select>
                        </div>
                        <div class="col-md-4">
                            <div id="new-card"></div>
                        </div>
                        <div class="col-md-4 text-right">
                            <button type="button" class="btn btn-primary" @click="subscribe">課金する</button>
                        </div>
                    </div>
                </div>
                <div v-else-if="isSubscribed">
                    <div v-if="isCancelled">
                        キャンセル済みです。(終了:<span v-text="details.end_date"></span>)
                        <button class="btn btn-info" type="button" @click="resume">元に戻す</button>
                    </div>
                    <!-- 課金中 -->
                    <div v-else>
                        現在、課金中です。
                        <button class="btn btn-warning" type="button" @click="cancel">キャンセル</button>

                        <hr>

                        <div class="row">
                            <div class="col-md-4">
                                課金中のプラン: <span v-text="details.plan"></span>
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-md-4">
                                <select class="form-control" v-model="plan">
                                    <option v-for="(value,key) in planOptions" :value="key" v-text="value"></option>
                                </select>
                            </div>
                            <div class="col-md-4">
                                <button class="btn btn-success" type="button" @click="changePlan">プランを変更する</button>
                            </div>
                        </div>

                        <hr>

                        <div class="row">
                            <div class="col-md-4">
                                カード情報(下4桁): <span v-text="details.card_last_four"></span>
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-md-4">
                                <div id="update-card"></div>
                            </div>
                            <div class="col-md-4">
                                <button class="btn btn-light" type="button" @click="updateCard">クレジットカードを変更する</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://js.stripe.com/v3/"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                stripe: null,
                stripeCard: null,
                publicKey: '{{ config('services.stripe.key') }}',
                status: '',
                details: {},
                plan: '',
                planOptions: {!! json_encode(config('services.stripe.plans')) !!}
            },
            methods: {
                subscribe() {

                    this.stripe.createToken(this.stripeCard).then((result) => {

                        if(result.token) {  // トークン取得成功

                            const token = result.token.id;
                            const url = '/user/ajax/subscription/subscribe';
                            const params = {
                                token: token,
                                plan: this.plan
                            };
                            axios.post(url, params)
                                .then(this.setStatus);

                        } else if(result.error) {   // 失敗

                            // 何かエラー処理

                        }

                    });

                },
                cancel() {

                    const url = '/user/ajax/subscription/cancel';
                    axios.post(url)
                        .then(this.setStatus);

                },
                resume() {

                    const url = '/user/ajax/subscription/resume';
                    axios.post(url)
                        .then(this.setStatus);

                },
                changePlan() {

                    const url = '/user/ajax/subscription/change_plan';
                    const params = { plan: this.plan };
                    axios.post(url, params)
                        .then(this.setStatus);

                },
                updateCard() {

                    this.stripe.createToken(this.stripeCard).then((result) => {

                        if(result.token) {  // トークン取得成功

                            const token = result.token.id;
                            const url = '/user/ajax/subscription/update_card';
                            const params = { token: token };
                            axios.post(url, params)
                                .then(this.setStatus);
                            this.stripeCard.clear();

                        } else if(result.error) {   // 失敗

                            // 何かエラー処理

                        }

                    });

                },
                setStatus(response) {

                    this.status = response.data.status;
                    this.details = response.data.details;

                }
            },
            computed: {
                isSubscribed() {

                    return (this.status === 'subscribed' || this.status === 'cancelled');

                },
                isCancelled() {

                    return (this.status === 'cancelled');

                }
            },
            watch: {
                status(value) {

                    Vue.nextTick(() => {

                        const selector = (value === 'unsubscribed') ? '#new-card' : '#update-card';
                        this.stripeCard = this.stripe.elements().create('card', {
                            hidePostalCode: true
                        });
                        this.stripeCard.mount(selector);

                    });

                }
            },
            mounted() {

                this.stripe = Stripe(this.publicKey);
                const url = '/user/ajax/subscription/status';
                axios.get(url)
                    .then(this.setStatus);

            }
        });

    </script>
</body>
</html>

少しコードが長くなってしまいましたが、やっていることは、以下になります。

  • Laravelから課金状態を Ajax で取得&その状態ごとに表示を切り替え
  • 新規の課金とクレジットカードを変更する場合は stripeからトークンを取得してから Ajax 送信
  • それ以外の場合は単に Ajax で Laravel 側へ送信

そして、重要な部分はwatchです。この中では、statusが変更したと同時にstripe.jsのクレジットカード入力ボックスを作成していますが、状態によってターゲットをその都度切り替えています。

なお、「前提として」でも書きましたが、stripe.jshttpsでの接続が必須となっていますので気をつけてください。

また、viewportも必須とのことですので忘れずにHTMLに追加しておいてください。

<meta name="viewport" content="width=device-width, initial-scale=1" />

実行すると以下のようになります。

(課金前)

(課金中)

(キャンセル済み)

お疲れ様でした!

ちなみに:テストのクレジットカード番号

Stripeでは以下のページで「テストのために使えるクレジットカード番号」をいくつか公開しています。

Test card numbers and tokens

なお、有効期限は未来の日付ならいつでもOKで、CVCはランダムな数字で問題ありません。

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

※ ただし、stripeの設定や.env、パッケージのインストールなどはご自身で準備する必要がありますのでご注意ください。

Laravelで課金する方法(Laravel Cashier + Stripe)

おわりに

実は移転する前のブログでLaravel Cashierの記事を公開しているのですが、Laravel CashierだけでなくstripeのAPIバージョンもアップデートされるなど変更点が多くなったため今回は、より詳しく記事をお届けしました。

そして、前回と今回を比較してみて一番いいなと思ったのは、「クレジットカードの入力ボックスをstripe.jsが作成してくれる」という部分です。

以前はわざわざクレジットカード番号、有効期限(月と年)、CVCの入力ボックスを作って独自に送信する流れだったかと思いますが、今はstripe.jsがこの部分を一手に引き受けてくれるようになっていてちょっと感動しました。(しかも、入力もしやすいですね😊✨)

ということで、現在は個人であっても課金システムを導入することがそれほど難しい時代ではなくなりました。

ぜひ皆さんもトライしてみてはいかがでしょうか。

ではでは〜!

この記事が役立ちましたらシェアお願いします😊✨