https://github.com/piedweb/splates
Native PHP template engine
https://github.com/piedweb/splates
Last synced: 4 months ago
JSON representation
Native PHP template engine
- Host: GitHub
- URL: https://github.com/piedweb/splates
- Owner: PiedWeb
- License: mit
- Created: 2024-12-16T15:21:10.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-02-27T16:52:10.000Z (4 months ago)
- Last Synced: 2026-02-27T21:27:44.536Z (4 months ago)
- Language: PHP
- Size: 119 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# Splates: Type-Safe PHP Template Engine
A native PHP template engine with full IDE autocompletion and PHPStan support. Fork of [league/plates](https://github.com/thephpleague/plates), redesigned for modern PHP development.
[](https://github.com/piedweb/splates/releases)
[](LICENSE)
[](https://github.com/piedweb/splates/actions)
## Why Splates?
- **Full IDE support** - Constructor parameters with `#[TemplateData]` provide autocomplete everywhere
- **PHPStan max level** - Every template is statically analyzable
- **No magic** - No string-based template names, no runtime errors from typos
- **Slots pattern** - Layouts are just components with `Closure` properties (no magic sections)
- **Value objects** - `Text`, `Html`, `Attr`, `Js` for context-aware escaping
- **Global services** - Inject dependencies via `Engine::addGlobal()` with `#[Inject]`
## Installation
```bash
composer require piedweb/splates
```
Requires PHP 8.2+.
## Quick Start
### 1. Create a template
```php
e()
) {}
public function __invoke(): void
{ ?>
= $this->name ?>
Email: = $this->email ?>
render(new ProfileTpl(
name: new Text('John Doe'),
email: new Text('john@example.com'),
));
```
---
## Core Concepts
### Template Creation Options
Templates implement `TemplateClassInterface` and can be created in several ways:
#### 1. Minimal (no helpers needed)
```php
class Hello implements TemplateClassInterface
{
public function __construct(public string $name) {}
public function __invoke(): void
{
echo "Hello, {$this->name}!";
}
}
```
#### 2. With `__invoke()` parameter injection
```php
class Profile implements TemplateClassInterface
{
public function __construct(public string $name) {}
public function __invoke(TemplateFetch $f, TemplateEscape $e): void
{
echo '
' . $e($this->name) . '
';
echo $f(new SidebarTpl());
}
}
```
#### 3. With `#[Inject]` property injection
```php
use PiedWeb\Splates\Template\Attribute\Inject;
class Profile implements TemplateClassInterface
{
#[Inject]
protected TemplateFetch $f;
#[Inject]
protected TemplateEscape $e;
public function __construct(public string $name) {}
public function __invoke(): void
{
echo '
' . ($this->e)($this->name) . '
';
echo ($this->f)(new SidebarTpl());
}
}
```
#### 4. Extending `TemplateAbstract` (full helper methods)
```php
class MyTemplate extends TemplateAbstract
{
public function __construct(
#[TemplateData]
public User $user,
#[TemplateData]
public array $items = [], // Optional with default
) {}
public function __invoke(): void
{
echo $this->render(new LayoutTpl(...));
echo $this->e($this->user->name);
}
}
```
### Helper Methods
Inside templates, you have access to:
| Method | Description |
| ----------------------------- | ------------------------------------ |
| `$this->e($value)` | Escape value for HTML output |
| `$this->render(new Tpl())` | Render a child template |
| `$this->capture(fn() => ...)` | Capture output as string |
| `$this->slot(fn() => ...)` | Create a lazy slot (syntactic sugar) |
### Escaping
Always escape user data:
```php
= $this->e($this->title) ?>
```
Or use auto-escaping value objects:
```php
use PiedWeb\Splates\Template\Value\Text;
// Text auto-escapes when converted to string
echo new Text('alert("XSS")');
// Output: <script>alert("XSS")</script>
```
### IDE Syntax Highlighting with Heredoc
When mixing PHP and HTML inside closures, VS Code's syntax highlighter often loses context. For better highlighting, use **heredoc syntax**:
```php
private function renderSidebar(): string
{
// Extract and escape values first
$dashboardUrl = $this->e($this->app->url('/dashboard'));
$usersUrl = $this->e($this->app->url('/users'));
// Heredoc provides proper HTML highlighting in VS Code
return <<
Quick Links
HTML;
}
```
**Trade-offs:**
- Heredoc: Better IDE highlighting, but requires pre-computing variables
- Closure with `?>`: Can use inline PHP expressions (``), but inconsistent highlighting
See `exampleTemplateClass/Templates/Profile.php` for a complete heredoc example.
---
## Layouts with Slots Pattern
Instead of magic sections, Splates uses typed `Closure` properties (slots):
### Layout Template
```php
= $this->e($this->title) ?>
= ($this->content)() ?>
scripts): ?>
= ($this->scripts)() ?>
slot()` helper for clean inline slots:
```php
slot() helper
echo $this->render(new LayoutTpl(
title: $this->user->getName(),
content: $this->slot(function() { ?>
= $this->e($this->user->getName()) ?>
Email: = $this->e($this->user->getEmail()) ?>
= $this->render(new SidebarTpl()) ?>
slot(function() { ?>
console.log("loaded")
render(new LayoutTpl(
title: $this->user->getName(),
content: fn() => $this->renderContent(), // Delegate to method
));
}
private function renderContent(): string
{
return $this->capture(function() { ?>
= $this->e($this->user->getName()) ?>
= $this->content ?>
render(new LayoutTpl(
title: 'My Page',
content: new Slot(fn() => '
Page content
'),));
```
---
## Global Services
Inject services that are available to ALL templates:
### Setup
```php
$engine = new Engine();
// Register global services
$engine->addGlobal('ext', $templateExtension);
$engine->addGlobal('router', $router);
```
### Usage in Templates
```php
use PiedWeb\Splates\Template\Attribute\Inject;
class MyTemplate extends TemplateAbstract
{
// Auto-injected from globals
#[Inject]
public TemplateExtension $ext;
#[Inject]
public RouterInterface $router;
public function __construct(
#[TemplateData]
public User $user,
) {}
public function __invoke(): void
{ ?>
= $this->e($this->user->getName()) ?>
ext->url($route, $params);
}
protected function user(): ?User
{
return $this->ext->getUser();
}
}
// All app templates extend AppTemplate
class DashboardTpl extends AppTemplate
{
public function __invoke(): void
{ ?>
Welcome, = $this->e($this->user()?->getName() ?? 'Guest') ?>
= $text ?>` |
| `Html` | Pre-escaped HTML | `= $html ?>` |
| `Attr` | HTML attributes | `
| `Js` | JavaScript values | `var x = <?= $js ?>;` |
### Examples
```php
use PiedWeb\Splates\Template\Value\{Text, Html, Attr, Js};
// Text - auto-escapes for HTML content
$name = new Text('bad');
echo "
$name
"; //<script>bad</script>
// Html - for trusted pre-escaped content
$content = Html::trusted('Safe HTML');
echo $content; // Safe HTML
// Attr - escapes for HTML attributes
$class = new Attr('my-class" onclick="bad');
echo "
// Js - JSON-encodes for JavaScript
$data = new Js(['user' => 'John', 'count' => 42]);
echo "var config = $data;";
// var config = {"user":"John","count":42};
```
---
## Caching
For production, enable reflection caching:
```php
$engine = new Engine(cacheDir: '/path/to/cache');
// Warm cache on deploy
$engine->getInjectResolver()->warmCache([
ProfileTpl::class,
LayoutTpl::class,
// ... all template classes
]);
```
---
## Development
```bash
composer test # Run tests
composer stan # Run PHPStan
composer format # Format code
```
---
## Migrating from league/plates
Splates is a fork of [league/plates](https://github.com/thephpleague/plates), redesigned around PHP classes, attributes, and PSR-4 autoloading instead of string-based template names.
### Key Differences
| league/plates | Splates |
|---|---|
| `new Engine('/templates', 'php')` | `new Engine(templateDir: '/templates')` |
| `$engine->render('profile', ['name' => 'John'])` | `$engine->render(new ProfileTpl(name: 'John'))` |
| `$engine->addData(['key' => 'val'])` | `$engine->addGlobal('key', $val)` |
| `$engine->registerFunction('upper', ...)` | Removed - use plain PHP |
| `$engine->loadExtension(new Asset(...))` | Removed - use `#[Inject]` |
| `$engine->addFolder('emails', '/path')` | Removed - use PSR-4 namespaces |
| `$this->e($value)` in templates | `$this->e($value)` (same) |
| `$this->fetch('partial')` | `$this->render(new PartialTpl())` |
| `$this->layout('layout')` | `echo $this->render(new LayoutTpl(content: ...))` |
| `$this->section('content')` | `($this->content)()` in layout |
| `$this->start('content')` ... `$this->stop()` | `content: $this->slot(function() { ... })` |
### Migration Steps
#### 1. Convert string templates to classes
**Before (league/plates):**
```php
// templates/profile.php
= $this->e($name) ?>
= $this->e($bio) ?>
```
**After (Splates):**
```php
class ProfileTpl extends TemplateAbstract
{
public function __construct(
#[TemplateData] public string $name,
#[TemplateData] public string $bio,
) {}
public function __invoke(): void
{ ?>
= $this->e($this->name) ?>
= $this->e($this->bio) ?>
render('path/to/template.php', ['name' => 'John']);
```
#### 2. Convert layouts to slots
**Before (league/plates):**
```php
// templates/layout.php
= $this->e($title) ?>
= $this->section('content') ?>
// templates/page.php
layout('layout', ['title' => $title]) ?>
start('content') ?>
= $this->e($title) ?>
stop() ?>
```
**After (Splates):**
```php
class LayoutTpl extends TemplateAbstract
{
public function __construct(
#[TemplateData] public string $title = 'App',
#[TemplateData] public ?Closure $content = null,
) {}
public function __invoke(): void
{ ?>
= $this->e($this->title) ?>
content): ?>
= ($this->content)() ?>
render(new LayoutTpl(
title: $this->title,
content: $this->slot(function() { ?>
= $this->e($this->title) ?>
loadExtension(new Asset('/assets'));
// In template: $this->asset('logo.png')
```
**After (Splates):**
```php
$engine = new Engine();
$engine->addGlobal('assetPath', '/assets');
// In template class:
class MyTpl extends TemplateAbstract
{
#[Inject] public string $assetPath;
public function __invoke(): void
{
echo $this->assetPath.'/logo.png';
}
}
```
#### 4. Verify
```bash
composer stan # PHPStan will catch most type errors
composer test # Run your tests
```
---
## Credits
- [Robin D. / Pied Web](https://piedweb.com) - Current Maintainer
Original **league/plates** contributors:
- [RJ Garcia](https://github.com/ragboyjr) - Original Plates Maintainer
- [Jonathan Reinink](https://github.com/reinink) - Original Plates Author
- [All Contributors](https://github.com/piedweb/splates/contributors)
## License
MIT License. See [LICENSE](LICENSE) for details.