API ที่ปฏิเสธการคาดเดา
Spec: ISO/IEC 25010 ISO/IEC 25010 Spec: ISO 32000-2 ISO 32000-2 Evidence: Code-backed
ภาพรวมโดยสรุป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสรุป”NextPDF กำหนดให้คุณระบุสิ่งที่ต้องการอย่างชัดเจน เมื่อเจตนามีผลต่อไบต์ของเอาต์พุต ไม่ว่าจะเป็นระดับของลายเซ็น ปลายทางของเอาต์พุต หรือเป้าหมายด้านความสอดคล้อง สิ่งนั้นต้องเป็นอาร์กิวเมนต์ที่ระบุอย่างชัดเจน ไม่ใช่สิ่งที่เอนจินอนุมานจากบริบท
หน้านี้แสดงจุดยืนดังกล่าวจากซอร์สโค้ดของเอนจินโดยตรง ทั้งลายเซ็นของเมธอด อาร์กิวเมนต์แบบมีชื่อ และจุดที่อินพุตคลุมเครือจะถูกปฏิเสธก่อนสร้างไบต์ใดๆ
เหตุใดเรื่องนี้จึงสำคัญ
หัวข้อที่มีชื่อว่า “เหตุใดเรื่องนี้จึงสำคัญ”การคาดเดาคือการตัดสินใจแทนคุณโดยไม่บอกให้ทราบ สำหรับช่องข้อความ เรื่องนี้อาจสร้างความรำคาญเพียงเล็กน้อย แต่สำหรับ PDF นี่คือข้อบกพร่องแฝง เพราะสิ่งที่คุณส่งมอบมักเป็นเอกสารทางกฎหมายหรือเอกสารสำหรับการจัดเก็บถาวร ซึ่งภายหลังบุคคลอื่นจะตรวจสอบความถูกต้องด้วยเครื่องมือตรวจสอบ
ลองพิจารณาลายเซ็น ค่าไดเจสต์ของลายเซ็นคำนวณจากช่วงไบต์ที่ประกาศไว้ ซึ่งตั้งใจไม่รวมค่าของลายเซ็นเข้าไปด้วย ( Spec: ISO 32000-2, §12.8 ISO 32000-2 §12.8 ) API ที่ “ช่วยเหลือ” อย่างเงียบๆ ด้วยการเขียนโครงสร้างใหม่ อนุมานระดับ หรือเติมพื้นที่ตัวยึดตำแหน่ง ไม่ได้ช่วยเลย แต่กลับเปลี่ยนไบต์ที่ลายเซ็นควรปกป้อง การคาดเดาที่ดูเป็นมิตร ณ จุดที่เรียกใช้ อาจกลายเป็นเหตุการณ์ผิดพลาดในระบบโปรดักชันหลายสัปดาห์ต่อมา ทั้งสองอย่างนี้เกิดจากโค้ดบรรทัดเดียวกัน
ฉบับย่อ
หัวข้อที่มีชื่อว่า “ฉบับย่อ”- หากตัวเลือกใดเปลี่ยนแปลงเอาต์พุตและไม่มีค่าเริ่มต้นที่ปลอดภัย NextPDF จะกำหนดให้ตัวเลือกนั้นเป็น อาร์กิวเมนต์ที่ต้องระบุ ไม่ใช่สิ่งที่อนุมานเอง
- อาร์กิวเมนต์ทางเลือกที่อ่านแล้วคลุมเครือจะเป็น อาร์กิวเมนต์แบบมีชื่อ เพื่อให้จุดที่เรียกใช้ระบุเจตนาได้ชัดเจน (
newLine: trueไม่ใช่trueเปล่าๆ) - อินพุตที่อาจไม่ปลอดภัยจะถูก ตรวจสอบก่อนการเรนเดอร์ และถูกปฏิเสธด้วยข้อยกเว้นแบบมีชนิดที่ระบุสาเหตุ
- อินสแตนซ์ของเอกสารเป็นแบบ ใช้ครั้งเดียว คือสร้างขึ้น ส่งออก แล้วทิ้งไป ไม่มี
reset()จึงไม่มีคำถามว่า “สิ่งนี้ถูกนำกลับมาใช้ซ้ำหรือไม่” ให้ต้องคาดเดา - เอนจินจะไม่สร้างผลลัพธ์ที่ดูสมเหตุสมผลขึ้นมาแทนผลลัพธ์ที่คุณร้องขอเด็ดขาด แต่จะปฏิเสธแทน
แนวทางของ NextPDF
หัวข้อที่มีชื่อว่า “แนวทางของ NextPDF”กลไกนี้เรียบง่าย และความเรียบง่ายนั้นคือสาระสำคัญ กลไกประกอบด้วยระบบชนิดข้อมูล อาร์กิวเมนต์แบบมีชื่อ 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 ISO/IEC 25010 §3.32 คือการป้องกันข้อผิดพลาดของผู้ใช้ ซึ่งเป็นคุณสมบัติด้านคุณภาพที่ API แบบปฏิเสธการคาดเดามีไว้เพื่อตอบสนอง ณ จุดที่เรียกใช้ หลักอ้างอิงที่สองคือ Spec: ISO 32000-2, §12.8 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 ที่อธิบายไว้คือการ์ดด้านความถูกต้องและความปลอดภัย การ์ดเหล่านี้ไม่ใช่ขอบเขตด้านความปลอดภัยในตัวมันเอง การตรวจสอบอินพุตเป็นเพียงชั้นหนึ่ง ปรัชญาการออกแบบ และเอกสารด้านความปลอดภัยอธิบายจุดยืนในภาพรวมที่กว้างกว่านี้
เอกสารที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เอกสารที่เกี่ยวข้อง”- ปรัชญาการออกแบบของ NextPDF คือหลักการที่หน้านี้สาธิตในบริบทของลำดับความสำคัญ
- ข้อผิดพลาดในฐานะฟีเจอร์ คือสิ่งที่ข้อยกเว้นแบบมีชนิดซึ่งการ์ดเหล่านี้โยนออกมาถูกออกแบบมาเพื่อบอกคุณ
- ชนิดแบบเข้มงวดในทุกที่ คือวิธีที่ระบบชนิดข้อมูลทำให้ “ระบุเจตนาของคุณ” เป็นสิ่งที่บังคับใช้ได้ แทนที่จะเป็นเพียงคำแนะนำ
อภิธานศัพท์
หัวข้อที่มีชื่อว่า “อภิธานศัพท์”- Code-backed (ระดับหลักฐาน) หน้าที่มีการตรวจสอบข้ออ้างเทียบกับซอร์สโค้ดของเอนจินเองหรือตัวอย่างที่รันได้ โดยยกมาจากแหล่งนั้นแทนการถอดความ
- Fail fast การปฏิเสธอินพุตที่ไม่ถูกต้อง ณ จุดที่เร็วที่สุด พร้อมระบุสาเหตุที่ชัดเจน แทนการดำเนินการต่อแล้วล้มเหลวอย่างคลุมเครือในภายหลัง
- อาร์กิวเมนต์แบบมีชื่อ ไวยากรณ์ ณ จุดที่เรียกใช้ของ PHP (
newLine: true) ที่ผูกค่าเข้ากับพารามิเตอร์ด้วยชื่อ ทำให้ค่าลิเทอรัลที่ปกติแล้วคลุมเครือกลายเป็นค่าที่อธิบายตัวเองได้ - วงจรชีวิตแบบใช้ครั้งเดียว สัญญา
Documentแบบใช้แล้วทิ้ง คือสร้างอินสแตนซ์ เขียน บันทึก แล้วทิ้ง ไม่มีreset()ไม่มีการนำกลับมาใช้ซ้ำ เวิร์กเกอร์จะสร้างอินสแตนซ์ใหม่ผ่านDocumentFactoryต่อหนึ่งคำขอ - PAdES PDF Advanced Electronic Signatures คือกลุ่มโปรไฟล์ของ ETSI สำหรับการลงนาม PDF ขยายความเมื่อใช้ครั้งแรก และครอบคลุมเชิงลึกในหน้าที่เกี่ยวกับการลงนาม