Luminova Framework

PHP Luminova: PHP Layout Composition and Inheritance

Last updated: 2025-12-07 13:40:36

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() and end().These methods mark where a named section starts and where it finishes.Anything printed between begin('name') and end() 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>&copy; 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>&copy; 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:

  • footer
  • footer.social
  • footer.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 template

Controller 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'); // ❌ Error

Correct 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(); // ✅ Correct

Placeholder 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:

ParameterTypeDescription
$basestring|nullOptional base folder under the views root (e.g. layouts).
$modulestring|nullOptional module name (when HMVC is enabled).
$viewView|nullOptional View instance; stored by reference (not cloned).
$isolationboolWhether 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:

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
): self

Parameters:

ParameterTypeDescription
$basestring|nullOptional base folder under the views root.
$modulestring|nullOptional module name when HMVC is enabled.
$viewView|nullOptional View instance.
$isolationboolWhether 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:

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): self

Parameters:

ParameterTypeDescription
$layoutstringTemplate path relative to base (e.g. /layouts/scaffolding or 'layouts/scaffolding.php).
$modulestring|nullOptional 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.php

HMVC Application Example:

echo $this->layout->import('/layouts/card', 'Admin')->extend();

// app/Modules/Admin/Views/layouts/card.php

Note:To change base, you must specify call base() before calling extend or all.


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): self

Parameters:

ParameterTypeDescription
$layoutstringTemplate path relative to base (e.g. /layouts/scaffolding or layouts/scaffolding.php).

Return Value:

self - Returns this Layout instance.

Throws:

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): self

Parameters:

ParameterTypeDescription
$basestringFolder name inside the views root (e.g. layouts or partials).

Return Value:

self - Returns this Layout instance.

Throws:

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.php

module

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 = ''): self

Parameters:

ParameterTypeDescription
$modulestringThe HMVC custom module name (e.g, Admin, Post).

Return Value:

self - Returns this Layout instance.

Throws:

Example:

$this->layout->module('Admin')
    ->template('/layouts/scaffolding');

// HMVC Custom Module → /app/Modules/Admin/Views/layouts/scaffolding.php

begin

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): void

Parameters:

ParameterTypeDescription
$namestringThe section name to begin (must not be empty).

Throws:

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): void

Parameters:

ParameterTypeDescription
$namestring|nullExpected section name for validation, or null to skip name checks.

Throws:

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 = []): string

Parameters:

ParameterTypeDescription
$sectionstring|nullThe name of the section to retrieve.
$optionsarray<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:

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 $options array is also used to replace any matching {{ key }}placeholders inside the resolved layout sections.

Each key in $options is 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 = []): string

Parameters:

ParameterTypeDescription
$optionsarray<string,mixed>Optional data passed into the layout scope.

Return Value:

string - Returns the rendered layout output.

Throws:

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 $options array is also used to replace any matching {{ key }}placeholders inside the resolved layout sections.

Each key in $options is 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 = []): self

Parameters:

ParameterTypeDescription
$optionsarray<string,mixed>Optional data passed into the layout scope.

Return Value:

self - Returns the Layout instance.

Throws:

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 $options array is also used to replace any matching {{ key }}placeholders inside the resolved layout sections.

Each key in $options is also extracted as a variable within the layout scope.


sections

Return all captured sections.

public sections(array<string,mixed> $options = []): array

Parameters:

ParameterTypeDescription
$optionsarray<string,mixed>Optional data passed into the layout scope.

Return Value:

array<string,string> - Returns an associative array of section name => content

Throws:

Note:The $options array is also used to replace any matching {{ key }}placeholders inside the resolved layout sections.

Each key in $options is 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): bool

Parameters:

ParameterTypeDescription
$sectionstringThe name of the section to check.

Return Value:

bool - Returns true if the section exists, false otherwise.

Throws:

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): self

Parameters:

ParameterTypeDescription
$sectionstringSection name to prepend content.
$contentstringThe content to prepend.

Return Value:

self - Returns the Layout instance.

Throws:

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): self

Parameters:

ParameterTypeDescription
$sectionstringSection name to append content.
$contentstringThe content to append.

Return Value:

self - Returns the Layout instance.

Throws:

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): self

Parameters:

ParameterTypeDescription
$namestringPlaceholder name without curly braces.
$valuestringThe replacement content.
$sectionstring|nullOptional 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 $section can be targeted.

public replaceAll(array $replacements, ?string $section = null): self

Parameters:

ParameterTypeDescription
$replacementsarray<string,string>Associative array of placeholder => value.
$sectionstring|nullOptional 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): self

Parameters:

ParameterTypeDescription
$sectionstringThe section name.
$contentstringThe default section content.

Return Value:

self - Returns the Layout instance.

Example:

$this->setDefault('meta', '<meta name="robots" content="noindex">')->extend('meta');