Eyrie-Templates/SPECS.md
2026-01-06 17:29:10 -06:00

10 KiB
Raw Blame History

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 serverside templating for PHP apps in the Phred ecosystem.
  • Separation of concerns: business logic in PHP, presentation logic in templates.
  • Security by default via autoescaping with contextual modes.
  • Predictable, minimal syntax optimized for HTML.
  • DRY composition with inheritance and partials.

2. NonGoals

  • 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 toplevel 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, taglike 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 autoescaping.
  • 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 } /> selfclosing. 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 (readonly): item
  • Loop (range/repeat):
    • <( loop from 0 to 10 )> ... <( endloop )>
    • Loop vars (readonly): 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

  • Tagbased 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).
  • Autoescaping: Component outputs are escaped by context unless a component deliberately returns a SafeHtmllike 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 Builtin 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. Autoescaping

  • 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 dotnotated 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), PSR16 (future).
  • Defaults: dir perms 0700; atomic writes; checksum integrity.

9. Public PHP API (draft)

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 readonly view of the current render context
     * @return mixed A string (escaped later) or a SafeHtmllike 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 SafeHtmllike 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)

  • Autoescape by default; safe is explicit optin.
  • 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.
  • PSR3 logger support.

14. Compatibility

  • Minimum PHP: propose 8.1+ (finalize).
  • UTF8 for sources and output.

15. Examples

Base base.eyrie.php:

<!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:

[[ 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:

[[ 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.
  • Builtin 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.