Przejdź do głównej zawartości

Błędy jako funkcja

Spec: ISO 9241-110, §5.6.4 Evidence: Code-backed

NextPDF traktuje hierarchię wyjątków jako powierzchnię API zaprojektowaną z taką samą starannością jak metody, które te wyjątki zgłaszają. Awaria jest konkretna, typowana, możliwa do przechwycenia na wymaganym poziomie szczegółowości i niesie ustrukturyzowany kontekst dla logów.

Ta strona pokazuje tę powierzchnię bezpośrednio w kodzie źródłowym silnika: typ bazowy, typowane podklasy, nazwane konstruktory, które wiążą pierwotną przyczynę z komunikatem, oraz ustrukturyzowany kontekst udostępniany przez każdy wyjątek NextPDF.

Komunikat o błędzie to głos silnika w najgorszym możliwym momencie: produkcja, 2 a.m. i dokument, który powinien był już zostać wysłany. To, co wtedy mówi komunikat, decyduje, czy kolejnym krokiem będzie naprawa, czy długie dochodzenie.

Ogólny RuntimeException: something went wrong nie daje żadnego punktu zaczepienia. Informuje, że silnik zawiódł, ale nie wskazuje, co zawiodło, gdzie ani tym bardziej co zrobić. Wytyczne z zakresu czynnika ludzkiego mówią o tym wprost. Błąd powinien wyjaśniać sytuację na tyle dobrze, aby naprawa była oczywistym kolejnym krokiem, a nie projektem badawczym ( Spec: ISO 9241-110, §5.6.4.3 ). Wyjątek, który nazywa przyczynę i wskazuje sposób naprawy, nie jest udogodnieniem. To różnica między pięciominutową a pięciogodzinną naprawą.

  • Każda awaria NextPDF rozszerza jedną abstrakcyjną klasę bazową, NextPdfException, więc wszystkie błędy biblioteki można przechwycić jednym typem.
  • Poniżej znajdują się konkretne, typowane podklasy — czcionka, której nie można odnaleźć, nieprawidłowa konfiguracja, nieudana operacja podpisu — dzięki czemu można przechwycić dokładnie tę awarię, którą da się obsłużyć.
  • Każdy wyjątek NextPDF implementuje ContextAwareExceptionInterface i udostępnia getContext(): ustrukturyzowaną, bezpieczną do logowania mapę, więc nigdy nie trzeba parsować łańcucha komunikatu, aby odzyskać dane diagnostyczne.
  • Komunikaty są gotowe do działania: nazwane konstruktory wiążą z komunikatem rzeczywistą pierwotną przyczynę (a często i sposób naprawy), zamiast ogólnego szablonu.
  • Każda klasa wyjątku dokumentuje, kto może na nią zareagować — programista, infrastruktura lub wywołujący bibliotekę — więc triage zaczyna się, zanim trzeba sięgnąć do śladu stosu.

Hierarchia jest płytka i przemyślana. Składa się z jednej klasy bazowej, warstwy typów specyficznych dla domeny oraz kontraktu, który spełnia każdy z nich.

Jedna klasa bazowa, przechwytująca wszystko z założenia. NextPdfException jest abstrakcyjna, rozszerza RuntimeException i implementuje ContextAwareExceptionInterface:

abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface
{
/** @return array<string, mixed> */
public function getContext(): array
{
return [];
}
}

Abstrakcyjność jest świadomą decyzją. Ponieważ klasa bazowa nie jest zgłaszana bezpośrednio, nie da się przypadkiem obsłużyć niejasnego wyjątku bazowego. Przechwytuje się ją świadomie, jako zabezpieczenie, a konkretną podklasę wtedy, gdy można zrobić coś konkretnego.

Konkretne, typowane podklasy. Brak czcionki nie jest ogólnym błędem; to FontNotFoundException i zawiera dane potrzebne do reakcji:

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()
}

Komunikat nazywa czcionkę i dokładne ścieżki, które przeszukano. Nie trzeba zgadywać, którego katalogu brakowało; wyjątek to wskazuje.

Ustrukturyzowany kontekst, a nie wydobywanie danych z łańcuchów. Każdy wyjątek zwraca mapę w stylu snake_case zawierającą wyłącznie typy proste, którą można bezpiecznie zserializować bezpośrednio do logu lub ładunku APM:

public function getContext(): array
{
return [
'config_key' => $this->configKey,
'given_value' => $this->givenValue,
'expected_type' => $this->expectedType,
];
}

Kontrakt jasno wyjaśnia, dlaczego. Warstwa logowania może wywołać $logger->error($e->getMessage(), $e->getContext()) dla dowolnego wyjątku NextPDF, nigdy nie parsując komunikatu. Komunikat jest dla ludzi. Kontekst jest dla maszyn. Żaden z nich nie musi pełnić funkcji drugiego.

Komunikaty gotowe do działania dzięki nazwanym konstruktorom. Tutaj błędy przestają być przypadkowe i stają się zaprojektowane. SignatureException nie poprzestaje na komunikacie „podpisywanie nie powiodło się na poziomie B-LT”. Oferuje nazwane konstruktory, które wiążą z komunikatem rzeczywistą pierwotną przyczynę, a często też dokładny sposób naprawy:

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

Komunikat podaje, co jest nie tak, i co z tym zrobić. Istnieją pokrewne konstruktory dla brakującego pakietu funkcjonalności, brakującego klienta HTTP, błędnie wybranego algorytmu obliczającego wyłącznie skrót, typu klucza, który nie pasuje do algorytmu, i innych. Każdy z nich zamienia klasę awarii w zdanie, które daje programiście podstawę do działania bez czytania kodu źródłowego silnika.

Celowo głośne awarie. Niektóre wyjątki istnieją właśnie po to, aby cicha luka ujawniła się głośno. NotImplementedException niesie czytelną dla maszyn etykietę feature oraz odwołanie 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,
);
}
}

Ścieżka, do której kod dochodzi, ale której jeszcze nie podłączono, zgłasza ten wyjątek zamiast zwracać pozornie poprawną pustą operację. Ta sama idea stoi za StrictModeViolation, którego podklasy niosą krótką, łatwą do wyszukania etykietę odbiegającej konstrukcji oraz opcjonalny kontekst lokalizacji i cytowania. Odstępstwo od specyfikacji staje się typowanym, kontekstowym zatrzymaniem, a nie po cichu błędnym renderowaniem.

Metadane triage’u w samej klasie. Każda klasa wyjątku w swoim docblocku wskazuje, kto może na nią zareagować. Na przykład FontNotFoundException to „Programista (sprawdź ścieżkę czcionki) lub Infrastruktura (popraw uprawnienia do plików)”. InvalidConfigException to „Programista (popraw konfigurację przed wywołaniem NextPDF)”. NotImplementedException to „Wywołujący bibliotekę — albo usuń wywołanie, albo przypnij je do przyszłej wersji”. Triage zaczyna się przed śladem stosu, ponieważ pytanie „czy to sprawa programisty, czy infrastruktury?” ma już zapisaną odpowiedź.

Tabela podsumowuje projekt oraz to, co daje każda właściwość.

Właściwość projektowaW kodzie źródłowymCo to daje
Jedna abstrakcyjna klasa bazowaNextPdfException (abstrakcyjna, implementuje interfejs kontekstu)Przechwycenie każdego błędu biblioteki jednym typem, bez ryzyka przypadkowego złapania niejasnej klasy bazowej
Konkretne typowane podklasyFontNotFoundException, InvalidConfigException, SignatureException, …Przechwycenie dokładnie tej awarii, którą potrafisz obsłużyć
Ustrukturyzowany kontekstgetContext() — wyłącznie typy proste w stylu snake_caseZalogowanie lub wysłanie do APM bez parsowania łańcucha komunikatu
Komunikaty gotowe do działaniaNazwane konstruktory wiążą pierwotną przyczynę + sposób naprawyZdanie dające podstawę do działania, a nie szablon
Celowo głośneNotImplementedException, StrictModeViolationCicha luka staje się typowanym, łatwym do wyszukania zatrzymaniem
Metadane triage’u„Actionable by:” w docblocku każdej klasyWiadomo, czyj to problem, zanim trzeba przeczytać ślad stosu

Ta strona jest Evidence: Code-backed : każda klasa, sygnatura i forma komunikatu są cytowane z przestrzeni nazw wyjątków silnika, a nie parafrazowane.

  • Abstrakcyjna klasa bazowa i jej kontrakt ContextAwareExceptionInterface, typowane podklasy, kształt getContext() oraz nazwane konstruktory SignatureException są cytowane dosłownie z kodu źródłowego.
  • Wiersze triage’u „Actionable by:” to kontrakty w docblockach klas znajdujące się w tych samych plikach.
  • Punktem odniesienia w zakresie czynnika ludzkiego jest Spec: ISO 9241-110 — §5.6.4.3, dotyczący objaśnień błędów na tyle jasnych, by można je było naprawić, oraz zasada odporności na błędy użytkowania z §6. Silnik traktuje programistę jako użytkownika, a wyjątek jako interfejs, który musi spełnić te klauzule.

Przechwytuj szeroko jako zabezpieczenie, przechwytuj konkretnie tam, gdzie możesz działać, i przekazuj ustrukturyzowany kontekst bezpośrednio do swojego loggera — bez parsowania komunikatu.

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

Konkretny blok catch odzyskuje działanie, ponieważ typ wyjątku wskazał, że odzyskanie było możliwe. Blok zabezpieczający loguje ustrukturyzowany kontekst dla wszystkich pozostałych przypadków. W żadnym momencie aplikacja nie czyta komunikatu, aby dowiedzieć się, co się stało.

Częsty błąd w interpretacji polega na tym, że głębokie drzewo wyjątków to przeinżynierowanie, a jeden typ błędu byłby prostszy. Byłby prostszy dla silnika, ale gorszy dla użytkownika biblioteki. Jeden typ oznacza, że każda awaria to ogólny ślad stosu, a logika odzyskiwania sprowadza się do dopasowywania łańcucha. Takie dopasowanie jest kruche; kolejne przeredagowanie komunikatu je psuje. Mała, konkretna hierarchia przenosi tę wiedzę do systemu typów, gdzie mogą z niej korzystać kompilator i bloki catch.

Drugie nieporozumienie polega na tym, że komunikat i kontekst są nadmiarowe. Nie są. Komunikat to tekst dla człowieka czytającego wiersz logu. Kontekst to typowana mapa do trasowania w kodzie, alarmowania lub pulpitów nawigacyjnych. Mieszanie tych ról to dokładnie ta pułapka parsowania łańcuchów, którą kontrakt getContext() ma usuwać.

Hierarchia jest celowo płytka. NextPDF nie tworzy odrębnej klasy wyjątku dla każdej wyobrażalnej awarii. Tworzy ją tam, gdzie przechwycenie tej konkretnej awarii jest działaniem, którego można rozsądnie oczekiwać od wywołującego. Nadmierne rozdrabnianie zamieniłoby problem parsowania łańcuchów na problem rozrastającej się listy bloków catch.

getContext() jest zaprojektowany pod kątem logów i APM, więc na mocy kontraktu zwraca wyłącznie typy proste i listy typów prostych, bez zagnieżdżonych obiektów. To kontekst diagnostyczny, a nie zserializowana migawka wewnętrznych struktur silnika. Nie jest też stabilnym formatem transmisji, na podstawie którego można budować zewnętrzne schematy.

Ta strona opisuje powierzchnię projektową wyjątków. Dokładny zestaw wyjątków i ich pól ewoluuje wraz z silnikiem. Klasy i kształty cytowane tutaj są aktualne na dzień tego przeglądu i mają charakter poglądowy dla kontraktu, a nie zamrożonego katalogu. Kontrakt — jedna klasa bazowa, typowane podklasy, ustrukturyzowany kontekst, komunikaty gotowe do działania — jest częścią stabilną.

  • Poparte kodem (poziom dowodów) — strona, której twierdzenia są weryfikowane na podstawie własnego kodu źródłowego silnika, cytowane zamiast parafrazowane.
  • Wyjątek świadomy kontekstu — wyjątek NextPDF implementujący ContextAwareExceptionInterface i udostępniający getContext(). Ta metoda zwraca mapę w stylu snake_case z prostymi polami diagnostycznymi, którą można bezpiecznie zserializować do logu lub ładunku APM bez parsowania łańcucha komunikatu.
  • Nazwany konstruktor — statyczna metoda fabryczna (na przykład SignatureException::tsaUrlEmpty()), która buduje wyjątek z komunikatem powiązanym z konkretną pierwotną przyczyną, a często też ze sposobem jej naprawy.
  • PAdES — PDF Advanced Electronic Signatures, rodzina profili ETSI do podpisywania plików PDF. Skrót rozwinięty przy pierwszym użyciu; szczegółowo omówiony na stronach dotyczących podpisywania.
  • TSA — Time-Stamping Authority, zaufana usługa wydająca znaczniki czasu RFC 3161 używane przez wyższe profile PAdES.