Todo Example

A complete working PHP Todo application built with Tplix. Covers MVC structure, form validation, PDO database integration, and clean separation of presentation from business logic.

Sample Application

Building a To-Do List

A classic CRUD application that demonstrates Tplix's form handling, conditional rendering, dynamic CSS classes, and template inheritance — all in plain PHP with no extra libraries.

What We'll Build

A single-page to-do app where you can add tasks, mark them complete, and delete them. Every Tplix feature that makes templates clean and readable is on display here.

➕

Add Tasks

Submit a form, validate input server-side, and redirect back — the Post/Redirect/Get pattern.

✅

Toggle Complete

Completed tasks get a strikethrough style driven by a Tplix conditional class — no JavaScript needed.

🗑️

Delete Tasks

A small POST form per task deletes the record and redirects, keeping the URL always clean.

Project Structure

Directory layout
todo/
├── templates/
│ ├── layout.tpl # base template
│ └── tasks.tpl # task list + add form
├── cache/ # Tplix compiled templates
├── Task.php # model (PDO)
├── TaskController.php # controller
└── index.php # front controller + router
1

Database Schema

One table with four columns — as simple as it gets.

schema.sql
CREATE TABLE tasks (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    title      VARCHAR(255) NOT NULL,
    done       TINYINT(1)   NOT NULL DEFAULT 0,
    created_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
);
2

The Model — Task.php

Four methods, one for each operation the app needs.

Task.php
<?php
class Task
{
    public function __construct(private PDO $pdo) {}

    /** All tasks, oldest first */
    public function all(): array
    {
        return $this->pdo
            ->query("SELECT * FROM tasks ORDER BY created_at ASC")
            ->fetchAll(PDO::FETCH_ASSOC);
    }

    /** Insert a new task */
    public function create(string $title): void
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO tasks (title) VALUES (?)"
        );
        $stmt->execute([trim($title)]);
    }

    /** Flip the done flag */
    public function toggle(int $id): void
    {
        $stmt = $this->pdo->prepare(
            "UPDATE tasks SET done = NOT done WHERE id = ?"
        );
        $stmt->execute([$id]);
    }

    /** Remove a task permanently */
    public function delete(int $id): void
    {
        $stmt = $this->pdo->prepare(
            "DELETE FROM tasks WHERE id = ?"
        );
        $stmt->execute([$id]);
    }
}
3

Front Controller — index.php

Bootstraps the app and routes POST actions to the right controller method.

index.php
<?php
require 'vendor/autoload.php';
require 'Task.php';
require 'TaskController.php';

$pdo = new PDO('mysql:host=localhost;dbname=todo;charset=utf8mb4', 'user', 'pass', [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$tplix = new Tplix\Engine([
    'template_dir' => __DIR__ . '/templates',
    'compile_dir'  => __DIR__ . '/cache',
    'caching'      => false,
]);

$controller = new TaskController($pdo, $tplix);
$action     = $_POST['action'] ?? 'list';

match ($action) {
    'add'    => $controller->add(),
    'toggle' => $controller->toggle(),
    'delete' => $controller->delete(),
    default  => $controller->index(),
};
4

The Controller — TaskController.php

Each action validates input, delegates to the model, then redirects (PRG pattern) or renders the template.

TaskController.php
<?php
class TaskController
{
    public function __construct(
        private PDO          $pdo,
        private Tplix\Engine $tplix
    ) {}

    /** GET /  — show all tasks + add form */
    public function index(): void
    {
        $model = new Task($this->pdo);

        $this->tplix->assign('tasks', $model->all());
        $this->tplix->assign('error', $_SESSION['error'] ?? null);
        unset($_SESSION['error']);

        $this->tplix->display('tasks.tpl');
    }

    /** POST action=add */
    public function add(): void
    {
        $title = trim($_POST['title'] ?? '');

        if ($title === '') {
            $_SESSION['error'] = 'Task title cannot be empty.';
        } else {
            (new Task($this->pdo))->create($title);
        }

        header('Location: /');
        exit;
    }

    /** POST action=toggle */
    public function toggle(): void
    {
        $id = (int)($_POST['id'] ?? 0);
        if ($id > 0) {
            (new Task($this->pdo))->toggle($id);
        }
        header('Location: /');
        exit;
    }

    /** POST action=delete */
    public function delete(): void
    {
        $id = (int)($_POST['id'] ?? 0);
        if ($id > 0) {
            (new Task($this->pdo))->delete($id);
        }
        header('Location: /');
        exit;
    }
}
5

The Templates

⬡ layout.tpl — base template

Minimal HTML shell. The only block child templates need to fill is content.

templates/layout.tpl
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>To-Do List</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <header>
    <h1>My To-Do List</h1>
    <p>{$.now|date_format:"%A, %B %e %Y"}</p>
  </header>

  <main>
    {block name="content"}{/block}
  </main>
</body>
</html>

⬡ tasks.tpl — task list + add form

This is where Tplix shines: {if} for the error banner, {foreach} for the list, and {if $task.done} to apply a CSS class conditionally.

templates/tasks.tpl
{extends file="layout.tpl"}

{block name="content"}

  {* Error banner — only shown when $error is set *}
  {if $error}
    <div class="alert alert-error">{$error|escape}</div>
  {/if}

  {* Add task form *}
  <form method="post" action="/">
    <input type="hidden" name="action" value="add">
    <input type="text" name="title" placeholder="What needs doing?" required>
    <button type="submit">Add Task</button>
  </form>

  {* Task list *}
  <ul class="task-list">
    {foreach $tasks as $task}
      <li class="task-item{if $task.done} done{/if}">

        {* Toggle button *}
        <form method="post" action="/">
          <input type="hidden" name="action" value="toggle">
          <input type="hidden" name="id"     value="{$task.id}">
          <button type="submit" class="btn-check"
                  title="{if $task.done}Mark incomplete{else}Mark complete{/if}">
            {if $task.done}✅{else}⬜{/if}
          </button>
        </form>

        {* Task title *}
        <span class="task-title">{$task.title|escape}</span>

        {* Added date *}
        <span class="task-date">{$task.created_at|date_format:"%b %e"}</span>

        {* Delete button *}
        <form method="post" action="/">
          <input type="hidden" name="action" value="delete">
          <input type="hidden" name="id"     value="{$task.id}">
          <button type="submit" class="btn-delete"
                  onclick="return confirm('Delete this task?')">✕</button>
        </form>

      </li>
    {foreachelse}
      <li class="task-empty">No tasks yet — add one above!</li>
    {/foreach}
  </ul>

  {* Summary line *}
  {if $tasks|count > 0}
    <p class="task-summary">
      {$tasks|count} task{if $tasks|count != 1}s{/if} total
    </p>
  {/if}

{/block}

Tplix Features Used in This Example

🧱

{extends} + {block}

The task list lives inside the shared layout without repeating a single line of HTML.

🔁

{foreach} + {foreachelse}

Iterates all tasks and falls through to a friendly empty-state message when the list is empty.

🎨

Inline {if} for CSS classes

class="task-item{if $task.done} done{/if}" applies the strikethrough style without any JavaScript.

📅

|date_format filter

Formats MySQL datetime strings for display and renders today's date in the header via $.now.

🔢

|count modifier

Counts the tasks array directly in the template for the summary line — no extra variable from the controller needed.

🛡️

|escape filter

Every user-supplied value — task titles, error messages — is HTML-escaped at the template layer, preventing XSS.

Where to Go Next

→
Add due dates — store a due_at column and use {if $task.due_at lt $.now} to highlight overdue items in red.
→
Priority levels — add a priority column and map values to badge colors with a Tplix custom modifier or a nested {if}/{elseif} block.
→
Filter by status — pass a $filter variable from the controller and use {if $filter === 'active'} to show active/completed tabs in the template.
→
Enable template caching — flip 'caching' => true in the engine config and call clearCache() after any write action to keep pages fast.

Ready to build?

Five files, one table, zero dependencies beyond Tplix — and you have a fully working CRUD app. That is the Tplix promise: clean templates, clean controllers, clean code.