Генерация PDF-документов в Wepps: От шаблона до готового файла

Нужно автоматически формировать счета, накладные или договоры? Раньше это требовало сложных библиотек, мучительной вёрстки в коде и головной боли с кириллицей. С платформой Wepps вы создаёте PDF так же просто, как обычную HTML-страницу.

17.01.2026

Нужно автоматически формировать счета, накладные или договоры? Раньше это требовало сложных библиотек, мучительной вёрстки в коде и головной боли с кириллицей. С платформой Wepps вы создаёте PDF так же просто, как обычную HTML-страницу — прямо в Smarty-шаблонах.

Возможности:

  • ✅ Вывод в браузер — клиент видит документ мгновенно
  • ✅ Скачивание файла — сохранение с кастомным именем
  • ✅ Отправка по почте — документ сразу в письме
  • ✅ Хранение на сервере — архив всех сформированных документов
  • ✅ Smarty-шаблоны — верстаете как обычную страницу
  • ✅ CSS-позиционирование — точная вёрстка таблиц и блоков
  • ✅ UTF-8 из коробки — любые языки (кириллица, иероглифы, арабская вязь)

Давайте разберёмся, как работает система генерации PDF в Wepps и создадим первый документ.

Как это работает: от шаблона до PDF

Представьте, что PDF — это снимок веб-страницы:

  1. Smarty-шаблон — вы пишете HTML с переменными, как обычную страницу сайта
  2. CSS-стили — оформляете документ: шрифты, таблицы, отступы
  3. PHP-класс — подставляет данные из базы, вызывает шаблоны
  4. mPDF-библиотека — преобразует HTML+CSS в PDF-файл
  5. Результат — готовый документ в браузере, на почте или на диске

Суть в том, что вы работаете с привычными технологиями веб-разработки, а платформа сама превращает это в PDF.

О библиотеке mPDF

mPDF подключена через Composer — это базовый принцип работы со всеми сторонними библиотеками в Wepps:

# В файле packages/composer.json
"require": {
    "mpdf/mpdf": "^8.0"
}

Преимущества использования Composer:

  • 🔒 Безопасность — автоматическое получение обновлений безопасности
  • 🔄 Актуальность — легкое обновление до последних стабильных версий
  • 📦 Зависимости — автоматическое управление всеми зависимостями библиотеки
  • Стабильность — проверенные версии из официального репозитория
  • 🚀 Простота — одна команда composer update обновляет все библиотеки

Это касается не только mPDF, но и всех других библиотек платформы: Smarty, PhpSpreadsheet, Curl и других сторонних компонентов.

Архитектура системы: из чего состоит PDF-генератор

В Wepps для работы с PDF используется расширение в папке:

packages/WeppsExtensions/Addons/Docs/Pdf/

Структура файлов:

Pdf/
├── Pdf.php              # Основной класс генерации
├── Request.php          # Точка входа (вызов из браузера)
├── Pdf.css              # Базовые стили для всех документов
├── PdfHeader.tpl        # Шаблон верхнего колонтитула
├── PdfFooter.tpl        # Шаблон нижнего колонтитула
├── PdfInvoice.tpl       # Шаблон счёта на оплату
├── PdfInvoice.css       # Стили для счёта
├── PdfOrder.tpl         # Шаблон заказа
├── PdfOrder.css         # Стили для заказа
├── PdfReceipt.tpl       # Шаблон квитанции
└── PdfReceipt.css       # Стили для квитанции

Как это работает вместе:

  1. Клиент запрашивает PDF: /ext/Docs/Pdf/Request.php?action=Order&id=a3f9b2c (ID хешируется для безопасности)
  2. Request.php создаёт объект класса Pdf и передаёт параметры
  3. Pdf.php определяет тип документа, загружает данные из БД
  4. Smarty рендерит шаблоны с подстановкой переменных
  5. mPDF превращает HTML+CSS в PDF и выводит в браузер

Примечание: В реальных проектах ID хешируется для защиты от перебора. Также можно настроить красивые URL через .htaccess: /pdf/order/a3f9b2c

Класс Pdf: сердце системы

Основной класс находится в Pdf.php. Вот упрощённая структура:

namespace WeppsExtensions\Addons\Docs\Pdf;

use WeppsCore\Smarty;
use WeppsCore\Data;
use WeppsCore\TextTransforms;

class Pdf {
    private $output;      // Содержимое документа (HTML)
    private $css;         // Стили документа
    private $header;      // Верхний колонтитул
    private $footer;      // Нижний колонтитул
    private $filename;    // Имя файла для скачивания

    function __construct($get) {
        // 1. Определяем тип документа (Order, Invoice, Receipt)
        $action = $get['action'];
        $id = $get['id'];

        // 2. Загружаем базовые элементы
        $smarty = Smarty::getSmarty();
        $this->css = $smarty->fetch('Pdf.css');
        $this->header = $smarty->fetch('PdfHeader.tpl');
        $this->footer = $smarty->fetch('PdfFooter.tpl');

        // 3. В зависимости от типа формируем содержимое
        switch ($action) {
            case "Order":
                // Безопасно загружаем заказ из БД через setParams()
                $obj = new Data("Orders");
                $obj->setParams([$id]);
                $order = $obj->fetch("Id=?")[0];
                $smarty->assign('order', $order);

                // Добавляем специфичные стили
                $this->css .= $smarty->fetch('PdfOrder.css');

                // Рендерим шаблон
                $this->output = $smarty->fetch('PdfOrder.tpl');

                // Имя файла при скачивании
                $this->filename = "Заказ {$order['Id']}.pdf";
                break;

            case "Invoice":
                // Аналогично для счёта
                break;
        }
    }

    function output($download = false) {
        // Создаём PDF
        $mpdf = new \Mpdf\Mpdf();

        // Подключаем стили
        $mpdf->WriteHTML($this->css, 1);

        // Устанавливаем колонтитулы
        $mpdf->SetHTMLHeader($this->header);
        $mpdf->SetHTMLFooter($this->footer);

        // Записываем содержимое
        $mpdf->WriteHTML($this->output);

        // Выводим в браузер или скачиваем
        if ($download == false) {
            $mpdf->Output(); // Показать в браузере
        } else {
            $mpdf->Output($this->filename, 'D'); // Скачать файл
        }
    }
}

Ключевые моменты:

  • Конструктор загружает данные, рендерит шаблоны, собирает HTML
  • Метод output() превращает HTML в PDF и отправляет клиенту
  • Smarty-шаблоны — привычная работа с переменными и циклами
  • CSS работает! Можете использовать обычные стили для оформления

Создание своего PDF-документа: пошаговое руководство

Создадим документ "Акт выполненных работ" с нуля.

Шаг 1. Создайте шаблон документа

Создайте файл PdfAct.tpl в папке Pdf/:

<div style="text-align: center; font-size: 16pt; font-weight: bold; margin-top: 20mm;">
    АКТ ВЫПОЛНЕННЫХ РАБОТ
</div>
<div style="text-align: center; margin-bottom: 10mm;">
    № {$act.Id} от {$act.ActDate|date_format:"%d.%m.%Y"}
</div>

<table width="100%" style="margin-bottom: 5mm;">
    <tr>
        <td style="width: 30mm;"><strong>Исполнитель:</strong></td>
        <td>{$shopInfo.Name}</td>
    </tr>
    <tr>
        <td><strong>Заказчик:</strong></td>
        <td>{$act.ClientName}</td>
    </tr>
</table>

<table class="invoice_items" width="100%" cellpadding="3" cellspacing="0">
    <thead>
        <tr>
            <th style="width: 15mm;"></th>
            <th>Наименование работ</th>
            <th style="width: 25mm;">Кол-во</th>
            <th style="width: 30mm;">Цена, руб.</th>
            <th style="width: 30mm;">Сумма, руб.</th>
        </tr>
    </thead>
    <tbody>
        {foreach name="out" item="item" from=$actItems}
        <tr>
            <td align="center">{$smarty.foreach.out.iteration}</td>
            <td>{$item.Name}</td>
            <td align="right">{$item.Qty}</td>
            <td align="right">{$item.Price|money}</td>
            <td align="right">{$item.Summ|money}</td>
        </tr>
        {/foreach}
    </tbody>
</table>

<table width="100%" style="margin-top: 5mm;">
    <tr>
        <td></td>
        <td style="width: 30mm; font-weight: bold;">ИТОГО:</td>
        <td style="width: 30mm; font-weight: bold; text-align: right;">
            {$act.Total|money} руб.
        </td>
    </tr>
</table>

<div style="margin-top: 10mm;">
    <p>Всего оказано услуг на сумму: {$actTotalLetter}</p>
</div>

<table width="100%" style="margin-top: 15mm;">
    <tr>
        <td width="50%">
            <div>Исполнитель: _________________</div>
            <div style="margin-top: 3mm;">({$shopInfo.DirectorName})</div>
        </td>
        <td width="50%">
            <div>Заказчик: _________________</div>
            <div style="margin-top: 3mm;">({$act.ClientName})</div>
        </td>
    </tr>
</table>

Шаг 2. Создайте стили документа

Файл PdfAct.css:

table.invoice_items {
    border: 1px solid #000;
    border-collapse: collapse;
}

table.invoice_items th,
table.invoice_items td {
    border: 1px solid #000;
    padding: 3mm;
}

table.invoice_items th {
    background-color: #f0f0f0;
    font-weight: bold;
}

Шаг 3. Добавьте обработку в класс Pdf

В Pdf.php добавьте case в конструктор:

switch ($action) {
    // ... существующие case

    case "Act":
        // Оптимизированный способ: JOIN вместо трёх запросов
        $sql = "
            SELECT 
                a.*,
                c.Name as ClientName,
                c.Address as ClientAddress,
                c.Phone as ClientPhone,
                c.Email as ClientEmail
            FROM ServiceActs a
            LEFT JOIN Clients c ON c.Id = a.ClientId
            WHERE a.Id = ?
        ";
        $act = Connect::$instance->fetch($sql, [$id])[0];

        // Загружаем позиции акта
        $sql = "SELECT * FROM ServiceActItems WHERE ActId = ? ORDER BY Priority";
        $actItems = Connect::$instance->fetch($sql, [$id]);

        // Преобразуем сумму прописью
        $actTotalLetter = TextTransforms::num2str($act['Total']);

        // Передаём данные в шаблон
        $smarty->assign('act', $act);
        $smarty->assign('actItems', $actItems);
        $smarty->assign('actTotalLetter', $actTotalLetter);

        // Подключаем стили
        $this->css .= $smarty->fetch('PdfAct.css');

        // Рендерим шаблон
        $this->output = $smarty->fetch('PdfAct.tpl');

        // Имя файла
        $this->filename = "Акт {$act['Id']}.pdf";
        break;
}

Обратите внимание: вместо трёх запросов используется один JOIN. Данные клиента теперь доступны как $act['ClientName'], $act['ClientAddress'] и т.д.

Шаг 4. Используйте в коде

Вывод в браузере:

// URL: /ext/Docs/Pdf/Request.php?action=Act&id=f8e4a1b
// ID хешируется перед передачей в URL для безопасности
$obj = new Pdf($_REQUEST);
$obj->output(false); // false = показать в браузере

Скачивание файла:

// Реальный ID получаем из хеша
$realId = decryptId($_GET['id']); // Ваша функция дешифрования
$obj = new Pdf(['action' => 'Act', 'id' => $realId]);
$obj->output(true); // true = скачать с именем "Акт 15.pdf"

Отправка по почте:

use WeppsExtensions\Addons\Messages\Mail\Mail;

// Создаём PDF во временном файле
$orderId = 15; // Реальный ID из базы
$obj = new Pdf(['action' => 'Act', 'id' => $orderId]);
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML($obj->css, 1);
$mpdf->SetHTMLHeader($obj->header);
$mpdf->SetHTMLFooter($obj->footer);
$mpdf->WriteHTML($obj->output);

// Сохраняем временно
$tmpFile = '/tmp/act_15.pdf';
$mpdf->Output($tmpFile, 'F');

// Отправляем письмо
$mail = new Mail('html');
$mail->setAttach([$tmpFile]);
$mail->mail(
    'client@example.com',
    'Акт выполненных работ №15',
    '<p>Направляем акт выполненных работ во вложении.</p>'
);

// Удаляем временный файл
unlink($tmpFile);

Вот и вся магия! Теперь у вас есть полноценный генератор PDF-документов.

Работа с шаблонами: колонтитулы и содержимое

Верхний колонтитул (PdfHeader.tpl)

Отображается на каждой странице сверху:

<div class="header">
    <div class="img">
        {if $shopInfo.Logo_FileUrl}
        <img src="{$projectDev.protocol}{$projectDev.host}{$shopInfo.Logo_FileUrl}" 
             width="120"/>
        {else}
        <img src="{$projectDev.protocol}{$projectDev.host}{$projectInfo.logo}" 
             width="120"/>
        {/if}
    </div>
    <div class="descr">
        <div>{$shopInfo.Name2}</div>
        <div>{$shopInfo.City}</div>
        <div>{$shopInfo.Address}</div>
    </div>
    <div class="descr2">
        <div>{$shopInfo.Phone}</div>
        <div>{$shopInfo.Email}</div>
        <div>{$shopInfo.Site}</div>
    </div>
    <hr/>
</div>

Нижний колонтитул (PdfFooter.tpl)

Отображается внизу каждой страницы:

<div class="footer">
    <hr />
    <div class="descr">{$shopInfo.Name2}</div>
    <div class="descr2">Страница {literal}{PAGENO}{/literal}</div>
    <div class="clear"></div>
</div>

Специальная переменная:

  • {literal}{PAGENO}{/literal} — автоматическая нумерация страниц

Базовые стили (Pdf.css)

Применяются ко всем документам:

body {
    font-family: DejaVuSansCondensed, sans-serif;
    font-size: 11pt;
}

h1 {
    font-size: 26pt;
    color: #000066;
    border-top: 0.075cm solid #000000;
    border-bottom: 0.075cm solid #000000;
}

h2 {
    font-size: 12pt;
    font-weight: bold;
    text-transform: uppercase;
}

table {
    border-collapse: collapse;
}

hr {
    height: 1px;
    color: #999999;
}

Полезные возможности mPDF

Размеры и отступы

// В конструкторе Pdf.php
$mpdf = new \Mpdf\Mpdf([
    'format' => 'A4',           // Формат: A4, A5, Letter
    'orientation' => 'P',        // P = Portrait, L = Landscape
    'margin_left' => 15,         // Левый отступ в мм
    'margin_right' => 15,        // Правый отступ в мм
    'margin_top' => 25,          // Верхний отступ в мм
    'margin_bottom' => 25,       // Нижний отступ в мм
    'margin_header' => 10,       // Отступ колонтитула сверху
    'margin_footer' => 10        // Отступ колонтитула снизу
]);

Разрыв страницы

В шаблоне:

<div>Первая страница</div>

<pagebreak />

<div>Вторая страница</div>

Или через CSS:

h1 {
    page-break-after: avoid; /* Не разрывать после заголовка */
}

table.invoice_items {
    page-break-inside: avoid; /* Не разрывать таблицу */
}

Водяной знак

$mpdf->SetWatermarkText('КОПИЯ');
$mpdf->showWatermarkText = true;
$mpdf->watermarkTextAlpha = 0.1; // Прозрачность 0-1

Защита паролем

$mpdf->SetProtection([
    'copy',  // Запретить копирование
    'print'  // Запретить печать
], 'user_password', 'owner_password');

Безопасность: защита от SQL-инъекций

Важно! При работе с данными всегда используйте подготовленные запросы через метод setParams(). Это защищает от SQL-инъекций.

❌ Небезопасно (не делайте так):

// Прямая подстановка переменной в условие
$obj = new Data("ServiceActs");
$act = $obj->fetch("Id='{$id}'")[0];

// Уязвимость: если $id = "1' OR '1'='1", то можно получить все записи

✅ Безопасно (правильный способ):

// Используйте плейсхолдеры ? и setParams()
$obj = new Data("ServiceActs");
$obj->setParams([$id]); // Параметры передаются отдельно
$act = $obj->fetch("Id=?")[0]; // ? заменяется безопасно

// Для нескольких параметров
$obj = new Data("Orders");
$obj->setParams([$userId, $status]);
$orders = $obj->fetch("UserId=? AND Status=?");

Как это работает:

  1. Метод setParams() принимает массив параметров
  2. В условии fetch() используются плейсхолдеры ?
  3. Платформа автоматически экранирует значения через PDO
  4. SQL-инъекции становятся невозможными

Все примеры ниже используют этот безопасный подход.

Оптимизация: один запрос вместо трех

В примере выше мы делали три отдельных запроса к БД. Это наглядно, но неэффективно. Если можно сделать одним запросом — делайте одним!

❌ Три запроса (медленнее):

// Запрос 1: Акт
$obj = new Data("ServiceActs");
$obj->setParams([$id]);
$act = $obj->fetch("Id=?")[0];

// Запрос 2: Позиции акта
$obj = new Data("ServiceActItems");
$obj->setParams([$id]);
$actItems = $obj->fetch("ActId=?");

// Запрос 3: Клиент
$obj = new Data("Clients");
$obj->setParams([$act['ClientId']]);
$client = $obj->fetch("Id=?")[0];

✅ Один запрос через JOIN (быстрее):

use WeppsCore\Connect;

// Получаем акт с клиентом одним запросом
$sql = "
    SELECT 
        a.*,
        c.Name as ClientName,
        c.Address as ClientAddress,
        c.Email as ClientEmail
    FROM ServiceActs a
    LEFT JOIN Clients c ON c.Id = a.ClientId
    WHERE a.Id = ?
";
$act = Connect::$instance->fetch($sql, [$id])[0];

// Позиции акта отдельно (связь many-to-one)
$sql = "SELECT * FROM ServiceActItems WHERE ActId = ? ORDER BY Priority";
$actItems = Connect::$instance->fetch($sql, [$id]);

// Теперь $act содержит и данные акта, и данные клиента
$smarty->assign('act', $act);
$smarty->assign('actItems', $actItems);

Когда использовать Data, а когда Connect::$instance:

Ситуация Используйте Причина
Простая выборка из одной таблицы Data Удобно, читаемо, автоматическая пагинация
JOIN двух таблиц Connect::$instance Быстрее, один запрос вместо нескольких
Сложные агрегации (SUM, COUNT, GROUP BY) Connect::$instance Больше контроля над SQL
Массовые операции (обновление 1000+ записей) Connect::$instance Меньше overhead
Нужна схема таблицы и автоматические связи Data Работает через s_ConfigFields

Золотое правило: если запрос можно написать одним JOIN — пишите через Connect::$instance->fetch($sql, $params). Это дешевле по производительности и надёжнее (меньше обращений к БД).

Практические сценарии

Сценарий 1: Генерация счёта для клиента

Задача: При оформлении заказа клиент должен получить счёт на оплату.

Решение:

  1. В момент создания заказа формируем PDF:

    $obj = new Pdf(['action' => 'Invoice', 'id' => $orderId]);
    $mpdf = new \Mpdf\Mpdf();
    $mpdf->WriteHTML($obj->css, 1);
    $mpdf->WriteHTML($obj->output);
    $pdfFile = "/files/invoices/invoice_{$orderId}.pdf";
    $mpdf->Output($pdfFile, 'F'); // Сохраняем на диск
  2. Отправляем письмо с вложением:

    $mail = new Mail('html');
    $mail->setAttach([$pdfFile]);
    $mail->mail(
    $order['Email'],
    "Счёт на оплату #{$order['Id']}",
    "<p>Ваш счёт во вложении. Оплатите в течение 3 дней.</p>"
    );
  3. Добавляем кнопку "Скачать счёт" в личном кабинете:

    {* ID хешируется для безопасности *}
    <a href="/ext/Docs/Pdf/Request.php?action=Invoice&id={$order.IdHash}" 
    target="_blank">
    Скачать счёт
    </a>
    {* Или через красивый URL, если настроен .htaccess: /pdf/invoice/{$order.IdHash} *}

Сценарий 2: Автоматическая отправка актов в конце месяца

Задача: Каждый месяц формировать акты для всех клиентов и отправлять.

Решение: Создайте задачу в Cron (или используйте системное расширение):

// packages/WeppsAdmin/ConfigExtensions/Processing/ActsGeneration.php

$obj = new Data("ServiceActs");
$acts = $obj->fetch("DATE_FORMAT(ActDate, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')");

foreach ($acts as $act) {
    // Загружаем клиента для каждого акта
    $objClient = new Data("Clients");
    $objClient->setParams([$act['ClientId']]);
    $client = $objClient->fetch("Id=?")[0];
    // Генерируем PDF
    $pdfObj = new Pdf(['action' => 'Act', 'id' => $act['Id']]);
    $mpdf = new \Mpdf\Mpdf();
    $mpdf->WriteHTML($pdfObj->css, 1);
    $mpdf->WriteHTML($pdfObj->output);

    // Сохраняем
    $file = "/files/acts/act_{$act['Id']}.pdf";
    $mpdf->Output($file, 'F');

    // Отправляем клиенту
    $mail = new Mail('html');
    $mail->setAttach([$file]);
    $mail->mail(
        $client['Email'],
        "Акт выполненных работ за " . date('m.Y'),
        "<p>Направляем акт выполненных работ.</p>"
    );
}

Сценарий 3: Массовая печать накладных

Задача: Сформировать один PDF-файл со всеми накладными для печати.

Решение:

$mpdf = new \Mpdf\Mpdf();

// Загружаем заказы для печати
$objOrders = new Data("Orders");
$orders = $objOrders->fetch("Status='ready_to_print'", 1000, 1, "Id");

foreach ($orders as $key => $order) {
    $obj = new Pdf(['action' => 'Order', 'id' => $order['Id']]);

    if ($key > 0) {
        $mpdf->AddPage(); // Новая страница для каждой накладной
    }

    $mpdf->WriteHTML($obj->css, 1);
    $mpdf->WriteHTML($obj->output);
}

$mpdf->Output('Накладные_' . date('Y-m-d') . '.pdf', 'D');

Особенности вёрстки для PDF

Поддерживаемые CSS-свойства

Работает:

  • width, height, margin, padding
  • border, border-collapse
  • font-family, font-size, font-weight, color
  • text-align, vertical-align
  • background-color (для ячеек таблиц)
  • page-break-before, page-break-after

Не работает:

  • position: absolute/fixed (ограниченная поддержка)
  • float (частично работает)
  • flexbox, grid
  • CSS-анимации
  • Сложные селекторы

Рекомендации по вёрстке

  1. Используйте таблицы для макета:

    <table width="100%">
    <tr>
        <td width="50%">Левая колонка</td>
        <td width="50%">Правая колонка</td>
    </tr>
    </table>
  2. Указывайте единицы измерения в мм:

    .header {
    height: 30mm;
    margin-bottom: 5mm;
    }
  3. Избегайте внешних ресурсов:

    {* Плохо: внешняя картинка *}
    <img src="https://cdn.example.com/logo.png" />

{ Хорошо: локальная картинка с полным путём } <img src="{$projectDev.protocol}{$projectDev.host}/pic/logo.png" />

4. **Проверяйте разрывы страниц:**
```css
table.important {
    page-break-inside: avoid; /* Не разрывать таблицу */
}

Работа с многоязычным контентом (UTF-8)

mPDF автоматически работает с UTF-8 кодировкой, что позволяет использовать любые языки мира:

// По умолчанию используется DejaVuSansCondensed с UTF-8
body {
    font-family: DejaVuSansCondensed, sans-serif;
}

Важно: Платформа Wepps автоматически использует UTF-8 для всех документов:

  • Шаблоны .tpl сохраняйте в UTF-8
  • Данные из БД передаются в UTF-8
  • mPDF корректно обрабатывает UTF-8 без дополнительных настроек
  • Шрифт DejaVu поддерживает большинство языков: кириллица (русский, украинский, белорусский), латиница с диакритикой, греческий, арабский, иврит, китайские иероглифы и многие другие

Примеры многоязычных документов:

<p>Русский: Счёт на оплату №123</p>
<p>English: Invoice #123</p>
<p>Deutsch: Rechnung Nr. 123</p>
<p>中文: 发票 #123</p>
<p>العربية: فاتورة #123</p>

Всё работает из коробки — просто пишите текст на нужном языке в шаблоне.

Преобразование чисел прописью

Для финансовых документов используйте класс TextTransforms:

use WeppsCore\TextTransforms;

// 15432.50 → "пятнадцать тысяч четыреста тридцать два рубля 50 копеек"
$sumInWords = TextTransforms::num2str(15432.50);

// В шаблоне
$smarty->assign('totalInWords', $sumInWords);
<p>Итого к оплате: {$order.Total|money} руб.</p>
<p>Сумма прописью: {$totalInWords}</p>

Форматирование дат

// В шаблоне Smarty
{$order.Date|date_format:"%d.%m.%Y"}      // 29.01.2026
{$order.Date|date_format:"%d %B %Y г."}  // 29 января 2026 г.

Отладка и тестирование

Сохранение HTML для проверки

Перед генерацией PDF проверьте, как выглядит HTML:

$obj = new Pdf(['action' => 'Invoice', 'id' => 15]);

// Сохраните HTML в файл
file_put_contents('/tmp/invoice_debug.html', $obj->output);

// Откройте в браузере и проверьте вёрстку

Включение отладки ошибок

$mpdf = new \Mpdf\Mpdf();
$mpdf->showImageErrors = true;  // Показывать ошибки картинок
$mpdf->debug = true;             // Режим отладки

Проверка размера файла

Слишком большие PDF могут быть проблемой:

$mpdf->Output($filename, 'F');
$filesize = filesize($filename);

if ($filesize > 2 * 1024 * 1024) { // Более 2 МБ
    // Оптимизируйте изображения или уменьшите качество
}

Лучшие практики

✅ Делайте так:

  1. Используйте переменные проекта:

    {$shopInfo.Name}
    {$projectInfo.email}
    {$projectDev.host}
  2. Создавайте общие компоненты:

    {* Переиспользуемая таблица реквизитов *}
    {include file="PdfCompanyDetails.tpl"}
  3. Проверяйте данные перед генерацией:

    if (!$order || empty($order)) {
    Exception::error404();
    }
  4. Кэшируйте PDF если данные не меняются:

    $pdfFile = "/files/invoices/invoice_{$orderId}.pdf";
    if (!file_exists($pdfFile)) {
    // Генерируем только если файла нет
    $mpdf->Output($pdfFile, 'F');
    }

❌ Не делайте так:

  1. Не генерируйте PDF на лету для больших объёмов:
    // Плохо: генерация 1000 PDF в цикле
    foreach ($orders as $order) {
    $obj = new Pdf(['action' => 'Order', 'id' => $order['Id']]);
    $obj->output();
    }

// Хорошо: генерируйте асинхронно и сохраняйте

2. **Не забывайте про права доступа:**
```php
// Проверяйте, что пользователь может видеть документ
if ($order['UserId'] != Connect::$projectData['user']['Id']) {
    Exception::error(403);
}
  1. Не используйте сложную вёрстку:
    /* Плохо */
    .layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    }

/ Хорошо / <table width="100%"> <tr> <td width="50%">...</td> <td width="50%">...</td> </tr> </table>

## Интеграция с другими модулями

### Отправка через Telegram

```php
use WeppsExtensions\Addons\Messages\Telegram\Telegram;

// Генерируем PDF
$obj = new Pdf(['action' => 'Invoice', 'id' => $orderId]);
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML($obj->css, 1);
$mpdf->WriteHTML($obj->output);
$pdfFile = "/tmp/invoice_{$orderId}.pdf";
$mpdf->Output($pdfFile, 'F');

// Отправляем в Telegram
$tg = new Telegram();
$tg->sendDocument($chatId, $pdfFile, "Ваш счёт №{$orderId}");

unlink($pdfFile);

Архивирование документов

// Создайте системное расширение для архива документов
function archivePdf($type, $id, $pdfContent) {
    $dir = "/files/archive/{$type}/" . date('Y/m');

    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    $filename = "{$dir}/{$type}_{$id}_" . date('Y-m-d_H-i-s') . ".pdf";
    file_put_contents($filename, $pdfContent);

    // Записываем в БД для индексации
    Connect::$instance->insert('DocumentsArchive', [
        'DocType' => $type,
        'DocId' => $id,
        'FilePath' => $filename,
        'CreateDate' => date('Y-m-d H:i:s')
    ]);
}

Выводы

Генерация PDF в Wepps — это:

  • 🚀 Быстро — создаёте шаблон как обычную страницу
  • 🎨 Просто — верстаете в Smarty+CSS, без сложных библиотек
  • 💪 Мощно — вывод в браузер, скачивание, отправка по почте
  • 🔧 Гибко — полный контроль над структурой и стилями
  • 🌍 Надёжно — отличная поддержка кириллицы

Начните с простого: скопируйте один из существующих шаблонов (Order, Invoice, Receipt), измените под свои данные, добавьте case в Pdf.php — и у вас готов собственный генератор документов!

Типичный путь разработки:

  1. Создайте .tpl файл — скопируйте структуру из PdfOrder.tpl
  2. Добавьте .css файл — оформите таблицы и отступы
  3. Добавьте case в конструктор Pdf.php — загрузите данные из БД
  4. Протестируйте через /ext/Docs/Pdf/Request.php?action=YourDoc&id=1
  5. Интегрируйте в свой функционал — отправка писем, кнопки скачивания

Вперёд, создавайте красивые документы! 📄✨

Как фреймворк: Гибкость разработки

Полный контроль над кодом, архитектурой и расширениями для сложных проектов

wapps framework

Как CMS: Простота управления

Интуитивная админ-панель для редакторов контента без программирования

wapps cms

Как платформа: Готовые решения

Быстрый старт проектов с возможностью глубокой кастомизации под любые задачи

wapps platform
Создание системного расширения в Wepps: расширяем возможности админки

Можно ли создать отдельный модуль в админке со своей навигацией, несколькими страницами и полным контролем над PHP-кодом? Именно для этого существуют системные расширения — мощный инструмент для добавления произвольного функционала в административную панель.

15.01.2026
Добавление полей в список в Wepps Platform

Данное руководство описывает процесс добавления новых полей в существующие списки (таблицы) платформы Wepps. Поля определяют структуру данных и интерфейс редактирования в административной панели.

13.01.2026
F
CSS-классы форм в Wepps Platform: Конструктор для красивых форм

В Wepps Platform есть готовая система CSS-классов для форм — как конструктор Lego. Вы берёте блоки (классы), собираете форму, и она сразу красивая. Давайте разберёмся, как это работает.

10.01.2026

☝️ Будьте в курсе: полезные статьи, новости проекта и практические советы по работе с платформой Wepps.