Github

Pesto - Expressive Templates

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.

pesto

Installation & Usage

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]);

Clean & Expressive Syntax

Pesto provides a clean syntax using custom HTML attributes. It understands the context of {{ variables }} and escapes them to prevent XSS.

Intuitive Attributes

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>

Clarity with <template>

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>

View Composition

Pesto makes it easy to reuse parts of your views.

The <template> Tag

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

Partials & Slots

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>

Nested Views

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>

Control Flow

Pesto provides foreach and if directives, sufficient for building any view.

If Attribute

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>

Loop

We can use to render a list of items based on an array or iterable objects.

<li php-foreach="$list as $item">
    {{ $item }}
</li>

Inline

Combine directives in one single tag.

<ul>
  <li php-foreach="$users as $user" php-if="$user->isAdmin()">
      {{ $user->name | title }};
  </li>
</ul>

Short Syntax

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-ifp:if
php-elseifp:elseif
php-elsep:else
php-foreachp:foreach
php-partialp:partial
php-withp:with
php-slotp: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.

Command Line

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 helpShow the help message

Both commands read the template from stdin when no path is given (or with -).

Compile

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; ?>

Lint

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.

Filters

Apply transformations to variables using the pipe | operator. You can also create your own.

Usage

<p>{{ $text | upper }}</p>

Chain Filters

<p>{{ $text | capitalize | truncate:50,... }}</p>

Filters with Arguments

<p>{{ $createAt | date:'m-d-Y' }}</p>

Built-in Filters

  • raw: Prevents escaping.
  • String: upper, lower, capitalize, title, trim, nl2br, strip_tags, slug, join.

Add Filters

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(),
    ]
]);

Benchmarks

Rendering time comparison — lower is better.

Pesto
Blade
Twig

Simple Template

28.4μs
Pesto
32.3μs
Blade
14.8μs
Twig

Loop (100 items)

316.4μs
Pesto
470.6μs
Blade
779.5μs
Twig

Conditional (50 items)

101.2μs
Pesto
200.4μs
Blade
225.3μs
Twig

Partial

65.8μs
Pesto
63.9μs
Blade
23.8μs
Twig

Peak Memory Usage

2.13 MB
Pesto
3.61 MB
Blade
2.74 MB
Twig

Run It Yourself

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.

How to use with

Pesto can be easily integrated with your favorite framework.