Examples
Practical templates for the formats Tsuku is used for most: CSV and XML feeds, JSON responses, HTML fragments and emails, configuration files and e-commerce documents. Every example on these pages is real: the template, the data and the output are files, and the output is produced by running the template with Tsuku 1.4.
How the examples work
Each example has four tabs:
| Tab | What it shows |
|---|---|
| Template | The template file, template.<format>.tsuku, exactly as Tsuku reads it |
| Data | data.php, which returns the $data array |
| Output | What the template renders with that data - generated, never edited by hand |
| PHP | The call: echo $tsuku->process($template, $data);, plus any function the example registers |
Run one yourself with a script like this:
<?php
use Qoliber\Tsuku\Tsuku;
require 'vendor/autoload.php';
$tsuku = new Tsuku();
$template = file_get_contents('template.csv.tsuku'); // the Template tab
$data = require 'data.php'; // the Data tab
echo $tsuku->process($template, $data); // prints the Output tabDates are formatted in UTC. The sources of all examples are in snippets/examples of the documentation repository; scripts/render-examples.php renders them, and the site is not published when an output no longer matches its template.
Example pages
CSV Exports →
A product catalog with escaping and line continuation, a Magento import file, orders with line items, a stock report, optional columns, a semicolon-separated file and an export that is safe to open in Excel.
XML Generation →
A Google Shopping feed, a sitemap with language alternates, an RSS feed with CDATA content, a SOAP request, an SVG chart and a feed streamed row by row.
JSON APIs →
A paginated product list, an order webhook, JSON-LD product data, a validation error response and a nested category tree.
HTML Generation →
A product grid, customer reviews, an order history table, an order confirmation email and a checkout form - each with a rendered preview.
Configuration Files →
Application settings in YAML, Docker Compose, a Kubernetes deployment, a .env file, an nginx server block and a Cargo.toml.
E-commerce →
A fixed-width invoice, a shipping label, a marketplace feed, an abandoned-cart email and a price list per customer group.
Quick examples
CSV export
@csv() quotes a value only when it needs it: Wholesale, EU contains a comma. @?{...} turns a boolean into yes / no.
email,firstname,lastname,group,newsletter
@for(customers as customer)
@csv(customer.email),@csv(customer.firstname),@csv(customer.lastname),@csv(customer.group),@?{customer.subscribed "yes" : "no"}
@end<?php
return [
'customers' => [
['email' => '[email protected]', 'firstname' => 'Anna', 'lastname' => 'de Vries', 'group' => 'Retail', 'subscribed' => true],
['email' => '[email protected]', 'firstname' => 'Lena', 'lastname' => 'Lindner', 'group' => 'Wholesale, EU', 'subscribed' => false],
],
];email,firstname,lastname,group,newsletter
[email protected],Anna,de Vries,Retail,yes
[email protected],Lena,Lindner,"Wholesale, EU",noecho $tsuku->process($template, $data);XML product feed
@xml() escapes the & in the product name. The @match starts after <stock> and its @end is followed by </stock>, so it is an inline block: the line breaks and indentation around each case are dropped and the value lands between the tags.
<?xml version="1.0" encoding="UTF-8"?>
<products>
@for(products as product)
<product sku="@xml(product.sku)">
<name>@xml(product.name)</name>
<price currency="EUR">@number(product.price, 2, ".", "")</price>
<stock>@match(product.qty)
@case(0)
out_of_stock
@default
in_stock
@end</stock>
</product>
@end
</products><?php
return [
'products' => [
['sku' => 'TEA-KYU-350', 'name' => 'Kyusu Teapot 350 ml', 'price' => 49.00, 'qty' => 12],
['sku' => 'TEA-MAT-SET', 'name' => 'Matcha Whisk & Bowl Set', 'price' => 29.90, 'qty' => 0],
],
];<?xml version="1.0" encoding="UTF-8"?>
<products>
<product sku="TEA-KYU-350">
<name>Kyusu Teapot 350 ml</name>
<price currency="EUR">49.00</price>
<stock>in_stock</stock>
</product>
<product sku="TEA-MAT-SET">
<name>Matcha Whisk & Bowl Set</name>
<price currency="EUR">29.90</price>
<stock>out_of_stock</stock>
</product>
</products>echo $tsuku->process($template, $data);JSON response
@json() prints a complete JSON string, quotes included, so it is not wrapped in "...". The array uses a separator in front of every item except the first (@if(index > 0), @end), because JSON allows no trailing comma.
{
"id": {customer.id},
"name": @json(customer.name),
"email": @json(customer.email),
"newsletter": @?{customer.subscribed "true" : "false"},
"groups": [@for(customer.groups as group, index)@if(index > 0), @end@json(group)@end]
}<?php
return [
'customer' => [
'id' => 4711,
'name' => 'Anna "Annie" de Vries',
'email' => '[email protected]',
'subscribed' => true,
'groups' => ['Retail', 'Tea Club'],
],
];{
"id": 4711,
"name": "Anna \u0022Annie\u0022 de Vries",
"email": "[email protected]",
"newsletter": true,
"groups": ["Retail", "Tea Club"]
}echo $tsuku->process($template, $data);Indented conditions
Lines that hold only directives print nothing - not even their indentation - so directives can be indented like the text they control. Each content line keeps its own indentation. See Whitespace Control.
@for(orders as order)
Order #{order.increment_id}
@if(order.status == "complete")
Delivered on @date("j M", order.delivered_at)
@elseif(order.tracking)
On its way, tracking number {order.tracking}
@else
Being packed
@end
@end<?php
return [
'orders' => [
['increment_id' => '000002154', 'status' => 'complete', 'delivered_at' => strtotime('2026-10-02'), 'tracking' => 'JVGL0611112222'],
['increment_id' => '000002203', 'status' => 'processing', 'tracking' => 'JVGL0612345678'],
['increment_id' => '000002207', 'status' => 'processing', 'tracking' => null],
],
];Order #000002154
Delivered on 2 Oct
Order #000002203
On its way, tracking number JVGL0612345678
Order #000002207
Being packedecho $tsuku->process($template, $data);A registered function
Formatting that the built-in functions do not cover goes into a PHP function registered under a name; the template calls it like any other function. See Custom Functions.
@for(orders as order)
#{order.increment_id} @money(order.grand_total, order.currency)
@end<?php
return [
'orders' => [
['increment_id' => '000002203', 'grand_total' => 1108.70, 'currency' => 'EUR'],
['increment_id' => '000002204', 'grand_total' => 29.90, 'currency' => 'GBP'],
['increment_id' => '000002205', 'grand_total' => 64.00, 'currency' => 'CHF'],
],
];#000002203 1.108,70 €
#000002204 £29.90
#000002205 CHF 64.00<?php
$tsuku->registerFunction('money', static fn (mixed $amount, string $currency = 'EUR'): string => match ($currency) {
'EUR' => number_format((float) $amount, 2, ',', '.') . ' €',
'GBP' => '£' . number_format((float) $amount, 2),
default => $currency . ' ' . number_format((float) $amount, 2),
});
echo $tsuku->process($template, $data);Using Tsuku in an application
Keep templates in files next to the code that fills them, and build plain arrays in PHP: the template only places values, it does not query anything.
Magento 2
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Qoliber\Tsuku\Tsuku;
class ProductCsvExport
{
public function __construct(
private readonly Tsuku $tsuku,
private readonly CollectionFactory $productCollectionFactory,
) {
}
public function export(): string
{
$collection = $this->productCollectionFactory->create()
->addAttributeToSelect(['name', 'description', 'price'])
->addAttributeToFilter('status', 1);
$products = [];
foreach ($collection as $product) {
$products[] = [
'sku' => $product->getSku(),
'name' => $product->getName(),
'description' => $product->getDescription(),
'price' => (float) $product->getPrice(),
'qty' => (int) $product->getExtensionAttributes()->getStockItem()?->getQty(),
];
}
// the product catalog template from the CSV Exports page
$template = file_get_contents(__DIR__ . '/templates/products.csv.tsuku');
return $this->tsuku->process($template, ['products' => $products]);
}
}The template is the product catalog from the CSV page. For catalogs with many thousands of products, render with processToStream() instead.
Laravel
use Illuminate\Http\Response;
use Qoliber\Tsuku\Tsuku;
class SitemapController
{
public function __invoke(Tsuku $tsuku): Response
{
$pages = Page::query()->where('published', true)->get()
->map(fn (Page $page) => [
'loc' => url($page->path),
'updated_at' => $page->updated_at->getTimestamp(),
'alternates' => $page->translations->pluck('url', 'locale')->all(),
])
->all();
$xml = $tsuku->process(file_get_contents(resource_path('feeds/sitemap.xml.tsuku')), ['pages' => $pages]);
return response($xml, 200, ['Content-Type' => 'application/xml; charset=UTF-8']);
}
}The template is the sitemap from the XML page.
Next Steps
- Getting Started → - install Tsuku and render a first template
- Whitespace Control → - why indented templates render clean output
- Built-in Functions → - escaping, numbers, dates, lists