Laravel المتقدم
بناء تطبيق SaaS
بناء تطبيق SaaS
تتطلب تطبيقات البرمجيات كخدمة (SaaS) اعتبارات معمارية خاصة بما في ذلك إدارة الاشتراكات، والتعددية للمستأجرين، والفوترة، والبنية التحتية القابلة للتوسع. توفر Laravel أدوات ممتازة من خلال Laravel Cashier وحزم أخرى لبناء منصات SaaS قوية.
أساسيات معمارية SaaS
تتضمن معمارية تطبيق SaaS النموذجية:
- التعددية للمستأجرين: عزل بيانات وموارد العملاء
- فوترة الاشتراكات: المدفوعات المتكررة وإدارة الخطط
- بوابات الميزات: التحكم في الوصول بناءً على مستوى الاشتراك
- تتبع الاستخدام: مراقبة استدعاءات API والتخزين والموارد
- التحليلات: تتبع سلوك المستخدم ومقاييس الأعمال
ملاحظة: اختر بين التعددية على مستوى قاعدة البيانات (قواعد بيانات منفصلة لكل مستأجر) أو التعددية على مستوى التطبيق (قاعدة بيانات مشتركة مع tenant_id) بناءً على متطلبات العزل والتوسع.
تثبيت Laravel Cashier
توفر Laravel Cashier واجهة تعبيرية لخدمات فوترة الاشتراكات من Stripe:
composer require laravel/cashier
php artisan migrate
# نشر التكوين
php artisan vendor:publish --tag="cashier-migrations"
إعداد نموذج المستخدم للفوترة
قم بتحديث نموذج المستخدم الخاص بك لتضمين سمة Billable من Cashier:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
/**
* الحصول على اسم خطة اشتراك المستخدم
*/
public function planName(): string
{
if ($this->subscribed('default')) {
return $this->subscription('default')->stripe_price;
}
return 'Free';
}
/**
* التحقق مما إذا كان المستخدم يمكنه الوصول إلى ميزة
*/
public function canAccessFeature(string $feature): bool
{
return match ($this->planName()) {
'price_starter' => in_array($feature, ['basic_reports', 'email_support']),
'price_professional' => in_array($feature, ['basic_reports', 'advanced_reports', 'email_support', 'api_access']),
'price_enterprise' => true, // جميع الميزات
default => in_array($feature, ['basic_reports']), // المستوى المجاني
};
}
/**
* التحقق مما إذا كان المستخدم قد تجاوز حدود الاستخدام
*/
public function hasExceededUsageLimit(string $resource): bool
{
$limits = [
'Free' => ['api_calls' => 100, 'storage_mb' => 100],
'price_starter' => ['api_calls' => 10000, 'storage_mb' => 1000],
'price_professional' => ['api_calls' => 100000, 'storage_mb' => 10000],
'price_enterprise' => ['api_calls' => PHP_INT_MAX, 'storage_mb' => PHP_INT_MAX],
];
$plan = $this->planName();
$currentUsage = $this->getUsage($resource);
return $currentUsage >= ($limits[$plan][$resource] ?? 0);
}
protected function getUsage(string $resource): int
{
// التنفيذ لتتبع الاستخدام من قاعدة البيانات أو الذاكرة المؤقتة
return cache()->get("user.{$this->id}.usage.{$resource}", 0);
}
}
إنشاء خطط الاشتراك
حدد خطط الاشتراك الخاصة بك في ملف تكوين أو قاعدة بيانات:
<?php
// config/subscriptions.php
return [
'plans' => [
[
'id' => 'starter',
'name' => 'Starter',
'price_id' => env('STRIPE_STARTER_PRICE_ID'),
'price' => 29,
'currency' => 'usd',
'interval' => 'month',
'trial_days' => 14,
'features' => [
'ما يصل إلى 10,000 استدعاء API شهريًا',
'1 جيجابايت تخزين',
'تقارير أساسية',
'دعم البريد الإلكتروني',
],
'limits' => [
'api_calls' => 10000,
'storage_mb' => 1000,
'team_members' => 3,
],
],
[
'id' => 'professional',
'name' => 'Professional',
'price_id' => env('STRIPE_PROFESSIONAL_PRICE_ID'),
'price' => 99,
'currency' => 'usd',
'interval' => 'month',
'trial_days' => 14,
'features' => [
'ما يصل إلى 100,000 استدعاء API شهريًا',
'10 جيجابايت تخزين',
'تقارير متقدمة',
'دعم بريد إلكتروني ذو أولوية',
'الوصول إلى API',
],
'limits' => [
'api_calls' => 100000,
'storage_mb' => 10000,
'team_members' => 10,
],
],
[
'id' => 'enterprise',
'name' => 'Enterprise',
'price_id' => env('STRIPE_ENTERPRISE_PRICE_ID'),
'price' => 299,
'currency' => 'usd',
'interval' => 'month',
'trial_days' => 30,
'features' => [
'استدعاءات API غير محدودة',
'تخزين غير محدود',
'تقارير مخصصة',
'دعم هاتفي على مدار الساعة',
'وصول متقدم إلى API',
'تكاملات مخصصة',
],
'limits' => [
'api_calls' => PHP_INT_MAX,
'storage_mb' => PHP_INT_MAX,
'team_members' => PHP_INT_MAX,
],
],
],
];
نصيحة: قم بتخزين معرفات أسعار Stripe في متغيرات البيئة لبيئات مختلفة (التطوير، الإنتاج المرحلي، الإنتاج). يتيح لك ذلك الاختبار بأسعار وضع الاختبار قبل الانتقال إلى الوضع المباشر.
وحدة التحكم في الاشتراكات
قم بإنشاء وحدة تحكم للتعامل مع عمليات الاشتراك:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SubscriptionController extends Controller
{
/**
* عرض خطط الاشتراك
*/
public function index()
{
$plans = config('subscriptions.plans');
$user = Auth::user();
return view('subscriptions.index', [
'plans' => $plans,
'currentPlan' => $user->planName(),
'onTrial' => $user->onTrial(),
'subscribed' => $user->subscribed('default'),
]);
}
/**
* إنشاء اشتراك جديد
*/
public function store(Request $request)
{
$request->validate([
'plan' => 'required|in:starter,professional,enterprise',
'payment_method' => 'required|string',
]);
$user = $request->user();
$plan = collect(config('subscriptions.plans'))
->firstWhere('id', $request->plan);
try {
// إنشاء اشتراك مع فترة تجريبية
$subscription = $user->newSubscription('default', $plan['price_id'])
->trialDays($plan['trial_days'])
->create($request->payment_method);
// تسجيل حدث الاشتراك
activity()
->causedBy($user)
->log("اشترك في خطة {$plan['name']}");
return redirect()
->route('subscriptions.success')
->with('success', "تم الاشتراك بنجاح في خطة {$plan['name']}!");
} catch (\Exception $e) {
return back()
->withErrors(['subscription' => 'فشل إنشاء الاشتراك: ' . $e->getMessage()]);
}
}
/**
* تحديث خطة الاشتراك
*/
public function update(Request $request)
{
$request->validate([
'plan' => 'required|in:starter,professional,enterprise',
]);
$user = $request->user();
$plan = collect(config('subscriptions.plans'))
->firstWhere('id', $request->plan);
if (!$user->subscribed('default')) {
return back()->withErrors(['subscription' => 'لم يتم العثور على اشتراك نشط.']);
}
try {
// التبديل إلى خطة جديدة (موزعة بشكل افتراضي)
$user->subscription('default')
->swap($plan['price_id']);
activity()
->causedBy($user)
->log("غير الاشتراك إلى خطة {$plan['name']}");
return back()->with('success', "تم التغيير بنجاح إلى خطة {$plan['name']}!");
} catch (\Exception $e) {
return back()->withErrors(['subscription' => 'فشل تحديث الاشتراك.']);
}
}
/**
* إلغاء الاشتراك
*/
public function destroy(Request $request)
{
$user = $request->user();
if (!$user->subscribed('default')) {
return back()->withErrors(['subscription' => 'لم يتم العثور على اشتراك نشط.']);
}
// الإلغاء في نهاية الفترة (لا تلغي على الفور)
$user->subscription('default')->cancel();
activity()
->causedBy($user)
->log('ألغى الاشتراك');
return back()->with('success', 'تم إلغاء اشتراكك. يمكنك الاستمرار في استخدامه حتى نهاية فترة الفوترة.');
}
/**
* استئناف الاشتراك الملغى
*/
public function resume(Request $request)
{
$user = $request->user();
if (!$user->subscription('default')->cancelled()) {
return back()->withErrors(['subscription' => 'الاشتراك غير ملغى.']);
}
$user->subscription('default')->resume();
return back()->with('success', 'تم استئناف اشتراكك!');
}
}
معالج Webhook
التعامل مع webhooks من Stripe لأحداث الاشتراك:
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
use Illuminate\Support\Facades\Notification;
use App\Notifications\SubscriptionRenewed;
use App\Notifications\PaymentFailed;
class WebhookController extends CashierController
{
/**
* معالجة حدث تجديد الاشتراك
*/
public function handleCustomerSubscriptionUpdated(array $payload)
{
$user = $this->getUserByStripeId($payload['data']['object']['customer']);
if ($user) {
// إرسال إشعار التجديد
$user->notify(new SubscriptionRenewed($payload['data']['object']));
// تسجيل الحدث
activity()
->causedBy($user)
->withProperties($payload)
->log('تجديد الاشتراك');
}
return $this->successMethod();
}
/**
* معالجة فشل الدفع
*/
public function handleInvoicePaymentFailed(array $payload)
{
$user = $this->getUserByStripeId($payload['data']['object']['customer']);
if ($user) {
// إخطار المستخدم بفشل الدفع
$user->notify(new PaymentFailed($payload['data']['object']));
// تسجيل الفشل
activity()
->causedBy($user)
->withProperties($payload)
->log('فشل الدفع');
// اختياريًا خفض إلى المستوى المجاني بعد فترة السماح
if ($this->exceedsGracePeriod($user)) {
$user->subscription('default')->cancelNow();
}
}
return $this->successMethod();
}
/**
* التحقق مما إذا كان فشل الدفع يتجاوز فترة السماح
*/
protected function exceedsGracePeriod($user): bool
{
$subscription = $user->subscription('default');
$gracePeriodDays = 7;
return $subscription->past_due_at
&& now()->diffInDays($subscription->past_due_at) > $gracePeriodDays;
}
}
تحذير: تحقق دائمًا من توقيعات webhook في الإنتاج للتأكد من أن الطلبات تأتي بالفعل من Stripe. يتعامل Laravel Cashier مع هذا تلقائيًا إذا قمت بتكوين سر webhook في ملف .env الخاص بك.
Middleware بوابة الميزات
قم بإنشاء middleware لتقييد الوصول بناءً على ميزات الاشتراك:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckSubscriptionFeature
{
/**
* معالجة طلب وارد
*/
public function handle(Request $request, Closure $next, string $feature)
{
$user = $request->user();
if (!$user || !$user->canAccessFeature($feature)) {
return redirect()
->route('subscriptions.index')
->with('error', 'تتطلب هذه الميزة خطة اشتراك أعلى.');
}
return $next($request);
}
}
// الاستخدام في routes/web.php
Route::middleware(['auth', 'subscription.feature:advanced_reports'])->group(function () {
Route::get('/reports/advanced', [ReportController::class, 'advanced']);
});
تمرين 1: أنشئ نظام اشتراكات بثلاث خطط: مجاني، Pro (19 دولارًا شهريًا)، وBusiness (49 دولارًا شهريًا). قم بتنفيذ بوابات الميزات لـ "advanced_analytics" (Pro+) و"api_access" (Business فقط). أضف نظام تتبع الاستخدام الذي يحد من استدعاءات API شهريًا بناءً على الخطة.
تمرين 2: قم ببناء بوابة فوترة تسمح للمستخدمين بعرض اشتراكهم الحالي، والترقية/التخفيض في الخطط، وتحديث طرق الدفع، وعرض الفواتير، وإلغاء الاشتراكات. قم بتضمين عد تنازلي لفترة التجربة وخيار "استئناف الاشتراك" للاشتراكات الملغاة.
تمرين 3: قم بتنفيذ معالج webhook الذي يرسل إشعارات بالبريد الإلكتروني لـ: تجديد الاشتراك، فشل الدفع، إلغاء الاشتراك، وانتهاء التجربة في 3 أيام. أضف لوحة معلومات المسؤول التي تعرض MRR (الإيرادات الشهرية المتكررة)، ومعدل الانسحاب، والاشتراكات النشطة حسب الخطة.
الملخص
في هذا الدرس، تعلمت:
- أساسيات معمارية SaaS وأنماط التعددية للمستأجرين
- إعداد Laravel Cashier لفوترة الاشتراكات
- إنشاء وإدارة خطط الاشتراك مع التجارب
- التعامل مع webhooks لأحداث الاشتراك
- تنفيذ بوابات الميزات وحدود الاستخدام