تخطَّ إلى المحتوى
getnextpdf.com

Enterprise الإصدار

⁨Webhook⁩ — مرجع متعمّق

يوفّر مجال الأسماء NextPDF\Enterprise\Webhook تسليم ⁨webhook⁩ محدّدًا بنطاق المستأجر لأحداث المهام. يتكوّن السطح العام من ستة رموز: WebhookManager وWebhookRegistration وWebhookPayload وWebhookDelivery وWebhookRetryPolicy وDeadLetterEntry. يسجّل المدير نقاط النهاية لكل مستأجر ويُرسِل أحداث المهام إلى التسجيلات المشتركة. يرسل محرك التسليم بطلب ⁨POST⁩ حمولة ⁨JSON⁩ موقَّعة بخوارزمية ⁨HMAC-SHA256⁩، ويتحقق من كل وجهة عبر بوّابة خروج ⁨SSRF⁩ في ⁨Core⁩، ويعيد المحاولة بتراجع أُسّي، ويسجّل الإخفاقات الدائمة في طابور رسائل ميتة في الذاكرة. اعتبارًا من ⁨3.1.0⁩، يربط التوقيع ترويسة X-NextPDF-Timestamp داخل السلسلة الأساس للـ ⁨MAC⁩، فيتحقق المستقبِلون من الحداثة والسلامة معًا. للاطّلاع على الدليل على مستوى سير العمل، راجع Webhook.

تُشحن هذه الإمكانية في ⁨NextPDF Enterprise⁩ (nextpdf/enterprise) وتُفعَّل بمظروف ترخيص من فئة ⁨Enterprise⁩. أي نشر بلا ذلك الاستحقاق لا يُحمِّل أصناف الإمكانية. قارن الإصدارات واحصل على ترخيص.

سطح ⁨webhook⁩ إمكانية ⁨Enterprise⁩ أساسية، متاحة بمجرد تثبيت حزمة ⁨Enterprise⁩؛ ولا توجد راية منفصلة لكل ميزة. لا يملك ⁨NextPDF Core⁩ (⁨Apache-2.0⁩) ولا ⁨NextPDF Pro⁩ أي سطح لتسجيل ⁨webhook⁩ أو تسليمه؛ إذ يُشحن المدير والتسجيل والحمولة ومحرك التسليم وسياسة إعادة المحاولة وإدخال الرسائل الميتة في nextpdf/enterprise فقط.

الرمزالمعاملاتالسلوك الافتراضيالقيمة المُعادةيرمي أو يفشل بـملاحظات
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullيُنشئ مديرًا بفهرس تسجيل فارغ في الذاكرةكائن WebhookManager جديدلا يرميتُفهرَس التسجيلات لكل مستأجر
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationيُلحق التسجيل بفهرس المستأجر المُستدعِيvoidInvalidArgumentException عندما لا يطابق مستأجرُ التسجيل مستأجرَ السياقيُرفض التسجيل عبر المستأجرين قبل التخزين
WebhookManager::unregisterTenantContext $tenant, string $registrationIdيستبدل التسجيل المطابق بنسخة مُعطَّلةboolلا يرمي؛ يُعيد false عند عدم العثور على المعرّفتعطيل ناعم؛ يُحفظ السجل
WebhookManager::activeRegistrationsTenantContext $tenantيُرشّح تسجيلات المستأجر إلى النشطة منهاlist<WebhookRegistration>لا يرميتظهر تسجيلات المستأجر المُستدعِي فقط
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventيُسلّم الحدث إلى كل تسجيل نشط مشترك في نوع الحدثint (عمليات التسليم الناجحة)يُمرّر JsonException عندما تكون بيانات الحدث غير قابلة للترميز بصيغة ⁨JSON⁩؛ إخفاقات التسليم لا ترمييُولَّد معرّف تسليم جديد من 32 خانة سُداسية عشرية لكل عملية تسليم لتسجيل
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullيخزّن القيم المُمرَّرة حرفيًاكائن WebhookRegistration جديدلا يوجد @throws مُعلَن؛ يرفع ⁨PHP⁩ النوع TypeError عند عدم تطابق أنواع الوسائط تحت strict_typesfinal readonly؛ ‏$events الفارغ يعني الاشتراك في الكل
WebhookRegistration::subscribesToJobEventType $eventTypetrue عندما يكون $events فارغًا أو يحتوي على النوعboolلا يرميمقارنة هوية صارمة
WebhookRegistration::deactivateيُعيد نسخة غير نشطةselfلا يرميالكائن الأصلي دون تغيير
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdينسخ معرّف المهمة ونوع الحدث والبيانات والطابع الزمني من الحدثselfلا يرميمصنع ساكن يستخدمه dispatch
WebhookPayload::toJsonيُسلسِل جسم الرسالة ذا الحقول الستة مع شرطات مائلة غير مهروبةnon-empty-stringJsonException عندما تكون بيانات الحدث غير قابلة للترميز بصيغة ⁨JSON⁩JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayيُعيد جسم الرسالة كمصفوفة ترابطيةarray<string, mixed>لا يرميالطابع الزمني منسَّق بصيغة ⁨RFC 3339⁩ الموسّعة
WebhookPayload::signedTimestampزمن الحدث بثوانٍ ⁨Unix⁩، مقيَّد إلى صفر أو أكبرint<0, max>لا يرمييُصدَر بوصفه X-NextPDF-Timestamp ويُربط داخل الـ ⁨MAC⁩
WebhookPayload::signstring $secret⁨HMAC-SHA256⁩ على السلسلة الأساس {signedTimestamp}.{jsonBody}non-empty-string (سُداسي عشري)JsonException عبر toJson() عندما يكون الجسم غير قابل للترميزيربط ترويسة الطابع الزمني بالجسم تشفيريًا
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullمحرك تسليم ⁨PSR-18/PSR-17⁩ بطابور رسائل ميتة فارغكائن WebhookDelivery جديدلا يرميالسياسة الافتراضية: 5 محاولات، أساس 1 ثانية، سقف 300 ثانية
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadيرسل الحمولة الموقَّعة بطلب ⁨POST⁩ مع التحقق من خروج ⁨SSRF⁩ لكل محاولة وتراجع أُسّيboolJsonException قبل المحاولة الأولى عندما يكون الجسم غير قابل للترميز؛ خلاف ذلك لا يرمي — ‏false يعني أن الحمولة وُجِّهت إلى طابور الرسائل الميتةtrue فقط عند استجابة ⁨2xx⁩
WebhookDelivery::deadLettersيُعيد كل الإدخالات المُسجَّلةlist<DeadLetterEntry>لا يرميفي الذاكرة، بنطاق العملية
WebhookDelivery::clearDeadLettersيُفرّغ طابور الرسائل الميتةvoidلا يرميلا رجعة فيه؛ صدّر الإدخالات أولًا إذا لزمت إعادة التشغيل
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300يخزّن قيم السياسةكائن WebhookRetryPolicy جديدلا يوجد @throws مُعلَن؛ المعاملات موثّقة على أنها positive-int$maxRetries يعُدّ إجمالي المحاولات
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1)، بحد أقصى maxDelaySecondspositive-intلا يرميأرقام المحاولات تبدأ من 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue طالما أن المحاولة الحالية دون الحد الأقصىboolلا يرمييُتخطّى الانتظار بعد المحاولة الأخيرة
WebhookRetryPolicy::default5 محاولات، أساس 1 ثانية، سقف 300 ثانيةselfلا يرميمصنع ساكن؛ الافتراضي للإنتاج
WebhookRetryPolicy::aggressive10 محاولات، أساس 2 ثانية، سقف 600 ثانيةselfلا يرميمصنع ساكن لنقاط النهاية الحرجة
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseيخزّن سجل الإخفاق حرفيًاكائن DeadLetterEntry جديدلا يوجد @throws مُعلَن؛ ‏TypeError تحت strict_typesfinal readonly؛ ‏$lastHttpStatus بقيمة null يعني إخفاق النقل
DeadLetterEntry::markReplayedيُعيد نسخة بـ replayed = trueselfلا يرميالمعرّف نفسه؛ الإدخال الأصلي دون تغيير
public function __construct(
private readonly WebhookDelivery $delivery,
private readonly ?LoggerInterface $logger = null,
) {}
public function register(TenantContext $tenant, WebhookRegistration $registration): void
public function unregister(TenantContext $tenant, string $registrationId): bool
public function activeRegistrations(TenantContext $tenant): array
public function dispatch(TenantContext $tenant, JobEvent $event): int
public function __construct(
public string $id,
public string $tenantId,
public string $url,
public array $events,
public string $secret,
public bool $active = true,
public ?string $description = null,
) {}
public function subscribesTo(JobEventType $eventType): bool
public function deactivate(): self
public static function fromJobEvent(
JobEvent $event,
string $tenantId,
string $deliveryId,
): self
public function toJson(): string
public function toArray(): array
public function signedTimestamp(): int
public function sign(string $secret): string
public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
private readonly WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(),
private readonly ?LoggerInterface $logger = null,
) {}
public function deliver(WebhookRegistration $registration, WebhookPayload $payload): bool
public function deadLetters(): array
public function clearDeadLetters(): void
public function __construct(
public int $maxRetries = 5,
public int $baseDelaySeconds = 1,
public int $maxDelaySeconds = 300,
) {}
public function delayForAttempt(int $attempt): int
public function shouldRetry(int $currentAttempt): bool
public static function default(): self
public static function aggressive(): self
public function __construct(
public string $id,
public string $registrationId,
public WebhookPayload $payload,
public int $attempts,
public string $lastError,
public ?int $lastHttpStatus,
public DateTimeImmutable $failedAt,
public bool $replayed = false,
) {}
public function markReplayed(): self
  • تُفهرَس التسجيلات لكل مستأجر. يرفض register() أي تسجيل لا يطابق معرّفُ مستأجره السياقَ المُستدعِي. وunregister() تعطيل ناعم: يُستبدَل التسجيل بنسخة غير نشطة، مع الحفاظ على السجل واستبعاده من أي إرسال مستقبلي.
  • يمرّ dispatch() فقط على تسجيلات المستأجر المُستدعِي النشطة المشتركة في نوع الحدث المُرسَل. وقائمة الأحداث المشتركة الفارغة تعني الاشتراك في الكل. وتَعُدّ القيمة المُعادة عمليات التسليم الناجحة.
  • كل عملية تسليم هي طلب ⁨HTTP POST⁩ بجسم ⁨JSON⁩ وخمس ترويسات: Content-Type: application/json وX-NextPDF-Signature (sha256=<hex>) وX-NextPDF-Timestamp (ثوانٍ ⁨unix⁩) وX-NextPDF-Delivery-Id وX-NextPDF-Event.
  • حقول جسم ⁨JSON⁩ هي delivery_id وjob_id وevent_type وdata وtimestamp (⁨RFC 3339⁩ الموسّعة) وtenant_id، مُسلسَلة بشرطات مائلة غير مهروبة. وتأتي قيم نوع الحدث من JobEventType في nextpdf/core: progress وcompleted وfailed وcancelled.
  • مخطط التوقيع (تغيّر في ⁨3.1.0⁩، تغيير كاسر). السلسلة الأساس لـ ⁨HMAC-SHA256⁩ هي {signedTimestamp}.{jsonBody}، مُفتاحها سرّ التسجيل — لا الجسم وحده. وقيمة X-NextPDF-Timestamp هي مكوّن الطابع الزمني في الـ ⁨MAC⁩، لذا فإن أي ترويسة طابع زمني مُتلاعَب بها أو مُعاد تشغيلها تُبطِل التوقيع.
  • تحقُّق المستقبِل: اقرأ ترويسة X-NextPDF-Timestamp بالقيمة T؛ وارفُض عندما تقع T خارج نافذة حداثة مقبولة (مثلًا 300 ثانية)؛ وأعِد حساب hash_hmac('sha256', T . '.' . rawBody, secret) على البايتات الخام المُستلَمة؛ وقارِن في زمن ثابت مع قيمة الترويسة بعد إزالة البادئة sha256=.
  • يُحسَب الجسم والتوقيع ومعرّف التسليم مرة واحدة لكل عملية تسليم ويبقون ثابتين عبر محاولات إعادة المحاولة.
  • بوّابة خروج ⁨SSRF⁩. قبل كل محاولة، تمرّ وجهة ⁨URL⁩ عبر بوّابة UrlValidator::validateExternalUrl() في ⁨Core⁩: مخطط ⁨HTTPS⁩ فقط؛ وتُحجَب نطاقات الاسترجاع (⁨loopback⁩) والخاصة والمحجوزة و⁨carrier-grade-NAT⁩ وبيانات السحابة الوصفية ونطاقات انتقال ⁨IPv6⁩ المضمّنة لـ ⁨IPv4⁩؛ وتُحلّ أسماء المضيفين عبر ⁨DNS⁩ (السجلّان ⁨A⁩ و⁨AAAA⁩) وتُرفَض المضيفات غير القابلة للحل برفض مُغلَق فشلًا. ولا تُرسَل وجهة ⁨URL⁩ محجوبة أبدًا: تُجهَض حلقة المحاولات وتُوجَّه الحمولة مباشرة إلى طابور الرسائل الميتة مع خطأ أخير Blocked SSRF destination: وحالة ⁨HTTP⁩ بقيمة null.
  • تصنيف النتيجة لكل محاولة: ⁨2xx⁩ نجاح ويعود فورًا؛ و⁨4xx⁩ غير 429 نهائي ويذهب مباشرة إلى الرسائل الميتة؛ وكل نتيجة أخرى — ⁨3xx⁩ أو 429 أو ⁨5xx⁩ أو استثناء نقل — قابلة لإعادة المحاولة حتى إجمالي عدد محاولات السياسة.
  • التراجع أُسّي: الانتظار قبل المحاولة التالية هو baseDelaySeconds × 2^(attempt − 1)، بحد أقصى maxDelaySeconds. ويُتخطّى الانتظار بعد المحاولة الأخيرة.
  • عندما لا تنجح أي محاولة، يسجّل DeadLetterEntry معرّفًا فريدًا، ومعرّف التسجيل، والحمولة الأصلية، وعدد المحاولات (المُقيَّد إلى الحد الأقصى للسياسة)، ورسالة الخطأ الأخيرة، وحالة ⁨HTTP⁩ الأخيرة (null عند إخفاق النقل أو حجب ⁨SSRF⁩)، والطابع الزمني للإخفاق.
  • طابور الرسائل الميتة في الذاكرة ونطاقه عمر العملية. ويُنتِج markReplayed() نسخة مُعلَّمة؛ لكنه لا يُعيد الإرسال، ويحتفظ الطابور بالإدخال الأصلي.

الحالات الحديّة وأنماط الإخفاق

قسم بعنوان «الحالات الحديّة وأنماط الإخفاق»
  • قائمة أحداث فارغة. يستقبل التسجيل كل نوع حدث. حدِّد نطاق القائمة صراحةً عندما يجب ألّا يرى المستقبِل كل الأحداث.
  • ⁨4xx⁩ النهائي مقابل إخفاق النقل. يسجّل رفض ⁨4xx⁩ قيمة معبّأة في lastHttpStatus؛ بينما يسجّل إخفاق الاتصال null. استخدم قيمة null للتمييز بين رفض المستقبِل وإخفاق النقل.
  • وجهة محجوبة بـ ⁨SSRF⁩. أي تسجيل يشير إلى عنوان ⁨HTTP⁩ أو خاص أو استرجاع (⁨loopback⁩) أو بيانات وصفية يُرحَّل إلى الرسائل الميتة في المحاولة الأولى مع خطأ Blocked SSRF destination: وحالة null. ولا يُرسَل أي طلب صادر. صحّح عنوان ⁨URL⁩ وسجِّل من جديد.
  • المستقبِلات القديمة بعد الترقية. أي مستقبِل ما زال يتحقق من ⁨HMAC⁩ القديم القائم على الجسم وحده (قبل ⁨3.1.0⁩) يفشل مُغلَقًا أمام عمليات تسليم 3.1.0. رحِّل المستقبِل إلى السلسلة الأساس {timestamp}.{body} واستهلك X-NextPDF-Timestamp.
  • بيانات حدث غير قابلة للترميز. ترمي toJson() وsign() النوع JsonException، الذي يُمرَّر خارج deliver() وdispatch() قبل إجراء أي محاولة.
  • حجب متزامن. تنام deliver() ضمنيًا بين المحاولات. ويبلغ التراجع التراكمي 15 ثانية تحت السياسة الافتراضية ونحو 17 دقيقة تحت السياسة العدوانية. أرسِل من عامل طابور عندما يكون زمن استجابة المستقبِل غير موثوق.
  • تقييد عدد المحاولات. لا يتجاوز عدد المحاولات المُسجَّل الحد الأقصى للسياسة أبدًا، رغم أن عدّاد الحلقة الداخلي يتقدّم متجاوزًا إياه عند الاستنفاد.
  • نمو الطابور ومتانته. ينمو طابور الرسائل الميتة بلا حدّ داخل العملية ويتلاشى عند إعادة التشغيل. صدّر الإدخالات عبر deadLetters() واحفظها خارجيًا قبل استدعاء clearDeadLetters() عندما تلزم إعادة تشغيل مُعمَّرة.
  • إعادة التشغيل مدفوعة بالمُشغِّل. إعادة التسليم تعني استدعاء deliver() مجددًا بحمولة الإدخال؛ أما markReplayed() فتسجّل الأمر على نسخة فقط.
  • بقية إعادة ربط ⁨DNS⁩. يُعاد التحقق من ⁨URL⁩ في كل محاولة، ما يضيّق نافذة إعادة الربط دون أن يغلقها: إذ لا يستطيع تجريد ⁨PSR-18⁩ تثبيت الاتصال على ⁨IP⁩ المُتحقَّق منه. أضِف ضوابط خروج على مستوى الشبكة حيثما كانت هذه البقية مهمّة.
  • التعامل مع السرّ. سرّ التسجيل بيانات اعتماد. ويوثّق ⁨HMAC⁩ السلامة والمصدر فقط — وليس السرّية. لا تضع في حمولة الحدث بيانات يجب ألّا يراها المستقبِل.

توقيع الحمولة هو ⁨HMAC-SHA256⁩ عبر دالة hash_hmac() في ⁨PHP⁩، لذا يعتمد على مزوّد التشفير المضيف. وفي بناء مُقيَّد بـ ⁨FIPS⁩، يفشل أي بدائي غير مُعتمَد عند الحدّ التشفيري بدلًا من التنازل إلى مستوى أدنى. ولا تضيف طبقة ⁨webhook⁩ أي سياسة تشفير خاصة بها.

  • يُنفِّذ توثيق الحمولة ⁨HMAC⁩، وهو رمز توثيق الرسائل بالتجزئة المُفتاحية وفق ⁨FIPS PUB 198-1⁩ §1، مُهيّأً بـ ⁨SHA-256⁩.
  • تتبع الحماية من إعادة التشغيل إرشادات أمن ⁨webhook⁩ في ⁨OWASP Cheat Sheet Series⁩: يسافر الطابع الزمني للحدث في ترويسة مخصّصة ويُبذَر في حساب التوقيع، لذا يفشل أي طابع زمني مُتلاعَب به في التحقق.
  • تستخدم الطوابع الزمنية للجسم صيغة التاريخ والوقت الموسّعة ⁨RFC 3339⁩. مُعلَن في الشيفرة: لم يُسترجَع ⁨RFC 3339⁩ من مجموعة ⁨RAG⁩ لهذه الصفحة.
  • هذه بيانات إمكانية مؤصَّلة في مصدر المنتج والبنود المُستشهَد بها. لا يقدّم ⁨NextPDF⁩ أي ادّعاء مطابقة أو اعتماد لهذا السطح.
  • تُعلن كل الأصناف strict_types=1 وهي final؛ وWebhookRegistration وWebhookPayload وWebhookRetryPolicy وDeadLetterEntry هي final readonly بخصائص عامة مُرقّاة.
  • تحمل الوحدة تعليق @since بقيمة 2.2.0؛ ومخطط التوقيع المرتبط بالطابع الزمني تغيير كاسر موثَّق في 3.1.0.
  • يأخذ محرك التسليم تجريدات ⁨PSR-18/PSR-17⁩، لذا يستطيع عميل ⁨HTTP⁩ وهمي تمرين مسار الإرسال وإعادة المحاولة والرسائل الميتة كاملًا دون اتصال. ويتخذ المسجِّل القيمة الافتراضية null؛ احقِن مسجِّل ⁨PSR-3⁩ في الإنتاج وإلا فلن تظهر الإخفاقات إلا عبر القيم المُعادة.
  • ينبغي لتطبيقات المستقبِل استخدام hash_equals() لمقارنة التوقيع وفرض نافذة حداثة على X-NextPDF-Timestamp.
  • اختبارات حدّية موصى بها: تسجيل بعدم تطابق المستأجر، وتوزيع بقائمة أحداث فارغة، و⁨4xx⁩ نهائي، واستنفاد إعادة المحاولة، وعنوان ⁨URL⁩ محجوب بـ ⁨SSRF⁩، ورفض توقيع مُتلاعَب بطابعه الزمني مقابل متجه ثابت، وتقييد عدد محاولات الرسائل الميتة.

توثّق هذه الصفحة السلوك القابل للملاحظة خارجيًا وسطح واجهة ⁨API⁩ العامة المدعوم فقط. أما مسارات مجالات الأسماء الداخلية والأصناف المساعِدة وجداول الآليات وأسماء ملفات كتيّبات التشغيل وبادئات التذاكر فخارج النطاق.