「カートに入れる」ボタンから決済完了まで、LaravelとStripeを組み合わせた実装手順を解説。Webhookの落とし穴や本番移行の注意点も現場経験をもとに紹介します。
こんな悩み、ありませんか?
「問い合わせ対応で受注しているけど、そろそろオンライン決済を導入したい」「既存のLaravelサイトにEC機能を後付けしたいが、どこから手をつければいいかわからない」——中小企業のWeb担当者からこういった相談が、弊社にも月に数件寄せられます。
結論から言うと、StripeとLaravelを組み合わせれば、最短1週間でオンライン決済機能を動かせます。ただし「動く」と「安全・安定に動く」は別の話。本記事では、実案件で得た知見をもとに、正しい実装手順と、見落としがちな落とし穴を丁寧に解説します。
あわせて読みたい
なぜStripeなのか?他の決済サービスとの比較
決済サービスを選定する際、よく候補に上がるのがStripe、PAY.JP、SBペイメントサービスの3つです。それぞれに特徴がありますが、Laravelとの相性という観点では、Stripeが頭ひとつ抜けています。
| 比較項目 | Stripe | PAY.JP | SBペイメント |
|---|---|---|---|
| 公式Laravel SDK | |||
| Webhook対応 | |||
| テスト環境 | |||
| 月額固定費 | なし | なし | あり |
| 手数料(目安) | 3.6% | 3.0% | 要見積 |
| ドキュメントの充実度 | ◎ | ○ | △ |
Stripeが特に優れているのは、Laravel Cashierという公式パッケージが提供されている点です。サブスクリプション管理からInvoice発行まで、よく使う機能が事前に実装されており、開発工数を大幅に削減できます。手数料は若干高めですが、開発コストを含めたトータルで見ると、Stripeを選ぶケースが多くなります。
ある神奈川の食品卸会社では、PAY.JPで実装しようとした見積もりより、Stripeを使った実装のほうが開発費用で約20万円安く収まりました。API設計のシンプルさが、そのまま工数削減に直結した好例です。
実装の全体像を把握する
実装前に、処理の流れ全体を把握しておくことが重要です。細かいコードに入る前に、まずフローを確認しましょう。
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)を必ず実装してください。これを省くと、偽のリクエストで注文が作成される脆弱性が生まれます。
よくある失敗パターンと対処法
実装スケジュールの目安
この技術、御社の業務にも活かせます
業務システム開発
Laravelを使った業務システムの構築・改修を20年以上の経験でサポートします
※ 通常1営業日以内にご返信します
まとめと次のステップ
LaravelとStripeを組み合わせた決済実装は、正しい手順を踏めば1週間のスプリントで十分実現できます。ただし、Webhookの署名検証や金額のサーバーサイド管理など、セキュリティ上の落とし穴を踏み抜くと後々の修正コストが大きくなります。
まず着手すべきことは以下の通りです。
「実装はできそうだが、セキュリティやリリース後の運用が不安」「既存のLaravelサイトへの組み込みを依頼したい」という方は、ぜひ弊社Fivenine Designにご相談ください。神奈川を拠点に20年以上、中小企業のWeb開発を支援してきた実績をもとに、最適な提案をいたします。