dekor/php-array-table
dekor/php-array-table is a lightweight PHP library that renders arrays as clean, customizable text tables. It is useful for CLI output, logs, reports, and debugging, with support for headers, column alignment, and simple styling—no heavy dependencies required.
composer require dekor/php-array-table
That is the entire setup. This is not a Laravel package — there is no service provider, no facade, no publishable config, and nothing to register in config/app.php. It is a single namespaced class with a static builder, so Composer's autoloader is all the wiring you need.
The first call looks like this:
use dekor\ArrayToTable;
$rows = [
['id' => 1, 'name' => 'Ada', 'role' => 'admin'],
['id' => 2, 'name' => 'Linus', 'role' => 'user'],
['id' => 3, 'name' => 'Grace', 'role' => 'admin'],
];
echo ArrayToTable::build($rows);
Which renders a +---+---+ bordered grid with a header row taken from the associative keys and one line per record. The second argument lets you restrict/reorder the visible columns (ArrayToTable::build($rows, ['id', 'name'])).
Where to look first: vendor/dekor/php-array-table/src/ArrayToTable.php. It's a few hundred lines of string concatenation and is the real documentation — read it before you depend on option names or alignment behaviour, because the README is thin and the project has been effectively dormant since its Feb 2023 release.
First use case: php artisan tinker, build a query result, and paste the output into a Slack thread or a bug ticket. It turns a wall of dd() output into something a PM can read without a screen share. That's 90% of what this class is good for.
Ad-hoc inspection in Tinker or a throwaway route. The class works directly on arrays, so the Eloquent gotcha is just remembering ->toArray():
use dekor\ArrayToTable;
$rows = User::query()
->latest()
->limit(10)
->get(['id', 'name', 'email'])
->toArray();
echo ArrayToTable::build($rows);
Normalise before you render. The class casts every cell to a string; it does not understand Carbon, enums, money, or relations. Flatten explicitly so the table stays predictable:
$rows = Order::with('customer')->get()->map(fn ($o) => [
'id' => $o->id,
'customer' => $o->customer->name,
'status' => $o->status->value,
'total' => number_format($o->total_cents / 100, 2),
'placed' => $o->created_at->toDateString(),
])->all();
Wrap it behind your own helper. Because there's no container binding or interface, every call site is coupled to dekor\ArrayToTable. A five-line wrapper in app/Support gives you one place to swap in Symfony Console's Table, league/CLImate, or plain json_encode(..., JSON_PRETTY_PRINT) when you inevitably want something else:
namespace App\Support;
class AsciiTable
{
public static function render(array $rows, array $columns = []): string
{
return \dekor\ArrayToTable::build($rows, $columns);
}
}
Blade output. Always <pre> plus escaping, otherwise the border characters collapse into unreadable prose and any < in a value becomes an XSS vector:
<pre>{{ \App\Support\AsciiTable::render($rows) }}</pre>
Artisan commands and log files. It works ($this->line(AsciiTable::render($rows))), but reach for $this->table($headers, $rows) first inside commands — Symfony Console's table is already there, handles wrapping, and prints with correct terminal widths. Use this package when you need the string rather than the terminal write: writing a snapshot into a log entry, an email body, or a test assertion.
Tests. Asserting on the built string is brittle but occasionally useful for golden-file style tests of report output. Keep the fixture small and assert on a substring rather than the full grid.
mb_strlen. Check the source before shipping anything user-facing with non-ASCII names; if it uses strlen, either fork it or pre-pad with your own mb_str_pad helper.notes or description field with a \n produces a row that's taller than one line and desynchronises every subsequent border. str_replace(["\r", "\n"], ' ', $value) everything first.$this->error() style helpers, or symfony/console tags), the width math breaks. This library is plain text only.email and row 2 not means row 2 renders short. Normalise with array_replace($template, $row) or build every row from the same array shape.roles array becomes Array (or throws an "Array to string conversion" warning). Join relations to a comma string upstream.1 / empty. No true/false or null placeholder — cast them yourself if the table is going to a human.Str::limit($value, 40) at the mapping step.build() is typed against arrays; an Eloquent Collection will either fatal or be coerced unpredictably depending on the version. ->toArray() or ->all() always.composer.json, don't expect upstream fixes for the multibyte issue, and be ready to fork the single class into app/Support — it's small enough that vendoring it is genuinely the lowest-risk option for anything beyond debug output.<pre> block, or a pasted ticket. If you need machine-readable output, json_encode is the answer.How can I help you explore Laravel packages today?