> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel Cashier (Stripe)

> 說明如何使用 Laravel Cashier (Stripe) 實作訂閱扣款、一次性付款、發票與 Webhook 的基本流程。

## 概觀

Laravel Cashier (Stripe) 是官方套件，讓你可以在 Laravel 中操作 Stripe 的計費功能。你可以透過統一的 API 實作訂閱的建立、狀態確認、取消、一次性付款、發票下載與 Webhook 處理。

## 安裝與設定

首先安裝 Cashier 並建立必要的資料表。

```shell theme={null}
composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
```

接著在 `App\Models\User` 等計費對象模型加入 `Billable` trait。

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}
```

在 `.env` 中設定 Stripe 金鑰。

```ini theme={null}
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
```

## 顧客管理

若尚未建立 Stripe Customer，可以使用 `createOrGetStripeCustomer()`。

```php theme={null}
$stripeCustomer = $user->createOrGetStripeCustomer();
```

若要明確建立，使用 `createAsStripeCustomer()`。

```php theme={null}
$stripeCustomer = $user->createAsStripeCustomer();
```

## 訂閱

### 建立新訂閱

透過 `newSubscription()` 與 `create()` 開始訂閱。
`$paymentMethodId` 請傳入透過 Stripe.js 等取得的 Payment Method ID。

```php theme={null}
$user->newSubscription('default', 'price_monthly')
    ->create($paymentMethodId);
```

`price_monthly` 只是範例。實作時請指定你在 Stripe Dashboard 中建立的實際 Price ID。

### 狀態確認

可透過 `subscribed()` 檢查訂閱是否有效。

```php theme={null}
if ($user->subscribed('default')) {
    // Active subscription...
}
```

### 取消與恢復

```php theme={null}
$user->subscription('default')->cancel();

if ($user->subscription('default')->onGracePeriod()) {
    // The user is on the grace period...
}

$user->subscription('default')->resume();
```

```mermaid theme={null}
stateDiagram-v2
    [*] --> Active
    Active --> GracePeriod: cancel()
    GracePeriod --> Active: resume()
    GracePeriod --> Canceled: grace period ends
    Active --> Canceled: cancelNow()
```

## 一次性付款 (Charge)

透過 `charge()` 可以進行一次性扣款。金額請以幣別的最小單位傳入（例如 USD 時，`100` 代表 `$1.00`）。
此處的 `$paymentMethodId` 同樣使用在 Stripe 建立的 Payment Method ID。

```php theme={null}
$payment = $user->charge(100, $paymentMethodId);
```

若扣款失敗，`charge()` 會拋出例外。

## Payment Element

使用 Stripe 的 [Payment Element](https://stripe.com/docs/payments/payment-element)，可以統一處理信用卡、Apple Pay、Google Pay、iDEAL 等多種付款方式。

### 用於訂閱

建立 Setup Intent 並傳入視圖。

```php theme={null}
return view('subscribe', [
    'intent' => $user->createSetupIntent()
]);
```

在 Blade 視圖中掛載 Payment Element，並傳入 Setup Intent 的 `client_secret`。

```html theme={null}
<div id="payment-element"></div>
<button id="submit">Subscribe</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $intent->client_secret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmSetup({
            elements,
            confirmParams: {
                return_url: '{{ route("subscription.complete") }}',
            },
        });

        if (error) {
            // 向使用者顯示錯誤訊息...
        }
    });
</script>
```

Stripe 的重新導向網址會附加 `setup_intent` 參數。可透過它取得 Payment Method ID 並建立訂閱。

```php theme={null}
use Illuminate\Http\Request;

Route::get('/subscription/complete', function (Request $request) {
    $setupIntent = $request->user()->findSetupIntent(
        $request->setup_intent
    );

    $paymentMethod = $setupIntent->payment_method;

    $request->user()
        ->newSubscription('default', 'price_xxx')
        ->create($paymentMethod);

    return redirect('/dashboard');
})->name('subscription.complete');
```

### 用於一次性付款

在一次性付款中透過 `pay()` 建立 Payment Intent。Order 模型需要 `user_id`、`amount`、`status`、`stripe_payment_intent_id` 欄位。

```php theme={null}
use App\Models\Order;
use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $amount = 1000;

    $payment = $request->user()->pay($amount);

    $order = Order::create([
        'user_id' => $request->user()->id,
        'amount' => $amount,
        'status' => 'pending',
        'stripe_payment_intent_id' => $payment->id,
    ]);

    return view('checkout', [
        'clientSecret' => $payment->client_secret,
        'order' => $order,
    ]);
});
```

掛載 Payment Element 並確認付款。

```html theme={null}
<div id="payment-element"></div>
<button id="submit">Pay Now</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $clientSecret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmPayment({
            elements,
            confirmParams: {
                return_url: '{{ route("payment.complete") }}',
            },
        });

        if (error) {
            // 向使用者顯示錯誤訊息...
        }
    });
</script>
```

重新導向後，透過 `payment_intent` 參數找出對應的 Order，確認 Payment Intent 屬於已認證使用者且狀態為 `succeeded`，再確定訂單。

```php theme={null}
use App\Models\Order;
use Illuminate\Http\Request;

Route::get('/payment/complete', function (Request $request) {
    $order = Order::where('user_id', $request->user()->id)
        ->where('stripe_payment_intent_id', $request->payment_intent)
        ->firstOrFail();

    $paymentIntent = $request->user()
        ->stripe()
        ->paymentIntents
        ->retrieve($request->payment_intent);

    if ($paymentIntent->customer === $request->user()->stripe_id &&
        $paymentIntent->status === 'succeeded') {
        $order->update(['status' => 'paid']);

        // 確定訂單的處理...
    }

    return redirect('/dashboard');
})->name('payment.complete');
```

```mermaid theme={null}
sequenceDiagram
    participant User as 使用者
    participant App as Laravel<br>應用程式
    participant Stripe

    User->>App: POST /pay
    App->>Stripe: 以 pay() 建立 PaymentIntent
    Stripe-->>App: client_secret
    App-->>User: 顯示 checkout 視圖
    User->>Stripe: confirmPayment()
    Stripe-->>User: 導向 return_url
    User->>App: GET /payment/complete
    App->>Stripe: 取得並驗證 PaymentIntent
    App->>App: order.status = paid
    App-->>User: 導向儀表板
```

## 發票

發票清單可透過 `invoices()` 取得。

```php theme={null}
$invoices = $user->invoices();
```

如需 PDF 下載，請安裝 `dompdf/dompdf` 並使用 `downloadInvoice()`。`$invoiceId` 請傳入透過 `invoices()` 取得的發票 ID。

```shell theme={null}
composer require dompdf/dompdf
```

```php theme={null}
return $user->downloadInvoice($invoiceId);
```

## 設定 Webhook

Cashier 會自動註冊 Stripe Webhook 專用的路由，預設使用 `/stripe/webhook`。請於 Stripe Dashboard 設定此 URL。

可透過 `cashier:webhook` 建立 Webhook。

```shell theme={null}
php artisan cashier:webhook
```

請將 `stripe/*` 排除於 CSRF 保護之外。

```php theme={null}
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
    ]);
})
```

在 `.env` 中設定 `STRIPE_WEBHOOK_SECRET`，Cashier 的簽章驗證中介軟體便可驗證 Webhook 請求。


## Related topics

- [從 Laravel 10 升級到 11](/zh-TW/blog/upgrade-10-to-11.md)
- [2026 年 6 月 Laravel 更新](/zh-TW/blog/changelog/202606.md)
- [CSRF 保護](/zh-TW/csrf.md)
- [Laravel 11 以後的應用程式結構](/zh-TW/advanced/app-structure.md)
- [Service Container](/zh-TW/service-container.md)
