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
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
Creating the Migration
Run the command to create a new migration:
php line make:migration CreateTodosTable
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);
}
};
Run the migration:
php line migrate
Implementing the Model
Generate a new model:
php line make:model Todo
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();
}
}
Creating the Controller
Generate a new controller:
php line make:controller TodoController
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'
]);
}
}
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>
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();
Running the Application
To start the development server, run:
php line serve
Access http://localhost:8000
in your browser to see the application in action.
Implemented Features
Our TodoList has the following features:
- Listing tasks in reverse chronological order
- Adding new tasks
- Marking tasks as completed/pending
- Removing tasks
- Responsive and user-friendly interface
- Visual feedback for all actions
- 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)