في هذا الدرس سنقوم بإنشاء صفحة دفع إلكتروني باستخدام خدمة Stripe من خلال إطار Laravel، وسنقوم بمقارنة طريقتين مختلفتين للتكامل: عبر HTTP Client المباشر، وعبر الحزمة الرسمية الخاصة بـ Stripe. سنتعلم كيفية إنشاء PaymentIntent، إرسال المفاتيح الآمنة، ومعالجة الأخطاء بطريقة احترافية.
حيث سنحصل على الشكل التالي
🚨 سنجرب طريقتين مختلفتين للتكامل:
وسنقارن النتائج، ونشرح لماذا يُفضل غالبًا استخدام الحزمة الرسمية إن وُجدت.
بنهاية هذا الدرس، ستكون قادرًا على:
Stripe تستخدم نظام يُعرف بـ Payment Intents لمعالجة المدفوعات بطريقة مرنة وآمنة.
عند إرسال طلب لإنشاء عملية دفع، نحصل على قيمة client_secret وهي ما نحتاجه لإتمام الدفع من واجهة المستخدم.
// routes/web.php
Route::get('/payment', [PaymentController::class, 'payment'])->name('index.payment');
use App\Services\PaymentService; use Illuminate\Support\Facades\Log; class PaymentController extends Controller { // 🟡 هنا قمنا بعمل inject لـ PaymentService من خلال المُنشئ (Constructor) // هذا يسمح لنا باستخدامه في جميع دوال هذا الكلاس دون الحاجة لإنشاء كائن جديد يدوياً public function __construct(private PaymentService $paymentService) {} // 🟢 هذه الدالة مسؤولة عن عرض صفحة الدفع للمستخدم // يتم فيها إنشاء "Payment Intent" عبر Stripe وإرسال بياناته إلى الواجهة public function payment() { // 🔹 هنا ننشئ رقم طلب (order ID) مميز لكل عملية شراء $orderId = uniqid('book_'); // 🔹 متغيرات لتخزين الخطأ وبيانات الدفع لاحقًا $error = null; $paymentData = null; try { // ✅ الخيار الأفضل: استخدام الحزمة الرسمية من Stripe لإنشاء عملية الدفع $clientSecret = $this->paymentService->createPaymentIntentWithPackage(5000, 'usd', $orderId); // ✅ تجهيز بيانات الدفع لإرسالها إلى الواجهة (Blade) $paymentData = [ 'client_secret' => $clientSecret, 'publishable_key' => config('services.stripe.public'), // مفتاح النشر العام 'amount' => 50.00, 'order_id' => $orderId, ]; } catch (\Exception $e) { // ⚠️ في حال حدوث أي خطأ أثناء إنشاء عملية الدفع، نقوم بتسجيل الخطأ Log::error('فشل في إنشاء عملية الدفع', [ 'error' => $e->getMessage(), 'order_id' => $orderId, ]); // ثم نمرر رسالة الخطأ لعرضها في الواجهة $error = $e->getMessage(); } // 🟣 نعيد عرض صفحة الدفع مع دمج البيانات (سواء بيانات الدفع أو رسالة الخطأ) return view('payment.form', array_merge($paymentData ?? [], ['error' => $error])); } }
هنا يجب إضافة مصفوفه جديد بإسم stripe مثلا من اجل، في ملف app/services.php
'stripe' => [ 'secret' => env('STRIPE_SECRET_KEY'), 'public' => env('STRIPE_PUBLISHABLE_KEY'), ],
بالطبع يجب ان نقوم بوضع في ملف .env والتي يمكن الحصول عليها من الموقع الرسمي
STRIPE_SECRET_KEY= STRIPE_PUBLISHABLE_KEY=
🔐 المفتاح السري (Secret) يستخدم فقط في الخادم (Laravel)، أما المفتاح القابل للنشر (Publishable Key) فيُستخدم في الواجهة الأمامية (JavaScript) لتكوين عناصر الدفع.
php artisan make:class Services/PaymentService
namespace App\Services; class PaymentService { private $secretKey; public function __construct() { $this->secretKey = config('services.stripe.secret'); } public function createPaymentIntent($amount, $currency = 'usd', $orderId = null): string { // سنشرح هذا لاحقًا } public function createPaymentIntentWithPackage($amount, $currency = 'usd', $orderId = null): string { // سنشرح هذا لاحقًا } }
1. التنفيذ
use Illuminate\Support\Facades\Http; public function createPaymentIntent($amount, $currency = 'usd', $orderId = null): string { // 🟡 إعداد رأس الطلب (Authorization Header) // نستخدم مفتاح Stripe السري (secret key) لتوثيق الطلب باستخدام Bearer Token $headers = [ 'Authorization' => 'Bearer ' . $this->secretKey, ]; // 🟢 إرسال طلب POST إلى Stripe لإنشاء Payment Intent // نرسل معلومات الدفع مثل المبلغ، العملة، وتفاصيل الطلب $response = Http::withHeaders($headers) ->asForm() // ✅ مهم جداً: Stripe يتطلب محتوى بصيغة x-www-form-urlencoded ->post('https://api.stripe.com/v1/payment_intents', [ 'amount' => $amount, // Stripe يتطلب أن يكون المبلغ بالـ "cents" (مثلاً 5000 = 50.00$) 'currency' => $currency, // العملة المستخدمة (USD أو غيرها) // ⚙️ تفعيل طرق الدفع التلقائية (بطاقات، Apple Pay، Google Pay...) 'automatic_payment_methods[enabled]' => 'true', // 📝 معلومات إضافية عن الطلب، مثل رقم الطلب واسم المنتج 'metadata[order_id]' => $orderId ?? uniqid('order_'), 'metadata[product]' => 'Man On The Sun - Book', ]); // ✅ في حالة النجاح (status 200)، نستخرج client_secret من الاستجابة if ($response->successful()) { return $response->json()['client_secret']; } // ❌ في حالة وجود خطأ، نمرر الاستجابة لدالة خاصة بالتعامل مع الأخطاء $this->handleApiError($response); }
private function handleApiError($response) { $statusCode = $response->status(); $errorData = $response->json(); switch ($statusCode) { case 400: throw new \InvalidArgumentException($errorData['error']['message'] ?? 'طلب غير صالح'); case 401: throw new \Exception('مفتاح API غير صالح'); case 402: throw new \Exception($errorData['error']['decline_code'] ?? 'تم رفض الدفع'); case 403: throw new \Exception('صلاحيات غير كافية'); case 429: throw new \Exception('تم تجاوز حد الطلبات'); default: throw new \Exception('خطأ غير معروف: ' . $response->body()); } }
composer require stripe/stripe-php
// 🟡 تعريف كائن stripeClient الذي يمثل الاتصال بالحزمة الرسمية لـ Stripe
private $stripeClient; public function __construct() { // 🔐 تحميل مفتاح Stripe السري من ملف config/services.php $this->secretKey = config('services.stripe.secret'); // 🧠 إنشاء كائن StripeClient باستخدام المفتاح السري // هذا الكائن يوفر كل وظائف Stripe (إنشاء دفعات، عملاء، استرداد، إلخ) $this->stripeClient = new \Stripe\StripeClient($this->secretKey); } public function createPaymentIntentWithPackage($amount, $currency = 'usd', $orderId = null): string { try { // ✅ إنشاء PaymentIntent باستخدام الحزمة الرسمية $intent = $this->stripeClient->paymentIntents->create([ 'amount' => $amount, // Stripe يتعامل مع المبالغ بالـ "cents" (مثلاً 5000 = 50.00$) 'currency' => $currency, // رمز العملة (مثل usd) // ✅ تفعيل ميزة الدفع التلقائي بطرق متعددة (بطاقة، Apple Pay، وغيرها) 'automatic_payment_methods' => ['enabled' => true], // 📝 معلومات إضافية مخصصة ضمن metadata 'metadata' => [ 'order_id' => $orderId ?? uniqid(), // رقم الطلب إذا لم يُمرر يتم إنشاؤه تلقائيًا 'product' => 'Man On The Sun - Book', // وصف المنتج ], ]); // 🔁 إرجاع client_secret المطلوب من الواجهة الأمامية لإتمام عملية الدفع return $intent->client_secret; } catch (\Stripe\Exception\CardException $e) { // ❌ تم رفض البطاقة (مثلاً: لا يوجد رصيد كافٍ أو معلومات غير صحيحة) throw new \Exception($e->getDeclineCode() ?? 'تم رفض البطاقة'); } catch (\Stripe\Exception\ApiErrorException $e) { // ❌ أي خطأ عام من واجهة Stripe API (شبكة - باراميتر غير صالح - إلخ) throw new \Exception('Stripe API error: ' . $e->getMessage()); } }
هنا سأقوم بحذف amount من paymentIntent سواء من خلال الدالة createPaymentIntent او createPaymentIntentWithPackage
$response = Http::withHeaders($headers) ->asForm() ->post('https://api.stripe.com/v1/payment_intents', [ 'currency' => $currency, 'automatic_payment_methods[enabled]' => 'true', // ✅ هنا النص "true" وليس القيمة المنطقية 'metadata[order_id]' => $orderId ?? uniqid('book_'), 'metadata[product]' => 'Man On The Sun', ]);
في ملف blade سوف أحصل على الشكل التالي، مع الخطأ بالتحديد
ملف blade
resources/views/payment.blade.php
وفي ملف blade ما يهمنا هو javascript
💻 واجهة الدفع: Stripe Elements (Frontend) @if (!isset($error) || !$error) <script> const stripe = Stripe('{{ $publishable_key ?? '' }}'); const elements = stripe.elements(); const style = { base: { fontSize: '16px', color: '#424770', '::placeholder': { color: '#aab7c4' }, }, invalid: { color: '#9e2146' }, }; const card = elements.create('card', { style }); card.mount('#card-element'); card.on('change', ({ error }) => { document.getElementById('card-errors').textContent = error ? error.message : ''; }); const form = document.getElementById('payment-form'); const submitButton = document.getElementById('submit-button'); const buttonText = document.getElementById('button-text'); const spinner = document.getElementById('spinner'); form.addEventListener('submit', async (event) => { event.preventDefault(); submitButton.disabled = true; buttonText.textContent = 'Processing...'; spinner.classList.remove('hidden'); try { const { error, paymentIntent } = await stripe.confirmCardPayment('{{ $client_secret ?? '' }}', { payment_method: { card: card, } }); if (error) { document.getElementById('card-errors').textContent = error.message; submitButton.disabled = false; buttonText.textContent = 'Pay ${{ number_format($amount ?? 50.00, 2) }}'; spinner.classList.add('hidden'); } else { window.location.href = '/payment/success?payment_intent=' + paymentIntent.id; } } catch (err) { document.getElementById('card-errors').textContent = 'An unexpected error occurred. Please try again.'; submitButton.disabled = false; buttonText.textContent = 'Pay ${{ number_format($amount ?? 50.00, 2) }}'; spinner.classList.add('hidden'); } }); </script> @endif
العنصر | HTTP Client | Stripe SDK (مُوصى به) |
---|---|---|
سهولة الاستخدام | ❌ معقد نسبيًا | ✅ بسيط جدًا |
التعامل مع الأخطاء | ❌ يدوي | ✅ تلقائي ومدعوم |
دعم الميزات المتقدمة | ❌ محدود | ✅ كامل |
الصيانة والتحديثات | ❌ غير مضمون | ✅ رسمي ومدعوم من Stripe |
الأداء والكفاءة | ❌ عرضة للأخطاء | ✅ محسّن وآمن |
في هذا الدرس:
## 🎓 الدرس المستفاد
يمكنك إستعراض وتنزيل المشروع عبر GitHub عبر الرابط التالي: