Laravel Cashier & Stripe: دليل شامل للفواتير والاشتراكات
يُبسط Laravel Cashier عالم الفواتير والاشتراكات المعقدة مع Stripe، ويوفر واجهة برمجية (API) معبرة وسلسة للتعامل مع المدفوعات المتكررة والفواتير والمزيد. سيقودك هذا البرنامج التعليمي خطوة بخطوة خلال إعداد Laravel Cashier (إصدار Stripe) في تطبيقك، من التثبيت إلى إدارة الاشتراكات والمدفوعات لمرة واحدة.
لماذا Laravel Cashier؟ #
قد يكون التكامل المباشر مع Stripe أمرًا شاقًا، ويتطلب معرفة واسعة بواجهة برمجة التطبيقات الخاصة بهم، والـ webhooks، ومنطق الفوترة المعقد. يُجرد Cashier الكثير من هذا التعقيد، مما يسمح لك بالتركيز على الميزات الأساسية لتطبيقك مع تفويض معالجة الدفع إلى حل قوي ومبني مسبقًا.
الميزات الرئيسية: #
- إدارة الاشتراكات: سهولة إنشاء الاشتراكات وتحديثها وإلغائها.
- التعامل مع الفواتير: إنشاء واسترداد فواتير PDF.
- طرق الدفع: تخزين وإدارة طرق دفع العملاء بشكل آمن.
- الـ Webhooks: مزامنة حالة الاشتراك تلقائيًا مع Stripe.
- المدفوعات لمرة واحدة: معالجة المدفوعات الفردية جنبًا إلى جنب مع الاشتراكات.
المتطلبات الأساسية #
قبل البدء، تأكد من أن لديك:
- تطبيق Laravel جديد أو موجود (يوصى بـ Laravel 10+).
- PHP 8.1+.
- Composer مثبتًا.
- حساب Stripe (التسجيل مجاني).
1. التثبيت #
أولاً، قم بتثبيت Cashier عبر Composer:
composer require laravel/cashier
بعد ذلك، قم بتشغيل ترحيلات قاعدة البيانات. سيضيف Cashier عدة أعمدة إلى جدول users الخاص بك وينشئ جدول subscriptions جديدًا.
php artisan migrate
أخيرًا، انشر ملف تكوين Cashier. يسمح لك هذا بتخصيص إعدادات مختلفة.
php artisan vendor:publish --tag="cashier-config"
2. تكوين مفاتيح API الخاصة بـ Stripe #
ستحتاج إلى مفاتيح Stripe API الخاصة بك (المفتاح القابل للنشر والمفتاح السري) من لوحة تحكم Stripe الخاصة بك. أضفها إلى ملف .env الخاص بك:
STRIPE_KEY="pk_test_YOUR_STRIPE_PUBLISHABLE_KEY"
STRIPE_SECRET="sk_test_YOUR_STRIPE_SECRET_KEY"
STRIPE_WEBHOOK_SECRET=null # سنقوم بتعيين هذا لاحقًا
3. إعداد نموذج المستخدم الخاص بك #
لتمكين نموذج User الخاص بك من التفاعل مع Cashier، تحتاج إلى استخدام سمة Billable. توفر هذه السمة طرقًا للتعامل مع الاشتراكات والمدفوعات والفواتير.
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Cashier\Billable; // استيراد سمة Billable
class User extends Authenticatable
{
use HasFactory, Notifiable, Billable; // استخدام سمة Billable
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'stripe_id', // أضف stripe_id إلى fillable
'pm_type', // أضف pm_type إلى fillable
'pm_last_four', // أضف pm_last_four إلى fillable
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
4. إنشاء المنتجات والأسعار في Stripe #
قبل أن تتمكن من إنشاء الاشتراكات، تحتاج إلى تحديد منتجاتك وأسعارها المرتبطة (الخطط) في لوحة تحكم Stripe.
- انتقل إلى لوحة تحكم Stripe الخاصة بك.
- انتقل إلى Products > Product catalog.
- انقر على + Add product.
- أعطِ منتجك اسمًا (على سبيل المثال، "الخطة الاحترافية").
- تحت Pricing، انقر على + Add another price.
- قم بتكوين السعر (على سبيل المثال، 10 دولارات شهريًا متكررة).
- لاحظ معرف السعر (Price ID) (على سبيل المثال،
price_12345ABCDEF). ستستخدم هذا في تطبيق Laravel الخاص بك.
كرر هذا لأي خطط أخرى تقدمها (على سبيل المثال، "الخطة المميزة").
5. التعامل مع الاشتراكات #
أ. جمع معلومات طريقة الدفع #
للاشتراك في مستخدم، تحتاج أولاً إلى جمع طريقة الدفع الخاصة به. توصي Stripe باستخدام Stripe Elements بطريقة آمنة ومتوافقة مع PCI للقيام بذلك. يوفر Cashier مساعدًا لإنشاء "نية إعداد" لهذا الغرض.
المسار (web.php):
use App\Http\Controllers\SubscriptionController;
Route::get('/subscribe', [SubscriptionController::class, 'showSubscriptionForm'])->name('subscription.form');
Route::post('/subscribe', [SubscriptionController::class, 'storeSubscription'])->name('subscription.store');
المتحكم (SubscriptionController.php):
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
class SubscriptionController extends Controller
{
public function showSubscriptionForm(Request $request)
{
return view('subscribe', [
'intent' => $request->user()->createSetupIntent()
]);
}
public function storeSubscription(Request $request)
{
$request->user()->newSubscription(
'default', 'price_1P6yQ2Rv0P7c38jUuO0M2zT4' // استبدل بمعرف السعر الخاص بك
)->create($request->paymentMethodId);
return redirect('/dashboard')->with('success', 'تم الاشتراك بنجاح!');
}
}
عرض Blade (subscribe.blade.php):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Subscribe</title>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<h1>اشترك في الخطة الاحترافية</h1>
<form id="payment-form" action="{{ route('subscription.store') }}" method="POST">
@csrf
<div id="card-element">
<!-- سيتم إدراج عنصر Stripe هنا. -->
</div>
<!-- يستخدم لعرض أخطاء النموذج. -->
<div id="card-errors" role="alert"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
اشترك
</button>
</form>
<script>
const stripe = Stripe('{{ config('cashier.key') }}'); // مفتاحك القابل للنشر
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const form = document.getElementById('payment-form');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
form.addEventListener('submit', async (e) => {
e.preventDefault();
cardButton.disabled = true;
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: '{{ Auth::user()->name }}' }
}
}
);
if (error) {
const errorDisplay = document.getElementById('card-errors');
errorDisplay.textContent = error.message;
cardButton.disabled = false;
} else {
let hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'paymentMethodId');
hiddenInput.setAttribute('value', setupIntent.payment_method);
form.appendChild(hiddenInput);
form.submit();
}
});
</script>
</body>
</html>
ب. إنشاء اشتراك #
بمجرد حصولك على paymentMethodId، يمكنك إنشاء اشتراك:
$user = Auth::user();
// إنشاء اشتراك جديد للخطة الافتراضية باستخدام معرف سعر محدد
$user->newSubscription('default', 'price_1P6yQ2Rv0P7c38jUuO0M2zT4')
->create($paymentMethodId);
// مع فترة تجريبية (على سبيل المثال، 7 أيام)
$user->newSubscription('default', 'price_1P6yQ2Rv0P7c38jUuO0M2zT4')
->trialDays(7)
->create($paymentMethodId);
ج. تبديل الاشتراكات #
يمكن للمستخدمين تغيير خططهم بسهولة:
$user = Auth::user();
$user->subscription('default')->swap('price_ANOTHER_PRICE_ID');
بشكل افتراضي، سيقوم Cashier بتوزيع الرسوم بالتناسب. لمنع التوزيع بالتناسب:
$user->subscription('default')->swap('price_ANOTHER_PRICE_ID')->noProrate();
د. إلغاء الاشتراكات #
$user = Auth::user();
$user->subscription('default')->cancel();
سيؤدي هذا إلى إبقاء المستخدم مشتركًا حتى نهاية فترة الفوترة الحالية. للإلغاء فورًا:
$user->subscription('default')->cancelNow();
هـ. استئناف الاشتراكات #
إذا قام مستخدم بإلغاء اشتراكه ولكنه يريد إعادة تنشيطه قبل انتهاء فترة الفوترة:
$user = Auth::user();
$user->subscription('default')->resume();
و. التحقق من حالة الاشتراك #
يوفر Cashier طرقًا ملائمة للتحقق من حالة اشتراك المستخدم:
$user = Auth::user();
if ($user->subscribed('default')) {
// المستخدم مشترك
}
if ($user->subscribedToPrice('price_1P6yQ2Rv0P7c38jUuO0M2zT4')) {
// المستخدم مشترك في سعر محدد
}
if ($user->onTrial('default')) {
// المستخدم في فترة تجريبية
}
if ($user->cancelled('default')) {
// المستخدم ألغى، ولكنه لا يزال ضمن فترة الفوترة
}
if ($user->ended('default')) {
// انتهى اشتراك المستخدم بالكامل
}
6. التعامل مع الـ Webhooks #
تعد الـ Webhooks ضرورية للحفاظ على مزامنة بيانات اشتراك تطبيقك مع Stripe. على سبيل المثال، إذا انتهت صلاحية بطاقة مستخدم، فستقوم Stripe بإعلام تطبيقك عبر webhook.
أ. تحديد مسار الـ Webhook #
يسجل Cashier تلقائيًا مسارًا للتعامل مع الـ webhooks. لتأمينه، تحتاج إلى تكوين سر الـ webhook.
ب. تكوين سر الـ Webhook #
-
انتقل إلى لوحة تحكم Stripe الخاصة بك > Developers > Webhooks.
-
انقر على + Add an endpoint.
-
بالنسبة إلى Endpoint URL، استخدم
https://your-domain.com/stripe/webhook(استبدلyour-domain.comبنطاقك الفعلي). -
حدد الأحداث التي تريد الاستماع إليها. بالنسبة لمعظم ميزات Cashier، يوصى بـ
customer.*،invoice.*،checkout.*،payment_intent.*،setup_intent.*،subscription.*، وwebhook_endpoint.*. -
بعد إنشاء نقطة النهاية، انقر عليها واكشف Signing secret (يبدأ بـ
wh_). -
أضف هذا السر إلى ملف
.envالخاص بك:STRIPE_WEBHOOK_SECRET="wh_YOUR_WEBHOOK_SECRET"
ج. التعامل مع أحداث الـ Webhook المحددة #
إذا كنت بحاجة إلى التعامل مع أحداث webhook محددة تتجاوز السلوك الافتراضي لـ Cashier، يمكنك توسيع WebhookController الخاص بـ Cashier:
// app/Http/Controllers/StripeWebhookController.php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierWebhookController;
class StripeWebhookController extends CashierWebhookController
{
/**
* Handle a cancelled customer subscription.
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function handleCustomerSubscriptionDeleted(array $payload)
{
// قم بتنفيذ إجراءات مخصصة عند حذف الاشتراك
// على سبيل المثال، إرسال بريد إلكتروني مخصص، تحديث أدوار المستخدم، إلخ.
return parent::handleCustomerSubscriptionDeleted($payload);
}
}
ثم، قم بتحديث ملف التكوين cashier.php للإشارة إلى المتحكم المخصص الخاص بك:
// config/cashier.php
'webhook' => [
'controller' => \App\Http\Controllers\StripeWebhookController::class,
// ...
],
7. المدفوعات لمرة واحدة #
يسمح لك Cashier أيضًا بإجراء مدفوعات لمرة واحدة على طريقة الدفع الافتراضية للمستخدم.
use Illuminate\Http\Request;
use Auth;
// ...
public function processOneTimePayment(Request $request)
{
$user = Auth::user();
try {
$user->charge(
1000, // المبلغ بالسنت (على سبيل المثال، 10.00 دولارات)
$request->paymentMethodId // أو null لاستخدام طريقة الدفع الافتراضية
);
return back()->with('success', 'تم الدفع بنجاح!');
} catch (\Exception $e) {
return back()->with('error', $e->getMessage());
}
}
لكي تعمل charge() دون تقديم paymentMethodId صراحةً، يجب أن يكون لدى المستخدم طريقة دفع افتراضية مسجلة (على سبيل المثال، من نية إعداد اشتراك سابقة).
8. الفواتير والإيصالات #
يجعل Cashier من السهل استرداد وعرض الفواتير لمستخدميك.
$user = Auth::user();
// الحصول على جميع الفواتير
$invoices = $user->invoices();
// الحصول على الفاتورة القادمة (إن وجدت)
$upcomingInvoice = $user->upcomingInvoice();
لتنزيل فاتورة PDF:
Route::get('/user/invoice/{invoiceId}', function (Request $request, $invoiceId) {
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Laravel World',
'product' => 'Pro Subscription',
]);
})->middleware(['auth']);
9. الاختبار #
عند الاختبار، استخدم دائمًا مفاتيح API الاختبار وبطاقات الاختبار التي توفرها Stripe في وثائقها. يمكنك أيضًا محاكاة أحداث webhook مباشرة من لوحة تحكم Stripe للاختبار الشامل.
الخلاصة #
يُبسط Laravel Cashier بشكل كبير دمج Stripe لفوترة الاشتراكات، مما يسمح للمطورين بتنفيذ ميزات دفع قوية بأقل قدر من التعليمات البرمجية. باتباع هذا الدليل، يجب أن يكون لديك الآن أساس متين لبناء نظام فوترة قوي في تطبيق Laravel الخاص بك. تذكر الرجوع إلى الوثائق الرسمية لـ Cashier لمزيد من الميزات المتقدمة والحالات الاستثنائية.