PHP Luminova: PHP Layout Composition and Inheritance
Template Layout allows PHP layout composition, including sections, nesting, and template selection. It provides a flexible system for building page layouts when using the default template engine.
Luminova allows you to build applications using template engines like Twig or Smarty. But PHP itself is already a capable template engine. To make PHP templates easier to work with, Luminova includes a simple Layout system.
The Layout class gives you an easy way to structure your pages. It supports layout composition, sections, nesting, and selecting which template to extend. This feature is optional and only applies when using the default PHP template engine.
The goal is to keep things simple and fast. The Layout system solves the common inheritance problem in plain PHP templates without adding extra complexity. Luminova already gives you the tools you need, this layout feature just fills the gap cleanly.
The core of the layout system relies on two methods:
begin()andend().These methods mark where a named section starts and where it finishes.Anything printed betweenbegin('name')andend()becomes the content of that section and can later be extended or overridden by another template.
Setup
The layout system can be turned on or off.When it is enabled, your PHP templates can access the $this->layout object directly.
If enableDefaultTemplateLayout is set to true and $templateEngine is set to default.
Enable Layout:
// /app/Config/Template.php
namespace App\Config;
final class Template
{
// Use PHP as the template engine
public string $templateEngine = 'default';
// Enable the default layout system
public bool $enableDefaultTemplateLayout = true;
}With the layout system enabled, your PHP templates can extend layout sections in a clear and predictable way, without slowing down your application.
Examples
In this example, we will create three template files inside the /resources/Views/ directory:
/layouts/scaffolding.php— the main layout of the application. Other templates can extend its sections./layouts/card.php— a smaller reusable layout that can be imported inside the main layout./index.php— the main view that a controller will render.
Layout Scaffolding
The file /resources/Views/layouts/scaffolding.php shows how to split your layout into named sections.These sections can then be extended or replaced by other templates.
<?php $this->layout->begin('head'); ?>
<head>
<link rel="stylesheet" href="<?= $this->_asset;?>css/style.css" />
<script type="text/javascript" src="<?= $this->_asset;?>js/script.js"></script>
<title><?= $this->_title;?></title>
<?php $this->layout->begin('meta'); ?>
<meta charset="utf-8"/>
<meta name="keywords" content="php, luminova, layout"/>
<?php $this->layout->end(); ?>
</head>
<?php $this->layout->end(); ?>
<body>
<?php $this->layout->begin('section'); ?>
<section>
<p>Main Template Section</p>
<div>
<?= $this->layout->import('/layouts/card')->render();?>
</div>
<?php $this->layout->begin('aside'); ?>
<aside>
<h1>Right Menu</h1>
<ul>
<li><a href="#">Item 1</a></li>
<li><a href="#">Item 2</a></li>
</ul>
<?php $this->layout->begin('button'); ?>
<button type="button">Submit</button>
<?php $this->layout->end(); ?>
</aside>
<?php $this->layout->end(); ?>
</section>
<?php $this->layout->end(); ?>
<?php $this->layout->begin('footer'); ?>
<footer>
<p>© Copyright 2023-2024 <a href="https://luminova.ng">Luminova</a></p>
<?php $this->layout->begin('social'); ?>
<ul>
<li><a href="#">Facebook</a></li>
<li><a href="#">Instagram</a></li>
</ul>
<?php $this->layout->end(); ?>
</footer>
<?php $this->layout->end(); ?>Reusable Layout
This file /resources/Views/layouts/card.php contains a simple reusable layout.It is imported inside the scaffolding file using $this->layout->import().
<table>
<thead>
<th>Number</th>
<th>Description</th>
</thead>
<tbody>
<?php for($i = 0; $i < 5; $i++):?>
<tr>
<td>#<?= $i;?></td>
<td>This is card row <?= $i;?></td>
</tr>
<?php endfor; ?>
</tbody>
</table>Template File
The controller-rendered template, this file /resources/Views/index.php extends and inherits content from bothlayouts/scaffolding.php and layouts/card.php.
<!DOCTYPE html>
<html>
<?php $tpl = $this->layout->template('/layouts/scaffolding'); ?>
<?= $tpl->extend('head'); ?>
<body>
<h1>On This Template:</h1>
<?= $tpl->extend('section'); ?>
<?= $tpl->extend('footer'); ?>
</body>
</html>Template Output
The above examples will generate template structure like this:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../public/assets/css/style.css" />
<script type="text/javascript" src="../public/assets/js/script.js"></script>
<title>Welcome to PHP Luminova</title>
</head>
<body>
<h1>On This Template:</h1>
<section>
<p>Website Page Section</p>
<div>
<table>
<thead>
<th>Number</th>
<th>Description</th>
</thead>
<tbody>
<tr><td>#0</td><td>This is card number 0</td></tr>
<tr><td>#1</td><td>This is card number 1</td></tr>
<tr><td>#2</td><td>This is card number 2</td></tr>
<tr><td>#3</td><td>This is card number 3</td></tr>
<tr><td>#4</td><td>This is card number 4</td></tr>
</tbody>
</table>
</div>
</section>
<footer>
<p>© Copyright 2023-2024 <a href="https://luminova.ng">Luminova</a></p>
</footer>
</body>
</html>Extending Examples
You can extend the entire layout or target a specific section inside it.
Extend the entire layout
<?= $this->layout->template('scaffolding')->render(); ?>
<?= $this->layout->import('scaffolding')->render(); ?>Extend a specific section
Pass the section name to the extend() method:
<?= $this->layout->template('scaffolding')->extend('section'); ?>
<?= $this->layout->import('scaffolding')->extend('section'); ?>Recommendation
For large templates, it is best to organize sections in a nested structure.You define the nesting in your layout file using begin('name'), and you extend nested sections using dot-notation:
footerfooter.socialfooter.social.links, etc.
When calling end(), you may also provide the section name. This acts as a safety check to ensure the section being closed matches the one that was opened. It helps catch mistakes early, especially in deeply nested layouts.
Example:
Extending a nested section called social, which itself belongs to the footer section:
<?= $this->layout->template('scaffolding')->extend('footer.social'); ?>Scope
Isolation Mode
When defining layout sections, always use the $this keyword inside layout files, regardless of whether template isolation is enabled.
Use $self only in controller-rendered templates or when accessing the view object and application object in isolation mode within layout files.
Example: Isolation Mode Enabled
When App\Config\Template->templateIsolation = true:
$self->app->appMethod();
$self->_title;
$self->_href;
$self->_asset;
$self->getOptions();Example: Isolation Mode Disabled
When App\Config\Template->templateIsolation = false:
$this->app->appMethod();
$this->_title;
$this->_href;
$this->_asset;
$this->getOptions();Controller-Rendered Template
A controller-rendered template is the template file specified when calling the view() method in a controller.Its scope is tied to the Luminova\Template\View class, which provides access to the $layout property for extending layouts.
Important:The begin() and end() methods of Layout are protected. You cannot call them directly from a controller-rendered template.To define sections, you must first load a layout file using template() or import(), which executes the layout in layout scope.
Template Structure:
/resources/Views/home.php # Controller-rendered template
/resources/Views/layouts/scaffolding.php # Layout templateController Template Example
namespace App\Controller\Http;
use Luminova\Base\Controller;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
#[Prefix('/(:base)')]
class MainController extends Controller
{
#[Route('/')]
public function home(): int
{
// Render 'home.php' template
return $this->view('home');
}
}Layout Template Extensions
Calling begin() or end() directly in a controller template will trigger an error:
// /resources/Views/home.php
$this->layout->template('scaffolding')->begin('foo'); // ❌ ErrorCorrect usage: call begin() and end() only inside the layout file:
// /resources/Views/layouts/scaffolding.php
$this->layout->begin('foo');
// layout content...
$this->layout->end();Then, in the controller template, render the layout or extract sections:
// /resources/Views/home.php
$this->layout->template('scaffolding')->render(); // ✅ CorrectPlaceholder Delimiter
The Layout engine includes a lightweight placeholder system.Placeholders let you define dynamic values inside layout sections, and those values are replaced when the layout is resolved during extend() or render().
Placeholders are matched against the $options array passed to these methods.
Defining Placeholders
Placeholders use double curly braces: {{ name }}Spaces around the name are allowed.
// /resources/Views/layouts/scaffolding.php
<?php $this->layout->begin('title'); ?>
<h1>Title On {{ year }}</h1>
<?php $this->layout->end('title'); ?>Replacing Placeholders
Pass an associative array to extend() or render().Keys must match the placeholder names.
// /resources/Views/index.php
// Replace only inside a specific section
<?= $this->layout->template('layouts/scaffolding')->extend('title', ['year' => 2025]); ?>
// Replace placeholders across all sections during full render
<?= $this->layout->template('layouts/scaffolding')->render(['year' => 2025]); ?>Class Definition
- Class namespace:
Luminova\Template\Engines\Layout - Class implements:
\Stringable
Properties
app
Provides access to the application instance inside templates.This allows calling application methods in a layout in the same way you would from a template rendered by a controller.
public ?\App\Application $app = null;Examples:
In controller rendered template:
// /resources/Views/home.php
// /app/Modules/<Module>/Views/home.php
<?= $this->app->appMethod(); ?>Inside a layout template extended by the controller template:
// /resources/Views/layouts/main.php
// /app/Modules/<Module>/Views/layouts/main.php
<?= $this->app->appMethod(); ?>layout
Available through magic __get, this property ensures consistent access to layout functionality in templates.
public ?\Luminova\Template\Engines\Layout $layout = null;Example:
Template rendered from controller class:
// /resources/Views/home.php
// /app/Modules/<Module>/Views/home.php
<?= $this->layout->template('/layouts/main')->render(); ?>The protected layout the main.php template extends:
// /resources/Views/layouts/main.php
// /app/Modules/<Module>/Views/layouts/main.php
<?= $this->layout->start(); ?> // <?= $this->start(); ?>
Hello World
<?= $this->layout->end(); ?> // <?= $this->end(); ?>
<?= $this->layout->template('layouts/sub')->render(); ?> // <?= $this->template('layouts/sub')->render(); ?>Note:
$this->template()is an alias for$this->layout->template(). Both work the same way in templates.
Methods
constructor
Create a new Layout instance.
public static __construct(
?string $base = null,
?string $module = null,
?Luminova\Template\View $view = null
bool $isolation = false
)Parameters:
| Parameter | Type | Description |
|---|---|---|
$base | string|null | Optional base folder under the views root (e.g. layouts). |
$module | string|null | Optional module name (when HMVC is enabled). |
$view | View|null | Optional View instance; stored by reference (not cloned). |
$isolation | bool | Whether template isolation is enabled. If true, the $self variable will hold the view object, otherwise $this is used directly in templates. |
Return Value:
self - Returns a shared Layout instance configured for the provided base/module/view.
Throws:
- \Luminova\Exceptions\RuntimeException - When the module view root is not found, or the base folder does not exist.
Example:
$layout = new Layout('layouts', view: $view);
echo $layout->template('/scaffolding')
->extend('section');of
Singleton method that returns a shared Layout instance.
public static of(
?string $base = null,
?string $module = null,
?Luminova\Template\View $view = null,
bool $isolation = false
): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$base | string|null | Optional base folder under the views root. |
$module | string|null | Optional module name when HMVC is enabled. |
$view | View|null | Optional View instance. |
$isolation | bool | Whether template isolation is enabled. If true, the $self variable will hold the view object, otherwise $this is used directly in templates. |
Return Value:
self - Returns a shared Layout instance configured for the provided base/module/view.
Throws:
- \Luminova\Exceptions\RuntimeException - When the module view root is not found, or the base folder does not exist.
Example:
$layout = Layout::of('layouts', 'Blog', $view);
echo $layout->template('/scaffolding')
->extend('section');import
Import a layout file quickly (convenience wrapper).
This creates a new instance (module may be provided) and selects the template immediately.
public import(string $layout, ?string $module = null): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$layout | string | Template path relative to base (e.g. /layouts/scaffolding or 'layouts/scaffolding.php). |
$module | string|null | Optional module name when HMVC is enabled. |
Return Value:
static - Returns a prepared Layout instance with the template selected.
Example:
// Immediately prepare the layout and then render a section
echo $this->layout->import('/layouts/card')->extend();
// app/resources/Views/layouts/card.phpHMVC Application Example:
echo $this->layout->import('/layouts/card', 'Admin')->extend();
// app/Modules/Admin/Views/layouts/card.phpNote:To change base, you must specify call
base()before callingextendorall.
template
Select the template file to render.
The filepath may end with ".php" or not. The method resolves the absolute file path using the configured root and base. The file must exist.
public template(string $layout): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$layout | string | Template path relative to base (e.g. /layouts/scaffolding or layouts/scaffolding.php). |
Return Value:
self - Returns this Layout instance.
Throws:
- \Luminova\Exceptions\RuntimeException - When the resolved layout file does not exist.
Example:
$this->layout->template('/scaffolding')->extend('head');Custom Object Example:
$layout = new Layout('layouts', null, $view);
echo $layout->template('/scaffolding')->extend('head');base
Set the base folder under the views root.
This method defines which directory under your view root should be treated as the layout container.
MVC view root: /resources/Views/HMVC view root: /app/Modules/<Module>/
public base(string $base): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$base | string | Folder name inside the views root (e.g. layouts or partials). |
Return Value:
self - Returns this Layout instance.
Throws:
- \Luminova\Exceptions\RuntimeException - When the base folder does not exist.
Example:
$this->layout->base('layouts')
->template('scaffolding');
// MVC → /resources/Views/layouts/scaffolding.php
// HMVC Main → /app/Modules/Views/layouts/scaffolding.php
// HMVC Custom Module → /app/Modules/<Module>/Views/layouts/scaffolding.phpmodule
Configure module-specific views root when HMVC is enabled.
This method switches the Layout system to load templates from a specific module.If HMVC is disabled, the method does nothing and simply returns the current instance.
MVC view root: /resources/Views/HMVC view root: /app/Modules/<Module>/
public module(string $module = ''): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$module | string | The HMVC custom module name (e.g, Admin, Post). |
Return Value:
self - Returns this Layout instance.
Throws:
- \Luminova\Exceptions\RuntimeException - When the module view root is not found.
Example:
$this->layout->module('Admin')
->template('/layouts/scaffolding');
// HMVC Custom Module → /app/Modules/Admin/Views/layouts/scaffolding.phpbegin
Begin capturing a layout section.
This marks the start of a named section. The section name is pushed onto the stack, and a new output buffer is started to capture everything printed until end() is called.
protected begin(string $name): voidParameters:
| Parameter | Type | Description |
|---|---|---|
$name | string | The section name to begin (must not be empty). |
Throws:
- \Luminova\Exceptions\RuntimeException - If empty section name was provide.
Example:
In template layout definition.
<?php $this->layout->begin('content'); ?>
<p>Hi</p>
<?php $this->layout->end(); ?>NoteAlways call
end()method where the current section ends to mark a section is completed and capture the output.
end
Close the current layout section and store its buffered output.
This method ends the most recently opened section. Sections must close in the same order they were opened. If $name is provided, it must match the section currently being closed.
This helps catch mistakes in large templates where nested sections can be hard to track.
protected end(?string $name = null): voidParameters:
| Parameter | Type | Description |
|---|---|---|
$name | string|null | Expected section name for validation, or null to skip name checks. |
Throws:
- \Luminova\Exceptions\RuntimeException - If there is no active section to close, or if
$namedoes not match the active section.
Example:
<?php $this->layout->begin('title'); ?>
<title>Page Title</title>
<?php $this->layout->end('title'); ?>extend
Get the content of a specific layout section.
This method ensures the template is rendered once and returns the output of the requested section. If the section does not exist, an empty string is returned.
public extend(string $section, array<string,mixed> $options = []): stringParameters:
| Parameter | Type | Description |
|---|---|---|
$section | string|null | The name of the section to retrieve. |
$options | array<string,mixed> | Optional data to pass into the layout scope. This $options is also used to replace any matching {{ key }} placeholders inside the resolved layout sections. |
Return Value:
string - Returns the content of the requested section, or an empty string if not found.
Throws:
- \Luminova\Exceptions\RuntimeException - If the template file cannot be loaded or resolved.
Example:
Render template and get the head section
echo $this->layout->extend('head');Render with options and get the footer section
echo $this->layout->extend('footer', ['year' => 2025]);Note:The
$optionsarray is also used to replace any matching{{ key }}placeholders inside the resolved layout sections.Each key in
$optionsis also extracted as a variable within the layout scope.
render
Render the selected layout template and return its full output.
This method renders the layout file and returns the final combined output. It does not perform section mapping; it simply executes the template and captures the output from top to bottom. When the object is cast to a string __toString() will call this method.
public render(array<string,mixed> $options = []): stringParameters:
| Parameter | Type | Description |
|---|---|---|
$options | array<string,mixed> | Optional data passed into the layout scope. |
Return Value:
string - Returns the rendered layout output.
Throws:
- \Luminova\Exceptions\RuntimeException - If the template file cannot be loaded or resolved.
Example:
// Direct render
$this->layout->template('/layouts/scaffolding')->render();
// Using import
$this->layout->import('/layouts/scaffolding')->render();
// String casting
echo $this->layout->template('/layouts/scaffolding');
echo $this->layout->import('/layouts/scaffolding');Note:The
$optionsarray is also used to replace any matching{{ key }}placeholders inside the resolved layout sections.Each key in
$optionsis also extracted as a variable within the layout scope.
resolve
Resolve the layout without performing placeholder replacement.
This method renders the layout and populates all sections, but it does not apply any {{ key }} delimiter replacements. This allows you to manually call replace() or replaceAll() afterward if you need full control over the substitution process.
public resolve(array<string,mixed> $options = []): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$options | array<string,mixed> | Optional data passed into the layout scope. |
Return Value:
self - Returns the Layout instance.
Throws:
- \Luminova\Exceptions\RuntimeException - If the template file cannot be loaded or resolved.
Example:
$tpl = $this->layout->template('/layouts/scaffolding')->resolve();
echo $tpl->replaceAll([
'name' => 'John',
'email' => '[email protected]'
]);
// Or single replace
echo $tpl->replace('name', 'John');Note:The
$optionsarray is also used to replace any matching{{ key }}placeholders inside the resolved layout sections.Each key in
$optionsis also extracted as a variable within the layout scope.
sections
Return all captured sections.
public sections(array<string,mixed> $options = []): arrayParameters:
| Parameter | Type | Description |
|---|---|---|
$options | array<string,mixed> | Optional data passed into the layout scope. |
Return Value:
array<string,string> - Returns an associative array of section name => content
Throws:
- \Luminova\Exceptions\RuntimeException - If the template file cannot be loaded or resolved.
Note:The
$optionsarray is also used to replace any matching{{ key }}placeholders inside the resolved layout sections.Each key in
$optionsis also extracted as a variable within the layout scope.
exists
Check if a named section exists in the layout.
This method determines whether the given section has been defined and captured.If the section has not been resolved yet, it attempts to render the layout to populate sections.
public exists(string $section): boolParameters:
| Parameter | Type | Description |
|---|---|---|
$section | string | The name of the section to check. |
Return Value:
bool - Returns true if the section exists, false otherwise.
Throws:
- \Luminova\Exceptions\RuntimeException - If the template file cannot be loaded or resolved.
Example:
$tpl = $this->layout->template('scaffolding');
if ($tpl->exists('footer')) {
echo $tpl->extend('footer');
}prepend
Prepend content to a section.
public prepend(string $section, string $content): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$section | string | Section name to prepend content. |
$content | string | The content to prepend. |
Return Value:
self - Returns the Layout instance.
Throws:
- \Luminova\Exceptions\RuntimeException - If the template file cannot be loaded or resolved.
Example:
echo $this->layout->template('scaffolding')
->prepend('meta', '<meta name="robots" content="noindex">')
->extend('meta');append
Append content to a section.
public append(string $section, string $content): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$section | string | Section name to append content. |
$content | string | The content to append. |
Return Value:
self - Returns the Layout instance.
Throws:
- \Luminova\Exceptions\RuntimeException - If the template file cannot be loaded or resolved.
Example:
echo $this->layout->template('scaffolding')
->append('footer', '<script src="/js/footer.js"></script>')
->extend('footer');replace
Replace a named placeholder in content with the provided value.
Named placeholders are defined with double curly braces, e.g. {{placeholder}}. Optional spaces around the placeholder name are allowed ({{ name }}).
public replace(string $name, string $value, ?string $section = null): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$name | string | Placeholder name without curly braces. |
$value | string | The replacement content. |
$section | string|null | Optional specific section to apply the replacement. If null, all sections are processed. |
Return Value:
self - Returns the Layout instance.
Example:
// Replace in all sections:
$this->layout->replace('data', 'World')->render();
// Replace in a specific section:
$this->layout->replace('title', 'My Page', 'head')->extend('head');replaceAll
Replace multiple placeholders in layout sections using an associative array.
Each array key represents the placeholder name (without braces), and the value is the replacement content.
Optionally, a specific
$sectioncan be targeted.
public replaceAll(array $replacements, ?string $section = null): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$replacements | array<string,string> | Associative array of placeholder => value. |
$section | string|null | Optional section to replace in. If null, all sections are processed. |
Return Value:
self - Returns the Layout instance.
Example:
// Replace multiple placeholders
$this->replaceAll([
'title' => 'My Page',
'content' => 'Hello World'
])->render();setDefault
Set default content for a section if not defined.
public setDefault(string $section, string $content): selfParameters:
| Parameter | Type | Description |
|---|---|---|
$section | string | The section name. |
$content | string | The default section content. |
Return Value:
self - Returns the Layout instance.
Example:
$this->setDefault('meta', '<meta name="robots" content="noindex">')->extend('meta');