ForgeView

A component that gives your app a view engine: plain PHP views, layered layouts, and reusable components you assemble the way you want.

Overview

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.

What it gives you

  • Plain PHP view files — no template syntax to learn
  • Layouts with nested parent layouts
  • Props, sections, and slots flowing into layouts
  • Reusable, module-aware components
  • View-only files resolved from your app paths
  • Module views and components via Module:path
  • Controller helpers and global template helpers

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.

Views

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.

Layouts & Parent Layouts

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.

Props, Sections & Slots

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

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.

Where Things Live

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 root is configurable. The defaults assume your app root is 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 resolve across multiple roots. The engine checks every configured modules root (for example 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.

From Your Code

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.

Helpers

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()).

Installation

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.