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

Contracts / Потоковая запись

Потоковый модуль включает два experimental-интерфейса: StreamingWriterInterface для инкрементного вывода PDF и CursorInterface для постраничной компоновки содержимого. Core поставляет финальный протестированный движок, реализующий оба интерфейса. Классы движка внутренние, поэтому вы используете публичный experimental-контракт, а не реализуете движок самостоятельно. Поскольку уровень стабильности — experimental, контракт может измениться в минорном выпуске с предварительным уведомлением об устаревании. Строго фиксируйте версию или оборачивайте контракт в собственный адаптер, прежде чем полагаться на него в продакшене.

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

Потоковый модуль записи сериализует каждую страницу по мере компоновки и может сбрасывать её в вывод до начала следующей страницы. Используйте его, когда документ может превысить доступный бюджет памяти. Модуль записи в памяти удерживает весь документ целиком. Потоковый модуль записи — нет. StreamingWriterInterface определяет строгий конечный автомат. Новый экземпляр находится в состоянии CLOSED. open() переводит его в OPEN и записывает заголовок PDF в поток, предоставленный вызывающей стороной. newPage() переводит его в PAGING и возвращает курсор. close() записывает структуру перекрёстных ссылок и трейлер, затем переводит экземпляр в терминальное состояние CLOSED. Поток перекрёстных ссылок сопоставляет каждый номер объекта с его байтовым смещением, как описано в ISO 32000-2 §7. Один экземпляр поддерживает только один сеанс. После close() экземпляр исчерпан. Ресурс потока принадлежит вызывающей стороне. Модуль записи пишет в него, но никогда его не закрывает.

CursorInterface — это поверхность записи на уровне страницы. Вы получаете курсор из StreamingWriterInterface::newPage(), и он остаётся действительным до явного завершения, автоматического завершения при следующем вызове newPage() или аннулирования через close(). Аннулирование необратимо. Курсор нельзя повторно активировать. Каждый метод недействительного курсора выбрасывает LogicException. Курсор записывает сырые операторы потока содержимого, задаёт активный шрифт и пишет позиционированный текст. Поток содержимого кодирует содержимое страницы как последовательность графических операторов, как описано в ISO 32000-2 §8. Курсор — низкоуровневая поверхность: он не выполняет ни оформление текста, ни переупорядочивание двунаправленного текста, ни перенос строк, ни какой-либо макет. Эти задачи остаются за уровнем Document. Инвариант единственного курсора соблюдается на всём протяжении: в любой момент времени действителен не более одного курсора.

Оба интерфейса имеют уровень experimental, и Core поставляет для них рабочий движок: финальную реализацию StreamingWriterInterface, её постраничный курсор и сток-приёмник, используемый для бенчмаркинга памяти. Эти классы движка внутренние и не входят в публичную поверхность. Чтобы использовать потоковую запись, задавайте зависимость от experimental-контракта и позволяйте Core поставлять реализацию. PHPDoc каждого типа указывает на ADR потокового модуля записи с описанием конечного автомата жизненного цикла и обоснованием области применения. Поскольку уровень стабильности — experimental, сигнатура контракта всё ещё может измениться в минорном выпуске с предварительным уведомлением об устаревании. Строго фиксируйте версию или оборачивайте контракт в собственный адаптер, прежде чем полагаться на него в продакшене.

ТипВидКлючевые членыСтабильностьНачиная с
StreamingWriterInterfaceинтерфейсopen(resource, Config), newPage(?PageSize): CursorInterface, close()experimental (поставляемый движок)3.1.0
CursorInterfaceинтерфейсwriteContent(string), setFont(string, string, float), writeText(float, float, string), finalizePage()experimental (поставляемый движок)3.1.0

open() выбрасывает InvalidArgumentException для потока, недоступного для записи, и LogicException, если модуль записи уже открыт. close() не идемпотентен. Двойной вызов выбрасывает исключение.

examples/contracts/streaming-quickstart.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;
use NextPDF\Core\Config;
/**
* Drive a streaming writer through one page.
*
* The parameter is the experimental contract; Core supplies the
* implementation. Type-hint the interface and let the engine satisfy it.
*
* @param StreamingWriterInterface $writer A Core-supplied streaming writer.
* @param resource $stream A writable, caller-owned stream.
*/
function writeOnePage(StreamingWriterInterface $writer, $stream): void
{
$writer->open($stream, new Config());
$cursor = $writer->newPage();
$cursor->setFont('helvetica', '', 12.0);
$cursor->writeText(72.0, 720.0, 'Streamed page.');
$cursor->finalizePage();
$writer->close();
// The caller closes $stream after close() returns.
}

Функция работает с experimental-интерфейсом, поэтому остаётся не привязанной к классу движка. Core внедряет рабочую реализацию в точке вызова.

examples/contracts/streaming-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;
use NextPDF\Core\Config;
use NextPDF\ValueObjects\PageSize;
use Psr\Log\LoggerInterface;
final readonly class LargeReportStreamer
{
public function __construct(
private StreamingWriterInterface $writer,
private LoggerInterface $logger,
) {}
/**
* Stream a multi-page report to a caller-owned file handle.
*
* @param resource $stream Writable file handle owned by the caller.
* @param list<list<string>> $pages One list of text lines per page.
*/
public function stream($stream, array $pages): void
{
$this->writer->open($stream, new Config());
try {
foreach ($pages as $lines) {
$cursor = $this->writer->newPage(PageSize::A4());
$cursor->setFont('helvetica', '', 11.0);
$y = 760.0;
foreach ($lines as $line) {
$cursor->writeText(72.0, $y, $line);
$y -= 14.0;
}
$cursor->finalizePage();
}
} finally {
$this->writer->close();
}
}
}

Блок finally гарантирует, что модуль записи закроется и трейлер будет записан, даже если цикл по страницам выбросит исключение. Поток по-прежнему принадлежит вызывающей стороне, и она же его закрывает.

  • Задавайте зависимость от интерфейса, а не от класса движка. Движок, реализующий оба контракта, внутренний и не входит в публичную поверхность. Не создавайте его через new и не ссылайтесь на него по имени. Указывайте тип StreamingWriterInterface и позволяйте Core поставлять реализацию.
  • Контракт имеет уровень experimental. Его сигнатура может измениться в минорном выпуске с предварительным уведомлением об устаревании. Строго фиксируйте версию или оборачивайте контракт в собственный адаптер, прежде чем полагаться на него в продакшене.
  • Курсор становится недействительным в тот момент, когда вызывается следующий newPage() или close(). Вызов метода устаревшего курсора выбрасывает LogicException. Завершайте явно для ясности.
  • close() не идемпотентен. Двойной вызов — это ошибка вызывающей стороны, а не восстановимое условие. Контракт выбрасывает исключение.
  • Модуль записи никогда не закрывает поток. Если вы забудете закрыть принадлежащий вызывающей стороне дескриптор после возврата close(), вы допустите утечку файлового дескриптора.
  • Движок сбрасывает каждую завершённую страницу, поэтому резидентная память не растёт с числом страниц. Точный профиль памяти относится к уровню experimental и может меняться между минорными выпусками. Не закрепляйте предположение жёстко на основе одного измерения.

Потоковая архитектура ограничивает пиковую память. Поставляемый движок сбрасывает каждую завершённую страницу и освобождает её буфер, поэтому резидентный набор не растёт с числом страниц, в отличие от модуля записи в памяти. Движок выгружает свои данные перекрёстных ссылок и дерева страниц во временные потоки на диске, чтобы удерживать объём памяти процесса почти постоянным. Конкретные значения памяти и времени относятся к уровню experimental и могут меняться между минорными выпусками, поэтому эта страница не утверждает никакого фиксированного числа. performance_budget в 1500 мс по времени и 64 МБ по пику — это ориентир, а не контрактная гарантия. Воспроизводимость — bitwise: одно и то же содержимое и конфигурация дают байт-идентичный вывод; это проверяют эталонные тесты движка с золотыми образцами.

Метод writeContent() курсора — это низкоуровневый обходной механизм. Он без изменений дописывает предоставленные байты в поток содержимого страницы и не проверяет ни синтаксис, ни семантику операторов. Недоверенный ввод, переданный в writeContent(), порождает повреждённый или вредоносный PDF. Считайте этот метод API только для доверенного ввода и предпочитайте writeText() для любого текста под влиянием вызывающей стороны. Поставляемый курсор экранирует текст, переданный в writeText(), в соответствии с грамматикой литеральных строк PDF, но не санитизирует сырые операторы. Модель потока, принадлежащего вызывающей стороне, также является свойством безопасности. Движок пишет в поток, но никогда не закрывает и не открывает его повторно, поэтому он не может перенаправить вывод. Во время выполнения поверхность атаки реальна, потому что движок поставляется. Вызывающие стороны никогда не должны передавать недоверенные байты в writeContent(), а движок должен соблюдать инварианты контракта.

УтверждениеСтандартПунктПодтверждение
Поток содержимого кодирует содержимое страницы как последовательность графических операторов, которые дописывает курсор.ISO 32000-2§8
Модуль записи испускает структуру перекрёстных ссылок, сопоставляющую каждый номер объекта с его байтовым смещением при закрытии.ISO 32000-2§7

Оба пункта закреплены через глоссарий и пересказаны своими словами. NextPDF не воспроизводит нормативный текст. ADR потокового модуля записи, на который ссылается PHPDoc контракта, содержит описание жизненного цикла и обоснование области применения.

Протестированный потоковый движок поставляется в открытом Core под этими experimental-контрактами. Классы движка внутренние, поэтому вы используете потоковую запись через публичный контракт, а не через имя конкретного класса. NextPDF Pro и NextPDF Enterprise следуют тому же контракту, поэтому код, написанный под StreamingWriterInterface в Core, остаётся действительным с реализацией Premium того же контракта. Ограничение связано с уровнем experimental, а не с редакцией или доступностью. Сигнатура может измениться в минорном выпуске с предварительным уведомлением об устаревании.