The composition capability your app uses to render message bodies — most often notification and email content — from plain PHP template files, with layouts and named regions for a consistent look.
ForgeTemplates is a deliberately small template composer. A template is just a PHP file that echoes text. You hand it some data, it renders, and you get a string back that you can send however you like — an email body through ForgeNotification, a text message, an email fragment.
Because templates are plain PHP, there's no new syntax to learn and no compiled markup to wrangle. You write the output directly, and the composer takes care of finding the file, extracting your data into the template's scope, and wrapping the result in a layout when you ask for one.
Two directories matter:
app/Common/Templates/.
src/Common/Templates/, referenced by
a module name prefix.
Framing: this is a component of your app, not a framework templating system. You keep your message markup in files, pass them your data, and render — the composition details stay in your hands.
php forge.php package:install-module --module=forge-templates
Installation registers the template manager and its helper. It has no database work and stands on its own — though it's most useful alongside the notification capability, whose messages you'll be composing.
A template is a regular PHP file whose output becomes
the rendered body. To compose with data, the passed
values are made available as variables in the template,
and a $props object too:
<?php /* app/Common/Templates/emails/welcome.php */ ?>
<h1>Welcome, <?= $name ?>!</h1>
<p>Thanks for joining <?= $props->appName ?>.</p>
You echo whatever bytes you want in the body. The earliest directory wins when the same template could live in more than one place, and the path is resolved from the same structure layout your app already uses.
Render any template by name with the
useTemplate() helper or the manager
itself:
$body = useTemplate('emails/welcome', [
'name' => 'Ada',
'appName' => 'Acme',
]);
The name resolves to
app/Common/Templates/emails/welcome.php.
You can also reach a module's templates by prefixing
the name with the module — a handy way to ship reusable
message layouts alongside a capability:
$body = useTemplate('ForgeTemplates:emails/welcome', $data);
The returned string is ready to hand to whatever consumes it. If a template can't be found you get a clear error naming the file it searched for.
Data comes as an array or an object. Either way it's
extracted into the template scope as named variables,
and also exposed through a $props object.
That gives you two convenient ways to reach a value:
useTemplate('receipt', [
'orderId' => 1024,
'total' => '19.99',
'currency' => 'USD',
]);
In the template you can echo $orderId,
$total, and $currency
directly, or reach the object with
$props->orderId. Which style you use is
up to you — both are always in scope.
Rather than repeat the chrome of every message — the email envelope, the header and footer — you can declare a layout inside a template and let it provide the wrapper. Inside the template you name the layout:
<?php /* app/Common/Templates/emails/welcome.php */ ?>
<?php $this->layout('email'); ?>
<h1>Welcome, <?= $name ?>!</h1>
The rendered body then becomes $content
inside the matching layout file, found at
app/Common/Templates/layouts/email.php:
<?php /* app/Common/Templates/layouts/email.php */ ?>
<html>
<body>
<header><h1>Acme</h1></header>
<main><?= $content ?></main>
<footer>Thanks for reading.</footer>
</body>
</html>
This lets every welcome, receipt, or notice share one wrapper while each supplies its own body — update the layout once and the whole family of messages follows.
The most common pairing is with the notification capability. Compose an email body or HTML with a template, then pass it to a notification channel:
$html = useTemplate('emails/welcome', ['name' => 'Ada']);
$notifications->email()
->to('ada@example.com')
->subject('Welcome to Acme')
->html($html)
->queue();
Because the template returns a plain string, it drops straight into any channel that takes content. Your message markup lives in files, your sending logic stays in your code, and a single template change updates every message that uses it.
You can depend on the template manager directly instead of the global helper. It's registered as an injectable, so it's available wherever your dependencies are:
use Modules\ForgeTemplates\Injectable\TemplateManager;
final class WelcomeMailer
{
public function __construct(
private readonly TemplateManager $templates,
) {}
public function send(string $email, string $name): void
{
$body = $this->templates->useTemplate('emails/welcome', [
'name' => $name,
]);
// ... hand $body to your email channel
}
}
If your class prefers traits, a
TemplateHelper trait exposes the same
useTemplate() method directly. Between the
global helper, the trait, and the injectable, the
rendering path stays the same no matter how you reach
it.