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

واجهة API ترفض التخمين

Spec: ISO/IEC 25010 Spec: ISO 32000-2 Evidence: Code-backed

تُلزمك ⁨NextPDF⁩ بالتصريح بما تقصده تحديداً. وكلما غيَّرت النية البايتات — مستوى توقيع، أو وجهة إخراج، أو هدف مطابقة — فهي وسيطة صريحة مطلوبة، وليست شيئاً يستنتجه المحرك من السياق.

تُظهر هذه الصفحة هذا الموقف في مصدر المحرك نفسه: توقيعات التوابع، والوسائط المُسمّاة، والمواضع التي يُرفَض فيها كل مُدخَل غامض قبل إنتاج أي بايت.

التخمين قرارٌ يُتَّخذ نيابةً عنك من دون إخبارك. في حقل نصي، قد يكون ذلك مزعجاً فحسب. أما في ملف ⁨PDF⁩، فهو عيبٌ كامِن، لأن ما تُسلِّمه غالباً أثرٌ قانوني أو أرشيفي ستُتحقَّق صحته لاحقاً من قِبل شخصٍ آخر باستخدام أداة تحقُّق.

تأمَّل توقيعاً. تُحسَب خلاصته على نطاق بايتات مُعلَن يستثني قيمة التوقيع نفسها عمداً ( Spec: ISO 32000-2, §12.8 ). واجهة API التي “تساعد” بهدوء — بإعادة كتابة البنية، أو استنتاج مستوى، أو حشو موضعٍ نائب — لا تساعد فعلاً. بل تغيِّر البايتات التي كان يُفترض أن يحميها التوقيع. والتخمين الذي يبدو ودوداً عند موضع الاستدعاء قد يتحوَّل بعد أسابيع إلى عطلٍ إنتاجي. كلاهما السطر نفسه من الشيفرة.

  • إذا كان خيارٌ ما يُغيِّر الإخراج وليس له افتراضي آمن، فإن ⁨NextPDF⁩ تجعله وسيطة مطلوبة، لا وسيطةً مُستنتَجة.
  • الوسائط الاختيارية التي تُقرأ بغموض تكون مُسمّاة، لكي يُعبِّر موضع الاستدعاء عن النية (newLine: true، لا قيمة true مُجرَّدة).
  • المُدخلات التي قد تكون غير آمنة تُخضَع لـالتحقُّق قبل العرض، وتُرفَض باستثناءٍ مُحدَّد النوع يُسمِّي السبب.
  • نسخة المستند تُستخدَم مرة واحدة: تُبنى، وتُصدَر، وتُتلَف. لا يوجد reset()، لذا لا مجال لتخمين “هل أُعيد استخدام هذا الشيء؟”.
  • لا يُصدِر المحرك أبداً أثراً يبدو معقولاً بدلاً من الأثر الذي طلبته. بل يرفض ذلك.

الآلية بسيطة، وهذا هو المقصود. إنها نظام الأنواع، والوسائط المُسمّاة، والتعدادات بدلاً من السلاسل السحرية، وعددٌ صغير من جُمَل الحراسة المتعمَّدة الموضوعة قبل الإخراج.

يُقارِن الجدول بضعة مُدخلات غامضة. ولكلٍ منها يُبيِّن ما قد تستنتجه مكتبةٌ “تساعد”، وما تفعله ⁨NextPDF⁩ بدلاً من ذلك. كل عمود من أعمدة ⁨NextPDF⁩ يعرض سلوكاً مقتبَساً من المصدر الوارد لاحقاً في هذه الصفحة.

مُدخَل غامضما تفعله مكتبةٌ تُخمِّنما تفعله ⁨NextPDF⁩
سلسلة اتجاه مثل "portait"يلجأ إلى افتراضي ويَعرِض على أي حاليأخذ addPage() تعداد Orientation، لا سلسلة نصية — فالخطأ المطبعي خطأ نوع، لا افتراضي صامت
قيمة true لاحقة مُجرَّدة في cell()يختار أي موضع منطقي يفترض أنك قصدتهالقيمة المنطقية مُسمّاة عند موضع الاستدعاء (newLine: true)؛ والقيمة الحرفية غير المُسمّاة هي موضع الالتباس الذي تُزيله الواجهة
مُغلِّف php:// أو مسار اجتياز يُمرَّر إلى save()”يبذل قصارى جهده” ويكتب في مكانٍ مايُرفَض قبل بناء ⁨PDF⁩، باستثناء مُحدَّد النوع InvalidConfigException يُسمِّي المفتاح والقيمة والنوع المتوقَّع
setSignature() ثم save() بينما المُوقِّع عالي المستوى غير موصوليُصدِر ملفاً غير مُوقَّع يظن المُستدعي أنه مُوقَّعيَرمي NotImplementedException قبل إنتاج البايتات، ويُسمِّي المسار المدعوم
إعادة استخدام نسخة Document لعرضٍ ثانٍيُخمِّن ما إذا كانت الحالة المتبقية لا تزال صالحةلا يوجد reset() ولا مسار لإعادة الاستخدام — نسخة جديدة لكل طلب عبر DocumentFactory، لذا لا توجد حالة متبقية للتخمين بشأنها

النية وسيطة مطلوبة. يأخذ العقد الأساسي، PdfDocumentInterface، الهندسة والمحاذاة على هيئة كائنات قيمة وتعدادات مُحدَّدة النوع، لا قيماً بدائية فضفاضة:

public function addPage(
?PageSize $size = null,
Orientation $orientation = Orientation::Portrait,
): static;
public function cell(
float $width,
float $height,
string $text = '',
bool|string $border = false,
bool $newLine = false,
Alignment $align = Alignment::Left,
bool $fill = false,
): static;

Orientation وAlignment تعدادان، لذا لا يستطيع موضع الاستدعاء تمرير "portait" وجعلها تعني “افتراضي” بصمت. وحين يوجد افتراضي، فهو افتراضي آمن (عمودي، يسار، بلا حدود)، لا تخمينٌ لما أردته على الأرجح.

القيم المنطقية الغامضة تُسمّى عند موضع الاستدعاء. عبر الأمثلة التي تعمل كمرجع فعلي للواجهة، يتكرَّر الشكل ذاته:

$document->cell(0, 15, 'Hello, NextPDF!', newLine: true);
$document->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$pdf = $document->output(dest: OutputDestination::String);

newLine: true لا لبس فيها. أما قيمة true اللاحقة المُجرَّدة فلن تكون كذلك. مستوى التوقيع هو SignatureLevel::PAdES_B_B، حالة تعداد — لا سلسلة نصية يتعيَّن على المحرك تفسيرها. وجهة الإخراج هي OutputDestination::String، لذا فإن “أعطِني البايتات، بلا ترويسات ⁨HTTP⁩، بلا ملف” مصرحٌ بها صراحة. ولا تُستنتَج من كون اسم ملفٍ قد مُرِّر أم لا.

المُدخَل غير الآمن يُرفَض قبل كتابة أي بايت. يتحقَّق save() من مسار الوجهة قبل أن يبني ⁨PDF⁩:

public function save(string $path): void
{
// Reject stream wrappers and null bytes
if (\str_contains($path, "\0") || \preg_match('#^[a-zA-Z]+://#', $path)) {
throw new InvalidConfigException(
configKey: 'output_path',
givenValue: $path,
expectedType: 'valid_path',
);
}
// Resolve the parent directory to prevent path traversal
$dir = \dirname($path);
$realDir = \realpath($dir);
if ($realDir === false) {
throw new InvalidConfigException(
configKey: 'output_path',
givenValue: $dir,
expectedType: 'existing_directory',
);
}
// ... only now is the PDF built and written atomically
}

لا “يبذل المحرك قصارى جهده” مع مُغلِّف php:// أو مسار اجتياز. بل يرفض، ويُسمِّي الاستثناء المفتاح والقيمة وما كان متوقَّعاً.

يرفض المحرك بدلاً من إصدار أثرٍ مُضلِّل. أقوى صور رفض التخمين هي الامتناع عن إنتاج الإخراج أصلاً عندما يكون ذلك الإخراج غير صادق. عندما يُهيَّأ توقيعٌ عالي المستوى لكن وصلة الكاتب التي ستُوقِّع فعلاً غير موصولة، يَرمي مسار البناء قبل إنتاج البايتات، بدلاً من إصدار ملفٍ غير مُوقَّع يظن المُستدعي أنه مُوقَّع:

if ($this->padesOrchestrator !== null) {
throw new NotImplementedException(
feature: 'Document::setSignature()->save()/output()/getPdfData()',
followUp: 'The high-level PAdES writer seam is not yet wired ... '
. 'Produce a signed PDF via the direct two-phase '
. 'PadesOrchestrator::signDocument() then finalizeSignature() '
. 'buffer API ...',
);
}

ملف ⁨PDF⁩ غير مُوقَّع يبدو مُوقَّعاً هو بالضبط نوع الأثر الخاطئ الذي يبدو معقولاً، وهو ما وُجِد هذا المبدأ لمنعه. ويظهر الموقف ذاته في مسار ⁨CSS⁩ الصارم. فأي انحرافٍ غير مُسجَّل عن المواصفة يَرمي StrictModeViolation عند نقطة الكشف، بدلاً من عرض تقريبٍ وترك الانحراف غير مكتشَف.

الاستخدام مرة واحدة يُزيل فئةً كاملة من التخمينات. Document كائنٌ قابل للتخلُّص — يُبنى، ويُصدَر، ويُتلَف. لا يوجد reset() ولا مسار لإعادة الاستخدام. يُنشئ العامل طويل الأمد نسخةً جديدة لكل طلب عبر DocumentFactory. ولا يضطر المحرك أبداً إلى التخمين بشأن ما إذا كانت الحالة المتبقية من مستندٍ سابق لا تزال ذات معنى، لأنه لا توجد حالةٌ كهذه بحُكم البناء.

هذه الصفحة Evidence: Code-backed : كل شكلٍ أعلاه مقتبَس من مصدر المحرك نفسه ومن أمثلته، لا مُعاد صياغته من النية.

  • التوقيعات المُحدَّدة الأنواع الحاملة للتعدادات هي العقد العام في PdfDocumentInterface. وأسلوب الاستدعاء بالوسائط المُسمّاة هو الشكل المُتَّسق عبر الأمثلة المعيارية التي تعمل كمرجع فعلي للواجهة.
  • التحقُّق من المسار قبل العرض، باستثنائه المُحدَّد النوع InvalidConfigException، وحارس الرفض-قبل-الإصدار NotImplementedException مقتبسان حرفياً من مسار الإخراج لواجهة المستند.
  • المرساة المعيارية هي Spec: ISO/IEC 25010, §3.32 — الحماية من خطأ المستخدم، خاصية الجودة التي وُجِدت واجهة رافضة للتخمين لتلبيتها عند موضع الاستدعاء. والمرساة الثانية هي Spec: ISO 32000-2, §12.8 ، وهذا سبب أن التخمين حول مستندٍ مُوقَّع ليس غير ضارٍّ أبداً. تُغطّي الخلاصة نطاق بايتاتٍ مُعلَناً يستثني قيمة التوقيع، لذا فإن أي إعادة كتابةٍ صامتة تُبطِلها.

يلي ذلك برنامجٌ صغير وكامل. كل سطرٍ قد يكون غامضاً يُعبِّر عن نيته. والمُدخَل غير الآمن الوحيد يُرفَض قبل تنفيذ أي عمل.

<?php
declare(strict_types=1);
use NextPDF\Contracts\OutputDestination;
use NextPDF\Core\Document;
use NextPDF\Exception\InvalidConfigException;
use NextPDF\ValueObjects\PageSize;
use NextPDF\Contracts\Orientation;
$document = Document::createStandalone();
$document->setTitle('Quarterly Report');
// Intent is explicit: a typed page size and an Orientation enum case,
// not a string the engine has to interpret.
$document->addPage(PageSize::a4(), Orientation::Landscape);
$document->setFont('helvetica', 'B', 16);
// Ambiguous boolean is named, so the call reads as intent.
$document->cell(0, 12, 'Quarterly Report', newLine: true);
try {
// Unsafe path is rejected before a byte is built.
$document->save('php://output/report.pdf');
} catch (InvalidConfigException $e) {
// "Invalid configuration for key "output_path": expected valid_path, ..."
error_log($e->getMessage());
// The String destination is explicit: bytes only, no HTTP headers,
// no file side effect. Nothing is inferred from a missing filename.
$bytes = $document->output(dest: OutputDestination::String);
}

لا يوجد مسارٌ يفعل فيه هذا البرنامج الشيء الخاطئ بهدوء. فهو إما يُعبِّر عن النية ويمضي، أو يُسمِّي المشكلة ويتوقَّف.

الاعتراض المتكرر هو “هذا مجرد إسهاب”. لكنه ليس إسهاباً، بل غيابٌ للافتراضيات الخفية. قيمة true المُجرَّدة أقصر من newLine: true بقدر الوضوح الذي تُزيله. يُقايِض المحرك بضعة محارف عند موضع الاستدعاء مقابل القضاء على فئةٍ من العلل — تلك التي تُصرَّف فيها الشيفرة، وتعمل، وتُنتج ملفاً، وتكون خاطئة.

مفهومٌ خاطئ ذو صلة هو أن الإخفاق السريع يعني “يَرمي كثيراً”. في الاستخدام العادي لا تَرمي ⁨NextPDF⁩ شيئاً. المُدخَل الصالح يمر بسلاسة. ولا تُفعَّل الحراسات إلا مع المُدخلات الغامضة أو غير الآمنة فعلاً — وهي بالضبط المُدخلات التي تريد أن تعرف بها فوراً، لا أن تُخمَّن نيابةً عنك.

ينطبق رفض التخمين على النية والأمان، لا على كل وسيلة راحة. لا تزال ⁨NextPDF⁩ تملك افتراضيات آمنة: اتجاه عمودي، محاذاة لليسار، بلا حدود. والمبدأ هو أن الافتراضي يُقدَّم فقط حين يكون آمناً وغير مفاجئ، ولا يُقدَّم أبداً حين يُنتج الاستنتاج الخاطئ مستنداً خاطئاً.

تُبيِّن هذه الصفحة المبدأ على سطح الواجهة العامة الأساسية (واجهة المستند، وعقده، ومسار الإخراج). للأنظمة الفرعية نقاط دخولها الخاصة، ويُوثِّق كلٌّ منها سلوك التحقُّق الخاص به. الأشكال المقتبسة هنا حديثة اعتباراً من هذه المراجعة. وهي تُوضِّح النمط؛ لكنها ليست فهرساً شاملاً لكل حارسٍ في المحرك.

الحراسات سريعة الإخفاق الموصوفة هي حراسات صحة وأمان. وهي ليست حدّاً أمنياً بذاتها. التحقُّق من المُدخلات طبقةٌ واحدة. فلسفة التصميم ووثائق الأمان تصف الموقف الأوسع.

  • مدعوم بالشيفرة (مستوى الأدلة) — صفحة تُتحقَّق ادعاءاتها مقابل مصدر المحرك نفسه أو مثالٍ قابل للتشغيل، اقتباساً لا إعادة صياغة.
  • الإخفاق السريع — رفض مُدخَلٍ غير صالح عند أبكر نقطة، وبسببٍ واضح، بدلاً من المضي ثم الإخفاق بغموضٍ لاحقاً.
  • الوسيطة المُسمّاة — صياغة عند موضع الاستدعاء في ⁨PHP⁩ (newLine: true) تربط قيمةً بمعامل بالاسم، فتجعل القيمة الحرفية التي كانت ستبدو غامضة وصفيةً لذاتها.
  • دورة حياة الاستخدام مرة واحدة — عقد Document القابل للتخلُّص: إنشاء، كتابة، حفظ، تخلُّص. لا reset()، ولا إعادة استخدام. يُنشئ العاملون نسخةً جديدة لكل طلب عبر DocumentFactory.
  • ⁨PAdES⁩ — توقيعات ⁨PDF⁩ الإلكترونية المتقدمة، عائلة ملامح ⁨ETSI⁩ لتوقيع ⁨PDF.⁩ تُوسَّع عند أول استخدام؛ وتُغطّى بعمق في صفحات التوقيع.