helgesverre/toon
TOON is a compact, human-readable data format that cuts token usage when sending structured data to LLMs. This PHP port offers simple encode and decode methods, works on PHP 8.1+, and supports arrays, objects, and primitives.
TOON is a single-purpose package: it converts PHP arrays into a compact, LLM-friendly text format and back. There is no service provider, no facade alias, no config file to publish before your first call — you install it and use the static Toon class.
composer require helgesverre/toon
use HelgeSverre\Toon\Toon;
$payload = User::query()->get(['id', 'name', 'plan'])->toArray();
$toon = Toon::encode($payload);
$data = Toon::decode($toon); // back to PHP arrays
The first use case to reach for is prompt stuffing: anywhere you currently interpolate json_encode($rows) into a system or user message, swap in Toon::encode($rows) and measure the delta. This is especially relevant in Laravel apps that use openai-php/laravel, laravel/ai, Prism, or raw HTTP calls to Anthropic/OpenAI — the integration point is always the same, a string in the message content.
Where to look first, in order:
A reasonable day-one deliverable: a ToonPromptFormatter service, a php artisan toon:preview {model} command that dumps before/after token estimates, and one A/B test against your current JSON prompts.
Wrap the static calls, don't scatter them. Toon::encode() in twelve controllers becomes unmaintainable. Centralise:
final class LlmPayload
{
public static function fromCollection(Collection $rows): string
{
return Toon::encode($rows->map(fn ($r) => Arr::only($r->toArray(), [
'id', 'name', 'plan',
]))->all());
}
}
Column projection matters more than the format does — dropping unused keys is usually a bigger token win than the encoding itself.
Use it for context, not for transport. Encode DB results, config dumps, API responses, or tool results you are feeding to a model. Never use TOON as the body of an HTTP request to a third-party API; JSON remains the wire format everywhere except inside the prompt.
Round-trip for structured output. If you ask the model to reply in TOON, you get a compact response you can Toon::decode() into an array and validate with Laravel's validator. This is the highest-leverage pattern for extraction tasks:
$reply = $client->chat(/* ... */);
$rows = Toon::decode($reply); // array
Validator::make(['rows' => $rows], [
'rows' => ['array'],
'rows.*.id' => ['required', 'integer'],
])->validate();
Pair this with the package's validation helper (see the README's Validating TOON section) to reject malformed output before decode() throws.
Token benchmarking as a habit. Do not assume savings. Measure per payload shape:
$json = json_encode($rows, JSON_THROW_ON_ERROR);
$toon = Toon::encode($rows);
// compare with your tokenizer of choice, log the ratio in a Pest test
Keep a small fixture set of your real payloads and assert the ratio stays favourable — TOON shines on arrays of uniform objects and can lose on deeply nested, irregular structures.
Cache aggressively, key on the config. If you cache encoded prompts (queue jobs, memoised tool output), include a hash of the TOON config in the cache key so config changes don't silently serve stale formatting.
Testing. Add a property test asserting Toon::decode(Toon::encode($data)) === $data for your fixture shapes. This catches escaping and type-fidelity surprises early.
Toon::decode().null, and empty arrays vs empty objects are the classic edge cases in any indentation-based format. If you have IDs like "007" or enum-ish strings, round-trip test them explicitly rather than trusting the default.encode/decode plus the config options. That's appropriate for a format library, but it means your application owns concerns like redaction, truncation, and per-model token budgets.composer.json, keep the wrapper class as your seam, and avoid leaking Toon:: calls into domain code so you can swap the encoder if the package stalls.How can I help you explore Laravel packages today?