ข้ามไปยังเนื้อหา

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() จึงไม่มีคำถามว่า “สิ่งนี้ถูกนำกลับมาใช้ซ้ำหรือไม่” ให้ต้องคาดเดา
  • เอนจินจะไม่สร้างผลลัพธ์ที่ดูสมเหตุสมผลขึ้นมาแทนผลลัพธ์ที่คุณร้องขอเด็ดขาด แต่จะปฏิเสธแทน

กลไกนี้เรียบง่าย และความเรียบง่ายนั้นคือสาระสำคัญ กลไกประกอบด้วยระบบชนิดข้อมูล อาร์กิวเมนต์แบบมีชื่อ enum แทนสตริงวิเศษ และการ์ดจำนวนหนึ่งที่จงใจวางไว้ ก่อน การสร้างเอาต์พุต

ตารางนี้เปรียบเทียบอินพุตคลุมเครือบางกรณี ในแต่ละกรณี ตารางจะแสดงสิ่งที่ไลบรารีซึ่ง “ช่วยเหลือ” จะอนุมาน และสิ่งที่ NextPDF ทำแทน ทุกคอลัมน์ของ NextPDF คือพฤติกรรมที่ยกมาจากซอร์สโค้ดซึ่งแสดงในส่วนถัดไปของหน้านี้

อินพุตที่คลุมเครือสิ่งที่ไลบรารีแบบเดาทำสิ่งที่ NextPDF ทำ
สตริงระบุการวางแนวอย่างเช่น "portait"ถอยกลับไปใช้ค่าเริ่มต้นแล้วเรนเดอร์ต่อไปaddPage() รับ enum Orientation ไม่ใช่สตริง การพิมพ์ผิดจึงเป็นข้อผิดพลาดด้านชนิด ไม่ใช่ค่าเริ่มต้นแบบเงียบๆ
การส่งค่า true ต่อท้ายเปล่าๆ ให้กับ cell()เลือกตำแหน่งบูลีนใดก็ตามที่คาดเดาว่าคุณหมายถึงค่าบูลีนถูกตั้งชื่อ ณ จุดที่เรียกใช้ (newLine: true) ค่าลิเทอรัลที่ไม่มีชื่อคือกลิ่นไม่ดีที่ API นี้กำจัดออกไป
ตัวห่อ php:// หรือเส้นทางที่ไล่ย้อนขึ้นไดเรกทอรีซึ่งส่งให้ save()”พยายามอย่างเต็มที่” แล้วเขียนลงที่ใดที่หนึ่งถูกปฏิเสธ ก่อน ที่จะสร้าง PDF ด้วย InvalidConfigException แบบมีชนิดที่ระบุคีย์ ค่า และชนิดที่คาดหวัง
setSignature() แล้วตามด้วย save() ขณะที่ตัวลงนามระดับสูงยังไม่ได้เชื่อมต่อปล่อยไฟล์ที่ไม่ได้ลงนามออกมาในขณะที่ผู้เรียกเชื่อว่ามีการลงนามแล้วโยน NotImplementedException ก่อนสร้างไบต์ พร้อมระบุเส้นทางที่รองรับ
การนำอินสแตนซ์ Document กลับมาใช้ซ้ำสำหรับการเรนเดอร์ครั้งที่สองเดาว่าสถานะที่เหลือค้างยังคงมีผลอยู่หรือไม่ไม่มี reset() และไม่มีเส้นทางการนำกลับมาใช้ซ้ำ คือสร้างอินสแตนซ์ใหม่ต่อหนึ่งคำขอผ่าน DocumentFactory จึงไม่มีสถานะที่เหลือค้างให้ต้องคาดเดา

เจตนาคืออาร์กิวเมนต์ที่ต้องระบุ สัญญาหลักคือ PdfDocumentInterface ซึ่งรับเรขาคณิตและการจัดแนวเป็นออบเจกต์ค่าและ enum แบบมีชนิด ไม่ใช่ค่าพื้นฐานที่หลวมๆ:

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 เป็น enum การเรียกใช้จึงไม่สามารถส่ง "portait" แล้วให้มันหมายถึง “ค่าเริ่มต้น” อย่างเงียบๆ ได้ ในจุดที่มีค่าเริ่มต้น ค่านั้นจะเป็นค่าที่ ปลอดภัย (แนวตั้ง ชิดซ้าย ไม่มีเส้นขอบ) ไม่ใช่การคาดเดาว่าคุณน่าจะต้องการอะไร

ค่าบูลีนที่คลุมเครือจะถูกตั้งชื่อ ณ จุดที่เรียกใช้ ในตัวอย่างต่างๆ ที่ทำหน้าที่เป็นเอกสารอ้างอิง API โดยพฤตินัย รูปแบบเดียวกันนี้ปรากฏซ้ำๆ:

$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 ซึ่งเป็นเคสของ enum ไม่เคยเป็นสตริงที่เอนจินต้องตีความ ปลายทางของเอาต์พุตคือ 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 รูปแบบทั้งหมดข้างต้นยกมาจากซอร์สโค้ดของเอนจินเองและจากตัวอย่างของเอนจิน ไม่ได้ถอดความจากเจตนา

  • ลายเซ็นแบบมีชนิดซึ่งมี enum กำกับคือสัญญาสาธารณะใน PdfDocumentInterface รูปแบบการเรียกใช้ด้วยอาร์กิวเมนต์แบบมีชื่อคือรูปแบบที่สอดคล้องกันตลอดในตัวอย่างมาตรฐานซึ่งทำหน้าที่เป็นเอกสารอ้างอิง API โดยพฤตินัย
  • การตรวจสอบเส้นทางก่อนเรนเดอร์พร้อม InvalidConfigException แบบมีชนิด และการ์ดแบบปฏิเสธก่อนปล่อยผลลัพธ์ NotImplementedException ถูกยกมาคำต่อคำจากเส้นทางเอาต์พุตของฟาซาดเอกสาร
  • หลักอ้างอิงมาตรฐานคือ Spec: ISO/IEC 25010, §3.32 คือการป้องกันข้อผิดพลาดของผู้ใช้ ซึ่งเป็นคุณสมบัติด้านคุณภาพที่ API แบบปฏิเสธการคาดเดามีไว้เพื่อตอบสนอง ณ จุดที่เรียกใช้ หลักอ้างอิงที่สองคือ 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 เท่ากับปริมาณความชัดเจนที่มันลดทอนไปพอดี เอนจินยอมแลกอักขระไม่กี่ตัว ณ จุดที่เรียกใช้ เพื่อกำจัดบั๊กทั้งหมวด คือบั๊กที่โค้ดคอมไพล์ผ่าน รันได้ สร้างไฟล์ออกมา และผิด

ความเข้าใจผิดที่เกี่ยวข้องกันคือการคิดว่า fail-fast หมายถึง “โยนข้อยกเว้นบ่อย” ในการใช้งานปกติ NextPDF ไม่โยนข้อยกเว้นใดๆ เลย อินพุตที่ถูกต้องจะไหลผ่านไปได้ การ์ดจะทำงานเฉพาะกับอินพุตที่คลุมเครือหรือไม่ปลอดภัยอย่างแท้จริง คืออินพุตที่คุณต้องการรับรู้ในทันที ไม่ใช่อินพุตที่คุณต้องการให้คาดเดา

การปฏิเสธการคาดเดาใช้กับ เจตนาและความปลอดภัย ไม่ใช่กับความสะดวกทุกอย่าง NextPDF ยังคงมีค่าเริ่มต้นที่ปลอดภัย ได้แก่ การวางแนวแบบแนวตั้ง การจัดแนวชิดซ้าย ไม่มีเส้นขอบ หลักการคือจะเสนอค่าเริ่มต้นเฉพาะในจุดที่ปลอดภัยและไม่สร้างความประหลาดใจ และจะไม่เสนอเลยในจุดที่การอนุมานผิดจะสร้างเอกสารที่ผิด

หน้านี้สาธิตหลักการบนพื้นผิว API สาธารณะหลัก (ฟาซาดเอกสาร สัญญาของมัน และเส้นทางเอาต์พุต) ระบบย่อยต่างๆ มีจุดเข้าใช้งานของตนเอง และแต่ละระบบย่อยจะระบุพฤติกรรมการตรวจสอบของตนเองในเอกสาร รูปแบบที่ยกมาในที่นี้เป็นรูปแบบปัจจุบัน ณ เวลาที่ทบทวนนี้ รูปแบบเหล่านี้แสดงแพตเทิร์น ไม่ใช่บัญชีรายการการ์ดทุกตัวในเอนจินอย่างครบถ้วน

การ์ดแบบ fail-fast ที่อธิบายไว้คือการ์ดด้านความถูกต้องและความปลอดภัย การ์ดเหล่านี้ไม่ใช่ขอบเขตด้านความปลอดภัยในตัวมันเอง การตรวจสอบอินพุตเป็นเพียงชั้นหนึ่ง ปรัชญาการออกแบบ และเอกสารด้านความปลอดภัยอธิบายจุดยืนในภาพรวมที่กว้างกว่านี้

  • Code-backed (ระดับหลักฐาน) หน้าที่มีการตรวจสอบข้ออ้างเทียบกับซอร์สโค้ดของเอนจินเองหรือตัวอย่างที่รันได้ โดยยกมาจากแหล่งนั้นแทนการถอดความ
  • Fail fast การปฏิเสธอินพุตที่ไม่ถูกต้อง ณ จุดที่เร็วที่สุด พร้อมระบุสาเหตุที่ชัดเจน แทนการดำเนินการต่อแล้วล้มเหลวอย่างคลุมเครือในภายหลัง
  • อาร์กิวเมนต์แบบมีชื่อ ไวยากรณ์ ณ จุดที่เรียกใช้ของ PHP (newLine: true) ที่ผูกค่าเข้ากับพารามิเตอร์ด้วยชื่อ ทำให้ค่าลิเทอรัลที่ปกติแล้วคลุมเครือกลายเป็นค่าที่อธิบายตัวเองได้
  • วงจรชีวิตแบบใช้ครั้งเดียว สัญญา Document แบบใช้แล้วทิ้ง คือสร้างอินสแตนซ์ เขียน บันทึก แล้วทิ้ง ไม่มี reset() ไม่มีการนำกลับมาใช้ซ้ำ เวิร์กเกอร์จะสร้างอินสแตนซ์ใหม่ผ่าน DocumentFactory ต่อหนึ่งคำขอ
  • PAdES PDF Advanced Electronic Signatures คือกลุ่มโปรไฟล์ของ ETSI สำหรับการลงนาม PDF ขยายความเมื่อใช้ครั้งแรก และครอบคลุมเชิงลึกในหน้าที่เกี่ยวกับการลงนาม