Przejdź do głównej zawartości

API, które odmawia zgadywania

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

NextPDF wymaga jednoznacznego wskazania intencji. Tam, gdzie intencja zmienia bajty — poziom podpisu, miejsce docelowe wyjścia, cel zgodności — jest wymaganym, jawnym argumentem, a nie czymś, co silnik wnioskuje z kontekstu.

Ta strona pokazuje to podejście bezpośrednio w kodzie źródłowym silnika: sygnatury metod, argumenty nazwane oraz miejsca, w których niejednoznaczne dane wejściowe są odrzucane, zanim powstanie jakikolwiek bajt.

Zgadnięcie oznacza decyzję podjętą w imieniu wywołującego, ale bez uprzedzenia. W przypadku pola tekstowego jest to co najwyżej irytujące. W przypadku pliku PDF jest to ukryta wada, ponieważ dostarczany plik bywa artefaktem prawnym lub archiwalnym, którego poprawność ktoś inny sprawdza później za pomocą walidatora.

Rozważmy podpis. Jego skrót jest obliczany na podstawie zadeklarowanego zakresu bajtów, który celowo pomija samą wartość podpisu ( Spec: ISO 32000-2, §12.8 ). API, które po cichu „pomaga” — przepisując strukturę, wnioskując poziom, uzupełniając obszar zastępczy — w rzeczywistości nie pomaga. Zmienia bajty, które podpis miał chronić. Zgadnięcie, które w miejscu wywołania wygląda przyjaźnie, kilka tygodni później staje się incydentem produkcyjnym. To ta sama linia kodu.

  • Jeśli wybór zmienia wynik i nie ma bezpiecznej wartości domyślnej, NextPDF czyni go wymaganym argumentem, a nie wnioskowanym.
  • Argumenty opcjonalne, które można odczytać niejednoznacznie, są nazwane, więc miejsce wywołania określa intencję (newLine: true, a nie samo true).
  • Dane wejściowe, które mogą być niebezpieczne, są walidowane przed renderowaniem i odrzucane wraz z typowanym wyjątkiem, który nazywa przyczynę.
  • Instancja dokumentu jest jednorazowa: zostaje zbudowana, wyemitowana i odrzucona. Nie ma metody reset(), więc nie ma też zgadywania „czy ten obiekt jest używany ponownie?”.
  • Silnik nigdy nie emituje wiarygodnie wyglądającego artefaktu zamiast tego, o który poprosiłeś. Zamiast tego odmawia.

Mechanizm jest prosty i o to właśnie chodzi. To system typów, argumenty nazwane, typy wyliczeniowe zamiast magicznych łańcuchów znaków oraz niewielka liczba celowych klauzul strażniczych umieszczonych przed emisją danych wyjściowych.

Tabela zestawia kilka niejednoznacznych danych wejściowych. Dla każdego przypadku pokazuje, co wywnioskowałaby biblioteka, która „pomaga”, oraz co zamiast tego robi NextPDF. Każda kolumna NextPDF opisuje zachowanie zacytowane z kodu źródłowego pokazanego w dalszej części tej strony.

Niejednoznaczne dane wejścioweCo robi biblioteka, która zgadujeCo robi NextPDF
Łańcuch znaków orientacji taki jak "portait"Cofa się do wartości domyślnej i renderuje mimo toaddPage() przyjmuje typ wyliczeniowy Orientation, a nie łańcuch znaków — literówka jest błędem typu, a nie cichą wartością domyślną
Samo, końcowe true przekazane do cell()Wybiera tę pozycję wartości logicznej, którą uznaje za zamierzonąWartość logiczna jest nazwana w miejscu wywołania (newLine: true); nienazwany literał to zapach kodu, który API eliminuje
Wrapper php:// lub ścieżka z przejściem po katalogach przekazana do save()Próbuje mimo wszystko i zapisuje w jakimś miejscuOdrzucone przed zbudowaniem pliku PDF, wraz z typowanym InvalidConfigException nazywającym klucz, wartość i oczekiwany typ
setSignature(), a następnie save(), podczas gdy wysokopoziomowy podpisywacz nie jest podłączonyEmituje niepodpisany plik, który wywołujący uważa za podpisanyZgłasza NotImplementedException przed wyprodukowaniem bajtów, nazywając obsługiwaną ścieżkę
Ponowne użycie instancji Document do drugiego renderowaniaPróbuje odgadnąć, czy stan szczątkowy nadal obowiązujeBrak reset() i brak ścieżki ponownego użycia — nowa instancja na każde żądanie poprzez DocumentFactory, więc nie ma stanu szczątkowego, który trzeba by zgadywać

Intencja jest wymaganym argumentem. Główny kontrakt, PdfDocumentInterface, przyjmuje geometrię i wyrównanie jako typowane obiekty wartości oraz typy wyliczeniowe, a nie luźno typowane wartości proste:

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 i Alignment to typy wyliczeniowe, więc w wywołaniu nie można przekazać "portait" i sprawić, że po cichu będzie to oznaczać „wartość domyślną”. Tam, gdzie istnieje wartość domyślna, jest ona bezpieczna (orientacja pionowa, wyrównanie do lewej, brak obramowania), a nie zgadnięciem prawdopodobnej intencji.

Niejednoznaczne wartości logiczne są nazywane w miejscu wywołania. W przykładach, które de facto pełnią rolę dokumentacji referencyjnej API, powtarza się ten sam kształt:

$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 jest jednoznaczne. Samo, końcowe true już nie. Poziom podpisu to SignatureLevel::PAdES_B_B, przypadek typu wyliczeniowego — nigdy łańcuch znaków, który silnik musiałby interpretować. Miejsce docelowe wyjścia to OutputDestination::String, więc „daj mi bajty, bez nagłówków HTTP, bez pliku” jest wyrażone wprost. Nie wynika z tego, czy przekazano nazwę pliku.

Niebezpieczne dane wejściowe są odrzucane przed zapisaniem jakiegokolwiek bajtu. save() waliduje ścieżkę docelową przed zbudowaniem pliku 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
}

Silnik nie „stara się jak może” w przypadku wrappera php:// ani ścieżki z przejściem po katalogach. Odmawia, a wyjątek nazywa klucz, wartość oraz to, czego oczekiwano.

Silnik odmawia, zamiast emitować mylący artefakt. Najsilniejszą formą odmowy zgadywania jest całkowita rezygnacja z wytworzenia wyniku, gdy ten wynik byłby nieprawdziwy. Gdy skonfigurowano podpis wysokopoziomowy, ale punkt integracji zapisu, który faktycznie by go złożył, nie jest podłączony, ścieżka budowania zgłasza wyjątek przed wyprodukowaniem bajtów, zamiast emitować niepodpisany plik, który wywołujący uważa za podpisany:

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

Niepodpisany plik PDF, który wygląda na podpisany, to dokładnie ten rodzaj wiarygodnie wyglądającego, błędnego artefaktu, któremu ta zasada ma zapobiegać. To samo podejście pojawia się w ścieżce obsługi ścisłego CSS. Niezarejestrowane odstępstwo od specyfikacji zgłasza StrictModeViolation już w momencie wykrycia, zamiast renderować przybliżenie i pozostawiać odstępstwo niewykryte.

Jednorazowość usuwa całą klasę zgadnięć. Document jest jednorazowy — zbudowany, wyemitowany i odrzucony. Nie ma reset() ani ścieżki ponownego użycia. Długo działający worker tworzy nową instancję na każde żądanie poprzez DocumentFactory. Silnik nigdy nie musi zgadywać, czy stan szczątkowy z poprzedniego dokumentu nadal ma znaczenie, ponieważ taki stan z założenia nie istnieje.

Ta strona ma status Evidence: Code-backed : każdy opisany wyżej wzorzec jest zacytowany z samego kodu źródłowego silnika i jego przykładów, a nie sparafrazowany z intencji.

  • Typowane sygnatury zawierające typy wyliczeniowe stanowią publiczny kontrakt w PdfDocumentInterface. Styl wywołania z argumentami nazwanymi to spójna forma we wszystkich kanonicznych przykładach, które de facto pełnią rolę dokumentacji referencyjnej API.
  • Walidacja ścieżki przed renderowaniem, wraz z typowanym InvalidConfigException, oraz strażnik NotImplementedException odmawiający przed emisją są cytowane dosłownie ze ścieżki wyjścia fasady dokumentu.
  • Kotwicą w standardach jest Spec: ISO/IEC 25010, §3.32 — ochrona przed błędem użytkownika, właściwość jakościowa, którą API odmawiające zgadywania ma spełniać w miejscu wywołania. Drugą kotwicą jest Spec: ISO 32000-2, §12.8 , dlatego zgadywanie wokół podpisanego dokumentu nigdy nie jest nieszkodliwe. Skrót obejmuje zadeklarowany zakres bajtów, który pomija wartość podpisu, więc każdy cichy ponowny zapis go unieważnia.

Poniżej znajduje się niewielki, kompletny program. Każda linia, która mogłaby być niejednoznaczna, wskazuje swoją intencję. Niebezpieczne dane wejściowe są odrzucane, zanim wykonana zostanie jakakolwiek praca.

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

Nie ma ścieżki, w której ten program po cichu robiłby coś niewłaściwego. Określa intencję i kontynuuje albo nazywa problem i zatrzymuje się.

Częstym zarzutem jest „to tylko rozwlekłość”. To nie jest rozwlekłość. To brak ukrytych wartości domyślnych. Samo true jest krótsze od newLine: true dokładnie o tyle przejrzystości, ile usuwa. Silnik wymienia kilka znaków w miejscu wywołania na wyeliminowanie całej kategorii błędów — tej, w której kod się kompiluje, działa, tworzy plik i mimo to jest błędny.

Pokrewnym nieporozumieniem jest przekonanie, że fail-fast oznacza „zgłasza dużo wyjątków”. W normalnym użyciu NextPDF nie zgłasza niczego. Prawidłowe dane wejściowe przechodzą bez przeszkód. Strażniki uruchamiają się tylko dla danych wejściowych, które są naprawdę niejednoznaczne lub niebezpieczne — dokładnie tych, o których chcesz wiedzieć natychmiast, a nie tych, które miałyby zostać odgadnięte.

Odmowa zgadywania dotyczy intencji i bezpieczeństwa, a nie każdej wygody. NextPDF nadal ma bezpieczne wartości domyślne: orientację pionową, wyrównanie do lewej, brak obramowania. Zasada jest taka, że wartość domyślną oferuje się tylko tam, gdzie jest bezpieczna i nie zaskakuje, i nigdy tam, gdzie błędne wnioskowanie tworzy błędny dokument.

Ta strona demonstruje tę zasadę na podstawowej, publicznej powierzchni API (fasadzie dokumentu, jej kontrakcie oraz ścieżce wyjścia). Podsystemy mają własne punkty wejścia i każdy dokumentuje własne zachowanie walidacji. Wzorce zacytowane tutaj są aktualne na dzień tego przeglądu. Ilustrują podejście; nie są wyczerpującym katalogiem każdego strażnika w silniku.

Opisane mechanizmy fail-fast to strażnicy poprawności i bezpieczeństwa. Same w sobie nie stanowią granicy bezpieczeństwa. Walidacja danych wejściowych to jedna warstwa. Filozofia projektowania oraz dokumentacja bezpieczeństwa opisują szersze podejście.

  • Poparte kodem (poziom dowodów) — strona, której twierdzenia są weryfikowane względem samego kodu źródłowego silnika lub uruchamialnego przykładu, na podstawie cytatów, a nie parafraz.
  • Fail fast — odrzucanie nieprawidłowych danych wejściowych w najwcześniejszym możliwym punkcie, z jasną przyczyną, zamiast kontynuowania i niejasnego niepowodzenia później.
  • Argument nazwany — składnia miejsca wywołania w PHP (newLine: true), która wiąże wartość z parametrem po nazwie, sprawiając, że w przeciwnym razie niejednoznaczny literał staje się samoopisowy.
  • Cykl życia jednorazowego użycia — jednorazowy kontrakt Document: utwórz instancję, wyemituj, zapisz do pliku, odrzuć. Brak reset(), brak ponownego użycia. Workery tworzą nową instancję na każde żądanie poprzez DocumentFactory.
  • PAdES — PDF Advanced Electronic Signatures, rodzina profili ETSI do podpisywania plików PDF. Rozwijany przy pierwszym użyciu; szczegółowo omówiony na stronach dotyczących podpisywania.