PHP Application Driver - An Inversion of Control Template Engine
PAD is a PHP template engine that inverts the traditional web application architecture. Instead of PHP code including templates, PAD templates drive the execution flow, seamlessly integrating data access, control structures, and presentation in a unified template syntax.
Requirements: PHP 8.3+ (padCurl sets CURLOPT_PROTOCOLS_STR, which PHP added in 8.3)
Traditional PHP frameworks follow a “code-first” approach where PHP scripts control the flow and include template fragments. PAD inverts this paradigm: templates are first-class citizens that orchestrate everything from data retrieval to output generation. This “template-first” approach creates a natural separation where the visual structure of the application mirrors its logical structure.
{tags}Every page consists of two files that PAD automatically pairs:
pagename.php - Returns data (variables, arrays)pagename.pad - Template that renders the dataA pagename.html file is a template exactly like a .pad: it pairs with the .php and PAD
tags resolve in it, so an existing html file dropped in the application directory is a page.
When both a .pad and an .html exist, the .pad wins.
Pages are accessed via query string:
/myapp/ → index.pad/myapp/?about → about.pad/myapp/?admin/users → admin/users.padPAD uses a hierarchical directory structure for inheritance:
APP/
├── _guard.php # Access check for every page below (FALSE answers 403)
├── _inits.pad # Initialization template (wraps all pages)
├── _inits.php # Initialization code (runs before pages)
├── _exits.pad # Exit template (wraps all pages)
├── _exits.php # Exit code (runs after pages)
├── _lib/ # Library files (auto-included)
│ └── helpers.php
├── index.pad # Homepage template
├── index.php # Homepage data
└── admin/
├── _inits.pad # Admin section wrapper
├── dashboard.pad # Inherits from parent _inits.pad
└── dashboard.php
Child directories automatically inherit parent templates. The _inits.pad files wrap content like layouts.
apps/myapp/www/myapp/index.php:<?php
include __DIR__ . '/../pad.php';
?>
www/pad.php detects the platform, derives the app name from the request URL, and includes pad/pad.php, which defines the framework constants (PAD, APP, APPS, DATA, COMMON).
index.php:
<?php
$message = 'Hello World!';
$items = ['Apple', 'Banana', 'Cherry'];
?>
index.pad:
<html>
<head><title>Hello</title></head>
<body>
<h1>{$message}</h1>
<ul>
{items}
<li>{$items}</li>
{/items}
</ul>
</body>
</html>
{# dropped before the template is scanned #}
{-- the same, in the form the editor kits toggle --}
{$simple} {# Simple variable #}
{$user.name} {# Object/array property #}
{$items.0} {# Array element by key - a dotted path #}
{!text} {# Raw - skips the sanitize chain #}
{$value | default('N/A')} {# Default value #}
Transform values with pipe functions:
{echo $name | upper} {# Uppercase #}
{echo $text | trim | lower} {# Chain multiple functions #}
{echo $date | date('Y-m-d')} {# With parameters #}
{echo $value | + 1} {# Arithmetic (space required) #}
{echo $text | after('@')} {# String manipulation #}
{echo $html | html} {# Escape HTML #}
{echo $name | contains('admin')} {# String contains #}
A field tag pipes as it stands - {$var | upper} - and a literal goes through {echo}: {echo 'text' | upper}.
Common functions: trim, upper, lower, html, url, date, replace, left, right, contains, in, between, exists
Conditionals:
{if $condition}
Content if true
{elseif $other}
Alternative
{else}
Default
{/if}
{case $status}
{when 'active'}Active
{when 'pending'}Pending
{else}Unknown
{/case}
Loops:
{users} {# Iterate data tag #}
<li>{$name} - {$email}</li>
{/users}
{while $i le 10} {# While loop #}
Item {$i}
{increment $i}
{/while}
{files 'images', base='app', mask='*.jpg'} {# _data, images ... of the application #}
<li>{$file}</li>
{/files}
Loop Control:
{continue 'tagname'} # Skip to next iteration
{cease 'tagname'} # Soft stop (graceful end)
{break 'tagname'} # Hard stop (immediate exit)
Access iteration state with property@tag syntax - as a tag pair (content renders only when
the property is true), in a ternary, or as a value in any condition ({if first@items}):
{items}
{first@items}<ul>{/first@items}
<li class="{odd@items ? odd : even}">{$name}</li>
{last@items}</ul>{/last@items}
{/items}
Properties: first@tag, last@tag, even@tag, odd@tag, current@tag, count@tag, remaining@tag, key@tag, data@tag
Control tag behavior with options:
{users sort='name', first=10} {# Sort and limit - options are comma-separated #}
{products sort='price DESC', page=1, rows=20}
{items shuffle, first=5} {# Random selection #}
{users sort='name', toData='sorted'} {# Data operations - the sorted rows as {sorted} #}
{data 'colors'}
["red", "green", "blue"]
{/data}
{colors}{$colors} {/colors}
Supports JSON, XML, YAML, and CSV formats.
Resolve naming conflicts with explicit type prefixes:
{app:mytag} # App tag from _tags/
{pad:tagname} # Built-in PAD tag
{php:strlen 'abc'} # Call PHP function - {echo $x | php:strlen(@)} in an expression
{data:items} # Defined data block
{pull:mySequence} # Stored sequence
{field "name from users"} # Database single value (the field: prefix reads a variable)
Create _tags/mytag.php in your app:
<?php
$format = padTagParm ( 'format', $padOpt [$pad] [1] ?: 'default' );
return "Output: $format";
?>
Use as {mytag 'value'} or {mytag format='value'}.
A tag with a .pad template is a component: the caller’s content goes to @content@,
{slot 'footer'}...{/slot} pairs in the content fill the template’s named slots - each use
of the tag has its own - and {parms title, tone='info'} declares the parameters, a bare name
required and name=default filled in.
// In PHP
$user = db("RECORD * FROM users WHERE id={0}", [$id]);
$users = db("ARRAY * FROM users ORDER BY name");
$count = db("FIELD COUNT(*) FROM users");
$exists = db("CHECK users WHERE email='{0}'", [$email]);
// In templates (the tag name becomes the db() command word)
{field "count(*) from users"}
{record "* from users where id=5"}
Name: {$name}
{/record}
{array "* from users order by name"}
<tr><td>{$name}</td></tr>
{/array}
// Or declare tables in _lib/select.php ($padSelect) and use them as tags
{users}
{$name} - {$email}
{/users}
See DATABASE.md for the db() command words and the Select subsystem.
120+ mathematical sequences with transformations:
{fibonacci rows=10}{$fibonacci} {/fibonacci}
{sequence '1..10', push='nums'}
{resume add=5}
{pull:nums}{$sequence} {/pull:nums}
See sequences/ for complete documentation.
When a request arrives, PAD is initialized via pad.php:
Request → pad.php → start/pad.php → config/config.php → start/pad/go.php
The entry point:
PAD constant (framework path)APP and DATA constantsThe build system assembles the complete page:
build/build.php
├── build/dirs.php → Create directory hierarchy
├── build/libs.php → Collect library files (build/lib.php)
├── build/base.php → Build template structure
│ ├── _inits.pad files (outer to inner)
│ ├── @page@ placeholder
│ └── _exits.pad files (inner to outer)
└── build/page.php → Process page
├── _inits.php execution
├── page.php execution → returns data
├── page.pad template
└── _exits.php execution
The @page@ placeholder is replaced with the page content, creating a nested structure where parent templates wrap child content.
PAD processes templates through a level-based system. Each tag creates a new level:
{outer} ← Level 0
{inner} ← Level 1
{field} ← Level 2
{/inner}
{/outer}
The level processor (level/level.php):
{ and }When a tag is encountered, PAD determines its type:
| Prefix | Type | Example |
|---|---|---|
$ |
Variable | {$name} |
# |
Option | {#param} |
& |
Property of the current tag | {¤t} |
! |
Raw field (no sanitize chain) | {!snippet} |
? |
Field as a query fragment | {?id} → &id=7 |
^ |
Field as JSON for an attribute | data-props="{^product}" |
@ |
Property | {first@tag} |
| (none) | Tag/Field | {users}, {if} |
Types are resolved in order: app → common → pad → pull → bool → content → select → data → include → property → field → array → parm → level → constant → local → script → php → sequence → action → function
When a tag has data (array), PAD iterates through each item:
{users} ← Data: [{name: 'A'}, {name: 'B'}]
<li>{$name}</li> ← Occurrence 1: name = 'A'
← Occurrence 2: name = 'B'
{/users}
The occurrence system (occurrence/) manages:
current@tag)Expressions in templates are parsed and evaluated by the eval subsystem:
{echo $price * 1.1 | round(@, 2)}
Evaluation pipeline:
Operator precedence: ** → ! and a sign before a value (-$x, -(1 + 2); -$x ** 2 is -($x ** 2)) → * / % → + - → . → lt le gt ge → eq ne → not → and → xor → or → ??, the ternary weakest; one group applies left to right, ** right to left
After processing, PAD generates the final output:
Template Processing → $padResult → $padOut → Output Buffer → Response
pad/
├── pad.php # Main entry point
│
├── start/ # Execution lifecycle
│ ├── page.php ... # Entry points (page, code, ajax, redirect, restart)
│ ├── pad/ # The run of one pass - go.php, level.php
│ ├── start/ # Initialization phase
│ └── end/ # Termination phase
│
├── build/ # Page assembly
│ ├── build.php # Main build orchestrator
│ ├── base.php # Template structure
│ ├── page.php # Page processing
│ └── libs.php # Library collection
│
├── level/ # Tag processing
│ ├── level.php # Main level processor
│ ├── setup.php # Level initialization
│ ├── parms/ # Parameter parsing
│ └── pipes/ # Pipe operations
│
├── occurrence/ # Data iteration
│ ├── occurrence.php # Main occurrence handler
│ ├── init.php # Occurrence initialization
│ ├── set.php # Variable setup
│ └── end.php # Occurrence finalization
│
├── walk/ # Tree walking
│ ├── next.php # Next iteration
│ └── end.php # Level completion
│
├── eval/ # Expression evaluation
│ ├── eval.php # Main evaluator (scanner, parser and evaluator in lib/eval/)
│ ├── parms/ # Calls with arguments - built-in, app, php:, tag, sequence
│ └── single/ # Lookups without arguments - field:, data:, array: ...
│
├── types/ # Tag type handlers (25)
├── tags/ # Template tags (90+)
├── functions/ # Pipe functions (55)
├── options/ # Tag options (25)
├── handling/ # Data handling (28 handlers)
├── properties/ # Tag properties (26 properties)
├── constructs/ # Special constructs (6 constructs)
│
├── lib/ # PHP library
├── config/ # Configuration
├── database/ # Database layer
├── cache/ # Caching system
├── error/ # Error handling
├── events/ # Event system
├── info/ # Debug/profiling
└── inits/ # Initialization
┌─────────────────────────────────────────────────────────────────┐
│ REQUEST │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ pad.php │
│ ├── Define PAD constant │
│ ├── Validate APP/DATA │
│ └── Include config & start │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ start/ │
│ ├── Initialize globals │
│ ├── Set up stores │
│ └── Begin execution │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ build/ │
│ ├── Collect _lib files │
│ ├── Build base structure (_inits.pad + @page@ + _exits.pad) │
│ ├── Execute _inits.php │
│ ├── Execute page.php → get data │
│ ├── Load page.pad template │
│ └── Replace @page@ with content │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ level/ + occurrence/ + walk/ │
│ ├── Find { } tag delimiters │
│ ├── Parse tag name, parameters, options │
│ ├── Detect type and load handler │
│ ├── For data tags: iterate occurrences │
│ ├── Process child content recursively │
│ └── Collect output │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ eval/ │
│ ├── Parse expressions │
│ ├── Resolve variables │
│ ├── Execute operators │
│ └── Apply pipe functions │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RESPONSE │
└─────────────────────────────────────────────────────────────────┘
apps/myapp/
├── index.php / index.pad # Page pair
├── _inits.php # Runs before all pages
├── _inits.pad # Wraps all pages (use @page@ placeholder)
├── _exits.php / _exits.pad # Runs after all pages
├── _lib/ # Auto-included PHP functions
├── _tags/ # Custom template tags
├── _functions/ # Custom pipe functions
├── _data/ # Static data files (JSON, XML)
├── _include/ # Template snippets
├── _callbacks/ # Iteration callbacks
├── _options/ # Custom tag options
├── _events/ # Event hooks: error, sql, curl, output - every request
└── _config/config.php # App configuration
Two levels. The page cache ($padCache) answers a whole anonymous GET from a store -
apcu, db, file, memcached or redis. The fragment cache keeps one section of a page
rendered while the rest stays dynamic:
{cache 'top-products', ttl=300} {# a named section, five minutes #}
{topProducts}<li>{$name}</li>{/topProducts}
{/cache}
{expensive_query cache=3600} {# any tag: its handler is skipped on a hit #}
...
{/expensive_query}
vary= adds what else the rendering depends on (vary=$userId); $padFragmentCache
picks the store - 'file' (default), 'apcu' or FALSE; padFragmentForget('top-products')
drops a named section when what it shows has changed.
{flush} in the wrapper, after </head>, sends the page rendered so far at once, so the
browser fetches stylesheets and fonts while a slow part of the page renders. Tidy, whole-body
gzip, the ETag and Content-Length are off for such a request - they need the whole body.
A path names a page too - /shop/products/42 - mapped onto the file tree, a bracketed name
binding the segment it stands for: products/[id].pad gets $id = '42', blog/[year]/[slug].pad
two variables, docs/[path+].pad the rest of the path. ?products/42 resolves the same way,
and every ?page URL keeps working. $padCleanUrls = TRUE makes $padGo write the clean
form; the server hands the paths to the entry point - FallbackResource /shop/index.php in
Apache, nothing at all under php -S.
The page’s .php already produces the data, so the same page can answer it instead of its
template - for ?orders&padFormat=json (or csv) and for an Accept: application/json (or
text/csv) header. Only what the page names leaves the server:
// orders.php
$orders = db ( "ARRAY * FROM orders" );
$total = 123.45;
$padExpose = [ 'orders', 'total' ];
JSON is one object keyed by those names, CSV the first of them that is a list, with a header
row. A page that exposes nothing renders HTML as always - asked outright with padFormat=,
it answers 406. $padOutputType = 'json' turns a whole application into one that answers
data.
A component declares the assets it needs where it is used; the layout prints them where
they belong. {stack} is filled in after the whole page has rendered, so a stack in the
<head> gets the pushes of everything below it:
<head>{stack 'scripts'}</head>
{push 'scripts', once='chart'} {# once per key, however often the component is used #}
<script src="chart.js"></script>
{/push}
Remote data has a cache of its own: {curl 'https://...', ttl=600}, data='https://...'
with a ttl= and a _data/*.curl file with a <ttl> keep the answer that long, and serve
the last good copy - logging the failure - when the source is down. $padCurlCache picks
the store: 'file' (default), 'apcu', 'redis', 'memcached' or FALSE.
A page picks a layout of its own and overrides its regions, resolved in the text while the page is assembled:
{extends '_layouts/report'} {# instead of the _inits.pad/_exits.pad frame #}
{block 'title'}Sales report{/block}
{block 'sidebar'}{parent}<a href="?export">Export</a>{/block}
Without {extends}, a page’s {block 'title'} overrides the region of that name in its
directories’ wrappers.
A page without PHP says what a .php would with {meta}, read while the page is assembled:
{meta title='Monthly report', layout='_layouts/print', cache=600}
Embed a page the browser fetches after load:
{ajax 'orders'} {# a <div> the browser fills with ?orders&padInclude #}
A named response fragment lets one template serve both the full page and the region an
update refreshes - ?orders&padFragment=order-list answers with that fragment alone:
{fragment 'order-list'}
{orders}<li>{$number}</li>{/orders}
{/fragment}
{ajax 'orders', fragment='order-list'} {# or hx-get="?orders&padFragment=order-list" #}
PAD runs on PHP compiled to WebAssembly: php wasm/build.php bundles the engine into
www/wasm/pad-bundle.json, and www/wasm/index.html loads PHP 8.4 from the @php-wasm
packages on jsDelivr, writes the engine into its memory and renders templates in the browser
tab, with no PAD server. Experimental - see wasm/README.md for what was verified.
pad pack notes puts an application, the engine and PHP compiled to WebAssembly into one
directory: opened from the disk it runs in the browser tab, served over https it installs as an
application whose service worker runs PHP for every window, offline after the first visit. What
the application writes under DATA/ - its SQLite database, uploads, sessions - is kept in the
browser’s IndexedDB between visits, and a panel exports and imports the database. The page’s
own fetch, XHR, {live}, {ajax}, fragments and EventSource reach PHP there as they would
a server; padSse answers one round per connection. Manual page Offline applications,
wasm/README.md; the notes, ledger and kanban applications show it.
A {live} region re-renders on the server when something in it is clicked, submitted or
changed, and swaps itself in - no JavaScript of the application’s own:
{live 'counter'}
<p>{$count}</p>
<button pad-click="add" pad-value="{$count}">+1</button>
{/live}
The page’s PHP reads the event with padLiveEvent() and padLiveValue(); the response to
the event is that region’s content alone.
Execute code in isolation:
{sandbox}
{# Isolated execution environment #}
{/sandbox}
{sandbox} isolates PAD’s state, not PHP. To try templates someone else typed, use the
playground (apps/playground, http://localhost/pad/playground/): it answers local
requests only, renders with $padPhpFunctions = [] and $padRequestVars = [], limits run
time and source and output size, and shows the result in a sandboxed frame. The URL hash
carries the template and its data, so a link shares an example.
An application hooks into four moments of every request through its _events/ directory -
error.php, sql.php (each db() statement, with its time), curl.php (each remote
fetch) and output.php (the final page, which the hook may change). The engine’s own hooks
in pad/events/ serve the info modes - trace, stats, xref - and run only under $padInfo.
Execute PHP code at specific points:
{staff callback='totals'}
{# _callbacks/totals.php runs at init, for each row and at exit #}
{/staff}
In _config/config.php or pad/config/config.php:
$padErrorAction // 'pad', 'boot', 'php', 'stop', 'exit', 'ignore', 'log', 'dump'
$padInfo // Debug: 'trace', 'stats', 'track', 'xml', 'xref'
$padOutputType // 'web', 'file', 'download', 'console', 'json', 'csv'
$padExpose // The page variables answered as JSON or CSV (set in the page's .php)
$padCleanUrls // Links in the clean form, /shop/products/42
$padCache // Enable caching
$padCheckOutput // Check the finished HTML of local requests (ids, alt, labels, links)
$padCoverage // Record template coverage of local requests (TRUE or a run name)
$padRecord // Record GET requests with their answers for replay (TRUE or a store name)
$padToolbar // 'local': the debug toolbar on this machine's own web pages
$padReload // 'local' / 'engine': live reload of this machine's own web pages
// Database
$padSqlHost
$padSqlDatabase
$padSqlUser
$padSqlPassword
template) and in
the log line; the build’s source map (pad/lib/source.php) traces the spot back through the
joined _inits/page/_exits text$padInfo = 'trace' for execution tracing$padToolbar = 'local' for a collapsible debug toolbar at the foot of every local web
page: time and memory, the levels and the tag tree, the SQL statements, page and fragment
cache, the template files read, the application’s variables and the session
(pad/exits/_lib/toolbar.php); never shown to a visitor, a &padInclude fragment or the page cache$padReload = 'local' in _config/config.php for live reload: a local page polls
?page&padReload for the newest file time under the application, its www/ directory and
_common ('engine': pad/ too) and reloads itself when a file is saved
(pad/exits/_lib/reload.php); never for a visitor, a fragment, the page cache or pad export{dump} tag for variable inspection{trace} tag for execution trace$padCheckOutput = TRUE to have every local HTML response checked once it has rendered: duplicate ids, images without alt, form fields without a label and ?page links to pages that do not exist are named in a panel at the end of the page and counted in a PAD-Output-Check header (one request: ?page&padCheckOutput; every application’s templates at once: develop/?links)$padCoverage = TRUE (or a run name) to record template coverage of local requests - which templates were read, which tags ran, which {if}/{case} branch was taken - in DATA/coverage/<run>.jsonl; develop/?coverage starts and stops a recording of every application around a suite run and shows each template with what never ran marked$padRecord = TRUE (or a store name) to keep every GET answered 200 - one that brought no cookie but PAD’s own ids - with its answer in DATA/replay/<store>/; develop/?replay replays a store against the current code and names every page whose answer changed. A replay never writes: db() refuses statements that change the database and padFilePut the application’s own writesDATA/ directory for error dumps and logsAPP/
├── _lib/ # Shared libraries
├── _inits.pad # Global layout
├── _inits.php # Global initialization
├── components/ # Reusable components
│ ├── header.pad
│ └── footer.pad
├── pages/ # Main pages
│ ├── home/
│ └── about/
└── api/ # API endpoints
└── users/
_inits.pad handle layoutsttl=padPrefetch ( [ 'rates' => $url1, 'weather' => $url2 ] )
in the page PHP fetches them in parallel and keeps each as named data for {rates}first, page, rows optionsWhat the engine itself keeps for the length of a request: which global names are its own -
every tag’s PHP half snapshots the application’s variables before and after it runs, and
sifting the symbol table name by name was a third of the time of a page full of tags - the
tokens of every clean expression, so a loop parses its expressions once, and the @else@ pair
scan, now made only when a level holds an @else@. Measured over ten pages of the manual,
reference, demo and sequence applications, a request takes about a third less time. The
template text is still rescanned each pass - there is no compiled form - and the page build
and the name lookups were each about 1% of a request, too little to be worth a cache that
outlives it.
| Operator | Meaning |
|---|---|
eq, == |
Equal |
ne, != |
Not equal |
gt, > |
Greater than |
lt, < |
Less than |
ge, >= |
Greater or equal |
le, <= |
Less or equal |
and (&&), or (\|\|) |
Logical operators |
range (a, b) |
Value in range |
| Function | Purpose |
|---|---|
upper, lower |
Case conversion |
trim |
Remove whitespace |
html, url |
Encoding |
date('fmt') |
Format date |
+ n, - n, * n, / n |
Arithmetic |
left(n), truncate(n) |
Shorten |
cut('x') |
Remove every occurrence of x |
after('x'), before('x') |
Extract substring |
contains('x') |
Check substring |
. 'str' |
Concatenate |
{echo} - {echo 'x' | upper}; a field tag pipes as it stands, {$var | upper}{echo $x | + 1} not | +1{count 'items'} not {count items}{ } as tags; use external filespadRedirect() - Don’t use exit or die in PAD appsRun the eight regression suites with ./ci.sh, or open regression/main/?index&test in a browser. Tests compare current output against stored answers.
Real traffic can be a suite too: with $padRecord = TRUE every GET request is kept with its answer, and develop/?replay re-asks every recorded page and names the ones whose answer changed.
To see what the suites reach, record template coverage around a run: develop/?coverage&start=suites, ./ci.sh, develop/?coverage&stop, then read develop/?coverage&run=suites.
Speed is tracked the same way: the develop application’s Benchmark times every page, keeps
each run with its commit in DATA/benchmark/, and names the pages that got slower than
their median over the last runs; its history page charts the runs. CI_BENCH=25 ./ci.sh
makes a 25% slowdown fail the gate like a failing suite.
See LICENSE file for details.