PHPackages                             phpdot/template - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. phpdot/template

ActiveLibrary[Templating &amp; Views](/categories/templating)

phpdot/template
===============

Swoole-safe Twig integration with auto-discovered extensions for the PHPdot ecosystem.

v1.0.1(1mo ago)02MITPHPPHP &gt;=8.3

Since Apr 30Pushed 1mo agoCompare

[ Source](https://github.com/phpdot/template)[ Packagist](https://packagist.org/packages/phpdot/template)[ RSS](/packages/phpdot-template/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (9)Versions (3)Used By (0)

phpdot/template
===============

[](#phpdottemplate)

Swoole-safe Twig integration for the PHPdot ecosystem. Lazy environment, namespaced template paths, auto-discovered extensions via the package manifest, and framework-native exceptions.

No global state. No `Twig_Environment` rebuilds per request. Works under Swoole, RoadRunner, FPM, or any PSR-15 stack.

Install
-------

[](#install)

```
composer require phpdot/template
```

RequirementVersionPHP&gt;= 8.3twig/twig^3.10phpdot/config^1.0phpdot/container^1.0phpdot/package^1.0Quick Start
-----------

[](#quick-start)

```
use PHPdot\Template\EngineFactory;
use PHPdot\Template\View;
use PHPdot\Template\TemplateConfig;

$config  = new TemplateConfig(paths: ['__main__' => [__DIR__ . '/views']]);
$factory = new EngineFactory($config, $manifest, $container);
$view = new View($factory);

echo $view->render('hello.twig', ['name' => 'Omar']);
```

Three objects. The `View` is the only thing your application code needs to touch.

---

Architecture
------------

[](#architecture)

### Worker Lifecycle

[](#worker-lifecycle)

```
                    BOOT TIME (once per worker)
┌─────────────────────────────────────────────────────────┐
│                                                         │
│   Container resolves EngineFactory (singleton)          │
│       │                                                 │
│       ▼                                                 │
│   First call to ->environment() builds Twig\Environment │
│       │                                                 │
│       ├── FilesystemLoader: register namespaced paths   │
│       ├── DebugExtension if config.debug                │
│       └── Manifest::allServices() → filter by           │
│             Twig\Extension\ExtensionInterface →         │
│             container->get() → addExtension()           │
│       │                                                 │
│       ▼                                                 │
│   Environment cached on the factory instance            │
│                                                         │
└─────────────────────────────────────────────────────────┘
                    RUNTIME (every request)
┌─────────────────────────────────────────────────────────┐
│                                                         │
│   $view->render('page.twig', $context)                 │
│       │                                                 │
│       ▼                                                 │
│   factory->environment() → cached instance              │
│       │                                                 │
│       ▼                                                 │
│   Twig\Error\* → wrapped into PHPdot\Template\Exception │
│                                                         │
└─────────────────────────────────────────────────────────┘

```

### Package Structure

[](#package-structure)

```
src/
├── Exception/
│   ├── TemplateException.php           Base exception
│   ├── TemplateNotFoundException.php   Loader miss
│   ├── TemplateSyntaxException.php     Syntax error (carries line)
│   └── TemplateRenderException.php     Runtime error (carries line)
│
├── TemplateConfig.php                  Immutable configuration (#[Config('template')])
├── EngineFactory.php                   Builds and caches Twig\Environment
└── View.php                        Public-facing API

```

---

View API
--------

[](#view-api)

### Render a Template

[](#render-a-template)

```
$view->render('mail/welcome.twig', [
    'user' => $user,
    'url'  => $signupUrl,
]);
```

### Render a Single Block

[](#render-a-single-block)

```
$view->renderBlock('mail/welcome.twig', 'subject', ['user' => $user]);
```

Useful for templates that hold both an email subject and body in one file.

### Check Existence

[](#check-existence)

```
if ($view->exists('admin/dashboard.twig')) {
    return $view->render('admin/dashboard.twig');
}
```

### Escape Hatch to Twig

[](#escape-hatch-to-twig)

```
$twig = $view->environment();
$twig->addRuntimeLoader(new MyRuntimeLoader());
```

`environment()` returns the underlying `Twig\Environment` for advanced needs (runtime loaders, custom token parsers, direct access to filters/functions).

---

Configuration
-------------

[](#configuration)

```
use PHPdot\Template\TemplateConfig;

$config = new TemplateConfig(
    paths: [
        '__main__' => [__DIR__ . '/views'],
        'admin'    => [__DIR__ . '/admin/views'],
        'mail'     => [__DIR__ . '/mail/views'],
    ],
    cache:           '/var/cache/templates',  // null disables caching
    debug:           false,                   // dump() + verbose errors
    strictVariables: true,                    // undefined vars throw
    charset:         'UTF-8',
    autoReload:      false,                   // recompile on change (dev)
    autoescape:      'html',                  // 'html' | false
);
```

All properties are `readonly`.

### Namespaced Paths

[](#namespaced-paths)

```
new TemplateConfig(paths: [
    '__main__' => ['/app/views'],
    'admin'    => ['/app/admin/views'],
]);
```

```
{% extends '@admin/layout.twig' %}

{% include 'partials/header.twig' %}    {# resolves under __main__ #}
{% include '@admin/sidebar.twig' %}     {# resolves under admin    #}
```

The `__main__` namespace is the default — references without `@namespace/` prefix resolve through it.

### Production vs Development

[](#production-vs-development)

SettingProductionDevelopment`cache`absolute path`null``debug``false``true``autoReload``false``true``strictVariables``true``true`---

Auto-Discovered Extensions
--------------------------

[](#auto-discovered-extensions)

Any class registered with the `phpdot/package` manifest that implements `Twig\Extension\ExtensionInterface` is added to the environment automatically.

```
namespace Acme\Greet;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class GreetingExtension extends AbstractExtension
{
    public function getFunctions(): array
    {
        return [
            new TwigFunction('greet', static fn(string $name): string => "hello, {$name}"),
        ];
    }
}
```

Register it as a singleton in your package manifest — the `EngineFactory` will pick it up at boot:

```
{{ greet('world') }}    {# hello, world #}
```

### Extension Lifecycle — Always Singleton

[](#extension-lifecycle--always-singleton)

Twig's `Environment::addExtension()` pins the instance you pass for the lifetime of the environment — i.e., for the worker. Marking an extension class `#[Scoped]` or `#[Transient]` is a no-op: the `EngineFactory` resolves the class once at boot, hands it to Twig, and Twig holds onto that single instance forever.

Concretely:

- `#[Singleton]` on an extension class — what you want, behaves as expected.
- `#[Scoped]` / `#[Transient]` on an extension class — silently behaves as singleton. Don't put per-request state on the extension itself; it will leak across coroutines.

For per-request behavior, see *Stateful Extensions* below.

### Stateful Extensions

[](#stateful-extensions)

Extensions that need request-scoped state (current user, locale, route params) should inject `ContainerInterface` and resolve scoped dependencies at call-time, not in the constructor:

```
final class AuthExtension extends AbstractExtension
{
    public function __construct(
        private readonly ContainerInterface $container,
    ) {}

    public function getFunctions(): array
    {
        return [
            new TwigFunction('current_user', fn(): ?User =>
                $this->container->get(SessionInterface::class)->user()
            ),
        ];
    }
}
```

The extension is a singleton (one per worker); the container resolves the scoped dependency per coroutine.

---

Exceptions
----------

[](#exceptions)

```
TemplateException (extends RuntimeException)
├── TemplateNotFoundException     loader miss
├── TemplateSyntaxException       compile-time syntax error (carries $templateLine)
└── TemplateRenderException       runtime error (carries $templateLine)

```

All leaf exceptions carry the `$template` name that failed:

```
use PHPdot\Template\Exception\TemplateNotFoundException;
use PHPdot\Template\Exception\TemplateRenderException;
use PHPdot\Template\Exception\TemplateSyntaxException;

try {
    $html = $view->render('page.twig', $context);
} catch (TemplateNotFoundException $e) {
    logger()->warning('Missing template', ['template' => $e->template]);
} catch (TemplateSyntaxException $e) {
    logger()->error('Syntax error', [
        'template' => $e->template,
        'line'     => $e->templateLine,
    ]);
} catch (TemplateRenderException $e) {
    logger()->error('Render failure', [
        'template' => $e->template,
        'line'     => $e->templateLine,
    ]);
}
```

> Note: the property is `$templateLine`, not `$line` — `\Exception::$line` is reserved for the PHP source line of the throw site.

---

Framework Integration
---------------------

[](#framework-integration)

### DI Wiring

[](#di-wiring)

`TemplateConfig`, `EngineFactory`, and `View` are all singletons (one per worker). No scoped wiring needed.

```
TemplateConfig::class => singleton(fn (Config $c) => new TemplateConfig(
    paths:           $c->array('template.paths'),
    cache:           $c->stringOrNull('template.cache'),
    debug:           $c->bool('template.debug'),
    strictVariables: $c->bool('template.strict_variables'),
    autoReload:      $c->bool('template.auto_reload'),
)),

EngineFactory::class => singleton(),
View::class      => singleton(),
```

With `phpdot/container` autowiring, the `#[Singleton]` attribute on `EngineFactory` and `View` makes the explicit declarations above optional.

### Controller Usage

[](#controller-usage)

```
use PHPdot\Template\View;

final class DashboardController
{
    public function __construct(
        private readonly View $view,
        private readonly ResponseFactory $response,
    ) {}

    public function index(SessionInterface $session): ResponseInterface
    {
        $html = $this->view->render('dashboard.twig', [
            'user' => $session->get('user'),
        ]);

        return $this->response->html($html);
    }
}
```

---

Swoole Safety
-------------

[](#swoole-safety)

Twig is process-safe but not coroutine-safe out of the box — `Twig\Environment` mutates internal state during template loading. This package guarantees safety by:

ConcernMitigationEnvironment mutationBuilt once per worker, cached on `EngineFactory`Cache directory writes`cache: null` for in-memory; otherwise compiled classes use content-hashed namesPer-request dataPassed as `$context` to `render()` — never stored on the environmentScoped state in extensionsInject `ContainerInterface`, resolve at call-timeThe environment is treated as read-only after boot. If you need to register extensions or runtime loaders dynamically, do it once at boot, not per request.

---

Development
-----------

[](#development)

```
composer test        # Run tests (26 tests, 59 assertions)
composer analyse     # PHPStan level 10 + strict rules
composer cs-fix      # Apply code style
composer cs-check    # Verify code style (dry run)
composer check       # All three
```

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance92

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

40d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/62e82421bda4b5d6ba9a47ba6d88caca060dcd0d1a2862f351f3a97657385db0?d=identicon)[phpdot](/maintainers/phpdot)

---

Top Contributors

[![phpdot](https://avatars.githubusercontent.com/u/252500?v=4)](https://github.com/phpdot "phpdot (2 commits)")

---

Tags

twigtemplateswoolephpdot

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/phpdot-template/health.svg)

```
[![Health](https://phpackages.com/badges/phpdot-template/health.svg)](https://phpackages.com/packages/phpdot-template)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k86.9M2.2k](/packages/symfony-symfony)[twig/intl-extra

A Twig extension for Intl

36667.2M317](/packages/twig-intl-extra)[symfony/ux-twig-component

Twig components for Symfony

21917.2M298](/packages/symfony-ux-twig-component)[symfony/ux-live-component

Live components for Symfony

1636.5M111](/packages/symfony-ux-live-component)[twig/cssinliner-extra

A Twig extension to allow inlining CSS

22919.7M79](/packages/twig-cssinliner-extra)[twig/inky-extra

A Twig extension for the inky email templating engine

16713.2M69](/packages/twig-inky-extra)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
