ข้อผิดพลาดในฐานะฟีเจอร์
Spec: ISO 9241-110, §5.6.4 ISO 9241-110 §5.6.4 Evidence: Code-backed
โดยสรุป
หัวข้อที่มีชื่อว่า “โดยสรุป”NextPDF ปฏิบัติต่อลำดับชั้น exception เป็นพื้นผิว API ที่ต้องออกแบบด้วยความตั้งใจเดียวกับเมธอดที่โยน exception เหล่านั้น ความล้มเหลวแต่ละแบบมีความเฉพาะเจาะจง มีชนิดกำกับ จับได้ตามระดับความละเอียดที่คุณต้องการ และมีบริบทเชิงโครงสร้างสำหรับล็อกของคุณ
หน้านี้แสดงพื้นผิวดังกล่าวในซอร์สโค้ดของเอนจินเอง: ชนิดฐาน คลาสย่อยที่มีชนิดกำกับ named constructor ที่ผูกสาเหตุที่แท้จริงเข้ากับข้อความ และบริบทเชิงโครงสร้างที่ exception ของ NextPDF ทุกตัวเปิดเผย
เหตุใดสิ่งนี้จึงสำคัญ
หัวข้อที่มีชื่อว่า “เหตุใดสิ่งนี้จึงสำคัญ”ข้อความแสดงข้อผิดพลาดคือเสียงของเอนจินที่พูดกับคุณในสถานการณ์ที่เลวร้ายที่สุด: บนระบบโปรดักชัน เวลา 2 a.m. พร้อมเอกสารที่ควรถูกส่งออกไปแล้ว สิ่งที่ข้อความนั้นบอกจะกำหนดว่าขั้นตอนต่อไปคือการแก้ไขหรือการสืบสวนที่ยืดเยื้อ
exception แบบทั่วไปอย่าง RuntimeException: something went wrong ไม่ได้บอกทางออกใด ๆ ให้คุณ ข้อความนั้นบอกเพียงว่าเอนจินล้มเหลว แต่ไม่บอกว่าอะไรล้มเหลว ล้มเหลวที่ใด และแน่นอนว่าไม่บอกว่าควรทำอะไรต่อ แนวทางด้านปัจจัยมนุษย์ระบุเรื่องนี้ไว้อย่างตรงไปตรงมา: ข้อผิดพลาดควรอธิบายตัวเองได้ดีพอให้ขั้นตอนการแก้ไขชัดเจนขึ้น ไม่ใช่กลายเป็นงานค้นคว้า ( Spec: ISO 9241-110, §5.6.4.3 ISO 9241-110 §5.6.4.3 ) exception ที่ระบุทั้งสาเหตุและวิธีแก้ไขไม่ใช่ความหรูหรา แต่คือความแตกต่างระหว่างการแก้ไขที่ใช้เวลาห้านาทีกับการแก้ไขที่ใช้เวลาห้าชั่วโมง
ฉบับย่อ
หัวข้อที่มีชื่อว่า “ฉบับย่อ”- ความล้มเหลวของ NextPDF ทุกแบบสืบทอดจากคลาสฐานนามธรรมหนึ่งเดียวคือ
NextPdfExceptionดังนั้นคุณจึงจับข้อผิดพลาดทั้งหมดของไลบรารีได้ด้วยชนิดเดียว - ภายใต้คลาสนั้นมีคลาสย่อย ที่เฉพาะเจาะจงและมีชนิดกำกับ — ฟอนต์ที่หาไม่พบ การกำหนดค่าที่ไม่ถูกต้อง การดำเนินการลายเซ็นที่ล้มเหลว — ดังนั้นคุณจึงจับความล้มเหลวที่คุณรับมือได้อย่างตรงจุด
- exception ของ NextPDF ทุกตัวอิมพลิเมนต์
ContextAwareExceptionInterfaceและเปิดเผยgetContext(): แมปเชิงโครงสร้างที่ปลอดภัยต่อการบันทึกล็อก คุณจึงไม่ต้องแยกวิเคราะห์ข้อความที่เป็นสตริงเพื่อดึงข้อมูลวินิจฉัยเลย - ข้อความ นำไปปฏิบัติได้จริง: named constructor ผูกสาเหตุที่แท้จริง (และมักรวมถึงวิธีแก้ไข) เข้ากับข้อความ แทนที่จะเป็นเทมเพลตทั่วไป
- คลาส exception แต่ละคลาสระบุไว้ว่า ใครสามารถจัดการได้ — นักพัฒนา ฝ่ายโครงสร้างพื้นฐาน หรือผู้เรียกใช้ไลบรารี — การคัดแยกจึงเริ่มได้ก่อนที่คุณจะอ่าน stack trace
NextPDF จัดการเรื่องนี้อย่างไร
หัวข้อที่มีชื่อว่า “NextPDF จัดการเรื่องนี้อย่างไร”ลำดับชั้นนี้ตื้นและจงใจออกแบบเช่นนั้น มีคลาสฐานหนึ่งเดียว ชั้นของชนิดเฉพาะโดเมนหนึ่งชั้น และสัญญาหนึ่งฉบับที่ทุกตัวยึดถือ
ฐานเดียว จับครอบคลุมทั้งหมดตามการออกแบบ NextPdfException เป็นคลาสนามธรรมที่สืบทอดจาก RuntimeException และอิมพลิเมนต์ ContextAwareExceptionInterface:
abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface{ /** @return array<string, mixed> */ public function getContext(): array { return []; }}การเป็นคลาสนามธรรมคือการตัดสินใจเชิงออกแบบ คุณจะไม่จับคลาสฐานที่คลุมเครือโดยบังเอิญ เพราะคลาสฐานนั้นไม่ถูกโยนออกมาโดยตรง คุณจับคลาสฐานนั้นอย่างจงใจในฐานะตัวรับสุดท้าย และจับคลาสย่อยที่เฉพาะเจาะจงเมื่อคุณสามารถทำอะไรบางอย่างที่เฉพาะเจาะจงได้
คลาสย่อยที่เฉพาะเจาะจงและมีชนิดกำกับ ฟอนต์ที่หายไปไม่ใช่ข้อผิดพลาดทั่วไป แต่คือ FontNotFoundException และมีข้อมูลที่คุณต้องใช้ในการลงมือ:
final class FontNotFoundException extends NextPdfException{ public function __construct( private readonly string $fontName, private readonly array $searchPaths, private readonly bool $fallbackAttempted, ?Throwable $previous = null, ) { parent::__construct( \sprintf('Font "%s" not found. Searched: [%s].', $fontName, \implode(', ', $searchPaths)), 0, $previous, ); } // getFontName(), getSearchPaths(), wasFallbackAttempted(), getContext()}ข้อความระบุชื่อฟอนต์และพาธที่ค้นหาอย่างแม่นยำ คุณไม่ต้องคาดเดาว่าไดเรกทอรีใดหายไป เพราะ exception บอกไว้แล้ว
บริบทเชิงโครงสร้าง ไม่ใช่การแกะข้อความจากสตริง exception ทุกตัวคืนค่าแมปแบบ snake_case ที่มีเฉพาะค่าปฐมภูมิ ซึ่งปลอดภัยเมื่อนำไป serialize ลงล็อกหรือ payload ของ APM ได้โดยตรง:
public function getContext(): array{ return [ 'config_key' => $this->configKey, 'given_value' => $this->givenValue, 'expected_type' => $this->expectedType, ];}สัญญานี้มีเหตุผลชัดเจน มิดเดิลแวร์สำหรับการบันทึกล็อกสามารถเรียก $logger->error($e->getMessage(), $e->getContext()) กับ exception ของ NextPDF ตัวใดก็ได้ โดยไม่ต้องแยกวิเคราะห์ข้อความเลย ข้อความมีไว้สำหรับมนุษย์ บริบทมีไว้สำหรับเครื่องจักร ทั้งสองอย่างไม่จำเป็นต้องทำหน้าที่แทนกัน
ข้อความที่นำไปปฏิบัติได้จริงผ่าน named constructor นี่คือจุดที่ข้อผิดพลาดเลิกเป็นเหตุบังเอิญและกลายเป็นสิ่งที่ออกแบบไว้ SignatureException ไม่ได้เพียงบอกว่า “การลงนามล้มเหลวที่ระดับ B-LT” แต่มี named constructor ที่ผูกสาเหตุที่แท้จริง และมักรวมถึงวิธีแก้ไขที่ตรงจุด เข้ากับข้อความ:
public static function tsaUrlEmpty(string $signatureLevel): self{ return new self('', $signatureLevel, null, 'TSA endpoint URL is empty: pass a non-empty `tsaUrl` to the TsaClient ' . 'constructor (e.g. "https://timestamp.example.com/tsa") or remove the ' . 'TSA client wiring if no timestamping is required at this signature level');}ข้อความระบุว่ามีอะไรผิดพลาด และควรทำอย่างไรกับมัน มี constructor พี่น้องสำหรับกรณีแพ็กเกจความสามารถที่หายไป HTTP client ที่ไม่มีอยู่ อัลกอริทึมแบบ digest-only ที่ถูกเลือกผิด ชนิดคีย์ที่ไม่ตรงกับอัลกอริทึม และอื่น ๆ แต่ละตัวเปลี่ยนความล้มเหลวหนึ่งประเภทให้กลายเป็นประโยคที่นักพัฒนานำไปลงมือได้โดยไม่ต้องอ่านซอร์สโค้ดของเอนจิน
ความล้มเหลวที่ส่งเสียงดังโดยเจตนา exception บางตัวมีไว้เพื่อเปลี่ยนช่องว่างที่เงียบงันให้เป็นช่องว่างที่ส่งเสียงดัง NotImplementedException มีป้ายกำกับ feature ที่ grep ได้ด้วยเครื่อง และการอ้างอิง followUp:
final class NotImplementedException extends NextPdfException{ public function __construct( public readonly string $feature, public readonly string $followUp, ?Throwable $previous = null, ) { parent::__construct( \sprintf('%s is not implemented in this release. %s', $feature, $followUp), 0, $previous, ); }}เส้นทางที่เข้าถึงได้แล้วแต่ยังไม่ได้เชื่อมต่อจะโยน exception นี้ แทนที่จะคืนผลลัพธ์แบบไม่ทำอะไรที่ดูสมเหตุสมผล แนวคิดเดียวกันนี้ขับเคลื่อน StrictModeViolation ซึ่งคลาสย่อยของมันมีป้ายกำกับสั้น ๆ ที่ grep ได้สำหรับโครงสร้างที่เบี่ยงเบน พร้อมบริบทของตำแหน่งและการอ้างอิงทางเลือก การเบี่ยงเบนจากข้อกำหนดจึงกลายเป็นการหยุดที่มีชนิดกำกับและมีบริบท ไม่ใช่การเรนเดอร์ที่ผิดพลาดอย่างเงียบ ๆ
เมทาดาทาสำหรับการคัดแยกอยู่ในตัวคลาสเอง คลาส exception แต่ละคลาสระบุ ว่าใครสามารถจัดการได้ ไว้ใน docblock ของมัน ตัวอย่างเช่น FontNotFoundException คือ “นักพัฒนา (ตรวจสอบพาธของฟอนต์) หรือฝ่ายโครงสร้างพื้นฐาน (แก้ไขสิทธิ์ของไฟล์)” InvalidConfigException คือ “นักพัฒนา (แก้ไขการกำหนดค่าก่อนเรียกใช้ NextPDF)” NotImplementedException คือ “ผู้เรียกใช้ไลบรารี — ให้ลบการเรียกใช้นั้นออก หรือปักหมุดไว้กับรุ่นในอนาคต” การคัดแยกจึงเริ่มได้ก่อนถึง stack trace เพราะคำถามที่ว่า “นี่เป็นเรื่องของฉันหรือของฝ่ายปฏิบัติการ” มีคำตอบบันทึกไว้แล้ว
ตารางนี้สรุปการออกแบบและสิ่งที่คุณสมบัติแต่ละข้อให้กับคุณ
| คุณสมบัติของการออกแบบ | ในซอร์สโค้ด | สิ่งที่มอบให้คุณ |
|---|---|---|
| คลาสฐานนามธรรมหนึ่งเดียว | NextPdfException (นามธรรม อิมพลิเมนต์อินเทอร์เฟซบริบท) | จับข้อผิดพลาดของไลบรารีทุกตัวด้วยชนิดเดียว ไม่จับคลาสฐานที่คลุมเครือโดยบังเอิญ |
| คลาสย่อยที่เฉพาะเจาะจงและมีชนิดกำกับ | FontNotFoundException, InvalidConfigException, SignatureException, … | จับความล้มเหลวที่คุณรับมือได้อย่างตรงจุด |
| บริบทเชิงโครงสร้าง | getContext() — ค่าปฐมภูมิแบบ snake_case เท่านั้น | บันทึกล็อกหรือส่งไปยัง APM ได้โดยไม่ต้องแยกวิเคราะห์ข้อความที่เป็นสตริง |
| ข้อความที่นำไปปฏิบัติได้จริง | named constructor ผูกสาเหตุที่แท้จริง + วิธีแก้ไข | ประโยคที่คุณนำไปลงมือได้ ไม่ใช่เทมเพลต |
| ส่งเสียงดังโดยเจตนา | NotImplementedException, StrictModeViolation | ช่องว่างที่เงียบงันกลายเป็นการหยุดที่มีชนิดกำกับและ grep ได้ |
| เมทาดาทาสำหรับการคัดแยก | ”Actionable by:” ใน docblock ของแต่ละคลาส | รู้ว่าเป็นปัญหาของใครก่อนอ่าน trace |
หลักฐานบอกอะไร
หัวข้อที่มีชื่อว่า “หลักฐานบอกอะไร”หน้านี้เป็นแบบ Evidence: Code-backed : ทุกคลาส ทุกลายเซ็น และทุกรูปแบบข้อความอ้างอิงโดยตรงจากเนมสเปซ exception ของเอนจิน ไม่ใช่การถอดความ
- คลาสฐานนามธรรม สัญญา
ContextAwareExceptionInterfaceของมัน คลาสย่อยที่มีชนิดกำกับ รูปแบบของgetContext()และ named constructor ของSignatureExceptionล้วนถูกอ้างอิงคำต่อคำจากซอร์สโค้ด - บรรทัดคัดแยก “Actionable by:” คือสัญญาใน docblock ของคลาสที่อยู่ในไฟล์เดียวกันนั้น
- หลักยึดด้านปัจจัยมนุษย์คือ Spec: ISO 9241-110 ISO 9241-110 — §5.6.4.3 ว่าด้วยข้อผิดพลาดที่อธิบายตัวเองได้ดีพอให้แก้ไขได้ และหลักการความทนทานต่อข้อผิดพลาดในการใช้งานใน §6 เอนจินปฏิบัติต่อนักพัฒนาในฐานะผู้ใช้ และปฏิบัติต่อ exception ในฐานะอินเทอร์เฟซที่ต้องเป็นไปตามข้อกำหนดเหล่านั้น
ตัวอย่างการใช้งานจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างการใช้งานจริง”จับแบบกว้างในฐานะตัวรับสุดท้าย จับแบบเฉพาะเจาะจงในจุดที่คุณลงมือได้ และส่งบริบทเชิงโครงสร้างเข้าสู่ตัวบันทึกล็อกของคุณโดยตรง — โดยไม่ต้องแยกวิเคราะห์ข้อความ
<?php
declare(strict_types=1);
use NextPDF\Core\Document;use NextPDF\Exception\FontNotFoundException;use NextPDF\Exception\NextPdfException;use Psr\Log\LoggerInterface;
function renderInvoice(LoggerInterface $logger): ?string{ try { $document = Document::createStandalone(); $document->setTitle('Invoice 2026-0042'); $document->addPage(); $document->setFont('BrandSans', '', 12); $document->cell(0, 10, 'Thank you for your business.', newLine: true);
return $document->getPdfData(); } catch (FontNotFoundException $e) { // Specific: we can recover — fall back to a built-in font. // getContext() is log-safe structured data, not a parsed string. $logger->warning($e->getMessage(), $e->getContext());
return null; // caller re-renders with 'helvetica' } catch (NextPdfException $e) { // Backstop: any other NextPDF failure, still with structured context. $logger->error($e->getMessage(), $e->getContext());
return null; }}บล็อก catch ที่เฉพาะเจาะจงสามารถกู้คืนได้เพราะชนิดของ exception บอกว่าการกู้คืนเป็นไปได้ ตัวรับสุดท้ายบันทึกบริบทเชิงโครงสร้างสำหรับทุกกรณีที่เหลือ ไม่มีจุดใดที่แอปพลิเคชันต้องอ่านข้อความเพื่อหาว่าเกิดอะไรขึ้น
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”การตีความผิดที่พบบ่อยคือคิดว่าต้นไม้ exception ที่ลึกเป็นการออกแบบเกินจำเป็น และชนิดข้อผิดพลาดเพียงชนิดเดียวจะง่ายกว่า แนวทางนั้นง่ายกว่าสำหรับเอนจิน แต่แย่กว่าสำหรับคุณ ชนิดเดียวหมายความว่าความล้มเหลวทุกอย่างกลายเป็น stack trace ทั่วไป และตรรกะการกู้คืนต้องจับคู่สตริง การจับคู่นั้นเปราะบาง การปรับถ้อยคำข้อความครั้งถัดไปก็ทำให้พังได้ ลำดับชั้นที่เล็กและเฉพาะเจาะจงย้ายความรู้นั้นเข้าสู่ระบบชนิด ซึ่งคอมไพเลอร์และบล็อก catch ของคุณนำไปใช้ได้
ความเข้าใจผิดประการที่สองคือคิดว่าข้อความและบริบทซ้ำซ้อนกัน ทั้งสองไม่ได้ซ้ำซ้อนกัน ข้อความเป็นร้อยแก้วสำหรับมนุษย์ที่อ่านบรรทัดล็อก บริบทเป็นแมปที่มีชนิดกำกับสำหรับการกำหนดเส้นทางในโค้ด การแจ้งเตือน หรือแดชบอร์ด การปนสองสิ่งนี้เข้าด้วยกันคือกับดักการแยกวิเคราะห์สตริงที่สัญญา getContext() ตั้งใจขจัดออกไป
ขีดจำกัดและขอบเขต
หัวข้อที่มีชื่อว่า “ขีดจำกัดและขอบเขต”ลำดับชั้นนี้ตื้นโดยตั้งใจ NextPDF ไม่ได้สร้างคลาส exception แยกต่างหากสำหรับความล้มเหลวทุกกรณีที่นึกออก แต่จะสร้างขึ้นในจุดที่การจับความล้มเหลว นั้น อย่างเฉพาะเจาะจงเป็นสิ่งที่ผู้เรียกใช้พึงทำอย่างสมเหตุสมผล การแยกย่อยมากเกินไปจะเท่ากับแลกปัญหาการแยกวิเคราะห์สตริงกับปัญหารายการ catch ที่ขยายไม่สิ้นสุด
getContext() ถูกจัดโครงสร้างสำหรับล็อกและ APM จึงคืนค่าเฉพาะค่าปฐมภูมิและลิสต์ของค่าปฐมภูมิเท่านั้น โดยไม่มีออบเจกต์ซ้อนกันตามที่ระบุไว้ในสัญญา นี่คือบริบทเพื่อการวินิจฉัย ไม่ใช่ภาพรวมแบบ serialize ของส่วนภายในของเอนจิน และไม่ใช่ wire format ที่เสถียรพอสำหรับสร้างสกีมาภายนอกอ้างอิง
หน้านี้อธิบาย พื้นผิวการออกแบบ ของ exception ชุด exception ที่แน่นอนและฟิลด์ของมันจะพัฒนาไปพร้อมกับเอนจิน คลาสและรูปแบบที่อ้างอิงไว้ในที่นี้เป็นปัจจุบัน ณ การทบทวนครั้งนี้ และเป็นเพียงตัวอย่างประกอบสัญญา ไม่ใช่แคตตาล็อกถาวร สัญญา — ฐานเดียว คลาสย่อยที่มีชนิดกำกับ บริบทเชิงโครงสร้าง ข้อความที่นำไปปฏิบัติได้จริง — คือส่วนที่เสถียร
เอกสารที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เอกสารที่เกี่ยวข้อง”- API ที่ปฏิเสธจะคาดเดา — การ์ดแบบ fail-fast ที่โยน exception เหล่านี้ออกมาตั้งแต่แรก
- ปรัชญาการออกแบบของ NextPDF — เหตุใด “ข้อผิดพลาดคือพื้นผิว API” จึงเป็นหลักการสำคัญลำดับต้น
- โมเดลไปป์ไลน์ — จุดที่ความล้มเหลวเหล่านี้ปรากฏขึ้นขณะที่เอกสารเคลื่อนผ่านเอนจิน และวิธีสังเกตการณ์ความล้มเหลวเหล่านั้น
อภิธานศัพท์
หัวข้อที่มีชื่อว่า “อภิธานศัพท์”- Code-backed (ระดับหลักฐาน) — หน้าที่ตรวจสอบข้อกล่าวอ้างกับซอร์สโค้ดของเอนจินเอง โดยอ้างอิงคำต่อคำแทนการถอดความ
- Context-aware exception — exception ของ NextPDF ที่อิมพลิเมนต์
ContextAwareExceptionInterfaceและเปิดเผยgetContext()เมธอดดังกล่าวคืนค่าแมปแบบ snake_case ของฟิลด์วินิจฉัยที่เป็นค่าปฐมภูมิ ซึ่งปลอดภัยเมื่อนำไป serialize ลงล็อกหรือ payload ของ APM โดยไม่ต้องแยกวิเคราะห์ข้อความที่เป็นสตริง - Named constructor — เมธอดแฟกทอรีแบบ static (ตัวอย่างเช่น
SignatureException::tsaUrlEmpty()) ที่สร้าง exception พร้อมข้อความซึ่งผูกเข้ากับสาเหตุที่แท้จริงหนึ่งรายการ และบ่อยครั้งรวมถึงวิธีแก้ไขของมันด้วย - PAdES — PDF Advanced Electronic Signatures ตระกูลโปรไฟล์ของ ETSI สำหรับการลงนาม PDF โดยจะขยายความเมื่อใช้ครั้งแรก และอธิบายโดยละเอียดในหน้าเกี่ยวกับการลงนาม
- TSA — Time-Stamping Authority บริการที่เชื่อถือได้ซึ่งออกการประทับเวลาแบบ RFC 3161 ที่ใช้โดยโปรไฟล์ PAdES ระดับสูง