📦 Marketplace⭐ GitHub
Guidesv2.0

Templates & Views

Build server-rendered pages with the ML Template Engine (.ml.php).


Table of Contents


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 NameFile Path
homeresources/views/home.ml.php
pages.aboutresources/views/pages/about.ml.php
posts.indexresources/views/posts/index.ml.php
layouts.appresources/views/layouts/app.ml.php
partials.headerresources/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

  1. @extends('layouts.app') — This page inherits from the app layout
  2. @section('title', 'Home') — Fills the title yield
  3. @section('content') ... @endsection — Fills the content yield
  4. @yield('title', 'My App') — Output section with fallback default
  5. @push('scripts') — Append to the scripts stack

Outputting Data

Escaped Output (Safe)

{{ $user->name }}
{{-- Output: &lt;script&gt; → 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">&times;</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>&copy; {{ $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