Lewati ke konten

API yang menolak untuk menebak

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

NextPDF mewajibkan Anda menyatakan dengan tepat apa yang Anda maksud. Saat maksud memengaruhi byte — level tanda tangan, tujuan keluaran, target konformansi — hal itu menjadi argumen eksplisit yang wajib, bukan sesuatu yang disimpulkan mesin dari konteks.

Halaman ini menunjukkan sikap tersebut di dalam kode sumber mesin itu sendiri: tanda tangan metode, argumen bernama, dan titik-titik tempat input ambigu ditolak sebelum byte apa pun dihasilkan.

Tebakan adalah keputusan yang dibuat atas nama Anda tanpa memberi tahu Anda. Untuk sebuah kolom teks, itu hanya sedikit mengganggu. Untuk sebuah PDF, itu adalah cacat laten, karena apa yang Anda kirimkan sering kali merupakan artefak legal atau arsip yang kebenarannya baru diperiksa kemudian oleh pihak lain menggunakan validator.

Ambil contoh tanda tangan. Digest-nya dihitung atas rentang byte yang dideklarasikan, yang sengaja mengecualikan nilai tanda tangan itu sendiri ( Spec: ISO 32000-2, §12.8 ). API yang diam-diam “membantu” — menulis ulang struktur, menyimpulkan sebuah level, menyisipkan placeholder — tidak benar-benar membantu. Ia telah mengubah byte yang seharusnya dilindungi oleh tanda tangan. Tebakan yang tampak ramah di lokasi pemanggilan menjadi insiden produksi beberapa minggu kemudian. Keduanya berasal dari baris kode yang sama.

  • Jika sebuah pilihan mengubah keluaran dan tidak memiliki nilai standar yang aman, NextPDF menjadikannya argumen wajib, bukan argumen yang disimpulkan.
  • Argumen opsional yang terbaca ambigu diberi nama, sehingga lokasi pemanggilan menyatakan maksud (newLine: true, bukan sekadar true telanjang).
  • Input yang berpotensi tidak aman divalidasi sebelum rendering, dan ditolak dengan exception bertipe yang menyebutkan penyebabnya.
  • Sebuah instance dokumen bersifat sekali pakai: dibangun, dikeluarkan, lalu dibuang. Tidak ada reset(), jadi tidak ada tebakan “apakah objek ini dipakai ulang?” .
  • Mesin tidak pernah mengeluarkan artefak yang terlihat masuk akal sebagai pengganti artefak yang Anda minta. Sebaliknya, ia menolak.

Mekanismenya sederhana, dan justru di situlah intinya: sistem tipe, argumen bernama, enum alih-alih magic string, serta sejumlah kecil guard clause yang sengaja ditempatkan sebelum keluaran.

Tabel ini membandingkan beberapa input ambigu. Untuk masing-masing, tabel ini menunjukkan apa yang akan disimpulkan oleh pustaka yang “membantu”, dan apa yang dilakukan NextPDF sebagai gantinya. Setiap kolom NextPDF berisi perilaku yang dikutip dari kode sumber yang ditampilkan di bagian akhir halaman ini.

Input yang ambiguApa yang dilakukan pustaka yang menebakApa yang dilakukan NextPDF
String orientasi seperti "portait"Beralih ke nilai standar dan tetap merenderaddPage() menerima enum Orientation, bukan string — salah ketik menjadi type error, bukan nilai standar yang diam-diam diterapkan
true tanpa nama di posisi akhir pada cell()Memilih posisi boolean mana pun yang diasumsikannya Anda maksudBoolean diberi nama di lokasi pemanggilan (newLine: true); literal tanpa nama adalah code smell yang dihilangkan oleh API ini
Wrapper php:// atau path traversal ke save()”Berusaha sebaik mungkin” lalu menulis ke suatu tempatDitolak sebelum PDF dibangun, dengan InvalidConfigException bertipe yang menyebutkan kunci, nilai, dan tipe yang diharapkan
setSignature() lalu save() sementara penanda tangan tingkat tinggi belum terhubungMengeluarkan berkas tidak bertanda tangan yang dianggap pemanggil sudah ditandatanganiMelempar NotImplementedException sebelum menghasilkan byte, menyebutkan rute yang didukung
Memakai ulang instance Document untuk rendering keduaMenebak apakah keadaan tersisa masih berlakuTidak ada reset() dan tidak ada jalur pemakaian ulang — instance baru per permintaan melalui DocumentFactory, sehingga tidak ada keadaan tersisa yang perlu ditebak

Maksud adalah argumen wajib. Kontrak inti, PdfDocumentInterface, menerima geometri dan perataan sebagai value object dan enum bertipe, bukan primitif lepas:

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 dan Alignment adalah enum, sehingga pemanggilan tidak dapat melewatkan "portait" lalu diam-diam mengartikannya sebagai “standar”. Di tempat nilai standar memang ada, nilainya aman (potret, kiri, tanpa bingkai), bukan tebakan tentang apa yang mungkin Anda inginkan.

Boolean yang ambigu diberi nama di lokasi pemanggilan. Di seluruh contoh yang berfungsi sebagai referensi API secara de facto, bentuk yang sama berulang:

$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 tidak mungkin disalahartikan. true tanpa nama di posisi akhir tidak demikian. Level tanda tangannya adalah SignatureLevel::PAdES_B_B, sebuah case enum — bukan string yang harus diinterpretasi oleh mesin. Tujuan keluarannya adalah OutputDestination::String, sehingga “berikan byte-nya, tanpa header HTTP, tanpa berkas” dinyatakan secara eksplisit. Hal itu tidak disimpulkan dari ada atau tidaknya nama berkas yang dilewatkan.

Input yang tidak aman ditolak sebelum satu byte pun ditulis. save() memvalidasi path tujuan sebelum membangun 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
}

Mesin tidak “berusaha sebaik mungkin” terhadap wrapper php:// atau path traversal. Ia menolak, dan exception tersebut menyebutkan kunci, nilai, serta apa yang diharapkan.

Mesin menolak alih-alih mengeluarkan artefak yang menyesatkan. Bentuk terkuat dari menolak menebak adalah menolak menghasilkan keluaran sama sekali ketika keluaran itu tidak jujur. Ketika tanda tangan tingkat tinggi dikonfigurasi tetapi writer seam yang sebenarnya akan menandatangani belum terhubung, jalur build melempar exception sebelum menghasilkan byte, alih-alih mengeluarkan berkas tidak bertanda tangan yang dianggap pemanggil sudah ditandatangani:

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 tidak bertanda tangan yang tampak sudah ditandatangani adalah jenis artefak keliru namun tampak masuk akal yang justru ingin dicegah oleh prinsip ini. Sikap yang sama muncul di jalur CSS ketat. Penyimpangan spec yang tidak terdaftar melempar StrictModeViolation pada titik deteksi, alih-alih merender hasil pendekatan dan membiarkan penyimpangan tidak terdeteksi.

Sekali pakai menghilangkan seluruh kelas tebakan. Sebuah Document bersifat sekali pakai — dibangun, dikeluarkan, lalu dibuang. Tidak ada reset() dan tidak ada jalur pemakaian ulang. Worker yang berjalan lama membuat instance baru per permintaan melalui DocumentFactory. Mesin tidak pernah harus menebak apakah keadaan tersisa dari dokumen sebelumnya masih bermakna, karena secara rancangan keadaan itu memang tidak ada.

Halaman ini Evidence: Code-backed : setiap bentuk di atas dikutip dari kode sumber mesin itu sendiri dan contoh-contohnya, bukan diparafrasekan dari niat desain.

  • Tanda tangan metode yang bertipe dan membawa enum merupakan kontrak publik dalam PdfDocumentInterface. Gaya pemanggilan dengan argumen bernama adalah bentuk yang konsisten di seluruh contoh kanonis yang berfungsi sebagai referensi API secara de facto.
  • Validasi path pra-render, dengan InvalidConfigException bertipe, dan penjaga tolak-sebelum-keluarkan NotImplementedException dikutip kata demi kata dari jalur keluaran façade dokumen.
  • Acuan standarnya adalah Spec: ISO/IEC 25010, §3.32 — perlindungan kesalahan pengguna, properti kualitas yang ingin dipenuhi oleh API yang menolak menebak di lokasi pemanggilan. Acuan kedua adalah Spec: ISO 32000-2, §12.8 , yang menjelaskan mengapa menebak-nebak di sekitar dokumen yang ditandatangani tidak pernah aman. Digest mencakup rentang byte yang dideklarasikan yang mengecualikan nilai tanda tangan, sehingga setiap penulisan ulang yang senyap membuatnya tidak valid.

Berikut sebuah program kecil yang lengkap. Setiap baris yang berpotensi ambigu menyatakan maksudnya. Satu-satunya input yang tidak aman ditolak sebelum pekerjaan apa pun dimulai.

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

Tidak ada jalur yang membuat program ini diam-diam melakukan hal yang salah. Ia menyatakan maksud lalu melanjutkan, atau ia menyebutkan masalahnya lalu berhenti.

Keberatan yang sering muncul adalah “ini cuma membuat kode bertele-tele”. Ini bukan verbositas. Ini adalah penghilangan nilai standar yang tersembunyi. true tanpa nama lebih pendek daripada newLine: true persis sebesar kejelasan yang dihilangkannya. Mesin menukar beberapa karakter di lokasi pemanggilan demi menghilangkan satu kategori bug — yakni bug ketika kode berhasil dikompilasi, berjalan, menghasilkan berkas, namun salah.

Kesalahpahaman terkait lainnya adalah bahwa gagal cepat berarti “sering melempar exception”. Dalam penggunaan normal, NextPDF tidak melempar apa pun. Input yang valid diproses begitu saja. Penjaga hanya aktif pada input yang benar-benar ambigu atau tidak aman — justru input yang ingin segera Anda ketahui, bukan input yang ingin Anda biarkan ditebak.

Menolak menebak berlaku untuk maksud dan keamanan, bukan untuk setiap bentuk kenyamanan. NextPDF tetap memiliki nilai standar yang aman: orientasi potret, perataan kiri, tanpa bingkai. Prinsipnya adalah nilai standar hanya ditawarkan di tempat yang aman dan tidak mengejutkan, dan tidak pernah di tempat ketika kesimpulan yang keliru menghasilkan dokumen yang salah.

Halaman ini mendemonstrasikan prinsip tersebut pada permukaan API publik inti (façade dokumen, kontraknya, dan jalur keluaran). Subsistem memiliki titik masuknya sendiri, dan masing-masing mendokumentasikan perilaku validasinya sendiri. Bentuk-bentuk yang dikutip di sini berlaku pada saat tinjauan ini. Bentuk-bentuk itu mengilustrasikan polanya; bukan katalog lengkap setiap penjaga dalam mesin.

Penjaga gagal cepat yang dijelaskan adalah penjaga kebenaran dan keamanan. Mereka bukan batas keamanan tersendiri. Validasi input adalah satu lapisan. Filosofi desain dan dokumentasi keamanan menjabarkan sikap yang lebih luas.

  • Filosofi desain NextPDF — prinsip yang didemonstrasikan halaman ini, dalam konteks prioritasnya.
  • Eror sebagai fitur — apa yang dirancang untuk diberitahukan oleh exception bertipe yang dilempar oleh penjaga ini.
  • Tipe ketat, di mana-mana — bagaimana sistem tipe menjadikan “nyatakan maksud Anda” sebagai keharusan, bukan sekadar anjuran.
  • Code-backed (level bukti) — halaman yang klaimnya diverifikasi terhadap kode sumber mesin itu sendiri atau contoh yang dapat dijalankan, dikutip alih-alih diparafrasekan.
  • Fail fast (gagal cepat) — menolak input yang tidak valid pada titik paling awal, dengan penyebab yang jelas, alih-alih melanjutkan lalu gagal secara samar di kemudian hari.
  • Named argument (argumen bernama) — sintaks PHP di lokasi pemanggilan (newLine: true) yang mengikat nilai ke sebuah parameter berdasarkan nama, sehingga literal yang sebaliknya ambigu menjadi menjelaskan maksudnya sendiri.
  • Siklus hidup sekali pakai — kontrak Document yang sekali pakai: instansiasi, tulis, simpan, buang. Tanpa reset(), tanpa pemakaian ulang. Worker membuat instance baru per permintaan melalui DocumentFactory.
  • PAdES — PDF Advanced Electronic Signatures, keluarga profil ETSI untuk penandatanganan PDF. Dijabarkan saat pertama kali digunakan; dibahas secara mendalam di halaman penandatanganan.