حزمة SDK لـ PHP

anis-ly/partners — لـ PHP 8.2 إلى 8.5، مع أي عميل HTTP يدعم PSR-18. تعمل مع PHP الخالصة وLaravel وSymfony.

أداة مساعدة اختيارية، تُقدَّم كما هي

كل حزم SDK من أنيس مجانية وتُقدَّم بموجب رخصة MIT «كما هي»، دون أي ضمان من أي نوع. ولا تتحمّل أنيس (Aniscom for Technical Services) أي مسؤولية أو التزام عن استخدامها أو عن أي خسارة تنشأ عنه. وتبقى أنت المسؤول عن ربط نظامك — تسجيل طلبات الشراء قبل إرسالها، والاستئناف، وحفظ المفاتيح، والاختبار. الشيفرة المصدرية علنية: اقرأها لتفهم بدقة ما تفعله قبل أن تعتمد عليها. ولا تحتاج إلى حزمة SDK أصلًا — يمكنك استدعاء واجهة API لشركاء أنيس مباشرة باتباع دليل الاستخدام وصفحة مرجع API.

حزمة SDK بالإصدار 2.2.0. إصدارات 1.x مهجورة؛ راجع سجل التغييرات.

التثبيت

composer require anis-ly/partners:2.2.0 guzzlehttp/guzzle

تعثر الحزمة على عميل PSR-18 ومصانع PSR-17 المثبّتة لديك — Guzzle، أو Symfony HTTP Client مع nyholm/psr7. اضبط مهلتي الاتصال والاستدعاء على هذا العميل، وأوقف اتباع إعادة التوجيه: فالاستدعاء الموقَّع يجب ألا يُرسَل مرة أخرى إلى عنوان آخر أبدًا.

الإعداد

use Anis\Partners\AnisPartnersClient;
use Anis\Partners\ClientOptions;
use Anis\Partners\Signing\PemP256Signer;

$options = ClientOptions::fromArray([
    'authority' => 'https://<the address Anis gave you>',
    'signatureLifetimeSeconds' => 60,
    'acceptLanguage' => 'Arabic',
    'signingKeyCacheSeconds' => 300,
]);
$signer = PemP256Signer::fromPemFile('/secure/partner-key.pem')->forKey($keyId);
$client = AnisPartnersClient::create($options, $signer);

تُفحص الإعدادات عند إنشاء العميل: فعنوان ليس HTTPS (يُقبل HTTP العادي على جهازك أنت فقط)، أو مدة صلاحية توقيع خارج النطاق من 1 إلى 60 ثانية، أو مدة حفظ للمفاتيح تتجاوز 5 دقائق، يوقفه عند تلك النقطة.

المُوقِّع خطوة منفصلة عن قصد. يوقّع PemP256Signer باستخدام ملف مفتاح لا يقرؤه سوى مستخدم تطبيقك. أما مع خزنة أسرار أو وحدة أمان عتادية، فنفّذ الواجهة P256Signer — دالة توقّع البايتات وتعيد التوقيع ذا الـ 64 بايت — ولا يدخل المفتاح إلى عملية PHP لديك أبدًا.

PHP-FPM وغيره من المضيفات متعددة العمليات: لكل عامل ذاكرته الخاصة، فمرّر ذاكرة مؤقتة مشتركة من نوع PSR-16 لكي تعيد العمليات استخدام وثيقة مفاتيح التوقيع العامة الخاصة بأنيس بدل جلبها مع كل عملية جديدة. استخدم ذاكرة مؤقتة لا يكتب فيها غير تطبيقك. تحتفظ الحزمة بقائمة مفاتيح أنيس لمدة لا تتجاوز 5 دقائق من وقت جلبها، حتى حين تأتي من الذاكرة المؤقتة المشتركة.

$client = AnisPartnersClient::create($options, $signer, keyCache: $psr16Cache);

الاستخدام مع Laravel

لا توجد حزمة منفصلة لـ Laravel: الحزمة تعمل في Laravel كما هي. سجّل عميلًا واحدًا مشتركًا في مزوّد خدمة (يسجّله الأمر php artisan make:provider AnisPartnersServiceProvider نيابةً عنك)، مع ذاكرة Laravel المؤقتة لتكون ذاكرة المفاتيح المشتركة، ومسجّل Laravel:

$this->app->singleton(AnisPartnersClient::class, function (Application $app): AnisPartnersClient {
    $config = $app['config']['anis-partners'];               // config/anis-partners.php, read from .env

    return AnisPartnersClient::create(
        new ClientOptions($config['authority'], acceptLanguage: AcceptLanguage::from($config['accept_language'])),
        PemP256Signer::fromPemFile($config['key_file'])->forKey($config['key_id']),
        http: new GuzzleClient(['timeout' => 30, 'connect_timeout' => 5, 'allow_redirects' => false]),
        keyCache: $app['cache']->store($config['key_cache_store']),   // every worker shares Anis's public keys
        logger: $app->make(LoggerInterface::class),
    );
});

ثم اطلب AnisPartnersClient في أي متحكّم أو مهمة أو أمر. استخدم مخزنًا مؤقتًا تتشاركه كل العمليات ولا يكتب فيه إلا تطبيقك — وليس array أبدًا. قدّم طلبات الشراء من مهمة في الطابور مع إيقاف إعادة المحاولة التلقائية في Laravel، واستأنف بـ resume() بالمعرّف نفسه. الدليل الكامل، مع ملف الإعداد وقواعد مهام الطابور، هو Using the SDK with Laravel.

استخدمها

$profile = $client->profile()->get();

foreach ($client->wallets()->list() as $wallet) {            // every page, followed for you
    printf("%s %s\n", $wallet->name ?? $wallet->id, $wallet->balance?->amount());
}

foreach ($client->catalogue()->listCards($walletId, $subcategoryId) as $card) {
    printf("%s %s\n", $card->id, $card->unitPrice?->amount());
}

$credential = $client->ownedCards()->reveal($walletId, $soldCardId);

المبالغ قيم من النوع Money بثلاث خانات عشرية بالضبط — وليست أعدادًا عشرية عائمة أبدًا. تجري Money::of('10.500', 'LYD') و$price->multiply(2) حسابًا دقيقًا.

ما الذي تتحقق منه

منذ الإصدار 2.0.1، تتحقق الحزمة من كل ردّ على الاستدعاءات التي توقّع أنيس ردودها — تقديم طلب شراء، وقراءة طلب شراء، وكشف بطاقة، وكشف بطاقات فاتورة، واستدعاءات تسجيل المفتاح الأربعة، وتعليق التطبيق، وإلغاء المفتاح، والفحص الذاتي للتوقيع — نجاحًا كان الردّ أم رفضًا. وتشترط وجود التوقيع هناك: فالردّ على أحد هذه الاستدعاءات الذي لا يحمل توقيعًا صحيحًا يُتخلَّص منه (UnverifiableResponseException)، ويعود في طلب الشراء في صورة OrderOutcomeUnknown. ولا تتحقق من استدعاءات المعلومات — ملفك التعريفي والمحافظ والكتالوج وبطاقاتك المملوكة (القائمة والقراءة): فردودها لا تحمل توقيعًا، وحماية TLS تكفيها. وما زالت توقّع كل استدعاء. راجع التحقق من الردود لمعرفة أي الردود موقَّعة.

طلبات الشراء

use Anis\Partners\Models\CreateOrderRequest;
use Anis\Partners\Models\OrderCompleted;
use Anis\Partners\Models\OrderNotPlaced;
use Anis\Partners\Models\OrderOutcomeUnknown;
use Anis\Partners\Models\OrderProcessing;
use Anis\Partners\Models\OrderReplayed;

$price = $card->unitPrice ?? throw new RuntimeException('This card has no price for this wallet.');
$operationId = \Ramsey\Uuid\Uuid::uuid4()->toString();   // yours — any UUID v4 generator works
$request = new CreateOrderRequest($card->id, 2, $price, $price->multiply(2));   // exact, three decimals
saveOrderIntent($operationId, $walletId, $request);       // store it BEFORE sending

$outcome = $client->orders()->create($walletId, $operationId, $request);

if ($outcome instanceof OrderCompleted) {
    storeCredentials($outcome->credentials);              // store FIRST, whatever happens next
}

match (true) {
    $outcome instanceof OrderCompleted && $outcome->codesWithheld
        => reportCodesWithheld($operationId),             // paid, codes withheld: do not buy again; write to support@anis.ly
    $outcome instanceof OrderCompleted      => markReady($operationId),
    $outcome instanceof OrderReplayed       => ensureStored($outcome->order),        // sent earlier: reveal by invoice if you never stored them
    $outcome instanceof OrderProcessing     => resumeLater($operationId, $outcome->retryAfterSeconds),
    $outcome instanceof OrderOutcomeUnknown => resumeLater($operationId, $outcome->suggestedDelaySeconds),
    $outcome instanceof OrderNotPlaced      => closeNotPlaced($operationId, $outcome->refusal),   // nothing bought: fix the cause, new order, new id
    default => throw new LogicException('Unexpected order outcome.'),
};

يرسل $client->orders()->resume($walletId, $operationId, $request) طلب الشراء نفسه مرة أخرى بالمعرّف نفسه — وهذه هي طريقة الاستعادة — ويعيد النتائج الخمس نفسها. والاستعادة دائمًا هي إعادة الإرسال الموقَّعة هذه، وليست حلقة من orders()->get() التي تقرأ الحالة فقط. وطلب الشراء الذي حالته RecoveryExhausted لم يفشل: واصل استئناف المعرّف نفسه ببطء (بالدقائق لا بالثواني)، وأخبر support@anis.ly بمعرّف العملية. راجع طلبات الشراء والاستعادة للاطلاع على القواعد كاملةً.

لا طبقة لإعادة المحاولة حول عميل HTTP

تعيد برمجية إعادة المحاولة الوسيطة على عميل PSR-18 لديك إرسال الاستدعاء الموقَّع نفسه بقيمة الاستخدام الواحد نفسها، فترفض أنيس النسخة المكرَّرة. استعِد طلبات الشراء بـ resume() التي توقّع استدعاءً جديدًا.

حالات الرفض

لا يرمي طلب الشراء الرفضَ استثناءً: يعود في صورة OrderNotPlaced أو OrderOutcomeUnknown. أما بقية الاستدعاءات فترمي AnisApiException (أو صنفًا فرعيًا منه مثل PriceChangedException أو InsufficientBalanceException) يحمل الرمز ومعرّف الاستدعاء وretryAfter وهل كان الاستدعاء مكرَّرًا. وكل صفحة خطأ تذكر الرمز.

الاستثناء المعنى في طلب الشراء
UnverifiableResponseException ردّ على استدعاء توقّع أنيس ردوده تعذّر إثبات أنه صادر من أنيس؛ فجرى التخلّص منه لا يُرمى — يعود OrderOutcomeUnknown: استأنف بالمعرّف نفسه
LimitExceededException (spending_limit_exceeded، 429) كان الطلب سيتجاوز سقف إنفاق التطبيق أو المحفظة في النافذة الزمنية الحالية؛ لم يُشترَ شيء ولم يُخصم شيء لا يُرمى — OrderNotPlaced. بعد ثواني retryAfter قدّم طلبًا جديدًا بمعرّف جديد؛ ولا تستأنف هذا الطلب
RequestSigningException أخفق المُوقِّع لديك؛ لم يُرسَل شيء يُرمى — أرسله مرة أخرى بعد أن يعمل المُوقِّع
InvalidArgumentException مدخلاتك خاطئة (معرّف ليس UUID، أو إجمالي لا يساوي سعر الوحدة × الكمية)؛ لم يُرسَل شيء يُرمى قبل إرسال أي شيء

كل إخفاق في الحزمة ينفّذ Anis\Partners\AnisPartnersException، فيكفي catch واحد لتغطيتها كلها.

المراقبة

السجلات عبر مسجّل PSR-3 الخاص بك؛ والتتبّعات والمقاييس عبر OpenTelemetry تحت anis-ly/partners. لا يُسجَّل أي مفتاح ولا توقيع ولا قيمة استخدام واحد ولا رمز بطاقة أبدًا. راجع المراقبة لمعرفة ما يستحق التنبيه عليه.

الدوال وردود البطاقات

تستخدم استدعاءات البطاقات المملوكة /v1/wallets/{walletId}/owned-cards، وقراءة /{soldCardId} وكشف /{soldCardId}/reveal، و/v1/wallets/{walletId}/invoices/{invoiceId}/owned-cards/reveal. وتستخدم قراءة بطاقة الكتالوج GET /v1/wallets/{walletId}/catalog/cards/{cardId}.

تُستخدم CardSummary في البطاقات المملوكة والكشف وsoldCards في طلب الشراء: معرّف البطاقة واسمها المحلي، والقيمة المطبوعة والشعار الاختياريان، والفئة الفرعية والفئة. تستخدم البطاقة المملوكة soldCardId وoperationId. وتشمل البطاقة المكشوفة داخل طلب الشراء معرّف الفاتورة وملخص البطاقة ووقت الشراء أيضًا. تُحذف النصوص الفارغة والنصوص المحلية الفارغة. المال نص بثلاث خانات عشرية ورمز عملة من ثلاثة أحرف كبيرة — ولا يكون سالبًا إلا رصيد المحفظة حين تستخدم الدين المسموح؛ اقرأ عملة المحفظة من balance.currency.

تشترط الحزمة وسم التوقيع v2، وترويستي توقيع الاستدعاء في قائمة الأجزاء، وفحوص دورة حياة المفاتيح المنشورة. راجع التحقق من الردود. يسري الإيقاف فورًا؛ ولا يستأنف التطبيق إلا موظفو أنيس، وإلغاء مفتاحك الحالي يتطلب تسجيلًا جديدًا. راجع الأمان.

$reason = "Suspected compromise";
$catalogueCard = $client->catalogue()->getCard($walletId, $cardId);
$suspended = $client->access()->suspendApplication($reason);
$revoked = $client->access()->revokeKey($keyId, $reason);

موارد المنتجات في SDK 2.2.0

يضيف الإصدار 2.2.0 من حزم SDK meterCharges وdirectRechargeProducts (المنتجات والمواقع وحساب السعر) وdirectRecharges (الإنشاء وقائمة عملياتك والقراءة وتعديل المستلم) وwebhookEndpoint وwebhookEvents وأداة للتحقق من توقيع الإشعارات وأنواع الأخطاء المحدّثة. حدّث من الإصدار 2.1.0 لاستخدامها. راجع أدلة العدادات والشحن المباشر والإشعارات لمعرفة سلوك HTTP. احفظ معرّف العملية ومحتواها كما سترسله قبل أي استدعاء ينقل مالًا؛ واستعد الردّ الضائع بالمعرّف نفسه. يذكر ملف README في كل حزمة وملف docs/webhooks.md فيها أسماء الدوال ومعاملات أداة التحقق.