DEV Community

Cover image for Creating a TodoList with PHP and the Lithe Framework: A Complete Guide
Lithe
Lithe

Posted on

4 1

Creating a TodoList with PHP and the Lithe Framework: A Complete Guide

In this tutorial, we will create a functional TodoList application using Lithe. You will learn how to structure your project, create interactive views, and implement a RESTful API to manage your tasks. This project will serve as an excellent example of how to build modern web applications with PHP.

Prerequisites

  • Composer installed
  • MySQL
  • Basic knowledge of PHP and JavaScript

Project Structure

First, let's create a new Lithe project:

composer create-project lithephp/lithephp todo-app
cd todo-app
Enter fullscreen mode Exit fullscreen mode

Setting Up the Database

Edit the .env file in the root of the project with the following configuration:

DB_CONNECTION_METHOD=mysqli
DB_CONNECTION=mysql
DB_HOST=localhost
DB_NAME=lithe_todos
DB_USERNAME=root
DB_PASSWORD=
DB_SHOULD_INITIATE=true
Enter fullscreen mode Exit fullscreen mode

Creating the Migration

Run the command to create a new migration:

php line make:migration CreateTodosTable
Enter fullscreen mode Exit fullscreen mode

Edit the generated migration file in src/database/migrations/:

return new class
{
    public function up(mysqli $db): void
    {
        $query = "
            CREATE TABLE IF NOT EXISTS todos (
                id INT(11) AUTO_INCREMENT PRIMARY KEY,
                title VARCHAR(255) NOT NULL,
                completed BOOLEAN DEFAULT FALSE,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
            )
        ";
        $db->query($query);
    }

    public function down(mysqli $db): void
    {
        $query = "DROP TABLE IF EXISTS todos";
        $db->query($query);
    }
};
Enter fullscreen mode Exit fullscreen mode

Run the migration:

php line migrate
Enter fullscreen mode Exit fullscreen mode

Implementing the Model

Generate a new model:

php line make:model Todo
Enter fullscreen mode Exit fullscreen mode

Edit the file src/models/Todo.php:

namespace App\Models;

use Lithe\Database\Manager as DB;

class Todo
{
    public static function all(): array
    {
        return DB::connection()
            ->query("SELECT * FROM todos ORDER BY created_at DESC")
            ->fetch_all(MYSQLI_ASSOC);
    }

    public static function create(array $data): ?array
    {
        $stmt = DB::connection()->prepare(
            "INSERT INTO todos (title, completed) VALUES (?, ?)"
        );
        $completed = false;
        $stmt->bind_param('si', $data['title'], $completed);
        $success = $stmt->execute();

        if ($success) {
            $id = $stmt->insert_id;
            return [
                'id' => $id,
                'title' => $data['title'],
                'completed' => $completed
            ];
        }

        return null;
    }

    public static function update(int $id, array $data): bool
    {
        $stmt = DB::connection()->prepare(
            "UPDATE todos SET completed = ? WHERE id = ?"
        );
        $stmt->bind_param('ii', $data['completed'], $id);
        return $stmt->execute();
    }

    public static function delete(int $id): bool
    {
        $stmt = DB::connection()->prepare("DELETE FROM todos WHERE id = ?");
        $stmt->bind_param('i', $id);
        return $stmt->execute();
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Controller

Generate a new controller:

php line make:controller TodoController
Enter fullscreen mode Exit fullscreen mode

Edit the file src/http/controllers/TodoController.php:

namespace App\Http\Controllers;

use App\Models\Todo;
use Lithe\Http\Request;
use Lithe\Http\Response;

class TodoController
{
    public static function index(Request $req, Response $res)
    {
        return $res->view('todo.index');
    }

    public static function list(Request $req, Response $res)
    {
        $todos = Todo::all();
        return $res->json($todos);
    }

    public static function store(Request $req, Response $res)
    {
        $data = (array) $req->body();
        $todo = Todo::create($data);
        $success = $todo ? true : false;

        return $res->json([
            'success' => $success,
            'message' => $success ? 'Task created successfully' : 'Failed to create task',
            'todo' => $todo
        ]);
    }

    public static function update(Request $req, Response $res)
    {
        $id = $req->param('id');
        $data = (array) $req->body();
        $success = Todo::update($id, $data);

        return $res->json([
            'success' => $success,
            'message' => $success ? 'Task updated successfully' : 'Failed to update task'
        ]);
    }

    public static function delete(Request $req, Response $res)
    {
        $id = $req->param('id');
        $success = Todo::delete($id);

        return $res->json([
            'success' => $success,
            'message' => $success ? 'Task removed successfully' : 'Failed to remove task'
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Views

Create the src/views/todo directory and add the index.php file:

<!DOCTYPE html>
<html>
<head>
    <title>TodoList with Lithe</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        }

        body {
            min-height: 100vh;
            background-color: #ffffff;
            padding: 20px;
        }

        .container {
            max-width: 680px;
            margin: 0 auto;
            padding: 40px 20px;
        }

        h1 {
            color: #1d1d1f;
            font-size: 34px;
            font-weight: 700;
            margin-bottom: 30px;
        }

        .todo-form {
            display: flex;
            gap: 12px;
            margin-bottom: 30px;
            border-bottom: 1px solid #e5e5e7;
            padding-bottom: 30px;
        }

        .todo-input {
            flex: 1;
            padding: 12px 16px;
            font-size: 17px;
            border: 1px solid #e5e5e7;
            border-radius: 10px;
            background-color: #f5f5f7;
            transition: all 0.2s;
        }

        .todo-input:focus {
            outline: none;
            background-color: #ffffff;
            border-color: #0071e3;
        }

        .add-button {
            padding: 12px 20px;
            background: #0071e3;
            color: white;
            border: none;
            border-radius: 10px;
            font-size: 15px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
        }

        .add-button:hover {
            background: #0077ED;
        }

        .todo-list {
            list-style: none;
        }

        .todo-item {
            display: flex;
            align-items: center;
            padding: 16px;
            border-radius: 10px;
            margin-bottom: 8px;
            transition: background-color 0.2s;
        }

        .todo-item:hover {
            background-color: #f5f5f7;
        }

        .todo-checkbox {
            width: 22px;
            height: 22px;
            margin-right: 15px;
            cursor: pointer;
        }

        .todo-text {
            flex: 1;
            font-size: 17px;
            color: #1d1d1f;
        }

        .completed {
            color: #86868b;
            text-decoration: line-through;
        }

        .delete-button {
            padding: 8px 12px;
            background: none;
            color: #86868b;
            border: none;
            border-radius: 6px;
            font-size: 15px;
            cursor: pointer;
            opacity: 0;
            transition: all 0.2s;
        }

        .todo-item:hover .delete-button {
            opacity: 1;
        }

        .delete-button:hover {
            background: #f5f5f7;
            color: #ff3b30;
        }

        .loading {
            text-align: center;
            padding: 20px;
            color: #86868b;
        }

        .error {
            color: #ff3b30;
            text-align: center;
            padding: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>My Tasks</h1>
        <form class="todo-form" id="todoForm">
            <input type="text" class="todo-input" placeholder="Add new task" id="todoInput" required>
            <button type="submit" class="add-button">Add</button>
        </form>
        <div id="loading" class="loading">Loading tasks...</div>
        <ul class="todo-list" id="todoList"></ul>
    </div>

    <

script>
        const todoInput = document.getElementById('todoInput');
        const todoForm = document.getElementById('todoForm');
        const todoList = document.getElementById('todoList');
        const loading = document.getElementById('loading');

        async function fetchTodos() {
            const response = await fetch('/todos');
            const todos = await response.json();
            loading.style.display = 'none';
            renderTodos(todos);
        }

        function renderTodos(todos) {
            todoList.innerHTML = '';
            todos.forEach(todo => {
                const todoItem = document.createElement('li');
                todoItem.className = 'todo-item';
                todoItem.dataset.id = todo.id;

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.className = 'todo-checkbox';
                checkbox.checked = todo.completed;
                checkbox.addEventListener('change', () => updateTodo(todo.id, checkbox.checked));

                const text = document.createElement('span');
                text.className = 'todo-text';
                text.textContent = todo.title;
                if (todo.completed) {
                    text.classList.add('completed');
                }

                const deleteButton = document.createElement('button');
                deleteButton.className = 'delete-button';
                deleteButton.textContent = 'Delete';
                deleteButton.addEventListener('click', () => deleteTodo(todo.id));

                todoItem.appendChild(checkbox);
                todoItem.appendChild(text);
                todoItem.appendChild(deleteButton);
                todoList.appendChild(todoItem);
            });
        }

        async function addTodo() {
            const title = todoInput.value;
            if (!title) return;

            const response = await fetch('/todos', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ title })
            });

            const todo = await response.json();
            if (todo.success) {
                fetchTodos();
                todoInput.value = '';
            }
        }

        async function updateTodo(id, completed) {
            const response = await fetch(`/todos/${id}`, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ completed })
            });

            if (response.ok) {
                fetchTodos();
            }
        }

        async function deleteTodo(id) {
            const response = await fetch(`/todos/${id}`, { method: 'DELETE' });
            if (response.ok) {
                fetchTodos();
            }
        }

        todoForm.addEventListener('submit', (e) => {
            e.preventDefault();
            addTodo();
        });

        fetchTodos();
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Aqui está a tradução para o inglês:


Setting Up the Routes

Update the src/App.php file to include the TodoList routes:

use App\Http\Controllers\TodoController;

$app = new \Lithe\App;

// Route for the main page
$app->get('/', [TodoController::class, 'index']);

// API routes
$app->get('/todos/list', [TodoController::class, 'list']);
$app->post('/todos', [TodoController::class, 'store']);
$app->put('/todos/:id', [TodoController::class, 'update']);
$app->delete('/todos/:id', [TodoController::class, 'delete']);

$app->listen();
Enter fullscreen mode Exit fullscreen mode

Running the Application

To start the development server, run:

php line serve
Enter fullscreen mode Exit fullscreen mode

Access http://localhost:8000 in your browser to see the application in action.

Implemented Features

Our TodoList has the following features:

  1. Listing tasks in reverse chronological order
  2. Adding new tasks
  3. Marking tasks as completed/pending
  4. Removing tasks
  5. Responsive and user-friendly interface
  6. Visual feedback for all actions
  7. Error handling

Conclusion

You now have a fully functional TodoList application built with Lithe. This example demonstrates how to create a modern web application with PHP, including:

  • Proper MVC code structure
  • RESTful API implementation
  • Interactive user interface
  • Database integration
  • Error handling
  • User feedback

From here, you can expand the application by adding new features such as:

  • User authentication
  • Task categorization
  • Due dates
  • Filters and search

To keep learning about Lithe, visit the Linktree, where you can access the Discord, documentation, and much more!

Retry later

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Retry later