Luna Templates
Introduction
Luna is the template engine included with Slenix. Luna provides a clean, expressive alternative to writing plain PHP in your views. Unlike some PHP template engines, Luna does not restrict you from using plain PHP code within your templates. In fact, all Luna templates are compiled into plain PHP code and cached until they are modified, meaning Luna adds essentially zero overhead to your application.
Luna template files use the .luna.php file extension and are typically stored in the views/ directory. Views may be returned from routes or controllers using the global view helper:
Router::get('/welcome', function ($request, $response) {
return view('welcome', ['name' => 'Cláudio']);
});Luna templates may also be rendered from controllers:
public function index(Request $request, Response $response): void
{
view('dashboard.profile', ['user' => $user]);
}Displaying Data
You may display data passed to your Luna views by wrapping the variable in curly braces. For example, given the following route:
Router::get('/greeting', function ($request, $response) {
return view('welcome', ['name' => 'Slenix']);
});You may display the contents of the name variable like so:
Hello, {{ $name }}.Luna's {{ }} echo statements are automatically sent through PHP's htmlspecialchars function to prevent XSS attacks.
Displaying Unescaped Data
By default, Luna {{ }} statements are HTML-escaped. If you do not want your data to be escaped, you may use the {!! !!} syntax:
Hello, {!! $name !!}.Warning: Be very careful when echoing content that is supplied by users of your application. You should typically use the escaped
{{ }}syntax to prevent XSS attacks when displaying user-supplied data.
Luna and JavaScript Frameworks
Since many JavaScript frameworks also use curly braces to indicate a given expression should be displayed in the browser, you should be aware that Luna only processes {{ }} at the server level. Content already inside <?php ?> blocks or JavaScript <script> tags is left untouched.
Luna Comments
Luna allows you to define comments in your views that will not be rendered in the final HTML returned to the browser:
{{-- This comment will not be present in the rendered HTML --}}Unlike HTML comments (<!-- -->), Luna comments are completely removed during compilation and never appear in the page source.
Luna Directives
In addition to template inheritance and displaying data, Luna also provides convenient shortcuts for common PHP control structures, such as conditional statements and loops.
If Statements
You may construct if statements using the @if, @elseif, @else, and @endif directives:
@if (count($users) === 1)
There is one user.
@elseif (count($users) > 1)
There are multiple users.
@else
There are no users.
@endifFor convenience, Luna also provides an @unless directive:
@unless ($user->isAdmin())
You do not have access to this section.
@endunlessIn addition to the conditional directives already discussed, the @isset directive may be used to check whether a variable is defined:
@isset($records)
<p>Records found: {{ count($records) }}</p>
@endissetSwitch Statements
Switch statements can be constructed using the @switch, @case, @default, @break, and @endswitch directives:
@switch($role)
@case('admin')
<span>Administrator</span>
@break
@case('editor')
<span>Editor</span>
@break
@default
<span>User</span>
@endswitchLoops
Luna provides simple directives for working with PHP's loop structures. Each of these directives functions identically to their PHP counterparts:
@foreach ($users as $user)
<p>{{ $user->name }}</p>
@endforeach
@for ($i = 0; $i < 10; $i++)
The current value is {{ $i }}.
@endfor
@while ($condition)
<p>Still looping.</p>
@endwhileWhen using the @foreach directive, Luna automatically injects a $loop variable that provides access to useful information about the current iteration:
@foreach ($users as $user)
@if ($loop->first)
<p>This is the first item.</p>
@endif
<p>{{ $loop->iteration }}. {{ $user->name }}</p>
@if ($loop->last)
<p>This is the last item.</p>
@endif
@endforeachThe $loop object contains the following properties:
| Property | Description |
|---|---|
$loop->index | The index of the current loop iteration (starts at 0) |
$loop->iteration | The current loop iteration (starts at 1) |
$loop->first | Whether this is the first iteration |
$loop->last | Whether this is the last iteration |
The Forelse Directive
The @forelse directive allows you to iterate over an array while also providing a fallback if the array is empty:
@forelse ($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
</article>
@empty
<p>No posts found.</p>
@endforelseThe Continue and Break Directives
You may use the @continue and @break directives to skip the current iteration or end the loop entirely:
@foreach ($users as $user)
@continue($user->type == 1)
<p>{{ $user->name }}</p>
@break($user->number == 5)
@endforeachTemplate Inheritance
Defining a Layout
Luna's template inheritance system allows you to define a base layout that child templates can extend. Consider a base layout defined in views/layouts/app.luna.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Slenix App')</title>
@stack('styles')
</head>
<body>
<nav>
@include('partials.nav')
</nav>
<main>
@yield('content')
</main>
<footer>
@include('partials.footer')
</footer>
@stack('scripts')
</body>
</html>Extending a Layout
When defining a child view, use the @extends directive to specify which layout the child view should "inherit". Views that extend a Luna layout may inject content into the layout's @yield sections using @section directives:
@extends('layouts.app')
@section('title', 'Dashboard')
@section('content')
<h1>Welcome to your dashboard.</h1>
<p>You are logged in as {{ session()->get('user_name') }}.</p>
@endsection
@push('scripts')
<script src="/js/dashboard.js"></script>
@endpushIn this example, the content section supplies content for the @yield('content') directive. The layout's @yield will be replaced with the contents of the named section when the view is rendered.
The @parent Directive
Sometimes you want to append to a layout section rather than replace it entirely. You may inject content from a parent layout into a section using the @parent directive:
@section('scripts')
@parent
<script src="/js/page-specific.js"></script>
@endsectionSection Checks
If you need to determine whether a section has content before rendering it, you may use the @hasSection and @sectionMissing directives:
@hasSection('navigation')
<div class="sidebar">
@yield('navigation')
</div>
@endif
@sectionMissing('navigation')
<div class="sidebar">
@include('partials.default-nav')
</div>
@endifStacks
Luna allows you to push to named stacks that can be rendered elsewhere in another view or layout. This can be particularly useful for specifying any JavaScript libraries required by your child views:
@push('scripts')
<script src="/js/chart.js"></script>
@endpushIf you would like to prepend content onto a stack, you should use the @prepend directive:
@prepend('scripts')
<script src="/js/polyfill.js"></script>
@endprependYou may push to a stack as many times as needed. To render the complete stack contents, pass the name of the stack to the @stack directive:
<head>
@stack('styles')
</head>
<body>
@stack('scripts')
</body>Including Subviews
Luna's @include directive allows you to include a Luna view from within another view. All variables that are available to the parent view will be made available to the included view:
<div>
@include('shared.errors')
<form>
<!-- form contents -->
</form>
</div>You may also pass additional data to the included view:
@include('partials.user-card', ['style' => 'full'])@includeIf
If you attempt to include a view that does not exist, Slenix will throw an error. If you would like to include a view that may or may not be present, you should use the @includeIf directive:
@includeIf('partials.sidebar', ['section' => 'overview'])@includeWhen
If you would like to include a view based on a given boolean condition, you may use the @includeWhen directive:
@includeWhen($user->isAdmin(), 'partials.admin-bar')@each
The @each directive allows you to render a template for each item in a collection, optionally specifying a fallback template if the collection is empty:
@each('partials.post-card', $posts, 'post', 'partials.no-posts')The arguments are, in order: the template to render for each item, the collection to iterate over, the variable name to expose inside the template, and an optional fallback template if the collection is empty. Inside the post-card template, the current item is available as $post.
Forms
CSRF Field
Any time you define an HTML form in your application that uses a POST, PUT, PATCH, or DELETE method, you should include a hidden CSRF token field so that the CSRF protection middleware can validate the request. You may use the @csrf directive to generate this field:
<form action="{{ route('profile.update') }}" method="post">
@csrf
<!-- form fields -->
</form>Method Field
Since HTML forms only support GET and POST HTTP verbs, you will need to add a hidden _method field to spoof PUT, PATCH, or DELETE requests. The @method directive can create this field for you:
<form action="{{ route('profile.update') }}" method="post">
@csrf
@method('PUT')
<!-- form fields -->
</form>Validation Errors
The @error and @enderror directives allow you to quickly check if validation error messages exist for a given attribute. Within an @error block, the $message variable is automatically available containing the error string:
<div>
<label for="email">E-mail</label>
<input
type="email"
id="email"
name="email"
value="@old('email')"
class="@class(['input-error' => has_error('email')])"
>
@error('email')
<p class="error-message">{{ $message }}</p>
@enderror
</div>Repopulating Old Input
The @old directive retrieves the previous input value that was flashed to the session during the last request. This is useful for repopulating form fields after a failed validation:
<input type="text" name="name" value="@old('name')">
{{-- With a default fallback --}}
<input type="text" name="city" value="@old('city', 'Luanda')">Environment and Authentication Directives
@auth and @guest
The @auth and @guest directives may be used to quickly determine if the current user is authenticated or is a guest:
@auth
<a href="{{ route('dashboard') }}">Dashboard</a>
<a href="{{ route('logout') }}">Sign out</a>
@endauth
@guest
<a href="{{ route('login.show') }}">Sign in</a>
<a href="{{ route('register.show') }}">Register</a>
@endguest@env
The @env directive allows you to render content only when the application is running in a specific environment:
@env('local')
<div class="debug-toolbar">
Running in local development mode.
</div>
@endenv@production and @debug
For convenience, @production and @debug are shorthand directives for the most common environment checks:
@production
<script src="https://cdn.example.com/analytics.js"></script>
@endproduction
@debug
@dump($user)
@enddebugOther Directives
@php
In some situations, it is useful to embed a block of plain PHP into your views. The @php directive allows you to execute a block of plain PHP:
@php
$platform = PHP_OS_FAMILY;
$version = phpversion();
@endphp
Running PHP {{ $version }} on {{ $platform }}.@json
The @json directive safely converts a PHP value to a JSON string with proper HTML entity encoding, making it safe to embed into JavaScript:
<script>
const appData = @json($user);
const config = @json(['debug' => false, 'locale' => 'pt_AO']);
</script>@class
The @class directive conditionally compiles a CSS class string. The directive accepts an array of classes, where the key is the class you wish to add, and the value is a boolean expression:
<span @class([
'badge',
'badge-success' => $user->isActive(),
'badge-danger' => !$user->isActive(),
])>
{{ $user->status }}
</span>@asset
The @asset directive generates a URL for the given path inside your public/ directory:
<img src="@asset('img/logo.png')" alt="Logo">
<link rel="stylesheet" href="@asset('css/app.css')">@route
The @route directive generates a URL for the given named route:
<a href="@route('users.index')">All Users</a>
<a href="@route('users.show', ['id' => $user->id])">View Profile</a>@vite
The @vite directive generates a <script type="module"> tag for the given asset path, useful when integrating with frontend build tools:
@vite('resources/js/app.js')@dump and @dd
During development, you may use the @dump and @dd directives to inspect variables. @dd additionally halts execution:
@dump($user)
@dd($user)Sharing Data With All Views
Occasionally, you may need to share a piece of data with all views that are rendered by your application. You may do so using Luna's share method. Typically, you should place calls to share within your application's bootstrap file:
use Slenix\Supports\Template\Luna;
Luna::share('appName', env('APP_NAME'));
Luna::share('currentYear', date('Y'));Shared data is merged with the data passed to individual views. Variables passed directly to a view take priority over shared variables.
Configuring Luna
Luna is configured once during your application's bootstrap phase. The recommended place for this is bootstrap/app.php or the application's service provider equivalent:
use Slenix\Supports\Template\Luna;
Luna::configure(
viewsDir: base_path('views'),
cacheDir: storage_path('views'),
logsDir: storage_path('logs'),
cache: env('APP_ENV') === 'production',
logging: true,
cacheTtl: 0, // 0 = invalidate by source file modification time
);| Option | Type | Default | Description |
|---|---|---|---|
viewsDir | string | views/ | Directory where .luna.php template files live |
cacheDir | string | storage/views/ | Directory where compiled templates are cached |
logsDir | string | storage/logs/ | Directory where luna.log is written |
cache | bool | false | Enable disk cache (recommended in production) |
logging | bool | false | Log compilation events and errors to luna.log |
cacheTtl | int | 0 | Cache TTL in seconds. 0 means invalidate by file mtime only |
Template Caching
For performance, Luna compiles your .luna.php templates into plain PHP and caches the compiled output in storage/views/. Compiled templates are only recompiled when the source template file is modified — making Luna essentially zero-overhead in production.
Cache Invalidation
The cache is considered invalid when either of the following is true:
- The source
.luna.phpfile was modified after the cached version was written. - A non-zero
cacheTtlis configured and the cache file is older than the TTL.
Bypassing the Cache in Development
When APP_DEBUG=true is set in your .env file, Luna automatically bypasses both the memory and disk cache entirely. This means every request recompiles every template from source, ensuring you always see your latest changes without having to manually clear the cache:
APP_DEBUG=trueClearing the Cache
If you need to clear the compiled template cache manually — for example, after a deployment — you may use the Celestial CLI:
php celestial viewOr call it programmatically:
$deleted = Luna::clearCache();
// $deleted = number of files removedCache Statistics
You may inspect the current state of the template cache at any time:
$stats = Luna::cacheStats();
// [
// 'files' => 12,
// 'size_bytes' => 45312,
// 'size_human' => '44.25 KB',
// 'directory' => '/path/to/storage/views',
// ]Debugging Templates
When a template fails to compile or render, Slenix throws a RuntimeException with a descriptive message indicating the template file that caused the error.
When APP_DEBUG=true, the exception message also includes a numbered listing of the compiled PHP output, making it much easier to identify the exact line that caused the syntax error:
Erro ao avaliar template [/path/to/views/auth/login.luna.php]:
syntax error, unexpected token ":"
--- Compiled template ---
1 | <!DOCTYPE html>
2 | <html lang="pt-AO">
3 | <?php if(!empty($_SESSION['user_id'])): ?>
...Directive Reference
Output
| Directive | Description |
|---|---|
{{ $expr }} | Echo with htmlspecialchars (XSS-safe) |
{!! $expr !!} | Echo raw HTML without escaping |
{{-- comment --}} | Luna comment, not rendered in HTML output |
Conditionals
| Directive | Description |
|---|---|
@if / @elseif / @else / @endif | Standard if statement |
@unless / @endunless | Negated if statement |
@isset / @endisset | Check if variable is set |
@switch / @case / @default / @break / @endswitch | Switch statement |
@auth / @endauth | Render if user is authenticated |
@guest / @endguest | Render if user is a guest |
@env('name') / @endenv | Render in a specific environment |
@production / @endproduction | Render in production only |
@debug / @enddebug | Render when APP_DEBUG=true |
@hasSection('name') | Check if a section has content |
@sectionMissing('name') | Check if a section is empty |
Loops
| Directive | Description |
|---|---|
@foreach / @endforeach | Foreach loop with $loop variable |
@forelse / @empty / @endforelse | Foreach with empty fallback |
@for / @endfor | Standard for loop |
@while / @endwhile | While loop |
@continue / @continue($cond) | Skip to next iteration |
@break / @break($cond) | Break out of loop |
Template Inheritance
| Directive | Description |
|---|---|
@extends('layout') | Extend a parent layout |
@section('name') / @endsection | Define a content section |
@section('name', 'value') | Define an inline section |
@yield('name') / @yield('name', 'default') | Output a section |
@show | End and immediately yield a section |
@parent | Include the parent section's content |
Includes
| Directive | Description |
|---|---|
@include('view') | Include a subview |
@include('view', $data) | Include with extra data |
@includeIf('view') | Include only if the file exists |
@includeWhen($cond, 'view') | Include when condition is true |
@each('view', $items, 'var', 'empty') | Render a view for each item |
Stacks
| Directive | Description |
|---|---|
@push('name') / @endpush | Append content to a stack |
@prepend('name') / @endprepend | Prepend content to a stack |
@stack('name') | Output the full stack |
Forms and Security
| Directive | Description |
|---|---|
@csrf | Hidden CSRF token input field |
@method('PUT') | Hidden _method field for HTTP verb spoofing |
@old('field') | Previous input value |
@old('field', 'default') | Previous input with fallback |
@error('field') / @enderror | Conditional error block with $message |
Utilities
| Directive | Description |
|---|---|
@php / @endphp | Raw PHP block |
@json($var) | Safe JSON output for use in JavaScript |
@class(['cls' => $bool]) | Conditional CSS class list |
@asset('path') | URL to a public asset |
@route('name') | URL for a named route |
@vite('path') | Module script tag for a Vite asset |
@dump($var) | Dump variable for debugging |
@dd($var) | Dump and die |