Ошибки как часть API
Spec: ISO 9241-110, §5.6.4 ISO 9241-110 §5.6.4 Evidence: Code-backed
NextPDF рассматривает свою иерархию исключений как часть API, спроектированную с той же тщательностью, что и методы, которые эти исключения выбрасывают. Сбой конкретен, типизирован, перехватывается с нужной вам гранулярностью и несёт структурированный контекст для ваших журналов.
Эта страница показывает эту часть API на собственном исходном коде движка: базовый тип, типизированные подклассы, именованные конструкторы, которые привязывают первопричину к сообщению, и структурированный контекст, сопровождающий каждое исключение NextPDF.
Почему это важно
Заголовок раздела «Почему это важно»Сообщение об ошибке — это движок, который обращается к вам в самый неподходящий момент: на проде, в 2 a.m., когда документ уже должен был уйти. От того, что это сообщение говорит в такой момент, зависит следующий шаг: исправление или долгое расследование.
Общее RuntimeException: something went wrong не оставляет вам ни одной зацепки. Оно сообщает, что движок дал сбой, но не говорит, что именно сломалось, где и тем более что делать. Рекомендации по эргономике прямо указывают на это. Ошибка должна объяснять себя настолько хорошо, чтобы её исправление было очевидным следующим шагом, а не исследовательским проектом ( Spec: ISO 9241-110, §5.6.4.3 ISO 9241-110 §5.6.4.3 ). Исключение, которое называет причину и способ исправления, — не приятная мелочь. Это разница между исправлением за пять минут и за пять часов.
Если коротко
Заголовок раздела «Если коротко»- Каждый сбой NextPDF расширяет один абстрактный базовый класс —
NextPdfException, поэтому вы можете перехватить все ошибки библиотеки одним типом. - Под ним находятся конкретные типизированные подклассы — шрифт, который не удаётся найти, недопустимая конфигурация, неуспешная операция подписи, — поэтому вы можете перехватить именно тот сбой, с которым способны справиться.
- Каждое исключение NextPDF реализует
ContextAwareExceptionInterfaceи предоставляетgetContext(): структурированную, безопасную для журналов карту, поэтому вам никогда не нужно разбирать строку сообщения, чтобы восстановить диагностику. - Сообщения помогают действовать: именованные конструкторы привязывают к сообщению фактическую первопричину (а часто и исправление) вместо общего шаблона.
- Каждый класс исключения документирует, кто может с ним что-то сделать — разработчик, инфраструктура или вызывающий код библиотеки, — поэтому сортировка начинается ещё до того, как вы прочитаете трассу стека.
Как к этому подходит NextPDF
Заголовок раздела «Как к этому подходит NextPDF»Иерархия неглубокая и продуманная. Есть один базовый класс, слой предметно-специфичных типов и контракт, которому следует каждый из них.
Один базовый класс, намеренный перехватчик для всего. NextPdfException является абстрактным, расширяет RuntimeException и реализует ContextAwareExceptionInterface:
abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface{ /** @return array<string, mixed> */ public function getContext(): array { return []; }}Абстрактность здесь — осознанное решение. Вы никогда не перехватите расплывчатый базовый класс случайно, потому что он не выбрасывается напрямую. Вы перехватываете его намеренно, как страховку, а конкретный подкласс — когда можете сделать что-то конкретное.
Конкретные типизированные подклассы. Отсутствующий шрифт — не общая ошибка; это FontNotFoundException, и оно несёт данные, нужные вам для действий:
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()}Сообщение называет шрифт и точные пути, которые были проверены. Вы не гадаете, какого каталога не хватало: исключение сообщает это напрямую.
Структурированный контекст вместо разбора строк. Каждое исключение возвращает карту в snake_case, содержащую только примитивы; её безопасно сериализовать прямо в журнал или в полезную нагрузку APM:
public function getContext(): array{ return [ 'config_key' => $this->configKey, 'given_value' => $this->givenValue, 'expected_type' => $this->expectedType, ];}Контракт явно объясняет, зачем это нужно. Промежуточный слой логирования может вызвать $logger->error($e->getMessage(), $e->getContext()) для любого исключения NextPDF, ни разу не разбирая сообщение. Сообщение — для людей. Контекст — для машин. Ни одному из них не приходится выполнять чужую роль.
Сообщения, которые помогают действовать, через именованные конструкторы. Именно здесь ошибки перестают быть случайными и становятся спроектированными. SignatureException не только сообщает “signing failed at level B-LT”. Оно предлагает именованные конструкторы, которые привязывают к сообщению настоящую первопричину, а часто и точный способ исправления:
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');}Сообщение указывает, что не так, и что с этим делать. Есть родственные конструкторы для отсутствующего пакета возможностей, отсутствующего HTTP-клиента, ошибочно выбранного алгоритма “только дайджест”, типа ключа, не соответствующего алгоритму, и других случаев. Каждый из них превращает класс сбоев в фразу, по которой разработчик может действовать, не читая исходный код движка.
Сбои, намеренно громкие. Некоторые исключения существуют именно для того, чтобы тихий пробел стал шумным. NotImplementedException несёт пригодную для grep метку feature и ссылку 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, ); }}Достигнутый, но неподключённый путь выбрасывает это исключение вместо того, чтобы вернуть правдоподобную пустую операцию. Та же идея лежит в основе StrictModeViolation: его подклассы несут короткую пригодную для grep метку отклоняющейся конструкции, а также необязательный контекст местоположения и цитирования. Отклонение от спецификации становится типизированной контекстной остановкой, а не тихой неправильной отрисовкой.
Метаданные сортировки в самом классе. Каждый класс исключения указывает в своём докблоке, кто может с ним что-то сделать. Например, FontNotFoundException — это “Developer (verify font path) or Infrastructure (fix file permissions)”. InvalidConfigException — это “Developer (fix configuration before calling NextPDF)”. NotImplementedException — это “Library callers — either remove the call or pin to a future release”. Сортировка начинается ещё до трассы стека, потому что на вопрос “is this mine or operations?” уже есть записанный ответ.
В таблице обобщён дизайн и то, что вам даёт каждое свойство.
| Свойство дизайна | В исходном коде | Что это вам даёт |
|---|---|---|
| Один абстрактный базовый класс | NextPdfException (абстрактный, реализует интерфейс контекста) | Перехватить любую ошибку библиотеки одним типом и случайно не зацепить расплывчатый базовый класс |
| Конкретные типизированные подклассы | FontNotFoundException, InvalidConfigException, SignatureException, … | Перехватить именно тот сбой, с которым вы способны справиться |
| Структурированный контекст | getContext() — только примитивы в snake_case | Записать в журнал или отправить в APM, не разбирая строку сообщения |
| Сообщения, которые помогают действовать | Именованные конструкторы привязывают первопричину и способ исправления | Фраза, по которой можно действовать, а не шаблон |
| Намеренно громкие | NotImplementedException, StrictModeViolation | Тихий пробел становится типизированной остановкой, пригодной для grep |
| Метаданные сортировки | «Actionable by:» в докблоке каждого класса | Знать, чья это проблема, ещё до чтения трассы |
Что говорят свидетельства
Заголовок раздела «Что говорят свидетельства»Эта страница имеет статус Evidence: Code-backed : каждый класс, сигнатура и форма сообщения процитированы из пространства имён исключений движка, а не пересказаны своими словами.
- Абстрактный базовый класс и его контракт
ContextAwareExceptionInterface, типизированные подклассы, формаgetContext()и именованные конструкторыSignatureExceptionпроцитированы дословно из исходного кода. - Строки сортировки “Actionable by:” — это контракты докблоков классов в тех же файлах.
- Эргономическая опора — это Spec: ISO 9241-110 ISO 9241-110 : §5.6.4.3, об ошибках, которые объясняют себя достаточно, чтобы их можно было исправить, и принцип устойчивости к ошибкам использования из §6. Движок рассматривает разработчика как пользователя, а исключение — как интерфейс, который должен удовлетворять этим пунктам.
Практический пример
Заголовок раздела «Практический пример»Перехватывайте широко как страховку, перехватывайте конкретно там, где можете действовать, и передавайте структурированный контекст прямо в ваш логгер — без разбора сообщения.
<?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; }}Конкретный catch выполняет восстановление, потому что тип исключения сообщил, что восстановление возможно. Страховка логирует структурированный контекст для всего остального. Приложение ни разу не читает сообщение, чтобы понять, что произошло.
Распространённое заблуждение
Заголовок раздела «Распространённое заблуждение»Распространённое недопонимание состоит в том, что глубокое дерево исключений — это переусложнение и что один тип ошибки был бы проще. Это было бы проще для движка и хуже для вас. Один тип означает, что каждый сбой — это общая трасса стека, а логика восстановления — сопоставление строк. Такое сопоставление хрупко: следующая переформулировка сообщения его сломает. Небольшая конкретная иерархия переносит это знание в систему типов, где компилятор и ваши блоки catch могут его использовать.
Второе заблуждение — что сообщение и контекст дублируют друг друга. Это не так. Сообщение — это текст для человека, читающего строку журнала. Контекст — типизированная карта для маршрутизации в коде, оповещений или панелей мониторинга. Смешивать их — та самая ловушка разбора строк, ради устранения которой и существует контракт getContext().
Пределы и границы
Заголовок раздела «Пределы и границы»Иерархия намеренно неглубокая. NextPDF не создаёт отдельный класс исключения для каждого мыслимого сбоя. Он создаёт его там, где перехват именно этого сбоя — разумное действие для вызывающего кода. Чрезмерное дробление заменило бы проблему разбора строк проблемой разросшегося списка перехватов.
getContext() структурирован для журналов и APM, поэтому по контракту он возвращает только примитивы и списки примитивов, без вложенных объектов. Это диагностический контекст, а не сериализованный снимок внутренних структур движка. Это также не стабильный сетевой формат, под который стоит строить внешние схемы.
Эта страница описывает элемент дизайна исключений. Точный набор исключений и их поля развиваются вместе с движком. Процитированные здесь классы и формы актуальны на момент этого обзора и иллюстрируют контракт, а не фиксированный каталог. Контракт — один базовый класс, типизированные подклассы, структурированный контекст, сообщения, которые помогают действовать, — это стабильная часть.
Связанные документы
Заголовок раздела «Связанные документы»- API, который отказывается гадать — отказоустойчивые проверки, которые первыми выбрасывают эти исключения.
- Философия дизайна NextPDF — почему “ошибки — это часть API” является первоклассным принципом.
- Модель конвейера — где эти сбои проявляются по мере прохождения документа через движок и как они наблюдаются.
Глоссарий
Заголовок раздела «Глоссарий»- Code-backed (уровень свидетельств) — страница, утверждения которой проверены по собственному исходному коду движка и процитированы, а не пересказаны.
- Контекстно-зависимое исключение — исключение NextPDF, реализующее
ContextAwareExceptionInterfaceи предоставляющееgetContext(). Этот метод возвращает карту в snake_case из примитивных диагностических полей, которую безопасно сериализовать в журнал или в полезную нагрузку APM без разбора строки сообщения. - Именованный конструктор — статический фабричный метод (например,
SignatureException::tsaUrlEmpty()), который создаёт исключение с сообщением, привязанным к конкретной первопричине и, часто, к способу её исправления. - PAdES — PDF Advanced Electronic Signatures, семейство профилей ETSI для подписания PDF. Расшифровывается при первом употреблении; подробно рассматривается на страницах о подписании.
- TSA — Time-Stamping Authority, доверенная служба, которая выдаёт метки времени RFC 3161, используемые более высокими профилями PAdES.