Bỏ qua để đến nội dung

Một API từ chối phỏng đoán

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

NextPDF buộc bạn nói rõ chính xác điều mình muốn. Khi ý định có thể làm thay đổi các byte — mức chữ ký, đích xuất, mục tiêu tuân thủ — nó phải là một đối số bắt buộc và tường minh, chứ không phải thứ engine suy ra từ ngữ cảnh.

Trang này chỉ ra quan điểm đó ngay trong mã nguồn của engine: chữ ký phương thức, đối số có tên, và các điểm mà đầu vào không rõ ràng bị từ chối trước khi bất kỳ byte nào được tạo ra.

Phỏng đoán là một quyết định được đưa ra thay bạn mà bạn không hề được báo trước. Với một trường văn bản, điều đó chỉ hơi khó chịu. Với một PDF, đó là một lỗi tiềm ẩn, bởi tài liệu bạn bàn giao thường là tài liệu pháp lý hoặc lưu trữ mà tính đúng đắn của nó sẽ được người khác kiểm tra sau bằng một trình kiểm định.

Hãy xét một chữ ký. Bản tóm lược (digest) của nó được tính trên một dải byte đã khai báo, cố ý loại trừ chính giá trị chữ ký ( Spec: ISO 32000-2, §12.8 ). Một API âm thầm “giúp đỡ” — viết lại cấu trúc, suy ra một mức, chèn đệm vào một chỗ giữ chỗ — không thật sự giúp ích. Nó đã thay đổi chính những byte mà một chữ ký lẽ ra phải bảo vệ. Phỏng đoán trông thân thiện tại điểm gọi sẽ trở thành sự cố trên môi trường sản xuất vài tuần sau đó. Vẫn là cùng một dòng mã.

  • Nếu một lựa chọn làm thay đổi đầu ra và không có giá trị mặc định an toàn, NextPDF biến nó thành đối số bắt buộc, chứ không phải một đối số được suy ra.
  • Các đối số tùy chọn khi đọc dễ gây mơ hồ thì được đặt tên, để điểm gọi nêu rõ ý định (newLine: true, chứ không phải một true trơ trọi).
  • Những đầu vào có thể không an toàn thì được kiểm tra trước khi kết xuất, và bị từ chối bằng một ngoại lệ có kiểu, nêu rõ nguyên nhân.
  • Một thực thể tài liệu là dùng-một-lần: nó được dựng, xuất ra, rồi bỏ đi. Không có reset(), nên cũng không cần phỏng đoán xem “liệu thứ này có bị dùng lại không?”.
  • Engine không bao giờ xuất ra một tài liệu trông có vẻ hợp lý để thay cho tài liệu mà bạn đã yêu cầu. Thay vào đó, nó từ chối.

Cơ chế này rất đơn giản, và đó chính là điều cốt lõi. Nó dựa vào hệ thống kiểu, đối số có tên, enum thay cho chuỗi mơ hồ, và một số ít mệnh đề kiểm tra được đặt có chủ đích trước khi xuất.

Bảng dưới đây đối chiếu một vài đầu vào không rõ ràng. Với mỗi trường hợp, bảng cho thấy một thư viện “giúp đỡ” sẽ suy ra điều gì, và NextPDF làm gì thay vào đó. Mỗi ô trong cột NextPDF là một hành vi được trích dẫn từ mã nguồn ở phần sau của trang này.

Đầu vào không rõ ràngMột thư viện phỏng đoán sẽ làm gìNextPDF làm gì
Một chuỗi hướng trang như "portait"Rơi về một giá trị mặc định rồi vẫn kết xuấtaddPage() nhận enum Orientation chứ không phải một chuỗi — gõ sai là lỗi kiểu, chứ không phải một mặc định âm thầm
Một true trơ trọi đặt ở cuối cell()Chọn một vị trí boolean nào đó mà nó đoán là bạn muốnGiá trị boolean được đặt tên tại điểm gọi (newLine: true); một literal không tên chính là dấu hiệu xấu mà API này loại bỏ
Một wrapper php:// hoặc đường dẫn duyệt ngược truyền vào save()”Cố hết sức” rồi ghi ra một nơi nào đóBị từ chối trước khi PDF được dựng, bằng một InvalidConfigException có kiểu nêu rõ khóa, giá trị và kiểu dữ liệu mong đợi
setSignature() rồi save() trong khi trình ký cấp cao chưa được kết nốiXuất ra một tệp chưa ký mà bên gọi lại tin là đã được kýNém NotImplementedException trước khi tạo ra byte nào, nêu rõ lối đi được hỗ trợ
Dùng lại một thực thể Document cho một lần kết xuất thứ haiPhỏng đoán xem trạng thái còn sót lại có còn áp dụng hay khôngKhông có reset() và không có cách dùng lại — mỗi yêu cầu có một thực thể mới thông qua DocumentFactory, nên không có trạng thái còn sót lại nào để phải phỏng đoán

Ý định là một đối số bắt buộc. Hợp đồng cốt lõi, PdfDocumentInterface, nhận hình học và canh lề dưới dạng value object có kiểu và enum, chứ không phải các kiểu nguyên thủy lỏng lẻo:

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;

OrientationAlignment là các enum, nên lời gọi không thể truyền "portait" rồi để nó âm thầm hiểu thành “mặc định”. Ở những nơi có giá trị mặc định, đó là một mặc định an toàn (dọc, canh trái, không viền), chứ không phải một phỏng đoán về điều bạn có thể đã muốn.

Các boolean mơ hồ được đặt tên tại điểm gọi. Trong các ví dụ thực tế đóng vai trò như tài liệu tham khảo API, cùng một khuôn dạng lặp đi lặp lại:

$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 thì không thể nhầm lẫn. Một true trơ trọi ở cuối thì không được như vậy. Mức chữ ký là SignatureLevel::PAdES_B_B, một trường hợp enum — không bao giờ là một chuỗi mà engine phải diễn giải. Đích xuất là OutputDestination::String, nên ý “trả về các byte cho tôi, không header HTTP, không ghi tệp” được nêu rõ. Nó không được suy ra từ việc có truyền tên tệp hay không.

Đầu vào không an toàn bị từ chối trước khi một byte nào được ghi. save() kiểm tra đường dẫn đích trước khi dựng 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
}

Engine không “cố hết sức” với một wrapper php:// hay một đường dẫn duyệt ngược. Nó từ chối, và ngoại lệ nêu rõ khóa, giá trị cùng điều được mong đợi.

Engine thà từ chối còn hơn xuất ra một tài liệu gây hiểu lầm. Hình thức mạnh nhất của việc từ chối phỏng đoán là không tạo ra đầu ra nào cả khi đầu ra đó sẽ không trung thực. Khi một chữ ký cấp cao đã được cấu hình nhưng điểm nối tới trình ghi chịu trách nhiệm ký thật sự chưa được kết nối, quy trình dựng sẽ ném lỗi trước khi tạo ra byte nào, thay vì xuất ra một tệp chưa ký mà bên gọi lại tin là đã được ký:

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 ...',
);
}

Một PDF chưa ký mà trông như đã ký chính là loại tài liệu sai nhưng trông có vẻ hợp lý mà nguyên tắc này sinh ra để ngăn chặn. Quan điểm tương tự xuất hiện ở đường xử lý CSS nghiêm ngặt. Một sai lệch khỏi đặc tả mà chưa được đăng ký sẽ ném ra một StrictModeViolation ngay tại điểm phát hiện, thay vì kết xuất một bản gần đúng và để sai lệch đó không bị phát hiện.

Dùng-một-lần loại bỏ cả một lớp phỏng đoán. Một Document là dùng-rồi-bỏ — được dựng, xuất ra, rồi bỏ đi. Không có reset() và không có cách dùng lại. Một worker chạy lâu dài tạo ra một thực thể mới cho mỗi yêu cầu thông qua DocumentFactory. Engine không bao giờ phải phỏng đoán xem trạng thái còn sót lại từ một tài liệu trước đó có còn ý nghĩa hay không, vì theo thiết kế, trạng thái như vậy không tồn tại.

Trang này là Evidence: Code-backed : mọi khuôn mẫu phía trên đều được trích dẫn từ chính mã nguồn và các ví dụ của engine, chứ không phải được diễn giải lại từ ý định.

  • Các chữ ký phương thức có kiểu và có enum chính là hợp đồng công khai trong PdfDocumentInterface. Phong cách gọi bằng đối số có tên là khuôn mẫu nhất quán xuyên suốt các ví dụ chuẩn vốn đóng vai trò là tài liệu tham khảo API trên thực tế.
  • Phần kiểm tra đường dẫn trước khi kết xuất, với InvalidConfigException có kiểu của nó, và bộ kiểm tra từ-chối-trước-khi-xuất NotImplementedException được trích dẫn nguyên văn từ đường xuất của facade tài liệu.
  • Mốc tiêu chuẩn là Spec: ISO/IEC 25010, §3.32 — bảo vệ chống lỗi người dùng, thuộc tính chất lượng mà một API từ chối phỏng đoán sinh ra để đáp ứng ngay tại điểm gọi. Mốc thứ hai là Spec: ISO 32000-2, §12.8 , đó là lý do vì sao việc phỏng đoán xoay quanh một tài liệu đã ký không bao giờ là vô hại. Bản tóm lược (digest) bao trùm một dải byte đã khai báo, loại trừ giá trị chữ ký, nên bất kỳ thay đổi âm thầm nào cũng làm nó mất hiệu lực.

Dưới đây là một chương trình nhỏ, hoàn chỉnh. Mọi dòng có khả năng gây mơ hồ đều nêu rõ ý định. Một đầu vào không an toàn duy nhất bị từ chối trước khi bất kỳ công việc nào được thực hiện.

<?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);
}

Không có đường chạy nào mà chương trình này âm thầm làm sai. Nó nêu rõ ý định rồi tiếp tục, hoặc nêu rõ vấn đề rồi dừng lại.

Lời phản bác thường gặp là “đây chỉ là dài dòng”. Đó không phải là dài dòng. Đó là sự vắng mặt của những giá trị mặc định ẩn. Một true trơ trọi ngắn hơn newLine: true đúng bằng phần rõ ràng mà nó lược bỏ. Engine đánh đổi vài ký tự tại điểm gọi để loại bỏ hẳn một loại lỗi — loại mà mã vẫn biên dịch, vẫn chạy, vẫn tạo ra một tệp, nhưng lại sai.

Một quan niệm sai liên quan là cho rằng báo lỗi sớm nghĩa là “ném lỗi rất nhiều”. Trong sử dụng thông thường, NextPDF không ném lỗi nào cả. Đầu vào hợp lệ trôi qua suôn sẻ. Các bộ kiểm tra chỉ kích hoạt với những đầu vào thực sự mơ hồ hoặc không an toàn — chính là những đầu vào bạn muốn biết ngay lập tức, chứ không phải những đầu vào bạn muốn được phỏng đoán.

Việc từ chối phỏng đoán áp dụng cho ý định và an toàn, chứ không phải mọi tiện ích. NextPDF vẫn có các giá trị mặc định an toàn: hướng dọc, canh trái, không viền. Nguyên tắc là một giá trị mặc định chỉ được cung cấp ở nơi nó an toàn và không gây bất ngờ, và không bao giờ ở nơi mà suy đoán sai sẽ tạo ra một tài liệu sai.

Trang này minh họa nguyên tắc đó trên bề mặt API công khai cốt lõi (facade tài liệu, hợp đồng của nó, và đường xuất). Các hệ thống con có điểm vào riêng, và mỗi hệ thống có tài liệu riêng cho hành vi kiểm tra của chính nó. Các khuôn dạng được trích dẫn ở đây là hiện hành tính đến lần rà soát này. Chúng minh họa cho khuôn mẫu; chúng không phải là một danh mục đầy đủ của mọi bộ kiểm tra trong engine.

Các bộ kiểm tra báo lỗi sớm được mô tả ở đây là các bộ kiểm tra về tính đúng đắn và an toàn. Bản thân chúng không phải là một ranh giới bảo mật. Kiểm tra đầu vào là một lớp. Phần triết lý thiết kế và tài liệu bảo mật mô tả quan điểm rộng hơn.

  • Có mã làm bằng chứng (mức bằng chứng) — một trang có các khẳng định được đối chiếu với chính mã nguồn của engine hoặc một ví dụ chạy được, được trích dẫn thay vì diễn giải lại.
  • Báo lỗi sớm — từ chối một đầu vào không hợp lệ ngay ở điểm sớm nhất, với một nguyên nhân rõ ràng, thay vì tiếp tục rồi thất bại một cách khó hiểu về sau.
  • Đối số có tên — một cú pháp tại điểm gọi của PHP (newLine: true) gắn một giá trị vào một tham số theo tên, khiến một literal vốn mơ hồ trở nên tự giải nghĩa.
  • Vòng đời dùng-một-lần — hợp đồng Document dùng-rồi-bỏ: khởi tạo, ghi, lưu, bỏ đi. Không reset(), không dùng lại. Worker tạo ra một thực thể mới cho mỗi yêu cầu thông qua DocumentFactory.
  • PAdES — PDF Advanced Electronic Signatures, họ hồ sơ ETSI dành cho việc ký PDF. Được viết đầy đủ ở lần dùng đầu tiên; được trình bày chuyên sâu trên các trang về ký.