Hỗ trợ đồ án, khóa luận Web, App, AI - Uy tín, chất lượng!
Hỗ trợ đồ án, khóa luận Web, App, AI - Uy tín, chất lượng!
Trang Chủ Hướng Dẫn Đồ Án Xuất file PDF báo cáo từ dữ liệu MySQL bằng TCPDF
Hướng Dẫn Đồ ÁnLập Trình Web

Xuất file PDF báo cáo từ dữ liệu MySQL bằng TCPDF

Chia sẻ
Chia sẻ

Trong các hệ thống quản trị và dashboard nội bộ, báo cáo PDF giúp tài liệu hiển thị nhất quán, dễ chia sẻ và lưu trữ dài hạn. Khi ứng dụng PHP của bạn đang đọc dữ liệu từ MySQL, việc tích hợp TCPDF cho phép dựng báo cáo có bố cục chuyên nghiệp, hỗ trợ Unicode tiếng Việt, bảng nhiều trang, QR code và watermark.

Quy trình triển khai trở nên đáng tin cậy hơn khi bạn chuẩn hóa kết nối cơ sở dữ liệu bằng PDO như trong bài hướng dẫn về kết nối MySQL bằng PDO trong PHP, tận dụng các thao tác CRUD PHP thuần để chuẩn bị dữ liệu sạch và có chỉ số rõ ràng, rồi áp dụng quản lý phiên bằng Session để bảo vệ endpoint xuất báo cáo cho đúng người dùng. Kết quả là một pipeline gọn: truy vấn đã tối ưu → dữ liệu được chuẩn hóa → render PDF nhất quán trên mọi thiết bị.

Chuẩn bị môi trường và cài đặt TCPDF bằng Composer

Trước khi viết bất kỳ dòng code nào, bạn cần môi trường PHP hoạt động ổn định (PHP 7.4+ khuyến nghị), MySQL server và Composer để quản lý thư viện. Composer giúp bạn cài TCPDF và cập nhật dễ dàng, đồng thời giữ phiên bản đồng nhất giữa các môi trường dev, staging, production.

Cấu trúc dự án nên tách biệt lớp hạ tầng (kết nối DB, cấu hình), lớp nghiệp vụ (lấy dữ liệu, mapping), và lớp trình bày (render PDF). Cách tách này giúp mở rộng dễ dàng: thêm loại báo cáo, thay đổi bố cục, hoặc thay một phần công nghệ mà không ảnh hưởng toàn hệ thống.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Ở bước cài đặt, bạn sẽ dùng Composer để kéo gói tecnickcom/tcpdf. Sau đó, bạn cần nạp autoload và kiểm tra khả năng render PDF tối thiểu. Hãy đảm bảo thư mục tạm để TCPDF ghi font/ảnh có quyền ghi phù hợp. Nếu chạy trong container, mount volume cho thư mục tạm để giảm I/O trong container.
  2. Với tiếng Việt, bạn nên đặt encoding mặc định là UTF-8 và dùng font hỗ trợ Unicode như DejaVu Sans. Trong TCPDF, có sẵn dejavusans/dejavusansmono. Điều này tránh lỗi vỡ dấu, dấu hỏi ngược, hoặc ký tự lạ khi in PDF.
composer require tecnickcom/tcpdf

Thiết kế bảng dữ liệu và mẫu báo cáo

Trước khi render PDF, hãy xác định mục đích báo cáochỉ số chính: ví dụ báo cáo doanh thu theo ngày, trạng thái đơn hàng, top sản phẩm, hay tồn kho. Từ đó thiết kế lược đồ bảng MySQL hợp lý và index phù hợp cho các truy vấn phổ biến theo thời gian, theo nhóm sản phẩm, theo khách hàng.

Bạn nên chuẩn hóa dữ liệu theo mức 3NF, dùng kiểu dữ liệu chính xác (DATE, DATETIME, DECIMAL cho tiền tệ), và thêm index cho các cột lọc/sort. Nếu báo cáo chạy theo chu kỳ, cân nhắc bảng tổng hợp (summary) hoặc materialized view cập nhật theo cron để rút ngắn thời gian render.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Bảng mẫu bên dưới chỉ mang tính minh hoạ cho báo cáo doanh thu. Trong thực tế, bạn có thể thay bằng bảng đơn hàng, bảng chi tiết dòng hàng, hoặc view đã tổng hợp. Điều cốt lõi là trả về dataset sạch, có đủ cột hiển thị và cột tính toán.
  2. Khi thiết kế để in PDF, lưu ý độ rộng cột. PDF có khổ trang cố định, nên những cột văn bản dài cần cắt bớt, xuống dòng tự động hoặc tooltip (không khả thi trong PDF, nên thường là xuống dòng). Bạn sẽ xử lý điều này ở phần MultiCellAuto Page Break của TCPDF.
CREATE TABLE reports_revenue_daily (
  id INT AUTO_INCREMENT PRIMARY KEY,
  report_date DATE NOT NULL,
  channel VARCHAR(50) NOT NULL,
  orders INT NOT NULL DEFAULT 0,
  gross_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
  net_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
  notes VARCHAR(255) NULL,
  KEY idx_report_date (report_date),
  KEY idx_channel (channel)
);

Kết nối MySQL bằng PDO và lớp Repository

Việc kết nối thông qua PDO giúp bạn có API thống nhất, prepared statements, và dễ xử lý ngoại lệ. Tách riêng lớp Database để tái sử dụng, và lớp ReportRepository để đóng gói truy vấn. Cách này thuận tiện test và bảo trì.

Bạn cũng cần cấu hình .env hoặc biến môi trường để không hard-code mật khẩu. Trên production, cấp ít quyền nhất cho user DB (nguyên tắc least privilege). Bật PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION để dễ debug trong giai đoạn phát triển, nhưng log cẩn thận khi lên production.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Đoạn code dưới đây dùng PDO với DSN MySQL và UTF-8. Nếu bạn đã có module kết nối dùng DI container, bạn chỉ cần áp dụng phần repository. Mục tiêu là chuẩn hoá pipeline: nhận filter từ request → repo query → dữ liệu chuẩn → render PDF.
  2. Chú ý sanitize input. Các biến ngày tháng nên validate định dạng Y-m-d. Các tham số text dùng whitelist hoặc regex đơn giản để tránh injection. Với PDO, bạn vẫn phải bind params đúng kiểu.
<?php
// src/Infrastructure/Database.php
class Database {
    private \PDO $pdo;

    public function __construct(
        string $host,
        string $db,
        string $user,
        string $pass,
        string $charset = 'utf8mb4'
    ) {
        $dsn = "mysql:host={$host};dbname={$db};charset={$charset}";
        $options = [
            \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
            \PDO::ATTR_EMULATE_PREPARES   => false,
        ];
        $this->pdo = new \PDO($dsn, $user, $pass, $options);
    }

    public function pdo(): \PDO {
        return $this->pdo;
    }
}

// src/Domain/ReportRepository.php
class ReportRepository {
    public function __construct(private \PDO $pdo) {}

    public function getRevenueDaily(\DateTimeInterface $from, \DateTimeInterface $to, ?string $channel = null): array {
        $sql = "SELECT report_date, channel, orders, gross_amount, net_amount, notes
                FROM reports_revenue_daily
                WHERE report_date BETWEEN :from AND :to";
        $params = [
            ':from' => $from->format('Y-m-d'),
            ':to'   => $to->format('Y-m-d'),
        ];
        if ($channel) {
            $sql .= " AND channel = :channel";
            $params[':channel'] = $channel;
        }
        $sql .= " ORDER BY report_date ASC, channel ASC";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }
}

Cài font Unicode và cấu hình TCPDF cho tiếng Việt

Để PDF hiển thị tiếng Việt chính xác, hãy dùng font Unicode như DejaVu Sans có sẵn trong TCPDF. Bạn chỉ cần gọi SetFont('dejavusans', '', 10) là đủ. Nếu bạn cần custom font, TCPDF có tiện ích tcpdf_addfont.php để nhúng TTF.

Ngoài ra, hãy bật auto page break và đặt margin chuẩn để header/footer không đè nội dung. Xác định khổ giấy A4 khổ dọc là phù hợp cho phần lớn báo cáo. Nếu có nhiều cột, cân nhắc A4 ngang hoặc A3 để tránh vỡ dòng quá mức.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Lớp PdfReport bên dưới kế thừa TCPDF và override Header()Footer(). Bạn có thể chèn logo PNG/JPG vào header và số trang vào footer. Chú ý toạ độ X/Y, chiều rộng ảnh, và căn chỉnh nội dung.
  2. Khi in bảng dữ liệu dài, hãy dùng Ln() để xuống dòng hợp lý, hoặc writeHTML/MultiCell để tự động xuống dòng. Đối với bảng nhiều trang, mỗi trang lặp lại header bảng giúp người đọc không bị lẫn cột.
<?php
// src/Application/PdfReport.php
require __DIR__ . '/../vendor/autoload.php';

use TCPDF;

class PdfReport extends TCPDF {
    protected string $titleText = 'BÁO CÁO DOANH THU HÀNG NGÀY';

    public function Header(): void {
        // Logo (tuỳ chọn)
        // $imageFile = __DIR__ . '/assets/logo.png';
        // if (file_exists($imageFile)) {
        //     $this->Image($imageFile, 10, 8, 18);
        // }

        // Tiêu đề
        $this->SetFont('dejavusans', 'B', 12);
        $this->Cell(0, 10, $this->titleText, 0, 1, 'C');
        $this->Ln(2);
        $this->Line(10, 22, $this->getPageWidth() - 10, 22);
        $this->Ln(2);
    }

    public function Footer(): void {
        $this->SetY(-15);
        $this->SetFont('dejavusans', '', 9);
        $this->Cell(0, 10, 'Trang ' . $this->getAliasNumPage() . '/' . $this->getAliasNbPages(), 0, 0, 'C');
    }

    public function setReportTitle(string $title): void {
        $this->titleText = $title;
    }
}

Render bảng dữ liệu: MultiCell, auto page break, màu dòng xen kẽ

Để trình bày dữ liệu dễ đọc, bạn nên vẽ header bảng với nền đậm và chữ trắng, sau đó in dữ liệu theo dòng xen kẽ màu nền nhẹ. TCPDF cung cấp SetFillColor, SetTextColor, CellMultiCell để tạo bố cục linh hoạt.

Bạn cần xác định độ rộng cột theo tỷ lệ trang. Ví dụ: ngày 25mm, kênh 35mm, đơn hàng 22mm, doanh thu 35mm, ghi chú còn lại. Nếu ghi chú dài, dùng MultiCell để xuống dòng tự động. Với bảng kéo dài nhiều trang, lặp lại header ở đầu mỗi trang hoặc sử dụng kỹ thuật kiểm tra getY() gần cuối trang và gọi AddPage().

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Đoạn script dưới đây minh hoạ một controller đơn giản nhận tham số from, to, channel qua GET, validate, lấy dữ liệu từ repository rồi vẽ PDF. Ở production, bạn nên thêm middleware auth, rate limit và log truy cập.
  2. Phần format tiền tệ dùng number_format. Nếu bạn cần định dạng theo locale, cân nhắc NumberFormatter của intl extension. Ngoài ra, có thể tính tổng cuối bảng và in ở dòng Total.
<?php
// public/report.php
require __DIR__ . '/../vendor/autoload.php';

require_once __DIR__ . '/../src/Infrastructure/Database.php';
require_once __DIR__ . '/../src/Domain/ReportRepository.php';
require_once __DIR__ . '/../src/Application/PdfReport.php';

$env = [
    'DB_HOST' => getenv('DB_HOST') ?: '127.0.0.1',
    'DB_NAME' => getenv('DB_NAME') ?: 'appdb',
    'DB_USER' => getenv('DB_USER') ?: 'appuser',
    'DB_PASS' => getenv('DB_PASS') ?: 'secret',
];

function parseDate(string $s, string $default): \DateTimeImmutable {
    $d = \DateTimeImmutable::createFromFormat('Y-m-d', $s);
    return $d ?: new \DateTimeImmutable($default);
}

$from = parseDate($_GET['from'] ?? '', 'first day of this month');
$to   = parseDate($_GET['to']   ?? '', 'today');
$channel = isset($_GET['channel']) ? preg_replace('/[^A-Za-z0-9_\- ]/', '', $_GET['channel']) : null;

$db = new Database($env['DB_HOST'], $env['DB_NAME'], $env['DB_USER'], $env['DB_PASS']);
$repo = new ReportRepository($db->pdo());
$data = $repo->getRevenueDaily($from, $to, $channel);

// Khởi tạo PDF
$pdf = new PdfReport('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->setReportTitle('BÁO CÁO DOANH THU HÀNG NGÀY');
$pdf->SetCreator('YourApp');
$pdf->SetAuthor('Your Company');
$pdf->SetTitle('BaoCao_'.$from->format('Ymd').'_'.$to->format('Ymd'));
$pdf->SetMargins(10, 25, 10);             // lề trái, trên, phải
$pdf->SetAutoPageBreak(true, 18);         // auto page break, lề dưới
$pdf->setPrintHeader(true);
$pdf->setPrintFooter(true);
$pdf->AddPage();
$pdf->SetFont('dejavusans', '', 10);

// Header bộ lọc
$pdf->SetFillColor(240, 240, 240);
$pdf->Cell(0, 8, 'Khoảng thời gian: '.$from->format('Y-m-d').' đến '.$to->format('Y-m-d')
    .($channel ? ' | Kênh: '.$channel : ''), 0, 1, 'L', 1);
$pdf->Ln(2);

// Header bảng
$w = [
    'date'    => 28,
    'channel' => 35,
    'orders'  => 22,
    'gross'   => 35,
    'net'     => 35,
    'notes'   => 0,  // phần còn lại
];

$pdf->SetFillColor(33, 37, 41);
$pdf->SetTextColor(255, 255, 255);
$pdf->SetFont('dejavusans', 'B', 10);
$pdf->Cell($w['date'],    8, 'Ngày',      1, 0, 'C', 1);
$pdf->Cell($w['channel'], 8, 'Kênh',      1, 0, 'C', 1);
$pdf->Cell($w['orders'],  8, 'Đơn',       1, 0, 'C', 1);
$pdf->Cell($w['gross'],   8, 'Gross',     1, 0, 'C', 1);
$pdf->Cell($w['net'],     8, 'Net',       1, 0, 'C', 1);
$pdf->Cell($w['notes'] ?: ($pdf->getPageWidth()-$pdf->GetX()-10), 8, 'Ghi chú', 1, 1, 'C', 1);

// Dữ liệu
$pdf->SetFont('dejavusans', '', 10);
$pdf->SetTextColor(0, 0, 0);
$fill = false;
$totalOrders = 0;
$totalGross = 0.0;
$totalNet = 0.0;

foreach ($data as $row) {
    $totalOrders += (int)$row['orders'];
    $totalGross  += (float)$row['gross_amount'];
    $totalNet    += (float)$row['net_amount'];

    // màu nền xen kẽ
    if ($fill) $pdf->SetFillColor(248, 249, 250); else $pdf->SetFillColor(255, 255, 255);

    $x = $pdf->GetX();
    $y = $pdf->GetY();

    // Các ô cố định chiều cao
    $pdf->Cell($w['date'], 8, (new \DateTimeImmutable($row['report_date']))->format('Y-m-d'), 1, 0, 'C', 1);
    $pdf->Cell($w['channel'], 8, $row['channel'], 1, 0, 'L', 1);
    $pdf->Cell($w['orders'], 8, number_format((int)$row['orders']), 1, 0, 'R', 1);
    $pdf->Cell($w['gross'], 8, number_format((float)$row['gross_amount'], 2), 1, 0, 'R', 1);
    $pdf->Cell($w['net'], 8, number_format((float)$row['net_amount'], 2), 1, 0, 'R', 1);

    // Ghi chú dùng MultiCell để xuống dòng
    $notesWidth = $w['notes'] ?: ($pdf->getPageWidth()-$pdf->GetX()-10);
    $pdf->MultiCell($notesWidth, 8, $row['notes'] ?? '', 1, 'L', 1, 1);
    $fill = !$fill;

    // Nếu gần cuối trang, thêm header bảng mới (tuỳ chọn nâng cao)
}

$pdf->Ln(2);
$pdf->SetFont('dejavusans', 'B', 10);
$pdf->Cell($w['date'] + $w['channel'], 8, 'Tổng', 1, 0, 'R', 0);
$pdf->Cell($w['orders'], 8, number_format($totalOrders), 1, 0, 'R', 0);
$pdf->Cell($w['gross'], 8, number_format($totalGross, 2), 1, 0, 'R', 0);
$pdf->Cell($w['net'], 8, number_format($totalNet, 2), 1, 0, 'R', 0);
$pdf->Cell($w['notes'] ?: ($pdf->getPageWidth()-$pdf->GetX()-10), 8, '', 1, 1, 'R', 0);

// Xuất file: inline hoặc download
$filename = 'bao_cao_doanh_thu_'.$from->format('Ymd').'_'.$to->format('Ymd').'.pdf';
$pdf->Output($filename, 'I'); // 'I' xem trên trình duyệt, 'D' tải về, 'F' lưu file trên server

Chèn hình, barcode, QR code, watermark và phân quyền PDF

Nhiều báo cáo cần logo công ty, watermark như “CONFIDENTIAL” hoặc QR code để truy dấu. TCPDF hỗ trợ Image() để nhúng ảnh, write2DBarcode() để tạo QR code nhanh trên PDF. Bạn cũng có thể dùng SetProtection() để hạn chế in, sao chép, hoặc đặt mật khẩu mở file.

Khi dùng watermark, nên vẽ ở layer dưới bằng cách giảm SetAlpha hoặc đặt màu xám nhạt, kích thước lớn, xoay nhẹ để không che nội dung chính. Với QR code, bạn có thể encode đường dẫn đến trang chi tiết báo cáo hoặc ID bản ghi để xác thực nguồn.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Ví dụ dưới minh hoạ cách chèn QR code ở góc trên bên phải của trang đầu tiên. Bạn có thể gọi nhiều lần để chèn ở mỗi trang hoặc chỉ trang bìa. Kích thước QR code nên đủ lớn để thiết bị di động quét dễ dàng.
  2. Phần SetProtection giúp bạn kiểm soát quyền đối với PDF. Không nên lạm dụng vì có thể gây khó khăn cho người dùng hợp lệ. Tùy chính sách mà áp dụng in hoặc chỉ xem.
<?php
// Bổ sung sau khi AddPage()
$pdf->SetFont('dejavusans', '', 9);
// QR code dữ liệu ví dụ
$qrData = 'https://dashboard.yourapp.local/report?id=123&from='.$from->format('Y-m-d').'&to='.$to->format('Y-m-d');
$style = [
    'border' => 0,
    'padding' => 1,
    'fgcolor' => [0,0,0],
    'bgcolor' => false
];
$pdf->write2DBarcode($qrData, 'QRCODE,M', $pdf->getPageWidth()-35, 25, 25, 25, $style, 'N');

// Watermark đơn giản (chỉ minh hoạ)
$pdf->StartTransform();
$pdf->SetAlpha(0.06);
$pdf->SetFont('dejavusans', 'B', 50);
$pdf->Rotate(35, $pdf->getPageWidth()/2, $pdf->getPageHeight()/2);
$pdf->Text(30, 150, 'CONFIDENTIAL');
$pdf->StopTransform();
$pdf->SetAlpha(1);

// Phân quyền PDF (tuỳ chọn)
$pdf->SetProtection(['print'], '', null, 0, null);

Xử lý báo cáo lớn: phân trang, bộ nhớ, thời gian chạy

Với báo cáo lớn (hàng chục nghìn dòng), render PDF có thể tốn thời gian và bộ nhớ. Chiến lược tốt là phân trang dữ liệu ở cấp SQL bằng LIMIT/OFFSET hoặc cursor/pagination theo report_date để stream từng phần vào PDF. Song song, cân nhắc tối ưu truy vấn và thêm bảng tổng hợp chạy cron để giảm tải.

Ngoài ra, đặt giới hạn cho người dùng trên giao diện lọc: tối đa 31 ngày mỗi lần, tối đa 5000 dòng. Nếu cần xuất lớn hơn, chuyển sang CSV để tải và chỉ dùng PDF cho bản tóm tắt. Với PHP-FPM, tăng max_execution_time hợp lý hoặc tách job xuất PDF sang queue worker rồi trả link tải khi xong.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Mẫu dưới đây minh hoạ cách phân trang dữ liệu ở repository. Bạn có thể loop qua nhiều batch để vẽ dần dần, giúp giảm peak memory. Đừng quên in header bảng lại khi sang trang mới.
  2. Khi xử lý batch, hãy flush output của TCPDF đúng cách. Tránh giữ các mảng tạm lớn trong RAM. Bật opcache trên production để tăng hiệu năng.
<?php
// Ví dụ repository có phân trang
class ReportRepository {
    public function __construct(private \PDO $pdo) {}

    public function getRevenueDailyPaged(
        \DateTimeInterface $from,
        \DateTimeInterface $to,
        ?string $channel,
        int $limit,
        int $offset
    ): array {
        $sql = "SELECT report_date, channel, orders, gross_amount, net_amount, notes
                FROM reports_revenue_daily
                WHERE report_date BETWEEN :from AND :to";
        $params = [
            ':from' => $from->format('Y-m-d'),
            ':to'   => $to->format('Y-m-d'),
        ];
        if ($channel) {
            $sql .= " AND channel = :channel";
            $params[':channel'] = $channel;
        }
        $sql .= " ORDER BY report_date ASC, channel ASC LIMIT :limit OFFSET :offset";
        $stmt = $this->pdo->prepare($sql);
        $stmt->bindValue(':from', $params[':from']);
        $stmt->bindValue(':to', $params[':to']);
        if ($channel) {
            $stmt->bindValue(':channel', $params[':channel']);
        }
        $stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll();
    }
}

Xuất inline, tải xuống, hoặc lưu file trên server và gửi email

Trong TCPDF, phương thức Output($filename, $dest) điều khiển cách xuất:

  • 'I': mở trực tiếp trên trình duyệt.

  • 'D': buộc tải về.

  • 'F': lưu file trên máy chủ.

  • 'S': trả về chuỗi PDF (binary) để bạn tự xử lý.

Khi bạn cần gửi PDF qua email hoặc lưu phiên bản báo cáo, dùng 'F' để lưu xuống thư mục tạm và dùng thư viện mail (PHPMailer, Symfony Mailer) để đính kèm. Đừng quên dọn dẹp file tạm bằng cron hoặc job background để tiết kiệm dung lượng.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Đoạn ví dụ dưới lưu file xuống /var/reports/ và gửi email. Hãy đảm bảo thư mục tồn tại và có quyền ghi. Với Docker, mount volume để dữ liệu sống qua vòng đời container.
  2. Khi gửi email, đặt subject rõ ràng và tên file có timestamp để người nhận dễ tìm. Tránh gửi file quá lớn. Nếu PDF > 10MB, cân nhắc nén hoặc gửi link tải.
<?php
$savePath = '/var/reports/';
$filename = 'bao_cao_'.$from->format('Ymd').'_'.$to->format('Ymd').'.pdf';
$pdf->Output($savePath.$filename, 'F');

// Ví dụ dùng mailer (giản lược)
// $mailer->send(
//   to: 'manager@company.com',
//   subject: 'Báo cáo doanh thu '.$from->format('Y-m-d').' đến '.$to->format('Y-m-d'),
//   body: 'Đính kèm PDF.',
//   attachments: [$savePath.$filename]
// );

Bảo mật và kiểm soát truy cập báo cáo

Báo cáo tài chính cần kiểm soát truy cập. Ở tầng HTTP, bắt buộc đăng nhập và phân quyền theo vai trò. Ở tầng dữ liệu, giới hạn query theo phạm vi của user. Khi lưu file trên server, đặt ngoài document root và chỉ cung cấp link signed URL tạm thời.

Ở mức PDF, SetProtection chỉ là lớp mỏng. Đừng dựa hoàn toàn vào mật khẩu của PDF cho mục đích an ninh. Thay vào đó, đảm bảo endpoint sinh PDF không lộ ra công khai và tất cả tham số được kiểm soát. Log truy cập và cảnh báo khi có tải bất thường.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Nếu hệ thống có nhiều tenant, mọi truy vấn phải gắn tenant_id vào WHERE để ngăn lộ dữ liệu chéo. Kể cả khi render PDF từ queue, vẫn cần context của user/tenant.
  2. Khi debug, không in thông tin nhạy cảm (DSN, mật khẩu) ra log. Sử dụng kho bí mật (Secret Manager) hoặc env vault để quản lý cấu hình.
<?php
// Ví dụ áp dụng protection nhẹ
$pdf->SetProtection(['print', 'copy'], '', null, 0, null);
// Kết hợp với xác thực JWT/session ở cấp ứng dụng trước khi cho phép truy cập report.php

Tối ưu trải nghiệm: mẫu giao diện lọc và nút xuất PDF

Từ góc nhìn người dùng, một form lọc rõ ràng và nút “Xuất PDF” là chìa khoá. Bộ lọc nên có: khoảng ngày, kênh, loại báo cáo. Hạn chế khoảng ngày quá dài để tránh PDF khổng lồ. Khi người dùng bấm xuất, chuyển hướng tới report.php?from=...&to=...&channel=... để tải hoặc xem trực tiếp.

Để tránh tải lại trang, bạn có thể mở PDF trong tab mới. Nếu người dùng thường xuyên xuất cùng cấu hình, hỗ trợ preset lưu bộ lọc. Kết hợp lịch flatpickr giúp chọn ngày nhanh hơn. Với mobile, cân nhắc A4 ngang hoặc tối ưu độ rộng cột.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Đoạn HTML/JS đơn giản dưới đây minh hoạ form lọc. Ở sản phẩm thật, tích hợp framework frontend (Vue/React) và gọi API tạo report dưới nền, sau đó trả URL tải khi hoàn tất.
  2. Validate đầu vào phía client và phía server. Không tin tưởng chỉ client-side.
<form method="get" action="/report.php" target="_blank">
  <label>Từ ngày</label>
  <input type="date" name="from" required />
  <label>Đến ngày</label>
  <input type="date" name="to" required />
  <label>Kênh</label>
  <input type="text" name="channel" placeholder="web, app..." />
  <button type="submit">Xuất PDF</button>
</form>

Kiểm thử, giám sát và bảo trì

Sau khi triển khai, hãy tạo bộ dữ liệu giả lập để test nhiều kịch bản: không có dữ liệu, dữ liệu rất nhiều, ghi chú dài, ký tự đặc biệt, lỗi DB. So sánh PDF giữa môi trường dev và prod để đảm bảo font và bố cục đồng nhất.

Đặt log hợp lý cho thời gian tạo PDF và kích thước file đầu ra. Theo dõi spike bất thường để điều chỉnh giới hạn và tối ưu query. Định kỳ cập nhật TCPDF phiên bản mới để vá lỗi và cải thiện hiệu năng. Viết tài liệu hướng dẫn nội bộ để dev mới có thể thêm báo cáo mới nhanh chóng.

Trước khi viết code, đọc kỹ hai đoạn này:

  1. Thiết lập test tự động cho repository bằng dataset nhỏ. Với rendering PDF, có thể so sánh số trang, có/không nội dung header/footer, hoặc dùng snapshot ảnh trang (phức tạp hơn) trong pipeline CI.
  2. Định chuẩn style: font, cỡ chữ, khoảng cách, màu sắc. Tạo base layout để mọi báo cáo dùng chung, chỉ thay tiêu đề và cột dữ liệu. Như vậy brand nhất quán và code tái sử dụng cao.
<?php
// Gợi ý test đơn giản pseudo-code
$rows = $repo->getRevenueDaily(new \DateTimeImmutable('2025-09-01'), new \DateTimeImmutable('2025-09-07'), null);
assert(is_array($rows));
// assert(count($rows) > 0);
// assert(isset($rows[0]['report_date']));

Kết luận

Xuất PDF báo cáo từ MySQL bằng TCPDF là giải pháp bền vững cho các ứng dụng PHP cần tài liệu chuyên nghiệp, nhất quán và dễ chia sẻ. Bạn đã đi qua toàn bộ quy trình: chuẩn bị môi trường, kết nối PDO, truy vấn dữ liệu, xử lý tiếng Việt bằng font Unicode, dựng bảng nhiều trang, thêm QR code, watermark, phân quyền, phân trang dữ liệu lớn, và chiến lược lưu/gửi file. Khi mở rộng, hãy trừu tượng hoá phần layout và repository để thêm dạng báo cáo mới nhanh chóng.

Hãy bắt đầu từ một báo cáo nhỏ, chuẩn hoá layout, rồi mở rộng dần. Kết hợp tối ưu SQL, hạn chế phạm vi lọc và tách job nặng ra queue sẽ giúp hệ thống xuất PDF bền bỉ ở quy mô lớn.

Tham Gia Nhóm – Hỗ Trợ Lập Trình

Hỗ Trợ Đồ Án – ThueDoAn.vn

Liên quan
Hướng Dẫn Đồ Án

Checklist 10 bước kiểm tra cuối cùng trước khi nhấn nút nộp đồ án

Thời điểm chuẩn bị nhấn nút nộp đồ án luôn mang lại...

Hướng Dẫn Đồ Án

Tuyệt chiêu đối phó với những giảng viên hướng dẫn ‘khó tính’ nhất

Giai đoạn làm đồ án tốt nghiệp luôn đi kèm với vô...

Hướng Dẫn Đồ Án

Tại sao việc quản lý Source Code bằng Git lại quan trọng hơn bạn nghĩ?

Trong thế giới lập trình hiện đại, mã nguồn chính là linh...

Hướng Dẫn Đồ Án

Những chức năng ‘thừa’ khiến bạn tốn thời gian mà không được cộng điểm

Làm đồ án tốt nghiệp là một cuộc đua thực thụ với...

Hướng Dẫn Đồ Án

Cách xử lý khi đề tài bị trùng lặp ý tưởng với các khóa trước quá nhiều

Lựa chọn đề tài đồ án luôn là bước khởi đầu đầy...

Hướng Dẫn Đồ Án

Làm sao để giải trình với giảng viên khi code bị nghi ngờ là đi chép?

Trong quá trình thực hiện đồ án, việc tham khảo mã nguồn...

Hướng Dẫn Đồ Án

Cảnh báo: Những nguồn share code ‘rác’ trên mạng cần tuyệt đối tránh

Trong quá trình làm đồ án, việc tìm kiếm sự hỗ trợ...

Hướng Dẫn Đồ Án

Sai lầm khi chọn công nghệ quá khó so với năng lực thực tế của bản thân

Chọn đề tài và công nghệ cho đồ án luôn là bước...

Liên Hệ
Liên hệ để được tư vấn & hỗ trợ đồ án CNTT nhé bạn!