في هذا الدرس سنقوم بإنشاء صفحة دفع إلكتروني باستخدام خدمة 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 عبر الرابط التالي: