Loops
Loops allow you to iterate over arrays and generate repeated content. Tsuku provides the @for directive for all looping needs.
Basic @for Loop
Iterate over an array:
$template = '@for(items as item)
- {item}
@end';
$data = [
'items' => ['Apple', 'Banana', 'Cherry'],
];
$result = $tsuku->process($template, $data);
// Output:
// - Apple
// - Banana
// - CherryLooping Over Objects
Loop over arrays of arrays, arrays of objects, or associative arrays:
$template = '@for(products as product)
Product: {product.name}
Price: ${product.price}
---
@end';
$data = [
'products' => [
['name' => 'Widget', 'price' => 29.99],
['name' => 'Gadget', 'price' => 39.99],
],
];
$result = $tsuku->process($template, $data);
// Output:
// Product: Widget
// Price: $29.99
// ---
// Product: Gadget
// Price: $39.99
// ---What Can Be Looped Over
@for(collection as item) takes a variable path - products, order.items, category.products (a getter such as getProducts() works too). The value must be a PHP array:
- a list or an associative array is iterated; its items can be arrays, objects or scalars
- a missing collection,
null, a string, a generator, anArrayIteratoror a collection object renders nothing - convert it first withiterator_to_array()or the object'stoArray() - a function call is not allowed as the collection (
@for(@split(tags) as tag)is aParseException) - prepare the array in your data
$template = '[@for(items as item){item}@end]';
echo $tsuku->process($template, ['items' => new ArrayIterator([1, 2])]);
// Output: []
echo $tsuku->process($template, ['items' => iterator_to_array(new ArrayIterator([1, 2]))]);
// Output: [12]For collections too large to hold in memory, render them row by row with processToStream(), which accepts any iterable.
Loop with Index/Key
Access both the value and the key (value comes first, then key):
$template = '@for(items as item, index)
{index}. {item}
@end';
$data = [
'items' => ['Apple', 'Banana', 'Cherry'],
];
$result = $tsuku->process($template, $data);
// Output:
// 0. Apple
// 1. Banana
// 2. CherryThe second name receives the array key, so a list counts from 0.
Associative Arrays
With associative arrays, the key is the array key:
$template = '@for(prices as price, sku)
SKU {sku}: ${price}
@end';
$data = [
'prices' => [
'WID-001' => 29.99,
'GAD-002' => 39.99,
],
];
$result = $tsuku->process($template, $data);
// Output:
// SKU WID-001: $29.99
// SKU GAD-002: $39.99Nested Loops
Loop within loops:
$template = '@for(categories as category)
Category: {category.name}
@for(category.products as product)
- {product.name}: ${product.price}
@end
@end';
$data = [
'categories' => [
[
'name' => 'Electronics',
'products' => [
['name' => 'Phone', 'price' => 699],
['name' => 'Laptop', 'price' => 1299],
],
],
[
'name' => 'Books',
'products' => [
['name' => 'Novel', 'price' => 15],
['name' => 'Guide', 'price' => 25],
],
],
],
];
$result = $tsuku->process($template, $data);
// Output:
// Category: Electronics
// - Phone: $699
// - Laptop: $1299
// Category: Books
// - Novel: $15
// - Guide: $25Loops with Conditionals
Combine loops with conditions:
$template = '@for(products as product)
@if(product.stock > 0)
{product.name}: ✓ In Stock ({product.stock})
@else
{product.name}: ✗ Out of Stock
@end
@end';
$data = [
'products' => [
['name' => 'Widget', 'stock' => 10],
['name' => 'Gadget', 'stock' => 0],
['name' => 'Tool', 'stock' => 5],
],
];
$result = $tsuku->process($template, $data);
// Output:
// Widget: ✓ In Stock (10)
// Gadget: ✗ Out of Stock
// Tool: ✓ In Stock (5)CSV Generation
Perfect for CSV exports:
$template = 'SKU,Name,Price,Stock
@for(products as product)
@csv(product.sku),@csv(product.name),@number(product.price, 2),{product.stock}
@end';
$data = [
'products' => [
['sku' => 'WID-001', 'name' => 'Widget, Premium', 'price' => 29.99, 'stock' => 100],
['sku' => 'GAD-002', 'name' => 'Basic Gadget', 'price' => 19.50, 'stock' => 50],
],
];
$result = $tsuku->process($template, $data);
// Output:
// SKU,Name,Price,Stock
// WID-001,"Widget, Premium",29.99,100
// GAD-002,Basic Gadget,19.50,50XML Generation
Generate XML elements:
$template = '<?xml version="1.0"?>
<products>
@for(products as product)
<product id="{product.id}">
<name>@xml(product.name)</name>
<price>{product.price}</price>
</product>
@end
</products>';
$data = [
'products' => [
['id' => 1, 'name' => 'Widget & Gadget', 'price' => 29.99],
['id' => 2, 'name' => 'Premium Tool', 'price' => 49.99],
],
];
$result = $tsuku->process($template, $data);
// Output:
// <?xml version="1.0"?>
// <products>
// <product id="1">
// <name>Widget & Gadget</name>
// <price>29.99</price>
// </product>
// <product id="2">
// <name>Premium Tool</name>
// <price>49.99</price>
// </product>
// </products>Empty Arrays
Empty arrays produce no output:
$template = 'Products:
@for(products as product)
- {product.name}
@end
Done';
$data = ['products' => []];
$result = $tsuku->process($template, $data);
// Output:
// Products:
// DoneWhitespace and Indentation
Lines that hold only directives (@for(...), @if(...), @end, ...) produce no output - not their indentation, not their line break. Indent them like the markup around them; content lines keep exactly the indentation you give them:
$template = '<ul>
@for(categories as category)
<li>{category.name}
<ul>
@for(category.products as product)
<li>{product.name}</li>
@end
</ul>
</li>
@end
</ul>';
$data = [
'categories' => [
['name' => 'Electronics', 'products' => [['name' => 'Phone'], ['name' => 'Laptop']]],
['name' => 'Books', 'products' => [['name' => 'Novel']]],
],
];
echo $tsuku->process($template, $data);Output:
<ul>
<li>Electronics
<ul>
<li>Phone</li>
<li>Laptop</li>
</ul>
</li>
<li>Books
<ul>
<li>Novel</li>
</ul>
</li>
</ul>A loop can also sit inside a line. The body of a @for is never trimmed, so it renders exactly as written:
$template = 'Tags: @for(tags as tag)[{tag}]@end
Done';
echo $tsuku->process($template, ['tags' => ['sale', 'new']]);
// Output:
// Tags: [sale][new]
// DoneThe full rules are in Whitespace Control.
Variables Inside a Loop
The loop body sees all your data plus the loop variables. A loop variable with the same name as an outer value hides it inside the loop only:
$template = '@for(items as name)
{name} {greeting}
@end
After: {name}';
$data = ['items' => ['Ann', 'Bob'], 'name' => 'outer', 'greeting' => 'says hi'];
echo $tsuku->process($template, $data);
// Output:
// Ann says hi
// Bob says hi
// After: outerPerformance Considerations
- Large datasets: Tsuku processes templates in a single pass; for exports with hundreds of thousands of rows use
processToStream()so only one row is in memory at a time - Deep nesting: Unlimited nesting depth, but keep reasonable for readability
- Object creation: If looping thousands of objects with getters, consider caching
Common Patterns
Templates have no arithmetic and no loop.first / loop.last helpers. The patterns below cover the usual needs with the array key, a little preparation in PHP, or a custom function.
Numbered List
{index + 1} is not supported - there is no arithmetic in templates (it is printed as literal text). Re-key the list so it starts at 1:
$template = '@for(items as item, number)
{number}. {item}
@end';
// Keys 1, 2, 3 - from a plain list: array_combine(range(1, count($list)), $list)
$data = ['items' => [1 => 'Apple', 2 => 'Banana', 3 => 'Cherry']];
echo $tsuku->process($template, $data);
// Output:
// 1. Apple
// 2. Banana
// 3. CherryOr register a small function and keep the list as it is:
$tsuku->registerFunction('position', fn($index) => $index + 1);
$template = '@for(items as item, index)
@position(index). {item}
@end';
echo $tsuku->process($template, ['items' => ['Apple', 'Banana']]);
// Output:
// 1. Apple
// 2. BananaFirst and Last Item
The key of a list tells you the first item:
$template = '@for(items as item, index)
@if(index == 0)
* {item} (first)
@else
- {item}
@end
@end';
echo $tsuku->process($template, ['items' => ['Apple', 'Banana', 'Cherry']]);
// Output:
// * Apple (first)
// - Banana
// - CherryFor the last item, number the list from 1 (as above) and compare with @length():
$template = '@for(items as item, number)
{item}@?{number == @length(items) "." : ","}
@end';
$data = ['items' => [1 => 'Apple', 2 => 'Banana', 3 => 'Cherry']];
echo $tsuku->process($template, $data);
// Output:
// Apple,
// Banana,
// Cherry.Comma-Separated List
$template = '@join(items, ", ")';
// Or manually:
$template = '@for(items as item, index)@if(index > 0), @end{item}@end';The manual version puts the separator in front of every item except the first (index > 0), so it never needs to know which item is last.
Lists only
index > 0 relies on the keys 0, 1, 2, .... With string keys it is true for the first item too ("sku-1" > 0 is true in PHP 8), so pass array_values($items) - or use @join().
Separators Between JSON Objects
The same leading-separator idea builds valid JSON arrays - the comma is printed before every object except the first:
$template = '{
"items": [@for(items as item, index)@if(index > 0),@end
{"name": @json(item)}@end
]
}';
echo $tsuku->process($template, ['items' => ['Apple', 'Banana', 'Cherry']]);Output:
{
"items": [
{"name": "Apple"},
{"name": "Banana"},
{"name": "Cherry"}
]
}@json() returns the complete quoted JSON string, so it is not wrapped in quotes. See JSON APIs for larger examples.
Table Rows
$template = '<table>
@for(products as product)
<tr>
<td>{product.name}</td>
<td>$@number(product.price, 2)</td>
</tr>
@end
</table>';
$data = [
'products' => [
['name' => 'Widget', 'price' => 29.99],
['name' => 'Gadget', 'price' => 1299.5],
],
];
echo $tsuku->process($template, $data);Output:
<table>
<tr>
<td>Widget</td>
<td>$29.99</td>
</tr>
<tr>
<td>Gadget</td>
<td>$1,299.50</td>
</tr>
</table>Next Steps
- Conditionals → - Add logic to loops
- Pattern Matching → - Match values in loops
- Built-in Functions → - Transform loop data