💳 الدرس الثاني: التكامل مع Stripe - Laravel HTTP Client أم Stripe PHP Package؟

💳 الدرس الثاني: التكامل مع Stripe - Laravel HTTP Client أم Stripe PHP Package؟

2025-08-19 وقت القراءه : 6 دقائق

في هذا الدرس سنقوم بإنشاء صفحة دفع إلكتروني باستخدام خدمة Stripe من خلال إطار Laravel، وسنقوم بمقارنة طريقتين مختلفتين للتكامل: عبر HTTP Client المباشر، وعبر الحزمة الرسمية الخاصة بـ Stripe. سنتعلم كيفية إنشاء PaymentIntent، إرسال المفاتيح الآمنة، ومعالجة الأخطاء بطريقة احترافية.


حيث سنحصل على الشكل التالي

🚨 سنجرب طريقتين مختلفتين للتكامل:

  • باستخدام Laravel HTTP Client
  • باستخدام الحزمة الرسمية الخاصة بـ Stripe (Stripe PHP SDK)


وسنقارن النتائج، ونشرح لماذا يُفضل غالبًا استخدام الحزمة الرسمية إن وُجدت.


🎯 ما ستتعلمه في هذا الدرس

بنهاية هذا الدرس، ستكون قادرًا على:

  • المقارنة بين استخدام HTTP Client والحزمة الرسمية
  • التعامل مع مصادقة Bearer Token في رؤوس الطلبات (Authorization Headers)
  • إدارة مفاتيح API الخاصة والعامة لخدمات الدفع
  • تنفيذ معالجة استثناءات دقيقة لواجهات الدفع
  • فهم بنية Payment Intent API في Stripe


🧠 مقدمة عن Stripe Payment Intents

Stripe تستخدم نظام يُعرف بـ Payment Intents لمعالجة المدفوعات بطريقة مرنة وآمنة.

عند إرسال طلب لإنشاء عملية دفع، نحصل على قيمة client_secret وهي ما نحتاجه لإتمام الدفع من واجهة المستخدم.

🛠️ بنية المشروع

1. تعريف المسارات

// routes/web.php

Route::get('/payment', [PaymentController::class, 'payment'])->name('index.payment');

2. الكنترولر: PaymentController

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) لتكوين عناصر الدفع.


💼 إنشاء خدمة الدفع: PaymentService

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
    {
        // سنشرح هذا لاحقًا
    }
}

⚠️ المحاولة الأولى: Laravel HTTP Client

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);
}


2. التعامل مع الأخطاء يدويًا

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());
    }
}

⚠️ المشاكل في هذا النهج:


  • يجب عليك معالجة الأخطاء بنفسك يدويًا
  • لا يتم تطبيق آلية إعادة المحاولة التلقائي
  • لا يوجد تصنيف واضح للأخطاء
  • بعض الطلبات قد تفشل رغم أنها صحيحة حسب التوثيق الرسمي!


✅ النهج الأفضل: Stripe PHP Package

1. تثبيت الحزمة

composer require stripe/stripe-php

2. التنفيذ

// 🟡 تعريف كائن 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 ❌ vs Stripe SDK ✅

العنصرHTTP ClientStripe SDK (مُوصى به)
سهولة الاستخدام❌ معقد نسبيًا✅ بسيط جدًا
التعامل مع الأخطاء❌ يدوي✅ تلقائي ومدعوم
دعم الميزات المتقدمة❌ محدود✅ كامل
الصيانة والتحديثات❌ غير مضمون✅ رسمي ومدعوم من Stripe
الأداء والكفاءة❌ عرضة للأخطاء✅ محسّن وآمن


## 🧠 الخلاصة

في هذا الدرس:

  • - تعرفنا على كيفية إنشاء عملية دفع باستخدام Stripe عبر طريقتين مختلفتين
  • - استخدمنا Laravel HTTP Client وتعلمنا قيوده
  • - جرّبنا Stripe SDK الرسمي ولاحظنا سهولة التعامل معه
  • - أنشأنا صفحة دفع متكاملة تحتوي على Stripe Elements وتعالج جميع الأخطاء الممكنة


## 🎓 الدرس المستفاد

  • > عند العمل مع خدمات خارجية مثل Stripe، يُفضل دائمًا استخدام الحزم الرسمية المعتمدة، لأنها توفر:
  • > - تجربة استخدام أسهل
  • > - دعم للأخطاء المختلفة
  • > - توافق مع التحديثات الرسمية
  • > - أمان أعلى في التعامل مع بيانات الدفع الحساسة


يمكنك إستعراض وتنزيل المشروع عبر GitHub عبر الرابط التالي:


إضافة تعليق
Loading...