Views

ZephyrPHP uses Twig as its templating engine, providing a clean, secure, and flexible way to build your HTML views with powerful features like composers, shared data, and components.

Creating Views

Views are stored in the pages/ directory with a .twig extension:

pages/
├── layouts/
│   └── app.twig
├── components/
│   ├── button.twig
│   └── alert.twig
├── users/
│   ├── index.twig
│   └── show.twig
└── home.twig

Rendering Views

Use the render() method in your controllers:

<?php

class UserController extends Controller
{
    public function index()
    {
        return $this->render('users/index', [
            'users' => User::query()->all(),
            'title' => 'All Users'
        ]);
    }
}

Using the view() Helper

// Simple render
return view('users/index', ['users' => $users]);

// Dot notation
return view('users.index', ['users' => $users]);

Fluent View Builder

use ZephyrPHP\View\View;

return View::getInstance()
    ->make('users.show')
    ->with('user', $user)
    ->with('posts', $user->posts())
    ->render();

// Magic methods
return View::getInstance()
    ->make('dashboard')
    ->withUser($user)
    ->withStats($stats)
    ->render();

Displaying Data

Output variables using double curly braces:

<h1>{{ title }}</h1>
<p>Welcome, {{ user.name }}!</p>

{# Output is automatically escaped for security #}
<p>{{ user_input }}</p>

{# To output raw HTML (use carefully!) #}
{{ raw_html|raw }}

Control Structures

Conditionals

{% if user %}
    <p>Hello, {{ user.name }}!</p>
{% else %}
    <p>Please log in.</p>
{% endif %}

{% if user.role == 'admin' %}
    <a href="/admin">Admin Panel</a>
{% elseif user.role == 'editor' %}
    <a href="/editor">Editor Panel</a>
{% endif %}

Loops

<ul>
{% for user in users %}
    <li>{{ user.name }} - {{ user.email }}</li>
{% else %}
    <li>No users found.</li>
{% endfor %}
</ul>

{# Loop with index #}
{% for user in users %}
    <p>{{ loop.index }}. {{ user.name }}</p>
{% endfor %}

{# Loop variables #}
{{ loop.index }}      {# Current iteration (1-indexed) #}
{{ loop.index0 }}     {# Current iteration (0-indexed) #}
{{ loop.first }}      {# True if first iteration #}
{{ loop.last }}       {# True if last iteration #}
{{ loop.length }}     {# Total number of items #}

Template Inheritance

Create a base layout and extend it in child templates:

pages/layouts/app.twig
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}ZephyrPHP{% endblock %}</title>
    <link rel="stylesheet" href="{{ asset('css/styles.css') }}">
    {% block styles %}{% endblock %}
</head>
<body>
    <nav>
        {% include 'partials/nav.twig' %}
    </nav>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        &copy; {{ "now"|date("Y") }} ZephyrPHP
    </footer>

    {% block scripts %}{% endblock %}
</body>
</html>
pages/users/index.twig
{% extends 'layouts/app.twig' %}

{% block title %}Users - ZephyrPHP{% endblock %}

{% block content %}
    <h1>Users</h1>
    <ul>
    {% for user in users %}
        <li>{{ user.name }}</li>
    {% endfor %}
    </ul>
{% endblock %}

View Composers

View composers automatically inject data into specific views, eliminating repetitive code in controllers.

Registering Composers

<?php

use ZephyrPHP\View\View;

// In a service provider or bootstrap file

// Inject data into specific views
View::composer('layouts/app', function($view) {
    $view->with('siteTitle', 'My Application');
    $view->with('currentUser', auth()->user());
});

// Multiple views
View::composer(['layouts/app', 'layouts/admin'], function($view) {
    $view->with('notifications', Notification::unread());
});

// Wildcard patterns
View::composer('admin.*', function($view) {
    $view->with('adminStats', Stats::getAdminStats());
});

View::composer('users.*', function($view) {
    $view->with('userCount', User::count());
});

Class-Based Composers

<?php

namespace App\Composers;

use ZephyrPHP\View\ViewContext;

class NavigationComposer
{
    public function compose(ViewContext $view)
    {
        $view->with('menuItems', $this->getMenuItems());
        $view->with('currentRoute', Route::currentRouteName());
    }

    private function getMenuItems(): array
    {
        return [
            ['label' => 'Home', 'route' => 'home'],
            ['label' => 'About', 'route' => 'about'],
            ['label' => 'Contact', 'route' => 'contact'],
        ];
    }
}

// Register the class
View::composer('partials/nav', NavigationComposer::class);

Shared Data

Share data with all views globally:

<?php

use ZephyrPHP\View\View;

// Share a single value
View::share('appName', 'My Application');
View::share('currentYear', date('Y'));

// Share multiple values
View::share([
    'appName' => 'My Application',
    'appVersion' => '1.0.0',
    'user' => auth()->user(),
]);

// Access in any template
//  - 

Components

Create reusable UI components:

pages/components/button.twig
<button
    type="{{ type|default('button') }}"
    class="btn btn-{{ variant|default('primary') }} {{ class|default('') }}"
    {{ disabled ? 'disabled' : '' }}
>
    {{ slot }}
</button>
pages/components/alert.twig
<div class="alert alert-{{ type|default('info') }}" role="alert">
    {% if title %}
        <strong>{{ title }}</strong>
    {% endif %}
    {{ slot }}
</div>

Using Components

{# Using the component function #}
{{ component('button', {variant: 'primary', type: 'submit'}, {default: 'Save Changes'}) }}

{{ component('alert', {type: 'success', title: 'Success!'}, {default: 'Your changes have been saved.'}) }}

{# Or using include with the components path #}
{% include 'components/button.twig' with {
    variant: 'danger',
    slot: 'Delete'
} %}

Including Templates

Include partial templates:

{# Simple include #}
{% include 'partials/header.twig' %}

{# Include with variables #}
{% include 'partials/user-card.twig' with {'user': user} %}

{# Include if exists #}
{% include 'partials/sidebar.twig' ignore missing %}

{# Include with only specific variables #}
{% include 'partials/user-card.twig' with {'user': user} only %}

View Existence

Check if views exist before rendering:

<?php

use ZephyrPHP\View\View;

$view = View::getInstance();

// Check if template exists
if ($view->exists('emails/welcome')) {
    return $view->render('emails/welcome', $data);
}

// Render if exists, otherwise return default
$content = $view->renderIfExists('partials/sidebar', [], '');

// Use first existing template
return $view->first([
    "users/{$type}/show",
    'users/show',
    'errors/404'
], $data);

Rendering Collections

Render a template for each item in a collection:

<?php

use ZephyrPHP\View\View;

// Render each user
$html = View::getInstance()->each(
    'partials/user-card',    // Template
    $users,                   // Data array
    'user',                   // Variable name for each item
    'partials/no-users'       // Optional empty template
);

echo $html;

Built-in Functions

ZephyrPHP provides many helper functions in templates:

Asset URLs

<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
<script src="{{ asset('js/app.js') }}"></script>
<img src="{{ asset('images/logo.png') }}">

Route Generation

<a href="{{ route('users.index') }}">All Users</a>
<a href="{{ route('users.show', {id: user.id}) }}">View User</a>

{# Check current route #}
{% if route_is('users.*') %}
    <span class="active">Users Section</span>
{% endif %}

{# Get current route name #}
{% set currentRoute = current_route() %}

CSRF Protection

<form method="POST">
    {{ csrf_field()|raw }}
    <!-- form fields -->
</form>

{# Or just the token value #}
<input type="hidden" name="_token" value="{{ csrf_token() }}">

Session & Flash Messages

{# Get session value #}
{{ session('user_id') }}

{# Flash messages #}
{% if flash('success') %}
    <div class="alert alert-success">
        {{ flash('success') }}
    </div>
{% endif %}

{% if flash('error') %}
    <div class="alert alert-danger">
        {{ flash('error') }}
    </div>
{% endif %}

Old Input

<input type="text" name="email" value="{{ old('email') }}">
<input type="text" name="name" value="{{ old('name', user.name) }}">

Request Data

{# Access request data #}
{{ request('search') }}
{{ request('page', 1) }}

Conditional Classes

<div class="{{ class_list({
    'card': true,
    'card-active': isActive,
    'card-featured': isFeatured
}) }}">
    Content
</div>

Filters

Twig filters transform data:

String Filters

{{ name|upper }}           {# JOHN #}
{{ name|lower }}           {# john #}
{{ name|capitalize }}      {# John #}
{{ name|title }}           {# John Doe #}
{{ text|striptags }}       {# Remove HTML #}
{{ text|nl2br }}           {# Convert newlines to <br> #}
{{ text|trim }}            {# Trim whitespace #}
{{ text|limit(100) }}      {# Truncate to 100 chars... #}
{{ text|slug }}            {# my-text-here #}

Array Filters

{{ users|length }}         {# Count items #}
{{ users|first }}          {# First item #}
{{ users|last }}           {# Last item #}
{{ users|join(', ') }}     {# Join with separator #}

Date Filters

{{ post.created_at|date('F j, Y') }}         {# January 1, 2024 #}
{{ post.created_at|date('Y-m-d H:i:s') }}    {# 2024-01-01 12:00:00 #}
{{ post.created_at|relative_time }}          {# 2 hours ago #}

Number Filters

{{ price|number_format(2, '.', ',') }}    {# 1,234.56 #}
{{ price|money }}                          {# $1,234.00 #}
{{ price|money('€') }}                     {# €1,234.00 #}
{{ fileSize|bytes }}                       {# 1.5 MB #}

Default Values

{{ user.nickname|default('Anonymous') }}

Template Namespaces

Organize templates with namespaces:

<?php

use ZephyrPHP\View\View;

$view = View::getInstance();

// Add a namespace
$view->addNamespace('admin', BASE_PATH . '/pages/admin');
$view->addNamespace('emails', BASE_PATH . '/pages/emails');

// Use namespace in templates
return $view->render('admin::dashboard', $data);
return $view->render('emails::welcome', $data);

Comments

{# This is a comment and won't appear in the HTML output #}

{#
    Multi-line
    comment
#}

Debugging

{# Dump a variable for debugging #}
{{ dump(user) }}

{# Dump and die #}
{{ dd(user) }}

Clearing View Cache

<?php

use ZephyrPHP\View\View;

// Clear compiled templates
View::getInstance()->clearCompiled();

// Via CLI
php craftsman view:clear
Security Note

Twig automatically escapes output to prevent XSS attacks. Only use |raw filter when you're certain the content is safe.

Complete Example

app/Providers/ViewServiceProvider.php
<?php

namespace App\Providers;

use ZephyrPHP\View\View;
use ZephyrPHP\Container\Container;
use ZephyrPHP\Container\ServiceProvider;

class ViewServiceProvider extends ServiceProvider
{
    public function boot(Container $container): void
    {
        // Share global data
        View::share('appName', env('APP_NAME', 'ZephyrPHP'));
        View::share('currentYear', date('Y'));

        // Register composers
        View::composer('layouts.*', function($view) {
            $view->with('user', auth()->user());
            $view->with('notifications', Notification::recent());
        });

        View::composer('admin.*', AdminComposer::class);
    }
}