コンテンツにスキップ

推測を拒否する API

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

NextPDF は、意図を明言させます。意図がバイト列を変える箇所では——署名レベル、出力先、適合対象——それは必須の明示的な引数であり、エンジンが文脈から推測するものではありません。

このページでは、その姿勢をエンジン自身のソースで示します。メソッドシグネチャ、名前付き引数、そして曖昧な入力がバイトを一切生成する前に拒否される地点を見ていきます。

推測とは、あなたに告げず、あなたの代わりに下される決定です。テキストフィールドなら、少し煩わしい程度で済むかもしれません。PDF では潜在的な欠陥になります。出荷するものは法的または保管用の成果物であることが多く、その正しさは後になって、別の誰かがバリデーターで検査するからです。

署名を考えてみましょう。そのダイジェストは、署名値そのものを意図的に除外する、宣言済みのバイト範囲にわたって計算されます( Spec: ISO 32000-2, §12.8 )。静かに「手助け」する API——構造を書き換える、レベルを推測する、プレースホルダーを詰める——は、手助けしていません。それは、署名が保護するはずだったバイト列を変えてしまっています。呼び出し箇所では親切に見える推測が、数週間後には本番障害になります。それは同じ一行のコードです。

  • ある選択が出力を変え、かつ安全なデフォルトがない場合、NextPDF はそれを推測任せの引数ではなく 必須引数 にします。
  • 曖昧に読める任意引数は 名前付き にし、呼び出し箇所で意図を宣言させます(newLine: true であって、裸の true ではありません)。
  • 安全でない可能性のある入力は レンダリング前に検証 され、原因を示す型付き例外とともに拒否されます。
  • ドキュメントのインスタンスは 使い切り です。構築され、出力され、破棄されます。reset() がないため、「これは再利用されているのか?」という推測もありません。
  • エンジンは、あなたが求めたものの代わりに、もっともらしく見える成果物を生成することは決してありません。代わりに拒否します。

仕組みは地味ですが、それこそが要点です。型システム、名前付き引数、マジック文字列の代わりに使う enum、そして出力の に置かれた少数の意図的なガード節から成り立っています。

この表では、いくつかの曖昧な入力を対比します。それぞれについて、「手助け」するライブラリが何を推測するか、NextPDF が代わりに何を行うかを示します。すべての NextPDF 列は、このページで後述するソースに基づく挙動です。

曖昧な入力推測するライブラリの挙動NextPDF の挙動
"portait" のような向きの文字列デフォルトにフォールバックし、それでもレンダリングaddPage() は文字列ではなく Orientation enum を取る——タイプミスは静かなデフォルトではなく型エラー
cell() への裸の末尾 trueあなたが意図したと想定する真偽値の位置を選ぶ真偽値は呼び出し箇所で名前付けされる(newLine: true)。名前のないリテラルこそ、この API が取り除くコードスメル
save() への php:// ラッパーまたはトラバーサルパス「最善を尽くそうとし」、どこかに書き込むPDF が構築される に拒否され、キー・値・期待される型を示す型付き InvalidConfigException
高レベルの署名処理が未配線のまま setSignature() の後に save()呼び出し元が署名済みだと信じる未署名ファイルを生成するバイトを生成する前に NotImplementedException をスローし、サポートされる経路を示す
Document インスタンスを 2 回目のレンダリングに再利用する残存する状態がまだ適用されるかどうかを推測する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;

OrientationAlignment は 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 ヘッダーもファイルも不要」という意図が、ファイル名が渡されたかどうかから推定されるのではなく、明示されます。

安全でない入力は 1 バイトも書き込まれる前に拒否される。 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 です——ユーザーエラー保護であり、推測を拒否する API が呼び出し箇所で満たすために存在する品質特性です。2 つ目のアンカーは Spec: 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);
}

このプログラムには、静かに誤ったことを行う経路がありません。意図を宣言して進むか、問題に名前を付けて停止するか、そのどちらかです。

よくある反論は「これは単なる冗長さだ」というものです。これは冗長さではありません。隠れたデフォルトがない、ということです。裸の truenewLine: true より短い一方で、その分だけ明快さを失わせます。エンジンは呼び出し箇所の数文字と引き換えに、あるバグのカテゴリ——コードがコンパイルされ、実行され、ファイルを生成し、しかも誤っている——を排除します。

関連する誤解は、フェイルファストとは「やたらとスローすること」だというものです。通常の使用では、NextPDF は何もスローしません。有効な入力はそのまま通ります。ガードが発動するのは、本当に曖昧または安全でない入力に対してだけです——まさに、推測されたくはなく、ただちに知りたい入力です。

推測の拒否が適用されるのは 意図と安全 であり、あらゆる利便性ではありません。NextPDF には依然として安全なデフォルトがあります。縦向き、左揃え、枠なしです。原則は、デフォルトが提供されるのは安全で意外性のない箇所だけであり、誤った推定が誤ったドキュメントを生む箇所では決して提供されない、というものです。

このページは、中核となる公開 API 面(ドキュメントファサード、その契約、そして出力経路)でこの原則を実証します。サブシステムにはそれぞれ独自のエントリポイントがあり、自身の検証挙動を文書化しています。ここで引用した形は、このレビュー時点での最新のものです。これらはパターンを示すものであり、エンジン内のすべてのガードを網羅した一覧ではありません。

記載したフェイルファストのガードは、正しさと安全のためのガードです。それら単独でセキュリティ境界となるわけではありません。入力検証は 1 つの層です。デザイン哲学 とセキュリティ文書が、より広い姿勢を説明しています。

  • コード裏付け(エビデンスレベル) ——主張がエンジン自身のソースまたは実行可能な例に照らして確認され、意図の言い換えではなく引用に基づいているページ。
  • フェイルファスト ——無効な入力を、後で不明瞭に失敗させるのではなく、明確な原因とともに最も早い時点で拒否すること。
  • 名前付き引数 ——値を名前でパラメータに束縛し、それだけでは曖昧なリテラルを自己説明的にする PHP の呼び出し箇所構文(newLine: true)。
  • 使い切りライフサイクル ——使い捨ての Document 契約。インスタンス化、書き込み、保存、破棄。reset() なし、再利用なし。ワーカーは DocumentFactory を通じてリクエストごとに新しいインスタンスを作成します。
  • PAdES ——PDF Advanced Electronic Signatures、PDF 署名向けの ETSI プロファイルファミリー。初出時に展開する用語で、署名関連のページで詳しく扱います。