Skip to content

Web Quickstart

Build a full-stack web application with Twig templates, sessions, and CSRF protection.

Prerequisites

  • PHP 8.3 or higher
  • Composer 2.x

Create Your Project

bash
# Create new project
composer create-project lalaz/web-starter my-app

# Navigate to project
cd my-app

# Start development server
php lalaz serve

Open http://localhost:8000 — you should see the welcome page.

Project Structure

my-app/
├── app/
│   └── Controllers/         # Your controllers
├── config/
│   └── app.php             # Configuration
├── public/
│   └── index.php           # Entry point
├── resources/
│   └── views/              # Twig templates
│       ├── layouts/        # Layout templates
│       └── home/           # Page templates
├── routes/
│   └── web.php             # Route definitions
├── storage/
│   ├── cache/              # Cache files
│   └── logs/               # Log files
├── .env                    # Environment config
└── lalaz                   # CLI tool

Creating Pages

1. Add a Route

Edit routes/web.php:

php
use App\Controllers\ContactController;

return function (Router $router): void {
    // Existing routes...
    
    $router->get('/contact', ContactController::class . '@form');
    $router->post('/contact', ContactController::class . '@submit');
};

2. Create the Controller

bash
php lalaz craft:controller ContactController

Edit app/Controllers/ContactController.php:

php
<?php

declare(strict_types=1);

namespace App\Controllers;

use Lalaz\Http\Request;
use Lalaz\Web\Http\RedirectResponse;
use Lalaz\Web\View\ViewResponse;
use Lalaz\Validator\Validator;

use function view;
use function redirect;
use function back;

class ContactController
{
    public function form(): ViewResponse
    {
        return view('contact/form', [
            'title' => 'Contact Us',
        ]);
    }

    public function submit(Request $request): RedirectResponse
    {
        $validator = new Validator($request->post());
        
        if (!$validator->validate([
            'name' => 'required|min:2',
            'email' => 'required|email',
            'message' => 'required|min:10',
        ])) {
            return back()
                ->withInput()
                ->withErrors($validator->errors());
        }

        // Process the form (send email, save to DB, etc.)
        
        return redirect('/')
            ->with('success', 'Thanks for your message!');
    }
}

3. Create the View

Create resources/views/contact/form.twig:

twig
{% extends "layouts/app.twig" %}

{% block content %}
<section style="max-width: 600px; margin: 0 auto; padding: 2rem 0;">
    <h1>{{ title }}</h1>
    
    <form method="POST" action="/contact" style="margin-top: 1.5rem;">
        {{ csrfField() | raw }}
        
        <div style="margin-bottom: 1rem;">
            <label for="name">Name</label>
            <input type="text" 
                   id="name" 
                   name="name" 
                   value="{{ old('name') }}"
                   style="width: 100%; padding: 0.5rem; {% if hasError('name') %}border-color: red;{% endif %}">
            {% if hasError('name') %}
                <span style="color: red; font-size: 0.875rem;">{{ error('name') }}</span>
            {% endif %}
        </div>
        
        <div style="margin-bottom: 1rem;">
            <label for="email">Email</label>
            <input type="email" 
                   id="email" 
                   name="email" 
                   value="{{ old('email') }}"
                   style="width: 100%; padding: 0.5rem; {% if hasError('email') %}border-color: red;{% endif %}">
            {% if hasError('email') %}
                <span style="color: red; font-size: 0.875rem;">{{ error('email') }}</span>
            {% endif %}
        </div>
        
        <div style="margin-bottom: 1rem;">
            <label for="message">Message</label>
            <textarea id="message" 
                      name="message" 
                      rows="5"
                      style="width: 100%; padding: 0.5rem; {% if hasError('message') %}border-color: red;{% endif %}">{{ old('message') }}</textarea>
            {% if hasError('message') %}
                <span style="color: red; font-size: 0.875rem;">{{ error('message') }}</span>
            {% endif %}
        </div>
        
        <button type="submit" style="background: #68d391; padding: 0.75rem 1.5rem; border: none; cursor: pointer;">
            Send Message
        </button>
    </form>
</section>
{% endblock %}

Key Concepts

View Responses

Return views from controllers using the view() helper:

php
use function view;

// Simple view
return view('home/index');

// View with data
return view('users/show', ['user' => $user]);

// View with custom status
return view('errors/not-found', [], 404);

Layouts

Create a base layout in resources/views/layouts/app.twig:

twig
<!DOCTYPE html>
<html>
<head>
    <title>{{ title | default('My App') }}</title>
    {% block head %}{% endblock %}
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
            <a href="/contact">Contact</a>
        </nav>
    </header>
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        &copy; {{ "now"|date("Y") }} My App
    </footer>
    
    {% block scripts %}{% endblock %}
</body>
</html>

Extend it in page templates:

twig
{% extends "layouts/app.twig" %}

{% block content %}
    <h1>Welcome!</h1>
{% endblock %}

CSRF Protection

All POST forms must include a CSRF token:

twig
<form method="POST" action="/submit">
    {{ csrfField() | raw }}
    <!-- form fields -->
</form>

Flash Messages

Set flash messages in controllers:

php
return redirect('/dashboard')
    ->with('success', 'Profile updated!');

Display them in views:

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

Old Input & Validation Errors

After validation fails, repopulate form fields:

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

{% if hasError('email') %}
    <span class="error">{{ error('email') }}</span>
{% endif %}

Method Spoofing

HTML forms only support GET and POST. For PUT/PATCH/DELETE:

twig
<form method="POST" action="/users/5">
    {{ csrfField() | raw }}
    {{ methodField('DELETE') | raw }}
    <button>Delete User</button>
</form>

Available CLI Commands

bash
# Start development server
php lalaz serve

# Create controller
php lalaz craft:controller UserController

# Create view template
php lalaz craft:view users/index

# List all routes
php lalaz routes:list

What's Included

FeatureDescription
Twig TemplatesPowerful templating with layouts, includes, and inheritance
Session ManagementSecure sessions with flash messages
CSRF ProtectionAutomatic form protection
Form Helpersold(), error(), hasError() for validation UX
RedirectsFluent redirect API with withInput(), withErrors()
View ComposersAutomatic data injection into views

Next Steps

Released under the MIT License.