Перейти к содержимому

Обработка ошибок с учётом иерархии исключений NextPDF

NextPDF выбрасывает типизированные исключения в исключительных ситуациях. Библиотека никогда не скрывает ошибку за возвратом false или null. Каждое доменное исключение расширяет один и тот же абстрактный базовый класс, NextPdfException, и предоставляет структурированный диагностический контекст через ContextAwareExceptionInterface. Этот рецепт показывает, где перехватывать исключения и как логировать структурированный контекст для конвейера APM (мониторинга производительности приложений). Он также показывает, какие сбои единый перехват по базовому типу не покрывает.

Окно терминала
composer require nextpdf/core:^3

Никаких дополнительных расширений не требуется.

Иерархия выглядит так:

RuntimeException
└── NextPdfException (abstract, implements ContextAwareExceptionInterface)
├── InvalidConfigException
├── FontNotFoundException
├── FontParsingException
├── ImageProcessingException
├── WriterException
├── SignatureException
├── EncryptionException
├── HtmlParsingException
├── … (every domain exception under NextPDF\Exception)
└── Strict\StrictModeViolation (abstract)
├── Strict\IncompatibleRenderingModeException
└── Strict\OracleConformanceFailure

Эта иерархия имеет два практических следствия, оба проверены по исходному коду:

  1. catch (NextPdfException $e) перехватывает каждое исключение в пространстве NextPDF\Exception, включая нарушения строгого режима. Все они расширяют абстрактный базовый класс.
  2. Он перехватывает не всё, что может выбросить библиотека. NextPDF\Support\DegradedException расширяет RuntimeException напрямую, а не NextPdfException. Поэтому catch (NextPdfException $e) не перехватывает сбой по политике деградации. Чтобы обработать его, перехватите DegradedException (или более широкий RuntimeException) явно. Этот рецепт показывает эту границу явно и не считает единый перехват по базовому типу полным покрытием.

NextPdfException::getContext() возвращает array<string, mixed> с ключами в snake_case и только примитивными значениями либо списками примитивных значений. Его можно напрямую сериализовать в массив контекста логгера PSR-3. PSR-3 §1.3 помещает исключение под ключ контекста 'exception'. Метод getContext() в NextPDF добавляет наряду с этим ключом доменные детали, а не сам объект исключения.

Эта поверхность API взята из PHPDoc для NextPDF\Exception\NextPdfException, NextPDF\Contracts\ContextAwareExceptionInterface, конкретных доменных исключений (например, NextPDF\Exception\FontNotFoundException, с методами getFontName() / getSearchPaths() / wasFallbackAttempted()) и NextPDF\Support\DegradedException (который содержит Capability и DegradationPolicy). В примерах ниже используются NextPdfException::getContext() и методы доступа для конкретных исключений.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\NextPdfException;
try {
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'Hello');
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
} catch (NextPdfException $e) {
// Every NextPDF\Exception\* (and strict-mode violation) lands here.
// $e->getContext() is APM-safe structured detail.
error_log($e->getMessage());
}

Полный пример показывает детализированный перехват, логирование структурированного контекста и границу DegradedException. Он также оставляет свободным канал вывода тестовой обвязки.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Contracts\ContextAwareExceptionInterface;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use NextPDF\Support\DegradedException;
/**
* A minimal PSR-3-shaped sink. In production this is your real logger;
* the exception goes under the 'exception' key (PSR-3 §1.3) and the
* NextPDF structured context is merged in as domain detail.
*
* @param array<string, mixed> $context
*/
function logError(string $message, array $context): void
{
fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");
}
$doc = Document::createStandalone();
$doc->setTitle('Exception handling patterns');
try {
$doc->addPage();
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 12, 'Exception-aware error handling', newLine: true);
// This call succeeds; the catch blocks below show the SHAPE of handling.
$doc->setFont('helvetica', '', 11);
$doc->cell(0, 8, 'Catch specifically, then fall back to the base.', newLine: true);
} catch (FontNotFoundException $e) {
// Most specific first: actionable, typed accessors.
logError('Font missing — using a fallback face', [
'exception' => $e::class,
'font_name' => $e->getFontName(),
'searched' => $e->getSearchPaths(),
'fallback' => $e->wasFallbackAttempted(),
]);
} catch (NextPdfException $e) {
// Catch-all for every NextPDF\Exception\* including strict violations.
$context = ['exception' => $e::class];
if ($e instanceof ContextAwareExceptionInterface) {
$context += $e->getContext();
}
logError($e->getMessage(), $context);
} catch (DegradedException $e) {
// BOUNDARY: DegradedException extends RuntimeException directly, NOT
// NextPdfException. The catch above would NOT have caught it. This
// explicit block (or a broader RuntimeException) is required.
logError('Capability degraded under the active policy', [
'exception' => $e::class,
'capability' => $e->capability->id,
'policy' => $e->policy->value,
]);
}
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
fwrite(STDERR, "Document built; handlers wired.\n");

STDOUT остаётся свободным для тестовой обвязки; PDF записывается только в NEXTPDF_COOKBOOK_OUTPUT.

  • Размещайте блоки catch от конкретного к общему. PHP сопоставляет первый совместимый catch. Блок catch (NextPdfException $e), размещённый перед catch (FontNotFoundException $e), делает конкретный блок недостижимым кодом.
  • DegradedException не является NextPdfException. Как проверено по исходному коду, он расширяет RuntimeException. Один лишь catch (NextPdfException $e) пропускает дальше сбой по строгой политике деградации. Перехватывайте его (или RuntimeException) явно, когда задействована политика деградации.
  • getContext() по контракту безопасен для APM. Ключи в snake_case. Значения — это примитивы или списки примитивов, без вложенных объектов и без ресурсов. Его можно сериализовать напрямую. Он никогда не содержит байты документа.
  • Не разбирайте сообщения исключений. Сообщения предназначены для чтения человеком и могут меняться. Используйте типизированные методы доступа (getFontName(), capability->id и так далее) и getContext() как стабильную машинную поверхность.
  • Предостережение об устаревшем количестве. Более старые материалы могут указывать фиксированное число “N доменных исключений”. Иерархия растёт от выпуска к выпуску. Полагайтесь на базовый тип NextPdfException и instanceof, а не на жёстко заданное число.
  • Подстановочные элементы PSR-3 остаются строками. При логировании оставляйте сообщение строкой с токенами {placeholder}, а значения помещайте в массив контекста (PSR-3 §1.2). Не подставляйте объект исключения в сообщение.

Обработка исключений не добавляет постоянных издержек. NextPDF выбрасывает исключения только в исключительных ситуациях, а getContext() по требованию строит небольшой массив. performance_budget (wall_ms: 2000, peak_mb: 96) ограничивает запуск тестовой обвязки для этого рецепта, а не произвольные документы.

  • getContext() спроектирован как безопасный для логирования: только примитивы, без полезной нагрузки документа и без байтов файла. Вы по-прежнему отвечаете за значения, которые добавляете в контекст лога. Очищайте всё, что предоставлено пользователем (например, путь к файлу), согласно вашей политике логирования, прежде чем оно попадёт в приёмник.
  • Не выводите конечным пользователям необработанные сообщения исключений так, чтобы это раскрывало структуру файловой системы. Показывайте обобщённое сообщение, а структурированный контекст логируйте на стороне сервера.
УтверждениеСпецификацияРазделreference_id (идентификатор ссылки)
Исключение помещается в контекст лога PSR-3 под ключом exception.PSR-3§1.3
Сообщения лога остаются строками; имена подстановочных элементов сопоставляются с ключами контекста.PSR-3§1.2
Дата изменения генерируется заново при каждом сохранении, поэтому вывод стабилен структурно (а не побайтово).ISO 32000-2§14.3

Этот рецепт проверен с профилем воспроизводимости structural. Вывод содержит /ID в трейлере и дату изменения, которые генерируются заново при каждом сохранении, поэтому побайтовая идентичность недостижима. Нормализованная qpdf структура остаётся стабильной.