Web Application Example
Web Application Example
This example builds a small but complete web application: a task board. It ties together routing, controllers, views/layouts, and the database layer into one working app, and shows the standard form submit → redirect → render flow used by most server-rendered Hazaar apps.
By the end you will have a page that lists tasks, a form that adds new ones, and a button that marks a task complete — each backed by a real database table.
Project Structure
app/
├── Config/
│ └── app.php
├── Controllers/
│ └── Tasks.php
├── Models/
│ └── Task.php
├── Views/
│ ├── app.tpl
│ └── tasks/
│ └── index.tpl
└── route.php1. Configure the Application
Enable the file router and a database connection in app/Config/app.php. SQLite needs no server, so it is the fastest way to follow along:
<?php
return [
'development' => [
'router' => [
'type' => 'file',
'file' => 'route.php',
],
'db' => [
'type' => 'sqlite',
'file' => 'application.db',
],
],
];2. The Task Model
Create app/Models/Task.php to encapsulate all database access for tasks. The constructor also creates the table on first run, so there is nothing else to set up:
<?php
namespace App\Models;
use Hazaar\DBI\Adapter;
class Task
{
protected Adapter $db;
public function __construct(?Adapter $db = null)
{
$this->db = $db ?? Adapter::create();
$this->db->exec('
CREATE TABLE IF NOT EXISTS task (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
complete INTEGER NOT NULL DEFAULT 0
)
');
}
public function all(): array
{
return $this->db->table('task')->order('id', SORT_DESC)->fetchAll();
}
public function create(string $title): bool
{
return false !== $this->db->table('task')->insert(['title' => $title, 'complete' => 0]);
}
public function complete(int $id): bool
{
return false !== $this->db->table('task')->update(['complete' => 1], ['id' => $id]);
}
}See Using Databases for more on the DBI table API.
3. The Controller
Create app/Controllers/Tasks.php. This extends Action so it can render views, and follows the standard POST → redirect → GET pattern: submitting the form never re-renders the page directly, it redirects back to index so a page refresh does not resubmit the form.
<?php
namespace App\Controllers;
use App\Models\Task;
use Hazaar\Controller\Action;
use Hazaar\Controller\Response\HTTP\Redirect;
class Tasks extends Action
{
protected Task $tasks;
public function __construct(\Hazaar\Application\Request $request)
{
parent::__construct($request);
$this->tasks = new Task();
}
public function index(): void
{
$this->view('tasks/index', [
'tasks' => $this->tasks->all(),
]);
}
public function store(): Redirect
{
$title = trim((string) $this->request->post('title'));
if ('' !== $title) {
$this->tasks->create($title);
}
return new Redirect('/tasks');
}
public function complete(int $id): Redirect
{
$this->tasks->complete($id);
return new Redirect('/tasks');
}
}4. Routes
Create app/route.php:
<?php
use App\Controllers\Tasks;
use Hazaar\Application\Router;
Router::get('/tasks', [Tasks::class, 'index']);
Router::post('/tasks', [Tasks::class, 'store']);
Router::post('/tasks/{int:id}/complete', [Tasks::class, 'complete']);See Routing for the full range of routing methods.
5. Layout and View
Create the layout at app/Views/app.tpl. The {layout} tag marks where the current view is injected:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task Board</title>
<style>
body { font-family: sans-serif; max-width: 480px; margin: 3rem auto; }
li.done { text-decoration: line-through; color: #888; }
form { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
</style>
</head>
<body>
<h1>Task Board</h1>
<main>{layout}</main>
</body>
</html>Create the view at app/Views/tasks/index.tpl. It loops over $tasks (passed from the controller) using Smarty's {foreach} tag:
<form method="post" action="/tasks">
<input type="text" name="title" placeholder="New task..." required>
<button type="submit">Add</button>
</form>
<ul>
{foreach $tasks as $task}
<li class="{if $task.complete}done{/if}">
{$task.title}
{if !$task.complete}
<form method="post" action="/tasks/{$task.id}/complete" style="display:inline">
<button type="submit">Done</button>
</form>
{/if}
</li>
{/foreach}
</ul>See Using Templates for more on layouts, views, and Smarty syntax.
6. Try It Out
Start the app and visit /tasks in a browser:
- Add a task using the form — the page redirects back to
/tasksand the new task appears. - Click Done next to a task — it is marked complete and shown with strikethrough text.
Each interaction is a full page reload driven entirely by server-side routing and rendering — no client-side JavaScript required.
Next Steps
- Controller Responses — the full set of response types, including redirects and files.
- Using Databases — more on models, queries, and the DBI table API.
- Middleware — add cross-cutting behavior like request logging or CSRF checks.
- Creating a REST API — expose the same kind of data as JSON instead of HTML.