An open API service indexing awesome lists of open source software.

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

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.

[![Latest Version](https://img.shields.io/github/release/piedweb/splates.svg?style=flat-square)](https://github.com/piedweb/splates/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/piedweb/splates/php.yml?style=flat-square)](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.