PHP في التقنية المالية

PHP الحديثة مع Laravel أو Symfony تشغّل من منظومة المدفوعات أكثر مما يظن كثيرون. أين تناسب التقنية المالية وأين لا تناسب، والأنماط التي تحافظ على صحة الأموال.

· · تحديث · 28 دقائق قراءة

رسم توضيحي لفيل PHP بجانب رموز بطاقة الدفع والويب هوك ودفتر الحسابات
في هذا المقال

لماذا تهم PHP في التقنية المالية

لدى PHP مشكلة في السمعة ومشكلة في الحصة السوقية، والمشكلتان تسيران في اتجاهين متعاكسين. السمعة عالقة عند عام 2008 تقريبًا: لغة قوالب نمت لها دوال بالمصادفة، وكان mysql_query ودمج النصوص هو الطريقة المعتادة للتخاطب مع قاعدة البيانات. أما الحصة السوقية فتروي قصة مختلفة. ما زالت PHP تشغّل الغالبية العظمى من المواقع التي يمكن التعرف على لغة الخادم فيها، وتشغّل صفحات الدفع في أكبر منصات التجارة الإلكترونية مفتوحة المصدر، ويكاد كل مزوّد خدمات دفع ينشر حزمة تطوير (SDK) بلغة PHP مبكرًا.

في التقنية المالية تحديدًا، الحقيقة الثانية أهم من الأولى. المال يتحرك عبر تكاملات التجار، ونصيب غير متناسب من هذه التكاملات مكتوب بلغة PHP. سواء كنت تبني بوابة دفع أو منتج «اشترِ الآن وادفع لاحقًا» أو محفظة أو منصة إقراض، فالكود على الطرف الآخر من واجهتك البرمجية هو على الأرجح PHP. فهم اللغة هو الطريقة التي تفهم بها عملاءك.

اللغة لم تعد كما كانت

PHP 8.x لغة مختلفة عن تلك التي يتذكرها معظم منتقديها. منذ PHP 7.0 أُعيد بناء المحرك من أجل السرعة، ومنذ 8.0 أصبح نظام الأنواع جادًا: أنواع الاتحاد والتقاطع، والخصائص والأصناف للقراءة فقط (readonly)، والتعدادات (enums)، والوسائط المسماة، وتعبيرات match، وصياغة الدوال من الدرجة الأولى، والألياف (fibers)، وdeclare(strict_types=1) كسطر أول في كل ملف داخل أي قاعدة كود مُدارة جيدًا. أضافت PHP 8.3 ثوابت الأصناف ذات الأنواع ودالة json_validate()، وأضافت PHP 8.4 خطافات الخصائص والرؤية غير المتماثلة. ومع PHPStan أو Psalm في خط التكامل المستمر، يمكنك كتابة قاعدة كود يرفض فيها كائن Money جمع الدراهم مع الريالات، وتكون فيها حالة الدفع تعدادًا لا نصًا سحريًا. هذا هو الحد الأدنى الذي تحتاجه التقنية المالية، وPHP توفره اليوم.

لماذا هذا المقال

أكتب هذا للمؤسسين وقادة التقنية والمهندسين الكبار في مصر والسعودية والإمارات، ممن يرثون قاعدة كود PHP أو يقررون ما إذا كانوا سيبدؤون واحدة. الإجابة الصادقة عن سؤال «هل ينبغي بناء التقنية المالية بلغة PHP؟» هي «يعتمد على أي جزء من التقنية المالية»، ويحاول هذا المقال أن يجعل تلك الإجابة دقيقة: أين تكون PHP خيارًا افتراضيًا قويًا، وأين تصبح عبئًا، وما الأنماط التي تفصل تطبيقات PHP التي تتعامل مع المال جيدًا عن تلك التي تنتج ملاحظات في التدقيق.

أكبر ميزة لـ PHP في التقنية المالية ليست اللغة نفسها، بل أن التاجر على الطرف الآخر من واجهتك البرمجية يشغّلها على الأرجح هو أيضًا.

أين تُستخدم

يلتزم هذا القسم بالاستخدامات الموثقة علنًا؛ أفضّل أن أسمي أنظمة أقل وأن أكون متأكدًا من كل واحد منها.

مزوّدو خدمات الدفع وحزم التطوير الخاصة بهم

تحافظ Stripe على مكتبة PHP رسمية هي stripe-php، وهي من أقدم حزم التطوير لديها. وتنشر Adyen وMollie وPayPal عملاء PHP رسميين. وفي سوق المنطقة، تنشر Paymob وFawry وTap وMoyasar وPayTabs جميعها حزم PHP أو أمثلة تكامل بلغة PHP في وثائق المطورين الخاصة بها. حين يريد مزوّد دفع أن يتكامل التجار معه بسرعة، تكون PHP من أوائل اللغات التي يدعمها، لأنها اللغة التي كُتب بها موقع التاجر أصلًا.

طبقات الفوترة والاشتراكات

يأتي Laravel مع Cashier، وهي حزمة رسمية تغلّف اشتراكات Stripe وPaddle والفواتير والفترات التجريبية والقسائم ومعالجة الويب هوك (webhook) في واجهة Laravel اصطلاحية. ولدى منظومة Symfony حزم مكافئة، ويحافظ المجتمعان على حزم لإصدار الفواتير وحساب الضرائب ومتابعة المتأخرات.

صفحات الدفع في التجارة الإلكترونية

WooCommerce وMagento (المعروف الآن باسم Adobe Commerce) وPrestaShop تطبيقات PHP، ومنظومات إضافات بوابات الدفع الخاصة بها مكتوبة بلغة PHP أيضًا. وShopware وSylius منصتا تجارة مبنيتان على Symfony. لكل منها صفحة دفع تسلّم الطلب إلى مزوّد الدفع وتستقبل ويب هوك وتحدّث حالة الطلب، وهذه السباكة كلها PHP. إذا احتاج منتجك أن يكون داخل صفحة الدفع لتاجر تجارة إلكترونية في المنطقة، فستكتب أو تدقق إضافة PHP.

أنظمة PHP كبيرة خارج التقنية المالية

لأن سؤال «هل تتحمل PHP الضغط؟» يُطرح في كل مراجعة معمارية، يجدر تذكّر ما شغّلته هذه اللغة. تعمل Wikipedia على MediaWiki، وهو تطبيق PHP. بُنيت خلفية Slack بلغة PHP ثم انتقلت إلى Hack على HHVM، كما وصفت Slack ذلك في مدونتها الهندسية. وMautic، منصة أتمتة التسويق مفتوحة المصدر، مبنية على Symfony. لا شيء من هذه الأنظمة بنك، لكنها جميعًا تُظهر أن نموذج التشغيل يتوسع حين تكون الهندسة المحيطة به كفؤة. وفي التقنية المالية بالمنطقة تحديدًا، كثيرًا ما تكون الطبقات الموجهة للتجار وبوابات المكاتب الخلفية وتكاملات مزوّدي الدفع وتنسيق إجراءات اعرف عميلك (KYC) مبنية على Laravel، بينما تُبنى أنوية دفاتر الحسابات أو المخاطر الأحدث غالبًا بلغات Go أو Java أو Python أو Kotlin؛ وبقية هذا المقال تشرح السبب.

نقاط القوة

عزل الطلبات ميزة لا قيد

نموذج تنفيذ PHP لا يتشارك شيئًا: يحصل كل طلب على حالة عملية جديدة، ثم ينفَّذ، ثم يُلقى كل شيء. يسمي المهندسون المعتادون على بيئات التشغيل طويلة العمر هذا هدرًا؛ وأنا أسميه في نظام مدفوعات خاصية أمان. تسرب الذاكرة في طلب واحد لا يسمم الطلب التالي. الخطأ الفادح يُسقط طلبًا واحدًا لا مجموعة العمال كلها. لا توجد ذاكرة مؤقتة قديمة في ذاكرة العملية ولا كائن مفرد قابل للتغيير يحمل سعر صرف الأمس بالمصادفة.

نظام الأنواع أصبح كافيًا للتعامل مع المال

مع declare(strict_types=1) تُفرض الأنواع الأولية بدلًا من تحويلها ضمنيًا. ومع الأصناف readonly تصبح كائنات القيمة غير قابلة للتغيير بحكم البناء. ومع التعدادات لا يمكن أن تكون حالة الدفع 'Settled' في ملف و'settled' في ملف آخر. ومع PHPStan بمستوى مرتفع في التكامل المستمر تُكتشف أخطاء القيم الفارغة قبل مراجعة الكود. لا شيء من هذا غريب؛ إنها عدة العمل القياسية لأي فريق PHP 8.

إطاران ناضجان بفلسفتين مختلفتين

الجانب Laravel Symfony إطار مصغّر أو PHP خام
الفلسفة كل شيء مضمّن، الاصطلاح أولًا مكوّنات وإعداد صريح الحد الأدنى، وأنت تجمّع الباقي
منحنى التعلم لفريق في المنطقة منخفض؛ مجتمع محلي كبير جدًا متوسط؛ الأقوى في أوروبا يعتمد كليًا على الفريق
أدوات قريبة من المدفوعات Cashier (Stripe وPaddle) وHorizon وOctane Messenger وWorkflow ومكوّن Security ما توصّله بنفسك
البنية على المدى الطويل يحتاج انضباطًا لتجنب النماذج المتضخمة يشجع على التصميم الطبقي لا يشجع على شيء
الأنسب لـ فرق المنتجات سريعة الحركة المنصات ذات السياقات المحدودة الكثيرة خدمات الأطراف والإضافات وكود حزم التطوير

Laravel هو الخيار الافتراضي لفرق المنتجات في المنطقة لأن مخزون التوظيف عميق ولأن الحزم الرسمية تغطي الطوابير والجدولة والإشعارات وتحديد المعدل والمصادقة والتشفير والفوترة منذ اليوم الأول. أما Symfony فهو الأنسب حين يكون النظام كبيرًا ومتعدد الفرق ويُتوقع له أن يعيش عقدًا من الزمن؛ فمكوّناته هي أساس معظم منظومة PHP، بما فيها Laravel نفسه.

منظومة المدفوعات عميقة على نحو غير معتاد

يمنحك Composer وPackagist مكتبات موثوقة للأجزاء الصعبة: brick/money وmoneyphp/money لحساب العملات، وramsey/uuid للمعرّفات، وMonolog للتسجيل المنظم، وقائمة طويلة من عملاء مزوّدي الدفع. ومعايير PSR لرسائل HTTP والوسطاء والتسجيل والتحميل التلقائي تعني أن هذه المكتبات تتركب عبر الأطر المختلفة. وتشغيليًا، PHP-FPM خلف Nginx من أكثر إعدادات الإنتاج فهمًا في الصناعة، ولفريق صغير عليه أيضًا اجتياز تفتيش الجهة الرقابية، فإن الملل ميزة.

المخاطر والمزالق

المال بالفاصلة العائمة

أكثر أخطاء المال شيوعًا في قواعد كود PHP هو float. فـ 0.1 + 0.2 لا تساوي 0.3 في PHP كما في كل لغة تتبع معيار IEEE 754، وتزيد PHP الأمر سوءًا بتحويل النصوص الرقمية إلى أعداد عائمة بصمت أثناء الحساب. إذا كانت مبالغك أعدادًا عائمة فستحصل في النهاية على دفتر حسابات لا يتوازن ولا سبيل لإثبات أي قيد هو الخاطئ. الحل غير قابل للتفاوض: خزّن المال واحسبه كأعداد صحيحة بالوحدة الصغرى للعملة، واحمل العملة مع المبلغ، ودع كائن قيمة يفرض القواعد.

كائن قيمة Money بأبسط صورة

المكتبات المذكورة أعلاه هي الخيار الصحيح للإنتاج؛ أما المقتطف أدناه فيُظهر كم هو قليل ما تحتاجه PHP 8.3 للتعبير عن القواعد. كل شيء للقراءة فقط، والعملة تعداد، والتوزيع يوزّع الوحدات الصغرى المتبقية بدلًا من إضاعتها.

<?php
declare(strict_types=1);

enum Currency: string
{
    case EGP = 'EGP';
    case SAR = 'SAR';
    case AED = 'AED';
}

final class CurrencyMismatch extends LogicException {}

/** Amounts are integers in minor units (piastres, halalas, fils). Never floats. */
final readonly class Money
{
    public function __construct(public int $minor, public Currency $currency) {}

    public function add(Money $other): self
    {
        $this->assertSameCurrency($other);
        return new self($this->minor + $other->minor, $this->currency);
    }

    /** Split by ratios; leftover minor units go to the first parts, so nothing is lost. */
    public function allocate(int ...$ratios): array
    {
        $total = array_sum($ratios);
        if ($total <= 0 || min($ratios) < 0 || $this->minor < 0) {
            throw new InvalidArgumentException('allocate() needs a non-negative amount and ratios');
        }
        $parts = [];
        $remainder = $this->minor;
        foreach ($ratios as $ratio) {
            $share = intdiv($this->minor * $ratio, $total);
            $parts[] = $share;
            $remainder -= $share;
        }
        for ($i = 0; $remainder > 0; $i++, $remainder--) {
            $parts[$i]++;
        }
        return array_map(fn (int $p) => new self($p, $this->currency), $parts);
    }

    private function assertSameCurrency(Money $other): void
    {
        if ($this->currency !== $other->currency) {
            throw new CurrencyMismatch("{$this->currency->value} vs {$other->currency->value}");
        }
    }
}

// 100.01 EGP split three ways: [3334, 3334, 3333] piastres, which still sums to 10001.
$parts = (new Money(10001, Currency::EGP))->allocate(1, 1, 1);

تم الاختبار مع PHP 8.3.

يحتاج النظام الحقيقي أيضًا إلى الطرح والمقارنة والضرب مع نمط تقريب صريح وأس العملة، وهذا بالضبط ما توفره brick/money وmoneyphp. استخدمهما؛ ولا تكتب نسختك الخاصة إلا لتتعلم الدرس.

المقارنة المتساهلة وتلاعب الأنواع

أصلحت PHP 8 أسوأ قواعد المقارنة بين النصوص والأرقام، لكن == ما زالت فخًا: فـ "1e3" == "1000" صحيحة. استخدم === لكل شيء وhash_equals() لكل ما هو سري، واضبط محلل الكود الثابت لديك ليبلّغ عن المقارنة المتساهلة.

سحر الإطار الذي يخفي منطق المال

Eloquent في Laravel متعة في عمليات CRUD وخطر على دفاتر الحسابات. الإسناد الجماعي والتحويلات الضمنية والمراقبون الذين يعملون عند كل حفظ ودوال الوصول التي تجري حسابات، كلها أماكن يمكن أن تختبئ فيها قاعدة مالية. القاعدة في قاعدة كود مالية: منطق المال يعيش في أصناف خدمات صريحة ذات أنواع وفي كائنات قيمة؛ أما نماذج ORM فللتخزين لا غير.

فخ الكود القديم

معظم أعمال PHP في التقنية المالية بالمنطقة ليست بناءً من الصفر. إنها بوابة تعمل منذ PHP 5.6، أو بوابة تجار على إطار مهجور، أو إضافة WordPress نمت حتى صارت منتجًا. هذه الأنظمة تعمل، ولهذا ما زالت موجودة، وهي أيضًا حيث تعيش كل ثغرة كلاسيكية: استعلامات SQL بلا معاملات مهيأة، وextract($_POST)، وتجزئة كلمات المرور بـ MD5، وأسرار في المستودع. ترقيتها عمل هندسي لا مجرد composer update. يؤتمت Rector قدرًا مفاجئًا من ترحيل الصياغة؛ لكنه لا يستطيع أتمتة المعمارية. وفخ مرتبط هو آلة الحالات المدفوعة بـ cron، حيث تعالج التشغيلات المتداخلة الدفعة مرتين ويترك التشغيل المنهار دفعة عالقة؛ انتقالات الحالة مكانها طابور بسياسات إعادة محاولة صريحة.

أنماط البنية والتكامل

طبقة التكامل مع مزوّد الدفع

لكل تطبيق PHP مالي يلمس مزوّد دفع الأسطح الثلاثة نفسها: عميل صادر ينشئ نوايا الدفع والتحصيل والاسترداد والتحويلات؛ ونقطة ويب هوك واردة تستقبل تغيرات الحالة غير المتزامنة؛ وعملية تسوية تقارن رؤية المزوّد برؤيتك. الخطأ الذي أراه أكثر من غيره هو ترك هذه الأسطح الثلاثة تتشارك حالة قابلة للتغيير عبر نماذج ORM. النمط الذي ينجح هو طبقة مضادة للفساد: واجهة لكل قدرة من قدرات المزوّد، ومحوّلات خاصة بكل مزوّد خلفها، ونموذج مجال مستقل عن المزوّد في المنتصف، بحيث يصبح تبديل البوابة تغييرًا في الإعداد لا إعادة كتابة.

التحقق من توقيعات الويب هوك

الويب هوك هو المكان الذي يعلم فيه نظام المدفوعات أن المال تحرك، وهذا يجعله أكثر نقطة مستهدفة بالهجوم في التطبيق. ثلاث قواعد: تحقق من التوقيع على نص الطلب الخام، وارفض أي شيء خارج نافذة الطابع الزمني لإيقاف هجمات الإعادة، وقارن في زمن ثابت. الصنف أدناه مستقل عن أي إطار ولا يستخدم إلا المكتبة القياسية.

<?php
declare(strict_types=1);

final class WebhookVerifier
{
    public function __construct(
        private readonly string $secret,
        private readonly int $toleranceSeconds = 300,
    ) {
        if ($secret === '') {
            throw new InvalidArgumentException('Webhook secret must not be empty');
        }
    }

    /**
     * $rawBody is the untouched request body (php://input), never re-encoded JSON:
     * one changed byte breaks the HMAC. Header format: "t=<unix>,v1=<hex>".
     */
    public function verify(string $rawBody, string $signatureHeader, ?int $now = null): bool
    {
        $now ??= time();
        $parts = [];
        foreach (explode(',', $signatureHeader) as $pair) {
            [$k, $v] = array_pad(explode('=', trim($pair), 2), 2, '');
            $parts[$k] = $v;
        }
        $timestamp = (int) ($parts['t'] ?? 0);
        $received = strtolower($parts['v1'] ?? '');
        if ($timestamp <= 0 || $received === '') {
            return false;
        }
        // Replay protection: a valid signature outside the window is still rejected.
        if (abs($now - $timestamp) > $this->toleranceSeconds) {
            return false;
        }
        // The timestamp is signed together with the body, so it cannot be swapped.
        $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $this->secret);
        // Constant-time comparison; never compare signatures with === or ==.
        return hash_equals($expected, $received);
    }
}

// Usage in any framework: feed the raw body, not the parsed array.
$verifier = new WebhookVerifier((string) getenv('PSP_WEBHOOK_SECRET'));
$rawBody = (string) file_get_contents('php://input');
$header = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!$verifier->verify($rawBody, $header)) {
    http_response_code(400);
    exit;
}
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
// Deduplicate on $event['id'] before doing any work; providers retry deliveries.

تم الاختبار مع PHP 8.3.

تختلف صيغ التوقيع بين المزوّدين؛ عدّل التحليل لكن حافظ على hash_equals() ونافذة الطابع الزمني وقاعدة النص الخام. بعد التحقق، أزل التكرار بمعرّف الحدث لدى المزوّد، لأن كل مزوّد جاد يعيد إرسال التسليمات، وردّ بـ 2xx بسرعة بعد وضع المعالجة الحقيقية في الطابور.

نقاط نهاية دفع متكررة الأثر (idempotent)

العملاء يعيدون المحاولة. شبكات الجوال تُسقط الطلبات في منتصف الطريق، وموازنات الأحمال تنتهي مهلتها، وإضافة PHP لدى التاجر ستعيد إرسال POST /payments نفسه ثلاث مرات بكل سرور. مفتاح تكرار الأثر (idempotency key) يحوّل إعادة المحاولة إلى إعادة للرد الأول بدلًا من خصم ثانٍ. المعالج أدناه يحجز المفتاح ويدرج الدفعة في معاملة واحدة، ويخزن الرد بجانب المفتاح، ويعيده عند التكرار.

<?php
declare(strict_types=1);

/** Tables: idempotency_keys(idem_key PK, request_hash, http_status NULL, body NULL, created_at)
 *  and payments(id, amount_minor, currency, merchant_ref, status, created_at). */
final class IdempotentPaymentHandler
{
    public function __construct(private readonly PDO $db) {}

    /** Returns [httpStatus, jsonBody]. SQL is PostgreSQL flavoured (ON CONFLICT, RETURNING). */
    public function handle(string $key, array $payload): array
    {
        $hash = hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
        $this->db->beginTransaction();
        try {
            // Claim the key first: a duplicate means a retry, not a new payment.
            $claim = $this->db->prepare('INSERT INTO idempotency_keys (idem_key, request_hash, created_at)
                VALUES (:k, :h, NOW()) ON CONFLICT (idem_key) DO NOTHING');
            $claim->execute([':k' => $key, ':h' => $hash]);
            if ($claim->rowCount() === 0) {
                $this->db->rollBack();
                return $this->replay($key, $hash);
            }
            $insert = $this->db->prepare('INSERT INTO payments (amount_minor, currency, merchant_ref, status, created_at)
                VALUES (:a, :c, :r, :s, NOW()) RETURNING id');
            $insert->execute([':a' => $payload['amount_minor'], ':c' => $payload['currency'],
                ':r' => $payload['merchant_ref'], ':s' => 'pending']);
            $body = json_encode(['id' => $insert->fetchColumn(), 'status' => 'pending'], JSON_THROW_ON_ERROR);
            $this->db->prepare('UPDATE idempotency_keys SET http_status = 201, body = :b WHERE idem_key = :k')
                ->execute([':b' => $body, ':k' => $key]);
            $this->db->commit(); // key, payment and stored response land together
            return [201, $body];
        } catch (Throwable $e) {
            if ($this->db->inTransaction()) {
                $this->db->rollBack();
            }
            throw $e;
        }
    }

    private function replay(string $key, string $hash): array
    {
        $stmt = $this->db->prepare('SELECT request_hash, http_status, body FROM idempotency_keys WHERE idem_key = :k');
        $stmt->execute([':k' => $key]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($row === false || $row['request_hash'] !== $hash) {
            return [422, '{"error":"idempotency key reused with a different payload"}'];
        }
        if ($row['http_status'] === null) {
            return [409, '{"error":"original request still in progress, retry later"}'];
        }
        return [(int) $row['http_status'], (string) $row['body']];
    }
}

تم الاختبار مع PHP 8.3.

تفصيلان أهم مما يبدوان. إعادة استخدام مفتاح مع حمولة مختلفة يجب أن تُرفض لا أن يُعاد الرد بصمت، لذا جزّئ صورة قانونية من الحمولة (الحقول المتحقق منها بمفاتيح مرتبة). وفي PostgreSQL يتوقف الطلب المكرر المتزامن عند المفتاح المحجوز حتى تنتهي المعاملة الأولى، فعندما تعمل replay() يكون الرد المخزن موجودًا بالفعل؛ أما فرع «قيد التنفيذ» فهو احتياط لقواعد بيانات ذات دلالات قفل مختلفة. اجعل المفاتيح خاصة بكل تاجر وأنهِ صلاحيتها بعد يوم تقريبًا.

الطوابير عمود فقري

لا شيء يتخاطب مع طرف ثالث يحدث داخل طلب HTTP. استدعاءات مزوّد الدفع واستدعاءات اعرف عميلك والرسائل القصيرة والبريد وتفريع قيود دفتر الحسابات كلها تذهب إلى طابور. في Laravel هذا هو نظام الطوابير مع Redis وHorizon للرؤية؛ وفي Symfony هو Messenger مع ناقلات AMQP أو Redis. مهمة طلب HTTP هي التحقق وحفظ النية ووضعها في الطابور وإرجاع 202 Accepted مع معرّف يمكن للعميل استطلاعه.

دفتر الحسابات مكانه قاعدة البيانات

دفاتر القيد المزدوج في PHP ممكنة تمامًا؛ الانضباط في قاعدة البيانات لا في اللغة. قيود يومية للإلحاق فقط، وأرصدة مشتقة من القيود لا مخزنة ومحدّثة، وقيود تكامل ترفض المعاملة غير المتوازنة، وSELECT ... FOR UPDATE أو عزل قابل للتسلسل حول أي عملية تقرأ رصيدًا ثم تكتب. دور PHP هو التعبير عن المجال بوضوح وعدم تجاوز هذه القيود أبدًا باستدعاء update() مريح.

المتراص المعياري قبل الخدمات المصغرة

لفريق تقنية مالية يقل عن ثلاثين مهندسًا، يتفوق المتراص المعياري في Laravel أو Symfony على الخدمات المصغرة في كل محور تقريبًا: نشر واحد للتدقيق، وقاعدة بيانات واحدة للنسخ الاحتياطي، وحد معاملة واحد حول الدفعة وقيودها. افصل الخدمات حين يحتاج مكوّن فعلًا إلى بيئة تشغيل مختلفة (محرك مطابقة بلغة Go، نموذج مخاطر بلغة Python) أو حد امتثال مختلف (التعامل مع بيانات البطاقات).

كل حد خدمة في نظام مدفوعات هو مكان يمكن أن تكتمل فيه معاملة موزعة نصف اكتمال. لا تضفه إلا حين يكون البديل أسوأ.

الأمان والامتثال

ما تريده الجهات الرقابية فعلًا من كودك

تختلف الأطر التنظيمية في المنطقة في التفاصيل لكنها تتلاقى عند المتطلبات الهندسية نفسها. يحكم PCI DSS كل ما يلمس بيانات البطاقات. وينشر كل من البنك المركزي السعودي (SAMA) ومصرف الإمارات المركزي (CBUAE) والبنك المركزي المصري (CBE) أطرًا للأمن السيبراني والإسناد الخارجي والترخيص، وتهم أنظمة الخدمات المصرفية المفتوحة على نمط PSD2 إذا كنت تتصل بواجهات مصرفية مفتوحة. لا يعبأ أي من هذه الأطر بأنك تستخدم PHP. لكنها جميعًا تعبأ بالتحكم في الوصول والتشفير والأسرار والتسجيل وإدارة التغيير وإدارة الثغرات ومكان إقامة البيانات.

أبقِ بيانات البطاقات خارج نطاقك

أكثر قرارات PCI فاعلية قرار معماري: لا تدع رقم البطاقة الأساسي يصل إلى خوادمك أبدًا. حقول الدفع المستضافة أو صفحات الدفع بإعادة التوجيه أو الترميز (tokenisation) من مزوّد الدفع تعني أن رقم البطاقة ينتقل من متصفح العميل أو تطبيقه مباشرة إلى المزوّد، ولا ترى خلفية PHP لديك سوى رمز (token). هذا يبقيك عادة في أخف فئات التقييم الذاتي.

أرخص ضابط PCI هو رقم البطاقة الذي لا تستقبله أصلًا.

إدارة الأسرار

ملفات .env وسيلة راحة للتطوير لا مخزنًا للأسرار. في الإنتاج تأتي الأسرار من المنصة (خزنة أو مدير أسرار سحابي)، وتُدوَّر وفق جدول، ولا تُرسل إلى المستودع أبدًا. يأتي Symfony بخزنة أسرار مشفرة لهذا الغرض بالضبط. وأضافت PHP 8.2 السمة #[\SensitiveParameter] التي تحجب قيمة المعامل من تتبعات المكدس؛ استخدمها في كل دالة تقبل مفتاحًا أو رمزًا أو كلمة مرور، لأن تتبعات المكدس في ملفات السجل طريقة شائعة لتسرب الأسرار.

Composer وسلسلة التوريد

أرسل composer.lock إلى المستودع. ثبّت بـ composer install من ملف القفل في التكامل المستمر والإنتاج، ولا تستخدم composer update فيهما أبدًا. شغّل composer audit في خط الأنابيب مقابل التحذيرات المعروفة وأفشل البناء عند وجود نتائج. ثبّت إصدار PHP عبر config.platform.php، وراجع أي حزمة لها سكربتات تثبيت قبل إضافتها. تطبيق Laravel بمئة وخمسين حزمة أمر طبيعي؛ وكل واحدة منها كود أنت مسؤول عنه أمام المدقق.

التعامل مع البيانات الشخصية وسجلات التدقيق

تحتفظ تطبيقات التقنية المالية بأرقام الهوية الوطنية وأرقام الهواتف والعناوين وبيانات الرواتب وأحيانًا القياسات الحيوية. شفّر الحقول الحساسة أثناء التخزين على مستوى التطبيق (تحويل encrypted في Laravel أو مكافئه في Symfony) ليصبح أي تفريغ لقاعدة البيانات بلا المفتاح عديم الفائدة. أخفِ البيانات الشخصية في السجلات بمعالج Monolog بدلًا من الأمل في أن يتذكر المطورون. حدد فترات الاحتفاظ واحذف فعلًا، وقلّل ما تجمعه: بيانات اعرف عميلك التي لم تحتجها قط هي بيانات عليك حمايتها لسنوات بموجب قوانين حماية البيانات الشخصية التي باتت لدى مصر والسعودية والإمارات جميعًا.

ستسأل الجهات الرقابية ومحققو الاحتيال أيضًا «من غيّر هذا ومتى». سجل التدقيق جدول للإلحاق فقط يحوي الفاعل والفعل والموضوع والقيم قبل وبعد والطابع الزمني ومعرّف الطلب، ويُكتب في المعاملة نفسها التي يصفها التغيير. أحداث نماذج الإطار وحدها لا تكفي، لأن الاستعلامات الخام والتحديثات الجماعية تتخطاها. احتفظ بسجلات التدقيق في مخزن لا يستطيع كود التطبيق تحديثه أو حذفه.

الجلسات وCSRF والمصادقة

يتعامل الإطاران الرئيسيان مع رموز CSRF وتجديد الجلسة عند تسجيل الدخول وأعلام الكعكات الآمنة افتراضيًا؛ الخطر فيما تعطّله الفرق. أبقِ SameSite وSecure وHttpOnly على كعكات الجلسة، وجدد معرّف الجلسة عند كل تغيير في الصلاحيات، واضبط أعمارًا قصيرة لكل ما يمكنه تحريك المال. تجزئة كلمات المرور تكون بـ password_hash() مع bcrypt أو Argon2id لا غير، ونقاط المصادقة محدودة المعدل بصرامة.

إقامة البيانات والتحصين

يتوقع البنك المركزي السعودي استضافة فئات معينة من بيانات المؤسسات المالية السعودية داخل المملكة؛ ولدى الإمارات توقعات توطين خاصة بقطاعات بعينها؛ ولنظام حماية البيانات في مصر قواعد للنقل عبر الحدود. طابورك وذاكرتك المؤقتة وتخزين الكائنات وخط التسجيل وخدمة تتبع الأخطاء السحابية كلها تُحسب أماكن تقيم فيها البيانات؛ وإرسال الاستثناءات مع حمولات الطلبات إلى متتبع أخطاء مستضاف في الخارج قد يخرق قواعد الإقامة دون أن يلاحظ أحد. وعلى جانب المنصة، شغّل PHP-FPM بمستخدم غير مميز، وأبقِ display_errors معطلة في الإنتاج، وأبقِ PHP ضمن نافذة الدعم؛ فإصدار PHP منتهي الدعم في نظام مدفوعات ملاحظة تدقيق تنتظر الكتابة.

الأداء والتوسع

كيف يتوسع PHP-FPM فعلًا

يشغّل PHP-FPM مجموعة من عمليات العمال؛ يعالج كل عامل طلبًا واحدًا في كل مرة من البداية إلى النهاية. ولذلك فالإنتاجية هي عدد العمال مقسومًا على متوسط زمن الطلب. الطلب الذي يقضي معظم وقته منتظرًا واجهة مزوّد الدفع يحتجز عاملًا طوال الانتظار، وهذا أكبر سبب منفرد لدفع استدعاءات الأطراف الثالثة إلى الطوابير. اضبط pm.max_children وفق الذاكرة المتاحة وأبقِ مسار الطلب قصيرًا؛ عندها يصبح التوسع الأفقي مجرد خوادم متطابقة إضافية خلف موازن أحمال.

OPcache والمترجم الفوري JIT

OPcache ليس اختياريًا في الإنتاج؛ فهو يخزن الكود الثنائي المترجم مؤقتًا كي لا تعيد PHP تحليل تطبيقك مع كل طلب. اضبط opcache.validate_timestamps=0 وأعد ضبط الذاكرة المؤقتة عند النشر. أما JIT، المتاح منذ PHP 8.0، فيساعد الكود المقيد بالمعالج مثل الحلقات الرقمية ولا يفعل إلا القليل لطلب التقنية المالية المعتاد المقيد بالإدخال والإخراج؛ قِس قبل أن تفعّله.

بيئات التشغيل طويلة العمر

تكلفة إقلاع PHP-FPM (تحميل الإطار وبناء الحاوية) تُدفع مع كل طلب. بيئات التشغيل طويلة العمر تبقي التطبيق محمّلًا في الذاكرة وتخدم الطلبات من عمال دائمين.

بيئة التشغيل النموذج ما تكسبه ما يجب مراقبته
PHP-FPM عملية لكل طلب البساطة والعزل ومعرفة تشغيلية شائعة تكلفة الإقلاع مع كل طلب
FrankenPHP خادم Go (Caddy) يضمّن PHP مع وضع العمال نشر بسيط وHTTP حديث ووضع عمال بلا امتدادات إضافية أحدث؛ خبراء تشغيل أقل
RoadRunner خادم تطبيقات بلغة Go مع عمال PHP ناضج ويعمل مع Octane وSymfony ملف تنفيذي منفصل يجب تشغيله
Swoole / Open Swoole حلقة أحداث بالروتينات المتزامنة داخل امتداد PHP أعلى إنتاجية وتزامن أصلي تثبيت الامتداد ومكتبات غير آمنة للروتينات وتسرب الحالة

يشغّل Laravel Octane تطبيق Laravel على FrankenPHP أو RoadRunner أو Swoole؛ ويعمل Symfony على FrankenPHP وRoadRunner عبر مكوّن بيئة التشغيل الخاص به. مكسب زمن الاستجابة حقيقي؛ والثمن أن كودك صار يحمل حالة تبقى بين الطلبات: الخصائص الثابتة، والكائنات المفردة التي التقطت مستخدمًا، والإعدادات المحفوظة التي تحوي معرّف مستأجر، كلها تصبح أخطاء. وفي واجهة مدفوعات هذا خطر على الصحة لا مجرد تفصيل أداء، ولن أتبنى بيئة تشغيل طويلة العمر إلا مع مجموعة اختبارات تعمل تحتها.

الطوابير والعمال والاتصالات

سواء شغّلت Horizon أو Messenger، فقواعد المال واحدة: اجعل كل مهمة متكررة الأثر، لأن التسليم مرة واحدة على الأقل يعني أنها ستعمل مرتين في النهاية؛ وحدد حدودًا صريحة لإعادة المحاولة والتراجع؛ ووجّه المهام الحرجة للمدفوعات إلى طابور خاص بها حتى لا يؤخر فيض من مهام البريد تسوية؛ وراقب عمق الطابور كمقياس إنتاج من الدرجة الأولى.

يفتح كل عامل PHP-FPM اتصاله الخاص بقاعدة البيانات، لذا يمكن لأسطول من الخوادم استنفاد حد اتصالات PostgreSQL بسهولة. ضع PgBouncer أو ProxySQL أمام قاعدة البيانات، واستخدم الاتصالات الدائمة بحذر، وأبقِ المعاملات قصيرة. تساعد نسخ القراءة في التقارير لكن يجب ألا تخدم أبدًا قراءة رصيد تسبق كتابة، ويخزن Redis مؤقتًا البيانات المرجعية مثل العملات وجداول الرسوم، ولا يخزن رصيدًا أبدًا.

حيث لا تناسب PHP

كن صادقًا بشأن الحدود. PHP خيار ضعيف للاتصالات طويلة العمر (خوادم WebSocket وبث بيانات السوق)، وللعمل الرقمي الثقيل (نماذج المخاطر والتسويات على ملايين الصفوف في الذاكرة)، ولكل ما يحتاج إلى مجدول متزامن حقيقي داخل العملية. يستطيع Swoole بعض ذلك، لكنك ستقاتل المنظومة. الخطوة الصحيحة هي خدمة منفصلة بلغة Go أو Rust أو Python أو JVM، مع PHP تنسّق عبر الطوابير والواجهات البرمجية.

الفريق والتوظيف في المنطقة

مخزون المواهب

لدى مصر واحد من أعمق مخزونات مواهب PHP في المنطقة، بُني على مدى عقدين من الوكالات وشركات المنتجات على WordPress وCodeIgniter وYii ومؤخرًا Laravel. وتوظف السعودية والإمارات جزءًا كبيرًا من طاقتها في PHP من مصر والأردن وباكستان والهند. يمكنك تشكيل فريق Laravel في القاهرة أسرع من أي منظومة خلفية أخرى تقريبًا، ويمكن لشركة تقنية مالية في الرياض أو دبي استقطاب مهندسي PHP كبار عن بُعد في المناطق الزمنية نفسها.

العمق متفاوت

الاتساع ليس عمقًا. تعلّم نصيب كبير من هذا المخزون PHP بوصفها «Laravel وEloquent وBlade» ولم يكتب يومًا كائن قيمة أو يضبط PHPStan أو يفكر في دقة الأعداد العائمة. وتعلّمها نصيب آخر عام 2010 ولم يغادر تمامًا؛ يعرفون اللغة عن ظهر قلب لكنهم يلجؤون إلى الحالة العامة وSQL الخام. لا أحد من هذين الملفين عديم الفائدة للتقنية المالية؛ وكلاهما يحتاج إلى معيار هندسي واضح وثقافة مراجعة كود تفرضه.

إشارات المقابلة المهمة

الأسئلة التي أجدها الأكثر تنبؤًا في أدوار PHP بالتقنية المالية:

  • اطلب منهم جمع مبلغين بعملتين مختلفتين. هل يسألون كيف تُخزن المبالغ؟
  • اسأل ماذا يحدث حين يعيد العميل محاولة طلب POST /payments. يجب أن تظهر كلمة «تكرار الأثر» (idempotency) دون تلقين.
  • اسأل كيف يتحققون من ويب هوك. أنصت إلى النص الخام وHMAC والمقارنة في زمن ثابت ونوافذ الإعادة.
  • اسأل ماذا يغيّر declare(strict_types=1) وما مستوى PHPStan الذي يشغّلونه.
  • اسأل متى لا يستخدمون ORM. تخبرك الإجابة ما إذا كانوا يفهمون دفاتر الحسابات.

رفع مهارات فريق قائم

أسرع طريق أعرفه من «لدينا فريق PHP 7» إلى «لدينا فريق PHP 8 للتقنية المالية»: فعّل strict_types في الملفات الجديدة، وأضف PHPStan وارفع مستواه شهريًا، واعتمد Rector للترقيات الميكانيكية، وأدخل كائنات القيمة للمال والمعرّفات، وانقل استدعاءات الأطراف الثالثة إلى الطوابير، وضع اختبارات Pest أو PHPUnit حول مسارات المال، واكتب معيارًا هندسيًا قصيرًا يستطيع المراجعون الإشارة إليه. لا شيء من هذا يتطلب إعادة كتابة؛ وكله يتراكم. سيقضي معظم الموظفين الجدد شهورهم الأولى في الكود القديم، فاجعل خطة التحديث مرئية: تغادر الفرق حين تعتقد أن القديم باقٍ إلى الأبد.

إطار اتخاذ القرار

متى تكون PHP مع Laravel أو Symfony القرار الصحيح

اختر PHP حين يكون النظام في الأساس عمل ويب وواجهات برمجية معاملاتي: بوابات التجار والمكاتب الخلفية وتكاملات مزوّدي الدفع وتنسيق اعرف عميلك والفوترة والإشعارات وواجهات الشركاء وإضافات التجارة الإلكترونية. واخترها حين تحتاج إلى التوظيف بسرعة في مصر أو السعودية أو الإمارات، أو حين يعرفها الفريق بالفعل، أو حين يجب أن يعيش منتجك داخل منظومة PHP لجهة أخرى. اختر Laravel لفريق منتج عليه الإطلاق خلال شهور؛ واختر Symfony لمنصة يُتوقع أن تعمّر بعد الفريق المؤسس.

متى تختار شيئًا آخر

اختر لغة مختلفة لنواة دفتر حسابات تحتاج إلى زمن استجابة منخفض جدًا أو إنتاجية قصوى، ولمحركات بيانات السوق أو المطابقة، ولنماذج المخاطر والاحتيال، وللاتصالات طويلة العمر، أو حيث تجعل قاعدة كود أصغر ومنفصلة حد الامتثال أنظف. واختر شيئًا آخر إذا كان مهندسوك الكبار أقوى فعلًا في غيرها؛ ففريق Go ممتاز يتفوق على فريق Laravel متوسط في كل مرة.

مصفوفة القرار

المكوّن PHP (Laravel / Symfony) Python Go / JVM / Rust ملاحظات
بوابات التجار والإدارة قوية مقبولة مبالغ فيها مخزون التوظيف يحسم
تكاملات مزوّدي الدفع والويب هوك قوية قوية قوية طابق حزمة التطوير والفريق
فوترة الاشتراكات قوية (Cashier) مقبولة مقبولة أدوات Laravel الرسمية
تنسيق اعرف عميلك والتسجيل قوية قوية مقبولة إدخال وإخراج وحالة في الغالب
دفتر قيد مزدوج بحجم متوسط مقبولة مقبولة قوية PostgreSQL يتحمل العبء
دفتر حسابات أو مطابقة عالية الإنتاجية ضعيفة ضعيفة قوية زمن الاستجابة والتزامن
نماذج المخاطر والاحتيال والتقييم ضعيفة قوية مقبولة المنظومة لا سرعة اللغة
البث الفوري وWebSockets ضعيفة مقبولة قوية اتصالات طويلة العمر
إضافات التجارة الإلكترونية وحزم التجار قوية ضعيفة ضعيفة التاجر يشغّل PHP
التسوية الدفعية على نطاق واسع مقبولة قوية قوية ذاكرة وعمل رقمي

خيار افتراضي معقول لشركة تقنية مالية في المنطقة

لو كان عليّ اختيار خيار افتراضي لشركة ناشئة في المدفوعات أو المحافظ في المنطقة اليوم: Laravel لكل ما يواجه التجار والمشغلين ولتنسيق مزوّدي الدفع، وPostgreSQL مصدرًا للحقيقة مع فرض قواعد دفتر الحسابات في المخطط، وطوابير Redis مع Horizon، وحقول مستضافة لدى مزوّد الدفع لإبقاء نطاق PCI في حده الأدنى، وخطة واضحة لفصل خدمة مستقلة يوم يحتاجها دفتر الحسابات أو محرك المخاطر. هذه المنظومة مملة وقابلة للتدقيق وقابلة للتوظيف، وهذا بالضبط ما يحتاجه المنتج الخاضع للرقابة.

الأسئلة الشائعة

هل PHP آمنة بما يكفي لمنتج مالي خاضع للرقابة؟

نعم، مع التحفظ الذي ينطبق على كل لغة: الأمان يعتمد على الهندسة المحيطة ببيئة التشغيل. PHP 8 مع الأنواع الصارمة وإصدار مدعوم وإعدادات الإطار الافتراضية مفعّلة واستعلامات مهيأة في كل مكان وأسرار خارج المستودع وcomposer audit في التكامل المستمر يمكن الدفاع عنها أمام SAMA أو CBUAE أو CBE مثل أي منظومة أخرى. معظم ملاحظات التدقيق على PHP تأتي من الكود القديم والإعدادات الافتراضية المعطلة لا من اللغة.

Laravel أم Symfony لمنصة مدفوعات؟

Laravel إذا كنت فريق منتج يحتاج إلى الإطلاق والتوظيف بسرعة في المنطقة. وSymfony إذا كنت تبني منصة كبيرة متعددة الفرق بحدود صريحة يجب أن تدوم عقدًا. كلاهما يستخدم مكتبات المال نفسها ويجتاز عمليات التدقيق نفسها؛ والخيار الخاطئ هو أن تختار أحدهما ثم تقاتل اصطلاحاته.

هل أعيد كتابة قاعدة كود PHP 7 القديمة لمنتجي المالي؟

نادرًا. رقِّ في مكانك: انتقل إلى إصدار PHP مدعوم بمساعدة Rector، وأضف التحليل الثابت والاختبارات حول مسارات المال، واستخرج كائنات القيمة والطوابير، وخنق أسوأ الوحدات واحدة تلو الأخرى. إعادة الكتابة الكاملة لأنظمة مدفوعات تعمل تستغرق أطول مما يقدّره أي أحد وتشغّل نظامين بالتوازي خلال أخطر فترة.

هل تتعامل PHP مع الميزات الفورية مثل الأرصدة الحية والإشعارات؟

جزئيًا. الإشعارات التي تطلقها عمال الطوابير أمر روتيني. والاحتفاظ بآلاف اتصالات WebSocket في PHP ممكن مع Swoole، لكن خدمة مخصصة أو مزوّد دفع إشعارات مُدار هو الخيار الأفضل عادة، مع PHP تنشر الأحداث إليه. الرصيد نفسه يبقى في قاعدة البيانات؛ والطبقة الفورية تحمل الإشعارات لا مصدر الحقيقة أبدًا.

كيف أبقي نطاق PCI DSS صغيرًا في تطبيق PHP؟

لا تستقبل رقم بطاقة أبدًا. استخدم حقول مزوّد الدفع المستضافة أو صفحة الدفع بإعادة التوجيه أو حزمة الجوال بحيث تذهب بيانات البطاقة مباشرة إلى المزوّد ولا تخزن خلفيتك سوى الرموز. ثم تأكد أن لا شيء في سجلاتك أو ذاكراتك المؤقتة أو نسخك الاحتياطية أو متتبعات الأخطاء يستطيع رؤية بيانات البطاقات أيضًا. ما يبقى هو تعاملك مع الرموز ووضعك الأمني العام، وهو تدقيق أصغر بكثير من بيئة بيانات بطاقات كاملة.

الخلاصة

  • تهم PHP في التقنية المالية لأن جانب التجار في الصناعة يعمل بها: حزم مزوّدي الدفع وصفحات الدفع التجارية وطبقات الفوترة في غالبيتها الساحقة PHP.
  • PHP 8.x مع strict_types وكائنات القيمة للقراءة فقط والتعدادات وPHPStan تمنحك أدوات الصحة التي يتطلبها المال؛ وسمعة 2008 عفا عليها الزمن.
  • خزّن المال واحسبه كأعداد صحيحة بالوحدات الصغرى مع عملة صريحة؛ واستخدم brick/money أو moneyphp في الإنتاج.
  • تحقق من الويب هوك على النص الخام بـ HMAC ونافذة طابع زمني وhash_equals()، ثم أزل التكرار بمعرّف الحدث.
  • كل نقطة نهاية تنشئ حركة مال متكررة الأثر، وكل مهمة تلمس المال تفترض التسليم مرة واحدة على الأقل.
  • أبقِ نطاق PCI صغيرًا بحقول الدفع المستضافة؛ وأبقِ الأسرار خارج الملفات؛ وعامل composer.lock وcomposer audit كأدلة امتثال.
  • يتوسع PHP-FPM أفقيًا بسهولة؛ وبيئات التشغيل طويلة العمر تشتري زمن الاستجابة بثمن انضباط الحالة؛ والأعمال الرقمية والاتصالات طويلة العمر مكانها خدمة أخرى.
  • خيار افتراضي سليم لشركة تقنية مالية في المنطقة: Laravel أو Symfony للسطح المعاملاتي، وPostgreSQL دفترًا للحسابات، وطوابير لكل استدعاء طرف ثالث، وخطة لفصل الخدمات المتخصصة حين تستحق ذلك.