Criando uma TodoList com PHP e o Framework Lithe: Um Guia Completo

lithephp

Lithe

Posted on November 16, 2024

Criando uma TodoList com PHP e o Framework Lithe: Um Guia Completo

Neste tutorial, vamos criar uma aplicação TodoList funcional usando o Lithe. Você aprenderá a estruturar seu projeto, criar views interativas, e implementar uma API RESTful para gerenciar suas tarefas. Este projeto servirá como um excelente exemplo de como construir aplicações web modernas com PHP.

Pré-requisitos

  • Composer instalado
  • MySQL
  • Conhecimentos básicos de PHP e JavaScript

Estrutura do Projeto

Primeiro, vamos criar um novo projeto Lithe:

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

Configurando o Banco de Dados

Edite o arquivo .env na raiz do projeto com as seguintes configurações:

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

Criando a Migração

Execute o comando para criar uma nova migração:

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

Edite o arquivo de migração gerado em 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

Execute a migração:

php line migrate
Enter fullscreen mode Exit fullscreen mode

Implementando o Modelo

Gere um novo modelo:

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

Edite o arquivo 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

Criando o Controlador

Gere um novo controlador:

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

Edite o arquivo 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 ? 'Tarefa criada com sucesso' : 'Falha ao criar tarefa',
            '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 ? 'Tarefa atualizada com sucesso' : 'Falha ao atualizar tarefa'
        ]);
    }

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

        return $res->json([
            'success' => $success,
            'message' => $success ? 'Tarefa removida com sucesso' : 'Falha ao remover tarefa'
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Criando as Views

Crie o diretório src/views/todo e adicione o arquivo index.php:

<!DOCTYPE html>
<html>
<head>
    <title>TodoList com 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>Minhas Tarefas</h1>
        <form class="todo-form" id="todoForm">
            <input type="text" class="todo-input" placeholder="Adicionar nova tarefa" id="todoInput" required>
            <button type="submit" class="add-button">Adicionar</button>
        </form>
        <div id="loading" class="loading">Carregando tarefas...</div>
        <ul class="todo-list" id="todoList"></ul>
    </div>

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

        // Carregar todas as tarefas
        async function loadTodos() {
            try {
                const response = await fetch('/todos/list');
                const todos = await response.json();
                loading.style.display = 'none';
                todoList.innerHTML = ''; // Limpar a lista antes de adicionar
                todos.forEach(todo => addTodoToDOM(todo));
            } catch (error) {
                loading.textContent = 'Não foi possível carregar as tarefas.';
                loading.classList.add('error');
            }
        }

        // Adicionar tarefa ao DOM
        function addTodoToDOM(todo) {
            console.log(todo)
            const li = document.createElement('li');
            li.className = 'todo-item';
            li.dataset.id = todo.id; // Associando o ID à tarefa
            li.innerHTML = `
        <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
        <span class="todo-text ${todo.completed ? 'completed' : ''}">${todo.title}</span>
        <button type="button" class="delete-button">Remover</button>
    `;

            const checkbox = li.querySelector('.todo-checkbox');
            checkbox.addEventListener('change', () => toggleTodo(todo.id, checkbox));

            const deleteButton = li.querySelector('.delete-button');
            deleteButton.addEventListener('click', () => deleteTodo(todo.id, li));

            todoList.appendChild(li);
        }

        // Criar nova tarefa
        todoForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            const title = todoInput.value.trim();
            if (!title) return;

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

                const result = await response.json();

                if (result.success) {
                    const todo = {
                        id: result.todo.id,
                        title,
                        completed: false
                    };
                    addTodoToDOM(todo);
                    todoInput.value = '';
                } else {
                    alert(result.message);
                }
            } catch (error) {
                alert('Não foi possível adicionar a tarefa');
            }
        });

        // Atualizar status da tarefa
        async function toggleTodo(id, checkbox) {
            try {
                console.log(`Atualizando tarefa com ID: ${id}`);
                const response = await fetch(`/todos/${id}`, {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        completed: checkbox.checked ? 1 : 0
                    })
                });

                const result = await response.json();

                if (result.success) {
                    const todoText = checkbox.nextElementSibling;
                    todoText.classList.toggle('completed');
                } else {
                    checkbox.checked = !checkbox.checked;
                    alert(result.message);
                }
            } catch (error) {
                checkbox.checked = !checkbox.checked;
                alert('Não foi possível atualizar a tarefa');
            }
        }

        // Remover tarefa
        async function deleteTodo(id, element) {
            try {
                console.log(`Deletando tarefa com ID: ${id}`);
                const response = await fetch(`/todos/${id}`, {
                    method: 'DELETE'
                });

                const result = await response.json();

                if (result.success) {
                    element.remove();
                } else {
                    alert(result.message);
                }
            } catch (error) {
                alert('Não foi possível remover a tarefa');
            }
        }

        // Carregar tarefas ao iniciar
        loadTodos();
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Configurando as Rotas

Atualize o arquivo src/App.php para incluir as rotas da TodoList:

use App\Http\Controllers\TodoController;

$app = new \Lithe\App;

// Rota para a página principal
$app->get('/', [TodoController::class, 'index']);

// Rotas da API
$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

Executando a Aplicação

Para iniciar o servidor de desenvolvimento, execute:

php line serve
Enter fullscreen mode Exit fullscreen mode

Acesse http://localhost:8000 no seu navegador para ver a aplicação em funcionamento.

Funcionalidades Implementadas

Nossa TodoList possui as seguintes funcionalidades:

  1. Listagem de tarefas em ordem cronológica reversa
  2. Adição de novas tarefas
  3. Marcação de tarefas como concluídas/pendentes
  4. Remoção de tarefas
  5. Interface responsiva e amigável
  6. Feedback visual para todas as ações
  7. Tratamento de erros

Conclusão

Você agora tem uma aplicação TodoList completamente funcional construída com o Lithe. Este exemplo demonstra como criar uma aplicação web moderna com PHP, incluindo:

  • Estruturação adequada de código MVC
  • Implementação de API RESTful
  • Interface de usuário interativa
  • Integração com banco de dados
  • Tratamento de erros
  • Feedback ao usuário

A partir daqui, você pode expandir a aplicação adicionando novas funcionalidades como:

  • Autenticação de usuários
  • Categorização de tarefas
  • Datas de vencimento
  • Filtros e pesquisa

Para continuar aprendendo sobre o Lithe, acesse a Linktree, onde você pode acessar o Discord, a documentação e muito mais!

💖 💪 🙅 🚩
lithephp
Lithe

Posted on November 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related