A component that gives your app a view engine: plain PHP views, layered layouts, and reusable components you assemble the way you want.
ForgeView is the piece your app uses to turn template files into HTML. It's not a framework layer you inherit — it's a capability you add. If you started from an HTTP blueprint or a web starter, it's already wired in; otherwise you install it yourself. Your app decides which view to render, what data to pass, which layout wraps it, and which components it composes. The engine supplies the mechanics.
Module:path
Because templates are just PHP, everything your app
already knows about PHP applies here — no separate
template language, no compiler, no new tooling. You
write the HTML you want and drop in
<?= where you need dynamic values.
A view is a PHP file. When you render it, the data you pass becomes local variables available to the template. A minimal page view inside your app:
# app/UI/views/pages/welcome.php
<h1>Welcome back, <?= $name ?></h1>
Render it through the container's view interface:
use Forge\Core\Contracts\ViewInterface;
$view = $container->get(ViewInterface::class);
$html = $view->render('pages/welcome', ['name' => 'Forge']);
Escape anything you don't trust with the
e() helper and output pre-rendered HTML
with raw(). Missing view files throw a
clear runtime error naming the file it searched for.
A layout is also just a view file. It receives the
rendered page content (as $content) and puts
it in place. You pick a layout with the
#[Layout] attribute on a controller method
or class:
use Modules\ForgeRouter\Attributes\Layout;
#[Layout('ForgeComponents:public')]
public function home(): Response
{
return $this->view('home');
}
The router carries that choice onto the route, and your controller renders into it. The strong feature here is parent layouts: a layout can declare a parent layout, and the engine wraps each one around the next. A module's admin layout sets its own parent:
// A layout file setting its parent
$parentLayout = 'ForgeComponents:root';
$layoutProps = array_merge($layoutProps ?? [], [
'bodyClass' => 'fc-admin',
]);
The top-level parent (root) finally emits
the <!DOCTYPE html>, <head>,
and <body>. Completed layouts snap into
the chain as full documents. The engine protects you from
accidental circular references and rejects them outright.
Why parent layouts matter: you don't
re-implement the HTML shell in every layout. Your app
keeps one root, and each layout adds a
layer — body class, navbar, footer — on top.
Views and layouts talk to each other through a few shared variables. All are plain PHP arrays merged as the layout chain is assembled.
| Variable | What it carries | Where it's read |
|---|---|---|
| $layoutProps | Arbitrary data — title, body class, sidebar config | Any layout in the chain |
| $layoutSections | Named HTML fragments injected at anchors (head_end, body_end, breadcrumbs) | Usually the outer layout |
| $layoutSlots | Named content slots for a layout to place | Layout that exposes the slot |
| $content | The rendered page or child-layout body | Every layout |
A page injects a header fragment and page-specific CSS
through $layoutSections, plus props the layout
uses to draw navigation:
$layoutSections = array_merge($layoutSections ?? [], [
'head_end' => '<link rel="stylesheet" href="/assets/site.css">',
'breadcrumbs' => component('ForgeComponents:admin/breadcrumbs', $crumbs),
]);
$layoutProps = array_merge($layoutProps ?? [], [
'title' => 'Dashboard',
'sidebar' => new SidebarDefinition(brand: 'Admin', groups: [...]),
]);
The outer layout reads those anchors and props:
<title><?= $layoutProps['title'] ?? 'Forge' ?></title>
<?= $layoutSections['head_end'] ?? '' ?>
<body class="<?= $layoutProps['bodyClass'] ?? '' ?>">
Within a component, named slots are read back with the
slot() helper, falling back to a default when
a caller doesn't supply one.
Components are small, reusable view fragments. They live
in your app's component directory or ship inside a
module. Render one with the component()
helper, passing props and, optionally, named slots:
component('ForgeComponents:alert');
component('ForgeComponents:admin/stats', [
'stats' => $stats,
'columns' => 4,
]);
component('ForgeComponents:admin/data-card', [
'title' => 'Recent Activity',
], slots: [
'default' => component('ForgeComponents:admin/activity-list', [
'activities' => $activities,
]),
]);
The Module:path form resolves a component from
a module; a bare name resolves from your app's own
component directory. A component reads its named slots:
<div class="topbar">
<?php if (slot('search')): ?>
<?= slot('search') ?>
<?php endif; ?>
<?= slot('user') ?>
<?= slot('actions') ?>
</div>
Nested components are first-class: a slot value can itself declare a component (name, props, slots), and the engine renders it on demand. Components render into their own isolated scope, so state doesn't leak between sibling components.
ForgeView resolves every view, layout, and component
through the kernel's StructureResolver.
That resolver reads your project's
forge_structure.php — the same file that
defines your app root, modules root, and namespaces — so
the locations below are defaults, not fixed
rules. You can move these directories and the
engine follows.
| Kind | Your app (default) | A module (default) |
|---|---|---|
| Views | app/UI/views | modules/<Name>/src/UI/views |
| Pages | app/UI/views/pages | modules/<Name>/src/UI/views/pages |
| Layouts | app/UI/views/layouts | modules/<Name>/src/UI/views/layouts |
| Components | app/UI/views/components | modules/<Name>/src/UI/views/components |
Two details make this flexible:
app, but
structure keys define it (see
Anatomy).
If your app root is app but that
directory doesn't exist, ForgeView transparently checks
a src/UI/... alternative so source-first
layouts keep working.
modules and capabilities),
first via the structure-configured paths, then a
src/UI/... fallback, for a given module.
Disabled modules are rejected early with a clear error.
In practice you rarely think about this — you write
<Name>:pages/dashboard and the engine finds
it. But knowing it's structure-driven means your
app can rename or relocate these folders and everything
still resolves.
Controllers that use the ViewHelper trait get
a convenient view() method. It loads the
route's chosen layout, resolves the module you're in, and
returns a ready-to-send response:
use Modules\ForgeView\Traits\ViewHelper;
use Modules\ForgeRouter\Http\Response;
final class HomeController
{
use ViewHelper;
public function index(): Response
{
return $this->view('home', ['name' => 'Forge']);
}
}
The file requested is views/pages/home.php
(the trait prepends pages/). If the controller
lives in a module, the module name is detected from its
namespace and the view resolves from that module instead.
The layout comes from the current route — set via
#[Layout] — unless you pass one explicitly.
Controller or raw: you can render
through the container's
ViewInterface anywhere, or use the trait
for the common controller path. Both reach the same
engine.
ForgeView registers a handful of template helpers you can use inside views and layouts:
| Helper | Purpose |
|---|---|
| component() | Render a reusable component (module-aware) |
| slot() | Read a named slot with an optional default |
| form_open() / form_close() | Open a form with CSRF input and method spoofing; close it |
| external_asset() | Emit an external CSS/JS link registered in your CSP config, with SRI |
| merge_classes() | Flatten and de-duplicate CSS class lists |
You also get the kernel's escaping helpers,
e() and raw(), plus whatever
helpers other capabilities register (for example
csrf_input()).
ForgeView is a core capability, so a web starter or an HTTP blueprint typically includes it already. If your project doesn't have it, add it like any capability:
# Install with the interactive wizard
php forge.php package:install-module
# Or name it directly
php forge.php package:install-module --module=ForgeView
Once installed, ForgeView binds its
ViewInterface in the container and registers
its helpers. It ships no commands and needs no
configuration — the paths it uses come from your
project's structure file.
Composing your app: ForgeView is what powers the pages, layouts, and components that other capabilities (and your own app) render. Pair it with templates for richer markup or progressive enhancement for front-end polish. See all capabilities to keep building.