Laravel 2026.07.10

LaravelでStripe決済を実装!中小企業が最短1週間でEC機能を追加する手順

約17分で読めます

「カートに入れる」ボタンから決済完了まで、LaravelとStripeを組み合わせた実装手順を解説。Webhookの落とし穴や本番移行の注意点も現場経験をもとに紹介します。

こんな悩み、ありませんか?

「問い合わせ対応で受注しているけど、そろそろオンライン決済を導入したい」「既存のLaravelサイトにEC機能を後付けしたいが、どこから手をつければいいかわからない」——中小企業のWeb担当者からこういった相談が、弊社にも月に数件寄せられます。

結論から言うと、StripeとLaravelを組み合わせれば、最短1週間でオンライン決済機能を動かせます。ただし「動く」と「安全・安定に動く」は別の話。本記事では、実案件で得た知見をもとに、正しい実装手順と、見落としがちな落とし穴を丁寧に解説します。


なぜStripeなのか?他の決済サービスとの比較

決済サービスを選定する際、よく候補に上がるのがStripe、PAY.JP、SBペイメントサービスの3つです。それぞれに特徴がありますが、Laravelとの相性という観点では、Stripeが頭ひとつ抜けています。

比較項目StripePAY.JPSBペイメント
公式Laravel SDK
Webhook対応
テスト環境
月額固定費なしなしあり
手数料(目安)3.6%3.0%要見積
ドキュメントの充実度

Stripeが特に優れているのは、Laravel Cashierという公式パッケージが提供されている点です。サブスクリプション管理からInvoice発行まで、よく使う機能が事前に実装されており、開発工数を大幅に削減できます。手数料は若干高めですが、開発コストを含めたトータルで見ると、Stripeを選ぶケースが多くなります。

ある神奈川の食品卸会社では、PAY.JPで実装しようとした見積もりより、Stripeを使った実装のほうが開発費用で約20万円安く収まりました。API設計のシンプルさが、そのまま工数削減に直結した好例です。


Laravelの開発・保守でお困りですか?

アップグレード代行・機能追加・パフォーマンス改善など、お気軽にご相談ください

無料で相談する

実装の全体像を把握する

実装前に、処理の流れ全体を把握しておくことが重要です。細かいコードに入る前に、まずフローを確認しましょう。

flowchart TD
    A[ユーザーが購入ボタンを押す] --> B[LaravelがPaymentIntentを作成]
    B --> C[フロントエンドにclient_secretを返す]
    C --> D[Stripe.jsが決済フォームを表示]
    D --> E{カード情報を入力・送信}
    E -->|成功| F[Stripeが決済処理]
    F --> G[WebhookでLaravelに通知]
    G --> H[注文ステータスを更新・メール送信]
    E -->|失敗| I[エラーメッセージを表示]

重要なのは、決済の「確定」はWebhookで受け取るという設計思想です。フロントエンドからの「成功しました」通知だけを信用してDBを更新すると、通信断などで不整合が発生します。この点は後述する「よくある失敗」でも詳しく触れます。


ステップ1:Stripe SDKとLaravel Cashierの導入

まずはパッケージを導入します。

composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate

次に、.envにStripeのAPIキーを設定します。Stripeダッシュボードの「開発者 → APIキー」から取得できます。

STRIPE_KEY=pk_test_xxxxxxxxxxxx
STRIPE_SECRET=sk_test_xxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx

config/cashier.phpが生成されますが、基本的にはenv経由で管理するだけで問題ありません。


ステップ2:UserモデルにBillableトレイトを追加

Laravel CashierはBillableトレイトを通じて動作します。

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
    // ...
}

これだけで、$user->charge()$user->invoices()などのメソッドが使えるようになります。


ステップ3:PaymentIntentを作成するAPIエンドポイント

購入ボタンが押されたとき、バックエンドでPaymentIntentを作成し、client_secretをフロントに返します。

// routes/api.php
Route::post('/payment/create-intent', [PaymentController::class, 'createIntent'])
    ->middleware('auth');
// app/Http/Controllers/PaymentController.php
use Stripe\PaymentIntent;
use Stripe\Stripe;

class PaymentController extends Controller
{
    public function createIntent(Request $request)
    {
        $request->validate([
             'amount' => 'required|integer|min:50',
             'product_id' => 'required|exists:products,id',
        ]);

        Stripe::setApiKey(config('cashier.secret'));

        $intent = PaymentIntent::create([
            'amount'   => $request->amount,
            'currency' => 'jpy',
            'metadata' => [
                'user_id'    => auth()->id(),
                'product_id' => $request->product_id,
            ],
        ]);

        return response()->json([
            'client_secret' => $intent->client_secret,
        ]);
    }
}

ポイントはmetadataにユーザーIDと商品IDを埋め込んでおくことです。Webhookで後処理するとき、この情報が注文データの紐付けに役立ちます。


ステップ4:フロントエンドでStripe.jsを組み込む

<script src="https://js.stripe.com/v3/"></script>
<div id="card-element"></div>
<button id="submit-btn">購入する</button>

<script>
const stripe = Stripe('{{ config("cashier.key") }}');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');

document.getElementById('submit-btn').addEventListener('click', async () => {
    const response = await fetch('/api/payment/create-intent', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}'},
        body: JSON.stringify({ amount: {{ $product->price }}, product_id: {{ $product->id }} })
    });
    const { client_secret } = await response.json();

    const result = await stripe.confirmCardPayment(client_secret, {
        payment_method: { card: cardElement }
    });

    if (result.error) {
        alert(result.error.message);
    } else {
        // Webhookで処理するため、ここでは「処理中」表示のみ
        window.location.href = '/orders/pending';
    }
});
</script>

ステップ5:Webhookで決済完了を受け取る

このステップが最も重要です。StripeはPaymentIntentの確定後、指定したURLへWebhookを送信します。

// routes/web.php(CSRFを除外する設定も必要)
Route::post('/stripe/webhook', [WebhookController::class, 'handle']);
// app/Http/Controllers/WebhookController::class
use Stripe\Webhook;
use Illuminate\Http\Request;
use App\Models\Order;

class WebhookController extends Controller
{
    public function handle(Request $request)
    {
        $payload = $request->getContent();
        $sigHeader = $request->header('Stripe-Signature');

        try {
            $event = Webhook::constructEvent(
                $payload,
                $sigHeader,
                config('cashier.webhook.secret')
            );
        } catch (\Exception $e) {
            return response('Invalid signature', 400);
        }

        if ($event->type === 'payment_intent.succeeded') {
            $intent = $event->data->object;
            Order::create([
                'user_id'           => $intent->metadata->user_id,
                'product_id'        => $intent->metadata->product_id,
                'stripe_payment_id' => $intent->id,
                'amount'            => $intent->amount,
                'status'            => 'paid',
            ]);
            // 確認メール送信など
        }

        return response('OK', 200);
    }
}

署名検証(Webhook::constructEvent)を必ず実装してください。これを省くと、偽のリクエストで注文が作成される脆弱性が生まれます。


よくある失敗パターンと対処法

Stripe.jsからの`result.paymentIntent.status === 'succeeded'`を受け取った直後にDBを更新する実装が散見されますが、これは危険です。通信エラーやユーザーのブラウザバックで二重処理や未処理が発生します。**DBの更新は必ずWebhookのみで行う**という原則を守ってください。
Laravelのデフォルトでは全POSTリクエストにCSRFチェックが入ります。WebhookのURLを`VerifyCsrfToken`ミドルウェアの`$except`配列に追加するか、`routes/api.php`に記述して対応してください。これを忘れると本番でWebhookが全て419エラーになります。弊社の実案件でも、リリース直後に発覚して急遽修正した経験があります。
Stripeはテスト用(`sk_test_...`)と本番用(`sk_live_...`)でキーが異なります。本番デプロイ時にキーの切り替えを忘れ、「購入できた気がしたのに売上が入っていない」というトラブルが起きやすいポイントです。デプロイチェックリストに必ず含めましょう。
購入金額をリクエストパラメータでそのまま受け取り、PaymentIntentに渡す実装は脆弱です。ユーザーが金額を書き換えて送信できてしまいます。**金額は必ずサーバーサイドでproduct_idをもとにDBから取得する**ように設計してください。

実装スケジュールの目安

Day1〜2
環境構築・SDK導入
Stripe登録、Cashierインストール、テストキー設定
Day3〜4
バックエンド実装
PaymentIntent作成API、Webhookエンドポイント構築
Day5
フロントエンド実装
Stripe.js組み込み、決済フォームUI
Day6
テスト
Stripeテストカードで全フローを検証、Webhookのローカルテスト(Stripe CLI)
Day7
本番リリース
キー切り替え、SSL確認、本番Webhook登録

Laravelの開発・保守でお困りですか?

アップグレード代行・機能追加・パフォーマンス改善など、お気軽にご相談ください

無料で相談する

この技術、御社の業務にも活かせます

業務システム開発

Laravelを使った業務システムの構築・改修を20年以上の経験でサポートします

200件以上の制作実績 顧客満足度97% 初回相談無料

※ 通常1営業日以内にご返信します

まとめと次のステップ

LaravelとStripeを組み合わせた決済実装は、正しい手順を踏めば1週間のスプリントで十分実現できます。ただし、Webhookの署名検証や金額のサーバーサイド管理など、セキュリティ上の落とし穴を踏み抜くと後々の修正コストが大きくなります。

まず着手すべきことは以下の通りです。

「実装はできそうだが、セキュリティやリリース後の運用が不安」「既存のLaravelサイトへの組み込みを依頼したい」という方は、ぜひ弊社Fivenine Designにご相談ください。神奈川を拠点に20年以上、中小企業のWeb開発を支援してきた実績をもとに、最適な提案をいたします。

この記事をシェア

Laravelの開発・運用、プロに任せませんか?

システム構築からバージョンアップまで、経験豊富なエンジニアがサポートします。 初回相談は無料です。

※ 1営業日以内にご返信いたします

この技術でお困りなら

無料でプロに相談できます

相談する
AIに無料相談