Templates and Layouts
Domovoy's template system uses plain PHP classes with explicit dependency injection — no template language, no magic.
Template Interface
A template is a callable class implementing the Template
marker interface:
- Constructor parameters are service dependencies (wired by a DI container).
- __invoke() parameters are the data contract (checked by PHPStan).
- __invoke()
returns a
Node.
use Domovoy\Template\Template;
use Domovoy\VirtualDom\Contract\Node;
use function Domovoy\VirtualDom\Generated\{div, h3, span};
final readonly class ProductCard implements Template
{
public function __construct(private PriceFormatter $formatter) {}
public function __invoke(Product $product): Node
{
return div(className: 'card')(
h3()($product->name),
span(className: 'price')(
$this->formatter->format($product->price),
),
);
}
}
PHPStan validates __invoke()
signatures on concrete classes at max level,
so data contract mismatches are caught at analysis time.
TemplateResolver (PSR-11)
Use TemplateResolver
to pull template instances from any PSR-11 container:
use Domovoy\Template\TemplateResolver;
$resolver = new TemplateResolver($container);
$card = $resolver->resolve(ProductCard::class);
// $card is a fully wired ProductCard instance
The resolver preserves generic types, so resolve(ProductCard::class)
returns
ProductCard
, not just Template
.
Layout Inheritance with BlockRegistry
BlockRegistry
is an immutable registry of named content blocks. A parent
template defines block slots, and child templates override them.
Base Layout
use Domovoy\Template\BlockRegistry;
use Domovoy\Template\Template;
use Domovoy\VirtualDom\Contract\Node;
use function Domovoy\VirtualDom\Generated\{html, head, title, body, header, main, footer, nav, p};
final readonly class BaseLayout implements Template
{
public function __invoke(string $pageTitle, BlockRegistry $blocks): Node
{
return html(lang: 'en')(
head()(title()($pageTitle)),
body()(
header()($blocks->getOrDefault('nav', nav()())),
main()($blocks->get('content')),
footer()($blocks->getOrDefault('footer', p()('Default footer'))),
),
);
}
}
Child Layout
A child layout injects the parent and overrides blocks:
use function Domovoy\VirtualDom\Generated\{nav, a};
final readonly class AdminLayout implements Template
{
public function __construct(private BaseLayout $base) {}
public function __invoke(string $pageTitle, BlockRegistry $blocks): Node
{
return ($this->base)(
"Admin — {$pageTitle}",
$blocks->set('nav', nav()(
a(href: '/admin')('Dashboard'),
a(href: '/admin/users')('Users'),
)),
);
}
}
Page Assembly
A page template assembles the layout with data:
use function Domovoy\VirtualDom\Generated\{div, h1};
final readonly class DashboardPage implements Template
{
public function __construct(
private AdminLayout $layout,
private ProductCard $card,
) {}
public function __invoke(ProductCollection $products): Node
{
$card = $this->card;
return ($this->layout)(
'Dashboard',
BlockRegistry::create()
->set('content', div(className: 'dashboard')(
h1()('Product Dashboard'),
...array_map(
static fn (Product $p): Node => $card($p),
$products->toArray(),
),
)),
);
}
}
BlockRegistry API
BlockRegistry
is immutable — every mutation returns a new instance:
BlockRegistry::create()— empty registry$reg->set('name', $node)— returns new registry with block added/replaced$reg->get('name')— returns block or throwsRuntimeException$reg->getOrDefault('name', $fallback)— returns block or fallback node$reg->has('name')— checks if block exists