DEV Community

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

Posted on

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!

Top comments (0)