Templates & Views
Build server-rendered pages with the ML Template Engine (.ml.php).
Table of Contents
- How Templates Work
- Layouts & Sections
- Outputting Data
- Control Structures
- Includes & Partials
- Stacks (CSS / JS)
- Components
- View Composers
- Custom Directives
- Rendering in Controllers
How Templates Work
Templates use .ml.php files stored in resources/views/.
resources/views/
├── layouts/
│ └── app.ml.php ← Base layout
├── pages/
│ ├── home.ml.php ← Home page
│ └── about.ml.php ← About page
├── posts/
│ ├── index.ml.php ← Post listing
│ └── show.ml.php ← Single post
└── partials/
├── header.ml.php ← Reusable header
└── footer.ml.php ← Reusable footer
Dot notation maps to directory paths:
| View Name | File Path |
|---|---|
home | resources/views/home.ml.php |
pages.about | resources/views/pages/about.ml.php |
posts.index | resources/views/posts/index.ml.php |
layouts.app | resources/views/layouts/app.ml.php |
partials.header | resources/views/partials/header.ml.php |
Layouts & Sections
Base Layout
Create resources/views/layouts/app.ml.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', 'My App') — MonkeysLegion</title>
<link rel="stylesheet" href="/assets/css/app.css">
@stack('styles')
</head>
<body>
@include('partials.header')
<main class="container">
@yield('content')
</main>
@include('partials.footer')
@stack('scripts')
</body>
</html>
Child Page
Create resources/views/pages/home.ml.php:
@extends('layouts.app')
@section('title', 'Home')
@section('content')
<h1>Welcome to {{ $appName }}</h1>
<p>{{ $description }}</p>
@if($featuredPosts)
<h2>Featured Posts</h2>
@foreach($featuredPosts as $post)
<article>
<h3><a href="/posts/{{ $post->slug }}">{{ $post->title }}</a></h3>
<p>{{ $post->excerpt }}</p>
<time>{{ $post->created_at->format('M d, Y') }}</time>
</article>
@endforeach
@else
<p>No featured posts yet.</p>
@endif
@endsection
@push('scripts')
<script src="/assets/js/home.js" defer></script>
@endpush
How It Works
@extends('layouts.app')— This page inherits from the app layout@section('title', 'Home')— Fills the title yield@section('content') ... @endsection— Fills the content yield@yield('title', 'My App')— Output section with fallback default@push('scripts')— Append to the scripts stack
Outputting Data
Escaped Output (Safe)
{{ $user->name }}
{{-- Output: <script> → safe from XSS --}}
Always use {{ }} for user-provided data. It calls htmlspecialchars() automatically.
Raw Output (Unescaped)
{!! $post->body_html !!}
{{-- Output: <strong>Bold</strong> → rendered as HTML --}}
Warning: Only use
{!! !!}for content you trust (e.g., sanitized HTML from a WYSIWYG editor).
Comments
{{-- This comment will NOT appear in the rendered HTML --}}
<!-- This HTML comment WILL appear in the output -->
Control Structures
Conditionals
@if($user->isVerified)
<span class="badge badge-green">Verified</span>
@elseif($user->email_verified_at === null)
<span class="badge badge-amber">Pending</span>
@else
<span class="badge badge-red">Unverified</span>
@endif
Loops
{{-- foreach --}}
@foreach($products as $product)
<div class="product-card">
<h3>{{ $product->name }}</h3>
<span class="price">{{ $product->formattedPrice }}</span>
</div>
@endforeach
{{-- for --}}
@for($i = 1; $i <= 5; $i++)
<span class="star">★</span>
@endfor
{{-- while --}}
@while($condition)
<p>Still going...</p>
@endwhile
Isset / Empty
@isset($subtitle)
<h2>{{ $subtitle }}</h2>
@endisset
@empty($posts)
<p class="empty-state">No posts found.</p>
@endempty
Environment Check
@env('dev')
<div class="debug-bar">
Debug Mode Active — {{ $phpVersion }}
</div>
@endenv
Includes & Partials
Basic Include
@include('partials.header')
@include('partials.footer')
Include with Data
@include('partials.alert', ['type' => 'success', 'message' => 'Saved!'])
Partial File
Create resources/views/partials/alert.ml.php:
<div class="alert alert-{{ $type }}">
{{ $message }}
</div>
Conditional Include
@if($showSidebar)
@include('partials.sidebar')
@endif
Stacks (CSS / JS)
Stacks let child templates inject assets into the layout.
In Layout
<head>
<link rel="stylesheet" href="/assets/css/app.css">
@stack('styles')
</head>
<body>
...
<script src="/assets/js/app.js"></script>
@stack('scripts')
</body>
In Child Page
@push('styles')
<link rel="stylesheet" href="/assets/css/gallery.css">
@endpush
@push('scripts')
<script src="/assets/js/gallery.js" defer></script>
<script>
Gallery.init('#photo-grid');
</script>
@endpush
Multiple @push blocks to the same stack are concatenated in order.
Components
Register a Component
In a service provider or bootstrap:
$view->component('alert', function (string $type, string $message, bool $dismissible = false) {
return <<<HTML
<div class="alert alert-{$type}" role="alert">
<p>{$message}</p>
{$dismissible ? '<button class="alert-close">×</button>' : ''}
</div>
HTML;
});
Use in Templates
<x-alert type="success" message="Product saved!" dismissible="true" />
<x-alert type="error" message="Something went wrong." />
View Composers
Composers automatically attach data to specific views, so you don't have to pass it from every controller.
Register a Composer
// In a service provider
$view->composer('layouts.app', function (array &$data) {
$data['currentYear'] = date('Y');
$data['appVersion'] = '2.0.0';
});
// Wildcard: attach to all views matching a pattern
$view->composer('admin.*', function (array &$data) use ($container) {
$data['pendingCount'] = $container->get(NotificationService::class)->pendingCount();
});
Use in Template
{{-- Available in layouts/app.ml.php automatically --}}
<footer>© {{ $currentYear }} MonkeysCloud</footer>
Custom Directives
Register a Directive
$view->addDirective('datetime', function (string $expression) {
return "<?php echo (new \\DateTimeImmutable({$expression}))->format('M d, Y H:i'); ?>";
});
$view->addDirective('money', function (string $expression) {
return "<?php echo '$' . number_format((float)({$expression}), 2); ?>";
});
Use in Templates
<p>Published: @datetime($post->published_at)</p>
<p>Price: @money($product->price)</p>
Rendering in Controllers
Web Controller
<?php
declare(strict_types=1);
namespace App\Controller;
use MonkeysLegion\Router\Attributes\Route;
use MonkeysLegion\Http\Message\Response;
use MonkeysLegion\Template\Renderer;
final class PostWebController
{
public function __construct(
private readonly Renderer $renderer,
private readonly PostRepository $posts,
) {}
#[Route('GET', '/posts', name: 'posts.web.index')]
public function index(): Response
{
$posts = $this->posts->findPublished();
return Response::html(
$this->renderer->render('posts.index', [
'title' => 'Blog',
'posts' => $posts,
])
);
}
#[Route('GET', '/posts/{slug}', name: 'posts.web.show')]
public function show(string $slug): Response
{
$post = $this->posts->findOneBy(['slug' => $slug])
?? throw new \RuntimeException('Post not found');
return Response::html(
$this->renderer->render('posts.show', [
'title' => $post->title,
'post' => $post,
])
);
}
}
Template: Post Listing
resources/views/posts/index.ml.php:
@extends('layouts.app')
@section('title', '{{ $title }}')
@section('content')
<h1>{{ $title }}</h1>
<div class="post-grid">
@foreach($posts as $post)
<article class="post-card">
<h2><a href="/posts/{{ $post->slug }}">{{ $post->title }}</a></h2>
<p class="post-excerpt">{{ $post->excerpt }}</p>
<footer class="post-meta">
<span>By {{ $post->author->name }}</span>
<time datetime="{{ $post->published_at->format('c') }}">
{{ $post->published_at->format('M d, Y') }}
</time>
@if($post->commentCount > 0)
<span>{{ $post->commentCount }} comments</span>
@endif
</footer>
</article>
@endforeach
</div>
@empty($posts)
<div class="empty-state">
<p>No posts published yet.</p>
</div>
@endempty
@endsection
Template: Single Post
resources/views/posts/show.ml.php:
@extends('layouts.app')
@section('title', '{{ $post->title }}')
@section('content')
<article class="post-full">
<header>
<h1>{{ $post->title }}</h1>
<div class="post-meta">
<span>{{ $post->author->name }}</span>
<time>{{ $post->published_at->format('F d, Y') }}</time>
</div>
</header>
<div class="post-body">
{!! $post->body !!}
</div>
@if($post->commentCount > 0)
<section class="comments">
<h2>Comments ({{ $post->commentCount }})</h2>
@foreach($post->comments as $comment)
@include('partials.comment', ['comment' => $comment])
@endforeach
</section>
@endif
</article>
@endsection