Payment Methodمن الصفر.
بناء طريقة دفع كاملة. بنية الـ gateway والـ commands، دورة حياة الدفع من التفويض للتحصيل، الواجهة في الـ checkout، والتعامل الآمن مع البيانات الحسّاسة.
طريقة الدفع في Magento 2 بتتبني بالـ Payment Gateway architecture: virtualType من Method\Adapter في di.xml مربوط بـ CommandPool فيه command لكل عملية authorize و capture و refund و void، وكل command بيتكوّن من Request Builder و Client و Validator و Response Handler. بيانات الكارت بتروح للبوابة مباشرة وإنت بتاخد توكن، والـ webhook لازم يتحقق من التوقيع ويحمي من التكرار.
دورة حياة الدفع
المفاهيم اللي كل حاجة بعدها بتبني عليها.
Authorize فقط
تفويض وقت الطلب، وتحصيل عند الشحن. أأمن للتاجر.
Authorize + Capture
الاتنين مرة واحدة وقت الطلب. أبسط، ومناسب للمنتجات الرقمية.
أنماط التكامل
تلات طرق، وكل واحدة ليها تكلفة أمنية مختلفة.
ابدأ بـ Hosted Fields لو البوابة بتدعمه — بيدي أحسن توازن بين التجربة والأمان. لو مش متاح، Redirect. والـ Direct API مايتفكّرش فيه إلا لو عندك شهادة PCI فعلاً وفاهم التزاماتها.
بنية الـ Gateway
إزاي Magento بيفكّك عملية الدفع.
بدل كلاس واحد ضخم بيعمل كل حاجة، Magento بيفكّك العملية لـ قطع صغيرة كل واحدة ليها مسؤولية واحدة. ده اسمه Payment Gateway architecture.
عشان تقدر تغيّر جزء من غير ما تلمس الباقي. البوابة غيّرت شكل الطلب؟ تعدّل الـ Request Builder بس. ضفت حقل للرد؟ Handler جديد. وكل قطعة قابلة للاختبار لوحدها.
الإعداد الأساسي
تعريف الطريقة وإعداداتها.
<config> <default> <payment> <mygateway> <!-- الكلاس اللي بيمثّل الطريقة --> <model>MyGatewayFacade</model> <active>0</active> <title>Pay by Card</title> <!-- authorize أو authorize_capture --> <payment_action>authorize</payment_action> <!-- الدول المسموحة --> <allowspecific>0</allowspecific> <!-- حدود المبلغ --> <min_order_total>0</min_order_total> <max_order_total>10000</max_order_total> <!-- العملات المدعومة --> <currency>SAR,AED,EGP</currency> </mygateway> </payment> </default> </config>
- payment_action — authorize للتفويض بس، authorize_capture للاتنين.
- min/max_order_total — حماية مهمة. بوابات كتير ليها حدود.
- currency — لو البوابة بتدعم عملات محددة، حدّدها هنا.
- active = 0 — دايماً معطّلة افتراضياً. التفعيل قرار واعي.
<group id="mygateway" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1"> <label>My Gateway</label> <field id="active" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> <label>Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <!-- المفتاح السري — لاحظ backend_model --> <field id="secret_key" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> <label>Secret Key</label> <backend_model> Magento\Config\Model\Config\Backend\Encrypted </backend_model> </field> </group>
الـ Commands
ربط العمليات بمكوّناتها.
<!-- الكلاس اللي Magento بيتعامل معاه --> <virtualType name="MyGatewayFacade" type="Magento\Payment\Model\Method\Adapter"> <arguments> <argument name="code" xsi:type="const"> Vendor\Payment\Model\Ui\ConfigProvider::CODE </argument> <!-- الواجهة في الـ checkout --> <argument name="formBlockType" xsi:type="string"> Magento\Payment\Block\Form </argument> <!-- إيه اللي الطريقة دي تقدر تعمله --> <argument name="valueHandlerPool" xsi:type="object"> MyGatewayValueHandlerPool </argument> <!-- العمليات المتاحة --> <argument name="commandPool" xsi:type="object"> MyGatewayCommandPool </argument> </arguments> </virtualType>
<virtualType name="MyGatewayCommandPool" type="Magento\Payment\Gateway\Command\CommandPool"> <arguments> <argument name="commands" xsi:type="array"> <item name="authorize" xsi:type="string"> MyGatewayAuthorizeCommand </item> <item name="capture" xsi:type="string"> MyGatewayCaptureCommand </item> <item name="refund" xsi:type="string"> MyGatewayRefundCommand </item> <item name="void" xsi:type="string"> MyGatewayVoidCommand </item> </argument> </arguments> </virtualType>
<virtualType name="MyGatewayAuthorizeCommand" type="Magento\Payment\Gateway\Command\GatewayCommand"> <arguments> <!-- تجهيز الطلب --> <argument name="requestBuilder" xsi:type="object"> MyGatewayAuthorizeRequest </argument> <!-- إرساله --> <argument name="transferFactory" xsi:type="object"> Vendor\Payment\Gateway\Http\TransferFactory </argument> <argument name="client" xsi:type="object"> Vendor\Payment\Gateway\Http\Client </argument> <!-- التحقق ومعالجة الرد --> <argument name="validator" xsi:type="object"> Vendor\Payment\Gateway\Validator\ResponseValidator </argument> <argument name="handler" xsi:type="object"> MyGatewayResponseHandlerComposite </argument> </arguments> </virtualType>
Request و Response
الكود الفعلي اللي هتكتبه.
namespace Vendor\Payment\Gateway\Request; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Gateway\Helper\SubjectReader; class AuthorizeBuilder implements BuilderInterface { public function build(array $buildSubject): array { // استخراج بيانات الدفع بالطريقة الصحيحة $paymentDO = SubjectReader::readPayment($buildSubject); $amount = SubjectReader::readAmount($buildSubject); $payment = $paymentDO->getPayment(); $order = $paymentDO->getOrder(); return [ // التوكن من الواجهة — مش بيانات كارت 'payment_token' => $payment->getAdditionalInformation( 'payment_token' ), // المبلغ بالعملة الأساسية 'amount' => $this->formatAmount($amount), 'currency' => $order->getCurrencyCode(), // مرجع فريد للمعاملة 'reference' => $order->getOrderIncrementId(), 'capture' => false, // تفويض بس ]; } // كتير من البوابات بتستقبل المبلغ بالوحدة الصغرى private function formatAmount(float $amount): int { return (int) round($amount * 100); } }
namespace Vendor\Payment\Gateway\Response; use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Payment\Gateway\Helper\SubjectReader; class TxnIdHandler implements HandlerInterface { public function handle( array $handlingSubject, array $response ): void { $paymentDO = SubjectReader::readPayment( $handlingSubject ); $payment = $paymentDO->getPayment(); // مرجع المعاملة عند البوابة — مهم جداً $payment->setTransactionId($response['transaction_id']); // المعاملة مش مقفولة — لسه فيه capture $payment->setIsTransactionClosed(false); // نحتفظ بالمرجع للعمليات الجاية $payment->setAdditionalInformation( 'gateway_reference', $response['reference'] ); } }
بيقول لـ Magento هل المعاملة خلصت ولا لسه. في الـ authorize بتحطها false لأن لسه فيه capture جاي. في الـ capture النهائي بتبقى true.
class ResponseValidator extends AbstractValidator { public function validate(array $validationSubject): ResultInterface { $response = SubjectReader::readResponse( $validationSubject ); $isValid = isset($response['status']) && $response['status'] === 'approved'; if ($isValid) { return $this->createResult(true); } // رسالة عامة للعميل — مش تفاصيل البوابة return $this->createResult(false, [ __('Payment could not be processed.') ]); } }
الواجهة في الـ Checkout
ربط الطريقة بواجهة العميل.
بيمرّر إعدادات الطريقة من PHP لواجهة الـ checkout (JavaScript).
namespace Vendor\Payment\Model\Ui; use Magento\Checkout\Model\ConfigProviderInterface; class ConfigProvider implements ConfigProviderInterface { public const CODE = 'mygateway'; public function __construct( private readonly Config $config ) {} public function getConfig(): array { return [ 'payment' => [ self::CODE => [ // المفتاح العام فقط — مش السري 'publicKey' => $this->config->getPublicKey(), 'sandbox' => $this->config->isSandbox(), ], ], ]; } }
- العميل يدخل بيانات الكارت في حقول البوابة (مش حقولك).
- مكتبة البوابة بتحوّل البيانات لـ توكن — البيانات راحت للبوابة مباشرة.
- التوكن بيتحط في additionalData وبيتبعت لـ Magento.
- الـ Request Builder بياخد التوكن ويبعته للبوابة مع الطلب.
الـ Webhooks
لما البوابة تبلّغك بتغيير.
عمليات كتير بتخلص بعدين: الدفع بتحويل بنكي، التحقق الإضافي، الاسترداد المتأخر. البوابة بتبعتلك إشعار لما الحالة تتغيّر.
class Index extends Action implements CsrfAwareActionInterface { // الـ webhook مالهوش form key public function validateForCsrf(RequestInterface $r): ?bool { return true; } public function createCsrfValidationException( RequestInterface $request ): ?InvalidRequestException { return null; } public function execute() { $payload = $this->getRequest()->getContent(); $signature = $this->getRequest() ->getHeader('X-Gateway-Signature'); // 1. التحقق من التوقيع — إلزامي if (!$this->signatureValidator->isValid( $payload, $signature )) { $this->logger->warning('Invalid webhook signature'); return $this->respond(401); } $data = json_decode($payload, true); // 2. الحماية من التكرار if ($this->log->isProcessed($data['event_id'])) { return $this->respond(200); } try { $this->processor->handle($data); $this->log->markProcessed($data['event_id']); } catch (\Exception $e) { $this->logger->error($e->getMessage()); // 500 عشان البوابة تعيد المحاولة return $this->respond(500); } return $this->respond(200); } }
- التحقق من التوقيع — بيثبت إن الإشعار من البوابة فعلاً.
- الحماية من التكرار — البوابات بتعيد الإرسال لو ماردتش بسرعة.
- الرد بـ 500 عند الفشل — عشان البوابة تعيد المحاولة بدل ما الحدث يضيع.
الأمان
القواعد اللي مفيش تفاوض عليها.
<!-- Magento بيوفّر آلية جاهزة --> <virtualType name="MyGatewayLogger" type="Magento\Payment\Model\Method\Logger"> <arguments> <argument name="debugReplaceKeys" xsi:type="array"> <item name="0" xsi:type="string">card_number</item> <item name="1" xsi:type="string">cvv</item> <item name="2" xsi:type="string">secret_key</item> </argument> </arguments> </virtualType>
الاختبار
إيه اللي لازم تجرّبه قبل الإطلاق.
- طلب كامل بتفويض وتحصيل.
- تحصيل جزئي (فاتورة على جزء من الطلب).
- استرداد كامل وجزئي.
- إلغاء قبل التحصيل (void).
- كارت مرفوض — الرسالة واضحة والأوردر مااتعملش؟
- رصيد غير كافٍ — نفس السؤال.
- انقطاع الشبكة وقت الطلب — إيه اللي بيحصل؟
- البوابة بتتأخر — فيه timeout؟ الأوردر بيتعمل ولا لأ؟
- العميل يقفل المتصفح بعد الدفع — الأوردر بيكمّل بالـ webhook؟
- webhook بتوقيع غلط — بيترفض؟
- webhook مكرّر — بيتعالج مرة واحدة؟
- كل السيناريوهات فوق على بيئة الاختبار للبوابة.
- تأكد إن السجلات نضيفة من أي بيانات حسّاسة.
- اختبر بمبالغ فيها كسور عشرية — للتأكد من التنسيق.
- اختبر بكل العملات المدعومة.
- معاملة حقيقية واحدة بمبلغ صغير على production، وتأكد من وصول الفلوس فعلاً في حساب البوابة.
الفخاخ الشائعة
اقرا دي حتى لو مش هتقرا حاجة تانية.
- تخزين بيانات كروت — التزامات قانونية وخطر تسريب. توكن بس.
- المفتاح السري في ConfigProvider — بيوصل لمتصفح العميل.
- webhook من غير تحقق توقيع — إشعارات مزيّفة وطلبات مجانية.
- تنسيق مبلغ غلط — خصم جزء من المبلغ أو مية ضعف.
- setIsTransactionClosed(true) في authorize — مش هتقدر تحصّل بعدين.
- عرض رسالة البوابة الخام للعميل — كشف تفاصيل داخلية.
- مفيش معالجة لـ webhook مكرّر — استرداد أو معالجة مضاعفة.
- نسيان انتهاء التفويض — شحن من غير تحصيل.
- الطريقة مفعّلة افتراضياً — عملاء بيحاولوا يدفعوا بطريقة مش متظبطة.
- اختبار المسار الناجح بس — مسارات الفشل هي اللي بتوجع في الإنتاج.
أسئلة الإنترفيو
الأسئلة المتكررة.
Payment Method ✓
دلوقتي فاهم دورة حياة الدفع، أنماط التكامل وتكلفتها الأمنية، بنية الـ gateway ومكوّناتها، الـ webhooks، وإيه اللي لازم تختبره.
الخلاصة: متلمسش بيانات الكارت — خُد توكن، تحقّق من توقيع كل webhook، واحسب المبلغ من السيرفر دايماً.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
إيه الفرق بين authorize و capture في Magento 2؟
الـ authorize بيتأكد إن الفلوس موجودة ويحجزها من غير خصم، والـ capture بيخصمها فعلًا. إعداد payment_action بيحدد الاستراتيجية: authorize بس والتحصيل عند الفاتورة أو الشحن، أو authorize_capture الاتنين وقت الطلب. للمنتجات المادية الأول أأمن لأن الـ void أنضف من الـ refund لو المنتج خلص، بس التفويض بينتهي بعد فترة فلازم تحسبها.
إزاي أبني طريقة دفع مخصّصة في Magento 2؟
بالـ Payment Gateway architecture: virtualType من Magento\Payment\Model\Method\Adapter في di.xml بيتربط بـ CommandPool فيه command لكل عملية، وكل GatewayCommand بيتكوّن من Request Builder و Transfer Factory و Client و Validator و Response Handler. القيم الافتراضية في etc/config.xml تحت payment والشاشة في system.xml، وابدأ بـ active = 0 دايمًا.
إزاي أتعامل مع بيانات الكروت بأمان في Magento 2؟
مابتلمسهاش أصلًا. استخدم Hosted Fields أو Redirect فالبيانات بتروح من متصفح العميل للبوابة مباشرة وإنت بتاخد توكن في additionalData، وده بيخرجك من نطاق PCI DSS تقريبًا. متخزّنش رقم ولا CVV في أي مكان حتى الـ logs، خزّن المفاتيح السرية بحقل obscure و backend_model Encrypted، وامنع تسجيل الحقول الحسّاسة بـ debugReplaceKeys في الـ Logger.
ليه التحقق من توقيع الـ webhook إلزامي في بوابة الدفع؟
لأن رابط الـ webhook مش سر. من غير تحقق أي حد يعرفه يقدر يبعت إشعار مزيّف يقول الدفع نجح ويطلب منتجات مجانًا. الـ controller لازم يطبّق CsrfAwareActionInterface لأنه مالوش form key، يتحقق من التوقيع ويرجّع 401 لو غلط، يحمي من التكرار بـ event_id لأن البوابات بتعيد الإرسال، ويرجّع 500 عند الفشل عشان البوابة تعيد المحاولة.
إيه اللي بيحصل لو العميل دفع وقفل المتصفح قبل ما يرجع للموقع؟
ده السيناريو اللي الـ webhook بيحله. الفلوس اتخصمت عند البوابة، فالبوابة بتبعت إشعار بتغيّر الحالة وإنت بتكمّل الأوردر بناءً عليه. من غير webhook سليم الفلوس اتخصمت والأوردر مش موجود، وده أسوأ سيناريو ممكن في أي تكامل دفع. اختبره صراحةً على sandbox البوابة قبل الإطلاق مع باقي مسارات الفشل.
ليه مش قادر أعمل capture من الأدمن بعد الـ authorize؟
غالبًا الـ Response Handler حط setIsTransactionClosed(true) في الـ authorize. القيمة دي بتقول لـ Magento إن المعاملة خلصت، فمش هيسمح بالتحصيل بعدها. في الـ authorize خليها false لأن لسه فيه capture جاي، وفي الـ capture النهائي true. وخزّن setTransactionId بمرجع البوابة لأن الرفاند والـ void محتاجينه.
ليه المبلغ بيوصل للبوابة غلط في تكامل الدفع؟
غالبًا التنسيق. بوابات كتير بتستقبل المبلغ بالوحدة الصغرى كعدد صحيح، فلو بعتّ 100.50 وهي متوقّعة 10050 هتخصم جزء من المبلغ أو مية ضعف. اقرا المبلغ من SubjectReader::readAmount في الـ Request Builder، واحسبه من الأوردر على السيرفر مش من أي قيمة جاية من العميل، واختبر بمبالغ فيها كسور وبكل العملات المدعومة.