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

ข้อผิดพลาดในฐานะฟีเจอร์

Spec: 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 ) exception ที่ระบุทั้งสาเหตุและวิธีแก้ไขไม่ใช่ความหรูหรา แต่คือความแตกต่างระหว่างการแก้ไขที่ใช้เวลาห้านาทีกับการแก้ไขที่ใช้เวลาห้าชั่วโมง

  • ความล้มเหลวของ NextPDF ทุกแบบสืบทอดจากคลาสฐานนามธรรมหนึ่งเดียวคือ NextPdfException ดังนั้นคุณจึงจับข้อผิดพลาดทั้งหมดของไลบรารีได้ด้วยชนิดเดียว
  • ภายใต้คลาสนั้นมีคลาสย่อย ที่เฉพาะเจาะจงและมีชนิดกำกับ — ฟอนต์ที่หาไม่พบ การกำหนดค่าที่ไม่ถูกต้อง การดำเนินการลายเซ็นที่ล้มเหลว — ดังนั้นคุณจึงจับความล้มเหลวที่คุณรับมือได้อย่างตรงจุด
  • exception ของ NextPDF ทุกตัวอิมพลิเมนต์ ContextAwareExceptionInterface และเปิดเผย getContext(): แมปเชิงโครงสร้างที่ปลอดภัยต่อการบันทึกล็อก คุณจึงไม่ต้องแยกวิเคราะห์ข้อความที่เป็นสตริงเพื่อดึงข้อมูลวินิจฉัยเลย
  • ข้อความ นำไปปฏิบัติได้จริง: named constructor ผูกสาเหตุที่แท้จริง (และมักรวมถึงวิธีแก้ไข) เข้ากับข้อความ แทนที่จะเป็นเทมเพลตทั่วไป
  • คลาส exception แต่ละคลาสระบุไว้ว่า ใครสามารถจัดการได้ — นักพัฒนา ฝ่ายโครงสร้างพื้นฐาน หรือผู้เรียกใช้ไลบรารี — การคัดแยกจึงเริ่มได้ก่อนที่คุณจะอ่าน stack trace

ลำดับชั้นนี้ตื้นและจงใจออกแบบเช่นนั้น มีคลาสฐานหนึ่งเดียว ชั้นของชนิดเฉพาะโดเมนหนึ่งชั้น และสัญญาหนึ่งฉบับที่ทุกตัวยึดถือ

ฐานเดียว จับครอบคลุมทั้งหมดตามการออกแบบ 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 — §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 ระดับสูง