Skip to content
Dragon edited this page Jun 4, 2026 · 4 revisions

Tables

Page::table() renders a data grid from an array (or any iterable) of associative rows. Columns are defined with Column value objects that describe which row key to read, a header label, a width policy, and optional alignment overrides. The renderer reuses the cell and image pipeline, supports automatic page-break with repeated header rows, zebra striping, configurable borders, and per-cell conditional styling.

Basic usage

use DragonOfMercy\PhpPdf\Color;
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Image;
use DragonOfMercy\PhpPdf\Table\Cell;
use DragonOfMercy\PhpPdf\Table\CellStyle;
use DragonOfMercy\PhpPdf\Table\Column;
use DragonOfMercy\PhpPdf\Table\TableBorders;
use DragonOfMercy\PhpPdf\Table\TableStyle;
use DragonOfMercy\PhpPdf\TextAlign;

$doc = new Document();
$doc->setAutoPageBreak(true);
$page = $doc->addPage();
$page->setFont($doc->getFont('Helvetica'), 10);

// One fill column takes the remaining width; two fixed columns get exact mm.
$columns = [
    Column::of('avatar', '')->width(14)->align(TextAlign::CENTER),
    Column::of('name', 'Name')->fill(),
    Column::of('role', 'Role')->width(40),
    Column::of('salary', 'Salary')->width(32)->align(TextAlign::RIGHT),
];

// Scalars are shorthand for Cell::of($value). Cell::image() embeds a raster or SVG image.
$rows = [
    ['avatar' => Cell::image(Image::fromFile('avatar1.png'), w: 10), 'name' => 'Alice Martin',  'role' => 'Engineer', 'salary' => '95,000'],
    ['avatar' => Cell::image(Image::fromFile('avatar2.png'), w: 10), 'name' => 'Bob Nguyen',    'role' => 'Designer', 'salary' => '82,000'],
    ['avatar' => Cell::image(Image::fromFile('avatar3.png'), w: 10), 'name' => 'Carol Schmidt', 'role' => 'Manager',  'salary' => '110,000'],
];

$style = TableStyle::default()
    ->withHeader(fill: Color::gray(220), bold: true)
    ->withBorder(TableBorders::GRID)
    ->withZebra(Color::rgb(255, 255, 255), Color::gray(247))
    ->withCellStyle(function (mixed $value, array $row, Column $col): ?CellStyle {
        if ($col->key === 'salary' && is_string($value) && (int) str_replace(',', '', $value) > 100000) {
            return CellStyle::new()->withTextColor(Color::rgb(180, 0, 0))->withBold(true);
        }
        return null;
    });

$result = $page->table($columns, $rows, x: 15, y: 30, width: 180, style: $style);
// $result->rowCount  -- number of data rows rendered
// $result->pageCount -- number of pages the table spans
// $result->page      -- last Page instance used (useful for further drawing)
// $result->x / ->y   -- cursor position after the last row

Page::table() parameters

Parameter Type Description
$columns Column[] Column definitions (at least one required).
$rows iterable Associative arrays. Scalar values become Cell::of() automatically.
x float Left edge in the document unit (mm by default).
y float Top edge in the document unit.
width float Total table width. Fixed columns are subtracted first; fill columns share the remainder.
style TableStyle Presentation and pagination config. Optional; defaults to TableStyle::default().
ln NextPosition Cursor advance after the table. Defaults to BELOW (cursor moves to the table's left edge, just under the last row) so content flows beneath it. Pass NONE to leave the cursor untouched.

The method returns a TableResult with public properties rowCount, pageCount, page, x, and y. Regardless of ln, TableResult::$y always reports the position just below the last row, so you can anchor a totals block there explicitly.

Column

Column::of(string $key, ?string $header = null) is the named constructor. Chain the following methods (each returns a new immutable Column):

Method Description
->width(float $mm) Fixed width in the document unit. Mutually exclusive with ->fill().
->fill(int $weight = 1) Fill policy: takes a proportional share of the remaining width. Multiple fill columns divide by weight. Mutually exclusive with ->width().
->align(TextAlign $align) Default horizontal alignment for data cells in this column (LEFT, CENTER, RIGHT, JUSTIFY). JUSTIFY fills wrapped cells to the column width, leaving each cell's last line ragged - see Justified text.
->verticalAlign(VerticalAlign $v) Default vertical alignment for data cells.
->padding(CellPadding $p) Default padding override for every cell in this column.

The $key property is public and readable in callbacks (e.g. $col->key === 'salary').

Cell types

A row value may be:

  • Scalar - converted implicitly to Cell::of((string) $value).
  • Cell::of(string|\Stringable $text) - text cell with optional style overrides: ->bold(), ->align(), ->verticalAlign(), ->textColor(), ->fill(), ->padding().
  • Cell::image(Image|string $src, ?float $w = null, ?float $h = null) - image cell. Aspect ratio is preserved and the image is clamped to the inner cell box. Defaults to TextAlign::CENTER / VerticalAlign::MIDDLE. A plain path string is accepted as $src (equivalent to Image::fromFile($path)).

Cell spanning (colspan)

Cell::of('x')->colSpan($n) makes a data cell span $n adjacent columns to its right. The merged cell is drawn once at the summed width of the covered columns; its border and fill cover the whole span, with no internal column edges. Any value present under a covered column's key in that row is ignored. Spanning never changes pagination (the table still breaks row by row). $n must be >= 1, and the span may not run past the last column (both throw PdfException).

$page->table(
    columns: [
        Column::of('a', 'Q1')->width(40)->align(TextAlign::RIGHT),
        Column::of('b', 'Q2')->width(40)->align(TextAlign::RIGHT),
        Column::of('c', 'Q3')->width(40)->align(TextAlign::RIGHT),
    ],
    rows: [
        ['a' => '100', 'b' => '120', 'c' => '90'],
        ['a' => '110', 'b' => '130', 'c' => '95'],
        ['a' => Cell::of('Total year: 870')->colSpan(3)->bold()->align(TextAlign::CENTER)],
    ],
    x: 20, y: 30, width: 120,
);

Grouped headers

TableStyle::withColumnGroups(ColumnGroup ...$groups) adds a band of group labels above the per-column header row. Each ColumnGroup::of($label, $span) spans $span columns; ColumnGroup::spacer($span = 1) is an unlabeled group that lets the column header beneath it rise to fill both bands (use it for a standalone leading column such as "Name"). The groups must cover every column exactly (the sum of their spans must equal the column count, else PdfException). A group cell inherits the header style and can override it with ->fill(), ->textColor(), ->bold(), and ->align(). The band repeats with the header on each continuation page when withRepeatHeader(true) (the default) is set.

use DragonOfMercy\PhpPdf\Table\ColumnGroup;

$style = TableStyle::default()
    ->withHeader(fill: Color::gray(238), bold: true)
    ->withColumnGroups(
        ColumnGroup::spacer(),                          // the "Name" column rises across both bands
        ColumnGroup::of('Q1', 3)->fill(Color::gray(220)),
        ColumnGroup::of('Q2', 3)->fill(Color::gray(220)),
    );
// columns: name (40), jan, feb, mar, apr, may, jun (20 each) -> spacer(1) + 3 + 3 = 7 columns

TableStyle

Build from TableStyle::default() and chain with* methods. All methods return a new immutable TableStyle.

Method Description
->withHeader(?Color $fill, bool $bold, ?Color $textColor) Header row appearance.
->withBorder(TableBorders $borders) Border preset (see below).
->withBorderStyle(Border $b) Line appearance (width, color, style) applied to all drawn borders.
->withZebra(Color $even, Color $odd) Alternate row fills. Even rows (0, 2, ...) use $even; odd rows use $odd.
->withRepeatHeader(bool $repeat) Whether the header row is re-drawn at the top of each continuation page. Default: true.
->withCellStyle(callable $fn) Per-cell conditional styling callback (see below).
->withRowPadding(CellPadding $p) Default padding applied to every data cell.

TableBorders

Case Effect
GRID All cell edges drawn (default).
HORIZONTAL Horizontal rules between rows only.
HEADER_UNDERLINE Single rule under the header row only.
NONE No borders.

Conditional cell styling (withCellStyle)

The callable receives (mixed $value, array $row, Column $col) and must return CellStyle|null. A non-null return overrides style for that cell only:

->withCellStyle(function (mixed $value, array $row, Column $col): ?CellStyle {
    if ($col->key === 'status' && $value === 'overdue') {
        return CellStyle::new()->withFill(Color::rgb(255, 220, 220))->withBold(true);
    }
    return null;
})

CellStyle::new() is immutable and supports ->withTextColor(Color), ->withFill(Color), ->withBold(bool), and ->withAlign(TextAlign).

Automatic page-break and header repeat

When Document::setAutoPageBreak(true) is active, a new page is started automatically whenever a row does not fit the remaining space. The header row is re-drawn on the new page unless ->withRepeatHeader(false) is set.

Limitations

  • No vertical cell spanning: rowspan is not supported (horizontal colSpan and grouped headers are - see above).
  • No content-derived auto-width: every column must have an explicit ->width() or ->fill() policy.

See also

Clone this wiki locally