265 lines
10 KiB
Markdown
265 lines
10 KiB
Markdown
# Eyrie Templates — Product Specification (Draft)
|
||
|
||
This specification captures the initial scope for Eyrie Templates, the templating engine used by the Phred Framework.
|
||
|
||
## 1. Goals
|
||
|
||
- Fast, safe, ergonomic server‑side templating for PHP apps in the Phred ecosystem.
|
||
- Separation of concerns: business logic in PHP, presentation logic in templates.
|
||
- Security by default via auto‑escaping with contextual modes.
|
||
- Predictable, minimal syntax optimized for HTML.
|
||
- DRY composition with inheritance and partials.
|
||
|
||
## 2. Non‑Goals
|
||
|
||
- No arbitrary code execution in templates; no general programming.
|
||
- No dynamic variable creation inside templates (only loop counters).
|
||
- No while loops or unbounded iteration constructs.
|
||
- No filesystem or network access from templates.
|
||
|
||
## 3. Installation
|
||
|
||
- Composer package: `composer require getphred/eyrie`
|
||
|
||
### 3.1 Engine configuration
|
||
|
||
- Directory roots can be configured for Layouts, Pages, Partials, and Components.
|
||
- Template names are referenced using dot-notation relative to their configured roots and do not include file extensions.
|
||
- Default template file extension: `.eyrie.php`.
|
||
|
||
## 4. Core concepts
|
||
|
||
- Template: Text file (typically HTML) with Eyrie syntax.
|
||
- Context: Associative array passed at render time.
|
||
- Page: A top‑level template representing a view. Pages may extend a Layout and fill named Blocks.
|
||
- Layout: A template that defines the overarching structure (shell) and exposes named Blocks with optional defaults.
|
||
- Block: A named content region. Pages/components can override block content; `[[ super ]]` can include parent content.
|
||
- Component: A reusable, tag‑like unit rendered with a custom element syntax (e.g., `<@ Movie id="1" />`). Components encapsulate their own rendering and accept props.
|
||
- Props: Data passed to components (and optionally pages) akin to attributes; values flow from the rendering context and are subject to escaping.
|
||
- Helper: Whitelisted callable exposed to templates.
|
||
- Filter: Unary transformation applied via `|` pipe.
|
||
- Tag: Control structure/directive (loops, conditionals, includes, etc.).
|
||
- Loader: Resolves template names to sources.
|
||
- Cache: Compiled/intermediate representation for speed.
|
||
|
||
## 5. Syntax overview
|
||
|
||
- Output: `<< expression >>` prints value with auto‑escaping.
|
||
- Calls/control: control blocks start with `<(` and end with `)>`; examples use `<( if ... )>` / `<( endif )>`.
|
||
- Blocks/extends: `[[ ... ]]` for inheritance, blocks, includes.
|
||
- Components: `<@ ComponentName prop1="value" prop2={ expr } />` self‑closing. Support for children/content projection is TBD.
|
||
|
||
### 5.1 Expressions
|
||
|
||
- Property/array access: `user.name`, `order.items[0]`.
|
||
- Literals: strings, numbers, booleans, null.
|
||
- Operators: arithmetic, comparison, logical, ternary.
|
||
- Filters: `value | lower | escape('attr') | ...`.
|
||
|
||
### 5.2 Output examples
|
||
|
||
- Basic: `<< user.name >>`
|
||
- With filters: `<< title | upper >>`
|
||
- Raw (discouraged): `<< user.bio | raw >>`
|
||
|
||
### 5.3 Control structures (inside `<( ... )>`)
|
||
|
||
- If/elseif/else/end:
|
||
- `<( if cond )>` ... `<( elseif other )>` ... `<( else )>` ... `<( endif )>`
|
||
- Foreach:
|
||
- `<( foreach item in items )>` ... `<( endforeach )>`
|
||
- Loop vars (read‑only): `item`
|
||
- Loop (range/repeat):
|
||
- `<( loop from 0 to 10 )>` ... `<( endloop )>`
|
||
- Loop vars (read‑only): `loop.index` (int), `loop.first` (bool), `loop.last` (bool), `loop.length` (int)
|
||
|
||
### 5.4 Inheritance and blocks (inside `[[ ... ]]`)
|
||
|
||
- Extends: `[[ extends "layouts.base" ]]` (dot-notated paths, final part is parent file)
|
||
- Extends syntax does not include the layout file extension.
|
||
- Extends must be the first line of a template file.
|
||
- Declare/override:
|
||
- `[[ block content ]] ... [[ endblock ]]`
|
||
- `[[ super ]]` inside override to include parent content
|
||
- Block context (optional): `[[ block sidebar with { user: user } ]]`
|
||
|
||
### 5.5 Partials
|
||
- Basic Include: `[[ include "partials.footer" ]]`
|
||
- Include with optional context: `[[ include "partials.footer" with { x: 1 } ]]` (dot-notated paths, final part is partial file)
|
||
- Include syntax does not include the partial file extension.
|
||
|
||
### 5.6 Components
|
||
|
||
- Tag‑based components are referenced by PascalCase names and invoked with the `<@` prefix: `<@ Movie id="1" />`.
|
||
- Props:
|
||
- String literal: `<@ Movie title="Jaws" />`
|
||
- Expression: `<@ Movie rating={ movie.rating } />`
|
||
- Boolean shorthand: `<@ Movie featured />` → `featured=true`
|
||
- Registration: components are registered in PHP (see API). Unregistered component names are a `SyntaxError` (configurable to `RuntimeError`).
|
||
- Auto‑escaping: Component outputs are escaped by context unless a component deliberately returns a `SafeHtml`‑like value.
|
||
- Context: Components receive props plus a limited view of the parent context (configurable); they do not mutate outer context.
|
||
- Layout/block interaction: Components can be used within Layouts and Pages but do not extend layouts themselves, and they do not define or fill block areas.
|
||
|
||
### 5.7 Built‑in tags
|
||
|
||
- `if/elseif/else/endif`, `foreach/endforeach`, `loop/endloop`, `include`, `extends`, `block/endblock`, `super`
|
||
|
||
### 5.8 Helpers and filters
|
||
|
||
- Helpers are registered by name in PHP and callable in `<( ... )>` or `<< >>`, e.g. `<< route('home') >>`.
|
||
- Filters chain with pipes, e.g. `<< text | truncate(120) | escape('attr') >>`.
|
||
|
||
## 6. Auto‑escaping
|
||
|
||
- Enabled by default for all `<< >>` outputs.
|
||
- Modes: `html` (default), `attr`, `url`, `js`.
|
||
- `escape(mode)` switches mode; `safe` marks trusted values to bypass escaping.
|
||
- Escaping happens after filters unless `safe` present.
|
||
- Component rendering occurs under the current escaping mode of the insertion site.
|
||
|
||
## 7. Template loaders
|
||
|
||
- FilesystemLoader: rooted directories, normalized paths, no `..` traversal.
|
||
- Names are dot‑notated within configured roots and omit the file extension (default `.eyrie.php`).
|
||
- Optional namespaces (if supported): `@emails.welcome` → resolved under the `emails` root.
|
||
- Future: StringLoader, ArrayLoader.
|
||
|
||
## 8. Caching
|
||
|
||
- Optional compiled cache keyed by template name + engine version + config hash.
|
||
- Stores: filesystem (default), PSR‑16 (future).
|
||
- Defaults: dir perms 0700; atomic writes; checksum integrity.
|
||
|
||
## 9. Public PHP API (draft)
|
||
|
||
```php
|
||
interface Loader {
|
||
public function getSource(string $name): string;
|
||
public function getCacheKey(string $name): string; // stable per source
|
||
public function exists(string $name): bool;
|
||
}
|
||
|
||
final class Environment {
|
||
public function __construct(Loader $loader, array $options = []) {}
|
||
public function addHelper(string $name, callable $helper): void {}
|
||
public function addFilter(string $name, callable $filter): void {}
|
||
/** Register a component renderer by tag name, e.g., 'Movie' */
|
||
public function addComponent(string $name, ComponentRenderer $component): void {}
|
||
public function render(string $name, array $context = []): string {}
|
||
public function compile(string $name): CompiledTemplate {}
|
||
}
|
||
|
||
/** Minimal component contract (draft) */
|
||
interface ComponentRenderer {
|
||
/**
|
||
* @param array $props Props/attributes supplied in the template
|
||
* @param array $context A read‑only view of the current render context
|
||
* @return mixed A string (escaped later) or a SafeHtml‑like value
|
||
*/
|
||
public function render(array $props, array $context = []): mixed;
|
||
}
|
||
|
||
final class CompiledTemplate {
|
||
public function render(array $context = []): string {}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- `strict_variables` option throws on missing variables.
|
||
- Helpers/filters return values still subject to escaping unless wrapped in a `SafeHtml`‑like type.
|
||
|
||
## 10. Errors and diagnostics
|
||
|
||
- Exceptions: `TemplateError`, `SyntaxError`, `RuntimeError`, `LoaderError`.
|
||
- Messages include template name, line/col, and a snippet when possible.
|
||
- Configurable verbosity for dev vs prod.
|
||
- Useful diagnostics for components: unknown component name, invalid/unknown props, and type errors include the component tag snippet.
|
||
|
||
## 11. Performance targets (initial)
|
||
|
||
- Cold render p95 < 10ms for a ~5KB template on typical server hardware.
|
||
- Warm render p95 < 2ms with cache.
|
||
- Watchdog timeouts to limit parse/render and mitigate DoS.
|
||
- Component recursion and nesting limits (e.g., max depth) to prevent pathological trees.
|
||
|
||
## 12. Security requirements (summary)
|
||
|
||
- Auto‑escape by default; `safe` is explicit opt‑in.
|
||
- No arbitrary PHP execution; only whitelisted helpers/filters.
|
||
- Loader prevents traversal; only configured roots.
|
||
- Sandboxed evaluation; no eval/reflection.
|
||
- Depth/iteration limits; template size/token limits.
|
||
- Restrictive cache dir permissions; validated paths.
|
||
- Components are pure render units: no filesystem/network access; no mutation of outer context; bounded recursion/depth.
|
||
|
||
## 13. Logging and telemetry
|
||
|
||
- Hook for timings, cache metrics, loader misses.
|
||
- PSR‑3 logger support.
|
||
|
||
## 14. Compatibility
|
||
|
||
- Minimum PHP: propose 8.1+ (finalize).
|
||
- UTF‑8 for sources and output.
|
||
|
||
## 15. Examples
|
||
|
||
Base `base.eyrie.php`:
|
||
|
||
```html
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title><< title | escape('html') >></title>
|
||
</head>
|
||
<body>
|
||
<header>[[ block header ]]Default Header[[ endblock ]]</header>
|
||
<main>[[ block content ]][[ endblock ]]</main>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
Child `home.eyrie.php`:
|
||
|
||
```html
|
||
[[ extends "base" ]]
|
||
|
||
[[ block content ]]
|
||
<h1>Hello, << user.name >>!</h1>
|
||
<ul>
|
||
<( foreach item in items )>
|
||
<li><< loop.index >>. << item.title | escape('attr') >></li>
|
||
<( endforeach )>
|
||
</ul>
|
||
[[ endblock ]]
|
||
```
|
||
|
||
Component usage example:
|
||
|
||
```html
|
||
[[ extends "base" ]]
|
||
|
||
[[ block content ]]
|
||
<h2>Featured</h2>
|
||
<@ Movie id={ featured.id } title={ featured.title } featured />
|
||
|
||
<ul>
|
||
<( foreach m in movies )>
|
||
<li>
|
||
<@ Movie id={ m.id } title={ m.title } rating={ m.rating } />
|
||
</li>
|
||
<( endforeach )>
|
||
</ul>
|
||
[[ endblock ]]
|
||
```
|
||
|
||
## 16. Open questions
|
||
|
||
- Final grammar (EBNF) for expressions/tags.
|
||
- Built‑in filters/helpers set.
|
||
- Parser error recovery strategy.
|
||
- Safe value interface semantics.
|
||
- Component children/content projection: do we allow `<Card>...children...</Card>`?
|
||
- Prop type checking and defaulting strategy.
|