Modern PHP template engine that provides an intuitive and expressive way to build web application views. It offers a clean syntax using custom HTML attributes and supports advanced templating features like view composition, slots, conditional rendering, loops, and built-in security measures.
PHP ^8.4 is required. Pesto is available via Composer and has no third-party dependencies.
composer require millancore/pesto
use MillanCore\Pesto\PestoFactory;
$pesto = PestoFactory::create([
templatesPath: __DIR__ . '/views',
cachePath: __DIR__ . '/cache',
// [ New CustomFilters(), ... ]
]);
$pesto->make('view.php', ['user' => $user]);
Pesto provides a clean syntax using custom HTML attributes.
It understands the context of {{ variables }} and escapes them to prevent XSS.
Use attributes like php-foreach and php-if
directly in your HTML.
<ul>
<li php-foreach="range(1, 10) as $number"
php-if="$number > 7">
Item {{ $number }}
</li>
</ul>
For greater clarity, use the <template> tag, which
will not be included in the final render.
<ul>
<template php-foreach="range(1, 10) as $number">
<li php-if="$number > 7">Item {{ $number }}</li>
</template>
</ul>
Pesto makes it easy to reuse parts of your views.
The <template> tag allows you to define
php-* attributes that will be evaluated, but the tag itself will not be included in the
final render.
Input
<p php-if="$user->isAdmin()">Admin</p>
Output
<p>Admin</p>
Input
<template php-if="$user->isAdmin()">Admin</template>
Output
Admin
When working with views composed of other views, you can use partials and slots to avoid repetition.
Layout: layouts/app.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ $title }}</title>
</head>
<body>
<header>{{ $header | slot }}</header>
<main>{{ $main | slot }}</main>
</body>
</html>
View: views/home.html
<template php-partial="layouts/app.html" php-with="['title' => 'Home']">
<!-- Named slot -->
<nav php-slot="header">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<!--Main Slot -->
<section>
<h1>Home</h1>
<p>Lorem ipsum...</p>
<section>
</template>
Pesto allows you to nest views, reusing the same layout multiple times in the same view.
<template php-partial="list.html">
<li>Item</li>
<li>
<ul php-partial="list.html">
<li>nested item</li>
....
</ul>
</li>
</template>
Pesto provides foreach and if directives, sufficient for building any view.
Conditionally render blocks. php-elseif and php-else
must be siblings of php-if.
<p php-if="$user->isAdmin()">Admin</p>
<p php-elseif="$user->isModerator()">Moderator</p>
<p php-else>Guest</p>
We can use to render a list of items based on an array or iterable objects.
<li php-foreach="$list as $item">
{{ $item }}
</li>
Combine directives in one single tag.
<ul>
<li php-foreach="$users as $user" php-if="$user->isAdmin()">
{{ $user->name | title }};
</li>
</ul>
Every php-* attribute has a shorter
p:* alias. Both forms work everywhere and can be mixed in the same template.
| Long form | Short form |
|---|---|
| php-if | p:if |
| php-elseif | p:elseif |
| php-else | p:else |
| php-foreach | p:foreach |
| php-partial | p:partial |
| php-with | p:with |
| php-slot | p:slot |
<ul p:if="count($items) > 0">
<li p:foreach="$items as $item">{{ $item }}</li>
</ul>
<p p:else>No items</p>
If an element has both forms of the same directive, the long form
wins. Since no client-side framework claims the p: prefix, it is safe to combine
with Vue, Alpine.js, or Lit bindings.
Pesto ships with a pesto binary (installed
at vendor/bin/pesto) to validate and inspect templates without rendering them.
vendor/bin/pesto help
| Command | Description |
|---|---|
| pesto compile <template_path> | Validate and compile a template, print the result |
| pesto lint <path> [<path>...] | Validate template files or directories |
| pesto help | Show the help message |
Both commands read the template from stdin when no path is given
(or with -).
Compiles a template and prints the resulting PHP, so you can see exactly
what Pesto generates. If the template is invalid, the errors are printed and the command exits
with 1.
echo '<li p:foreach="$items as $item" p:if="$item->visible">{{ $item->name | title }}</li>' | vendor/bin/pesto compile
<?php foreach($items as $item): ?><?php if ($item->visible): ?><li><?= $__pesto->output($item->name, ['title', 'escape']) ?></li><?php endif; ?><?php endforeach; ?>
Validates templates without rendering them: it checks for unclosed
{{ }} expressions, orphan php-else/php-elseif directives,
unprocessed directives, and PHP syntax errors in the compiled output.
# Single files or directories
# (scanned recursively for .html and .php)
vendor/bin/pesto lint views/home.php
vendor/bin/pesto lint views/ emails/
# Or from stdin
echo '<p php-else>Guest</p>' | vendor/bin/pesto lint
✗ <stdin>
- Orphan "php-else" directive on line 1: it must be an immediate sibling of a "php-if" element.
# With --views the linter also verifies that every
# php-partial reference exists in the templates root
vendor/bin/pesto lint --views views/
✓ views/home.php
✓ views/layouts/app.php
✓ views/partials/nav.php
Linted 3 templates: no errors found.
The exit code is 0 when all templates pass and
1 otherwise, so lint fits directly into a CI pipeline.
Apply transformations to variables using the pipe
| operator. You can also create your own.
<p>{{ $text | upper }}</p>
<p>{{ $text | capitalize | truncate:50,... }}</p>
<p>{{ $createAt | date:'m-d-Y' }}</p>
Create a class with public methods and register it.
1. Create a filter class
// CustomFilter.php
#[AsFilter(name: 'truncate')]
public function truncate(
string $value,
int $length,
string $end = '...'
) : string
{
//...
}
2. Register it in the factory
$pesto = PestoFactory::create([
templatesPath: => __DIR__ . '/views',
cachePath: => __DIR__ . '/cache', [
new CustomFilter(),
]
]);
Rendering time comparison — lower is better.
Clone the repo and run the benchmarks on your own machine.
git clone https://github.com/millancore/pesto.git
cd pesto && composer install
composer bench
Generate an HTML chart report with composer bench:chart
PHPBench — 100 iterations, 10 revolutions, 5 warmup runs. All engines with file caching enabled.
Pesto can be easily integrated with your favorite framework.