O que vamos construir?
CRUD é o acrónimo das quatro operações fundamentais de qualquer aplicação com base de dados: Create, Read, Update e Delete. Vamos construir um sistema de gestão de contactos completo, com formulários HTML, lógica PHP e base de dados MySQL.
Formulário
Lógica
Base de Dados
Resultado
Ferramentas Necessárias
Para desenvolver localmente precisamos de um servidor web com suporte a PHP e MySQL. O XAMPP instala tudo de uma vez. Para escrever código, recomendamos o VS Code.
XAMPP
Pacote que inclui Apache (servidor web), PHP e MySQL/MariaDB. Gratuito e multi-plataforma.
Visual Studio Code
Editor de código gratuito da Microsoft. Com extensões para PHP, HTML e acesso FTP.
phpMyAdmin
Interface web incluída no XAMPP para gerir a base de dados MySQL visualmente.
Browser
Chrome ou Firefox — com as DevTools para depurar erros. Abre localhost para ver o projeto.
Alternativas ao XAMPP — WAMP (Windows), MAMP (Mac/Windows) ou Laragon funcionam de forma semelhante. Este guia usa XAMPP por ser o mais universal.
Instalar e Configurar o XAMPP
O XAMPP é a forma mais rápida de ter um servidor PHP + MySQL a correr no teu computador.
-
1
Descarregar o XAMPP
Vai a apachefriends.org e descarrega a versão para o teu sistema operativo (Windows, Linux ou macOS). Escolhe a versão com PHP 8.x.
-
2
Instalar — Windows
Executa o instalador (
.exe). Mantém as opções por defeito. Instala emC:\xampp(recomendado — evita pastas com espaços). Aceita os avisos do antivírus/firewall. -
3
Iniciar os Serviços
Abre o XAMPP Control Panel. Clica em Start em:
- Apache — o servidor web (HTTP)
- MySQL — a base de dados
Ambos ficam a verde quando estão a correr.
-
4
Testar o Apache
Abre o browser e vai a
http://localhost. Deverás ver a página de boas-vindas do XAMPP. -
5
Abrir o phpMyAdmin
Vai a
http://localhost/phpmyadmin. Esta é a ferramenta para gerir bases de dados MySQL visualmente. -
6
Pasta do projeto — htdocs
Todos os teus ficheiros PHP devem ficar em
C:\xampp\htdocs\. Cria uma pasta para o projeto:C:\xampp\htdocs\contactos\. Acede emhttp://localhost/contactos/.
Porto 80 ocupado? Se o Apache não inicia, o porto 80 pode estar a ser usado pelo Skype ou IIS. No XAMPP Control Panel → Apache → Config → httpd.conf — muda Listen 80 para Listen 8080. Depois acede em http://localhost:8080.
Linux (Ubuntu/Debian): Podes instalar via terminal:
sudo apt install apache2 php php-mysql mysql-server
A pasta do projeto fica em /var/www/html/.
Configurar o VS Code
O Visual Studio Code é gratuito e tem excelente suporte para PHP com extensões.
-
1
Instalar o VS Code
Descarrega em code.visualstudio.com e instala normalmente.
-
2
Extensões recomendadas
Abre o VS Code → clica no ícone de extensões (Ctrl+Shift+X) → pesquisa e instala:
- PHP Intelephense — autocompletar e validação de PHP
- PHP Debug — depuração com XDebug
- HTML CSS Support — autocompletar em HTML/CSS
- SFTP (Natizyskunk) — upload de ficheiros para o servidor
- GitLens — controlo de versões Git
-
3
Abrir a pasta do projeto
File → Open Folder → navega até
C:\xampp\htdocs\contactos. O VS Code mostra todos os ficheiros na barra lateral.
Criar a Base de Dados no MySQL
Vamos criar a base de dados e a tabela de contactos usando o phpMyAdmin.
-
1
Abrir phpMyAdmin
Vai a
http://localhost/phpmyadmin. Utilizador:root. Palavra-passe: vazia (por defeito no XAMPP). -
2
Criar nova base de dados
Clica em "Nova" na barra lateral esquerda. Nome:
crud_contactos. Collation: utf8mb4_unicode_ci. Clica "Criar".
Criar a Tabela de Contactos
Com a base de dados selecionada, clica em "SQL" e executa o seguinte código:
-- Criar a base de dados (caso não exista) CREATE DATABASE IF NOT EXISTS crud_contactos CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE crud_contactos; -- Criar a tabela de contactos CREATE TABLE IF NOT EXISTS contactos ( id INT AUTO_INCREMENT PRIMARY KEY, nome VARCHAR(100) NOT NULL, email VARCHAR(150) NOT NULL UNIQUE, telefone VARCHAR(20), cidade VARCHAR(80), criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP, atualizado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- Inserir dados de exemplo INSERT INTO contactos (nome, email, telefone, cidade) VALUES ('Ana Silva', 'ana@exemplo.pt', '912345678', 'Lisboa'), ('Bruno Costa', 'bruno@exemplo.pt', '963214567', 'Porto'), ('Carla Mendes', 'carla@exemplo.pt', '917654321', 'Faro');
| Coluna | Tipo | Descrição |
|---|---|---|
| id | INT AUTO_INCREMENT PK | Identificador único (gerado automaticamente) |
| nome | VARCHAR(100) NOT NULL | Nome do contacto — obrigatório |
| VARCHAR(150) UNIQUE | Email — obrigatório e único | |
| telefone | VARCHAR(20) | Número de telefone — opcional |
| cidade | VARCHAR(80) | Cidade — opcional |
| criado_em | TIMESTAMP | Data de criação (automática) |
| atualizado_em | TIMESTAMP | Data da última atualização (automática) |
Estrutura do Projeto
Uma boa organização de ficheiros facilita a manutenção. Cria esta estrutura em C:\xampp\htdocs\contactos\:
Boas práticas: Separar a configuração da base de dados num ficheiro próprio (database.php) evita repetição de código e facilita alterações futuras — em produção, basta mudar as credenciais neste único ficheiro.
Ligar o PHP à Base de Dados — PDO
Vamos usar PDO (PHP Data Objects) — a forma moderna e segura de comunicar com o MySQL. O PDO usa prepared statements que protegem contra injeção SQL.
PDO vs MySQLi: Ambos funcionam. PDO é preferível porque é agnóstico ao motor de base de dados (funciona com MySQL, PostgreSQL, SQLite...) e a sintaxe é mais limpa.
<?php // Configurações da base de dados $host = 'localhost'; // servidor MySQL $dbname = 'crud_contactos'; // nome da BD $username = 'root'; // utilizador (XAMPP: root) $password = ''; // palavra-passe (XAMPP: vazia) $charset = 'utf8mb4'; // DSN (Data Source Name) $dsn = "mysql:host=$host;dbname=$dbname;charset=$charset"; // Opções do PDO $opcoes = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; try { $pdo = new PDO($dsn, $username, $password, $opcoes); } catch (PDOException $e) { // Em produção: não mostrar detalhes do erro ao utilizador! error_log($e->getMessage()); die('Erro ao ligar à base de dados. Tente mais tarde.'); } ?>
Em produção: Nunca uses root sem palavra-passe. Cria um utilizador MySQL com permissões limitadas à tua base de dados. Mais informação na secção de Segurança.
Header e Footer Reutilizáveis
Evita repetir o HTML base em cada ficheiro. Cria partials que são incluídos em todas as páginas.
<?php if (session_status() === PHP_SESSION_NONE) { session_start(); // para mensagens flash } ?> <!DOCTYPE html> <html lang="pt-PT"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Gestão de Contactos</title> <link rel="stylesheet" href="/contactos/css/estilo.css"> </head> <body> <header> <nav> <h1>📒 Contactos</h1> <a href="index.php">Listar</a> <a href="criar.php">+ Novo Contacto</a> </nav> </header> <main> <?php // Mostrar mensagens flash (sucesso/erro) if (isset($_SESSION['msg'])): ?> <div class="flash <?= htmlspecialchars($_SESSION['tipo']) ?>"> <?= htmlspecialchars($_SESSION['msg']) ?> </div> <?php unset($_SESSION['msg'], $_SESSION['tipo']); endif; ?>
</main> <footer> <p>CRUD PHP + MySQL © <?= date('Y') ?></p> </footer> </body> </html>
➕ Criar — Inserir Novo Contacto
O ficheiro criar.php faz duas coisas: mostra o formulário (GET) e processa os dados submetidos (POST).
<?php require_once 'config/database.php'; require_once 'includes/header.php'; $erros = []; $dados = ['nome' => '', 'email' => '', 'telefone' => '', 'cidade' => '']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 1. Recolher e limpar dados do formulário $dados['nome'] = trim($_POST['nome'] ?? ''); $dados['email'] = trim(strtolower($_POST['email'] ?? '')); $dados['telefone'] = trim($_POST['telefone'] ?? ''); $dados['cidade'] = trim($_POST['cidade'] ?? ''); // 2. Validação if (empty($dados['nome'])) { $erros[] = 'O nome é obrigatório.'; } if (!filter_var($dados['email'], FILTER_VALIDATE_EMAIL)) { $erros[] = 'Email inválido.'; } // 3. Se não há erros, inserir na BD if (empty($erros)) { try { $stmt = $pdo->prepare( "INSERT INTO contactos (nome, email, telefone, cidade) VALUES (:nome, :email, :telefone, :cidade)" ); $stmt->execute([ ':nome' => $dados['nome'], ':email' => $dados['email'], ':telefone' => $dados['telefone'], ':cidade' => $dados['cidade'], ]); // Guardar mensagem na sessão e redirecionar $_SESSION['msg'] = 'Contacto criado com sucesso!'; $_SESSION['tipo'] = 'sucesso'; header('Location: index.php'); exit; } catch (PDOException $e) { if ($e->getCode() == '23000') { $erros[] = 'Este email já está registado.'; } else { $erros[] = 'Erro ao guardar. Tente novamente.'; } } } } ?> <div class="container"> <h2>Novo Contacto</h2> <?php if (!empty($erros)): ?> <div class="flash erro"> <ul> <?php foreach ($erros as $erro): ?> <li><?= htmlspecialchars($erro) ?></li> <?php endforeach; ?> </ul> </div> <?php endif; ?> <form method="post" action="criar.php"> <div class="campo"> <label>Nome *</label> <input type="text" name="nome" value="<?= htmlspecialchars($dados['nome']) ?>" required> </div> <div class="campo"> <label>Email *</label> <input type="email" name="email" value="<?= htmlspecialchars($dados['email']) ?>" required> </div> <div class="campo"> <label>Telefone</label> <input type="tel" name="telefone" value="<?= htmlspecialchars($dados['telefone']) ?>"> </div> <div class="campo"> <label>Cidade</label> <input type="text" name="cidade" value="<?= htmlspecialchars($dados['cidade']) ?>"> </div> <button type="submit">Guardar Contacto</button> <a href="index.php">Cancelar</a> </form> </div> <?php require_once 'includes/footer.php'; ?>
Prepared Statements: O método prepare() + execute() evita SQL Injection. Os valores :nome, :email etc. são parâmetros — o PDO nunca os interpreta como SQL, independentemente do que o utilizador escreva.
📄 Listar — Mostrar Todos os Contactos
O ficheiro index.php é a página principal — lista todos os contactos da base de dados.
<?php require_once 'config/database.php'; require_once 'includes/header.php'; // Pesquisa (opcional) $pesquisa = trim($_GET['q'] ?? ''); if ($pesquisa !== '') { $stmt = $pdo->prepare( "SELECT * FROM contactos WHERE nome LIKE :q OR email LIKE :q OR cidade LIKE :q ORDER BY nome ASC" ); $stmt->execute([':q' => "%$pesquisa%"]); } else { $stmt = $pdo->query("SELECT * FROM contactos ORDER BY nome ASC"); } $contactos = $stmt->fetchAll(); $total = count($contactos); ?> <div class="container"> <div class="topo"> <h2>Contactos <span class="badge"><?= $total ?></span></h2> <a href="criar.php" class="btn-novo">+ Novo</a> </div> <!-- Barra de pesquisa --> <form method="get"> <input type="search" name="q" placeholder="Pesquisar por nome, email ou cidade..." value="<?= htmlspecialchars($pesquisa) ?>"> <button>Pesquisar</button> </form> <?php if (empty($contactos)): ?> <p class="vazio">Nenhum contacto encontrado.</p> <?php else: ?> <table> <thead> <tr> <th>Nome</th> <th>Email</th> <th>Telefone</th> <th>Cidade</th> <th>Ações</th> </tr> </thead> <tbody> <?php foreach ($contactos as $c): ?> <tr> <td><?= htmlspecialchars($c['nome']) ?></td> <td><?= htmlspecialchars($c['email']) ?></td> <td><?= htmlspecialchars($c['telefone']) ?></td> <td><?= htmlspecialchars($c['cidade']) ?></td> <td class="acoes"> <a href="editar.php?id=<?= $c['id'] ?>">✏️ Editar</a> <a href="apagar.php?id=<?= $c['id'] ?>" onclick="return confirm('Eliminar este contacto?')" class="apagar">🗑 Apagar</a> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> </div> <?php require_once 'includes/footer.php'; ?>
✏️ Editar — Atualizar um Contacto
O ficheiro editar.php recebe o id por GET, carrega o contacto, mostra o formulário preenchido e guarda as alterações por POST.
<?php require_once 'config/database.php'; require_once 'includes/header.php'; // Validar e obter o ID $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); if (!$id) { header('Location: index.php'); exit; } // Buscar o contacto existente $stmt = $pdo->prepare("SELECT * FROM contactos WHERE id = :id"); $stmt->execute([':id' => $id]); $contacto = $stmt->fetch(); if (!$contacto) { $_SESSION['msg'] = 'Contacto não encontrado.'; $_SESSION['tipo'] = 'erro'; header('Location: index.php'); exit; } $erros = []; $dados = $contacto; // pré-preencher com dados existentes if ($_SERVER['REQUEST_METHOD'] === 'POST') { $dados['nome'] = trim($_POST['nome'] ?? ''); $dados['email'] = trim(strtolower($_POST['email'] ?? '')); $dados['telefone'] = trim($_POST['telefone'] ?? ''); $dados['cidade'] = trim($_POST['cidade'] ?? ''); if (empty($dados['nome'])) $erros[] = 'O nome é obrigatório.'; if (!filter_var($dados['email'], FILTER_VALIDATE_EMAIL)) $erros[] = 'Email inválido.'; if (empty($erros)) { try { $stmt = $pdo->prepare( "UPDATE contactos SET nome=:nome, email=:email, telefone=:telefone, cidade=:cidade WHERE id=:id" ); $stmt->execute([ ':nome' => $dados['nome'], ':email' => $dados['email'], ':telefone' => $dados['telefone'], ':cidade' => $dados['cidade'], ':id' => $id, ]); $_SESSION['msg'] = 'Contacto atualizado com sucesso!'; $_SESSION['tipo'] = 'sucesso'; header('Location: index.php'); exit; } catch (PDOException $e) { $erros[] = 'Erro ao atualizar. Tente novamente.'; } } } ?> <div class="container"> <h2>Editar Contacto #<?= $id ?></h2> <?php if (!empty($erros)): ?> <div class="flash erro"> <?php foreach ($erros as $e): ?> <p><?= htmlspecialchars($e) ?></p> <?php endforeach; ?> </div> <?php endif; ?> <form method="post"> <div class="campo"> <label>Nome *</label> <input type="text" name="nome" value="<?= htmlspecialchars($dados['nome']) ?>" required> </div> <div class="campo"> <label>Email *</label> <input type="email" name="email" value="<?= htmlspecialchars($dados['email']) ?>" required> </div> <div class="campo"> <label>Telefone</label> <input type="tel" name="telefone" value="<?= htmlspecialchars($dados['telefone']) ?>"> </div> <div class="campo"> <label>Cidade</label> <input type="text" name="cidade" value="<?= htmlspecialchars($dados['cidade']) ?>"> </div> <button type="submit">Guardar Alterações</button> <a href="index.php">Cancelar</a> </form> </div> <?php require_once 'includes/footer.php'; ?>
🗑 Apagar — Eliminar um Contacto
O ficheiro apagar.php é simples: valida o ID, elimina o registo e redireciona. Não tem formulário — a confirmação é feita com JavaScript no lado do cliente.
<?php require_once 'config/database.php'; session_start(); // 1. Validar o ID recebido por GET $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); if (!$id) { header('Location: index.php'); exit; } try { // 2. Verificar se o contacto existe antes de apagar $check = $pdo->prepare("SELECT id FROM contactos WHERE id = :id"); $check->execute([':id' => $id]); if ($check->rowCount() === 0) { $_SESSION['msg'] = 'Contacto não encontrado.'; $_SESSION['tipo'] = 'erro'; header('Location: index.php'); exit; } // 3. Eliminar o registo $stmt = $pdo->prepare("DELETE FROM contactos WHERE id = :id"); $stmt->execute([':id' => $id]); $_SESSION['msg'] = 'Contacto eliminado com sucesso.'; $_SESSION['tipo'] = 'sucesso'; } catch (PDOException $e) { $_SESSION['msg'] = 'Erro ao eliminar o contacto.'; $_SESSION['tipo'] = 'erro'; } header('Location: index.php'); exit;
Nunca elimines por GET sem confirmação! Robots e crawlers podem seguir links de apagar. A solução mais segura é usar um formulário POST com um token CSRF. O confirm() de JavaScript é uma solução rápida mas não é suficiente em produção.
CSS — Estilo Básico
Um CSS funcional e limpo para o projeto:
/* Reset e base */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', sans-serif; background: #f4f6f9; color: #333; font-size: 15px; } /* Navegação */ header { background: #2c3e50; color: white; } header nav { max-width: 1100px; margin: 0 auto; padding: 14px 20px; display: flex; align-items: center; gap: 20px; } header nav h1 { font-size: 1.2rem; margin-right: auto; } header nav a { color: rgba(255,255,255,.8); text-decoration: none; font-size: 14px; padding: 6px 14px; border-radius: 6px; transition: background .2s; } header nav a:hover { background: rgba(255,255,255,.15); color: white; } /* Conteúdo principal */ main { max-width: 1100px; margin: 30px auto; padding: 0 20px; } footer { text-align: center; padding: 24px; color: #999; font-size: 13px; } /* Mensagens flash */ .flash { padding: 12px 18px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; } .flash.sucesso { background: #d1fae5; border: 1px solid #6ee7b7; color: #065f46; } .flash.erro { background: #fee2e2; border: 1px solid #fca5a5; color: #991b1b; } /* Container */ .container { background: white; border-radius: 12px; padding: 28px 32px; box-shadow: 0 2px 12px rgba(0,0,0,.07); } .topo { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .topo h2 { font-size: 1.4rem; } .badge { background: #e0e7ff; color: #4338ca; font-size: 12px; padding: 2px 8px; border-radius: 10px; margin-left: 8px; } /* Tabela */ table { width: 100%; border-collapse: collapse; margin-top: 16px; } thead th { background: #f8fafc; padding: 10px 14px; text-align: left; font-size: 12px; text-transform: uppercase; color: #64748b; border-bottom: 2px solid #e2e8f0; } tbody td { padding: 12px 14px; border-bottom: 1px solid #f1f5f9; } tbody tr:hover td { background: #fafbfc; } .acoes { display: flex; gap: 8px; } .acoes a { font-size: 13px; text-decoration: none; padding: 4px 10px; border-radius: 5px; transition: background .15s; } .acoes a:first-child { background: #eff6ff; color: #1d4ed8; } .acoes a:first-child:hover { background: #dbeafe; } .acoes a.apagar { background: #fef2f2; color: #dc2626; } .acoes a.apagar:hover { background: #fee2e2; } /* Formulários */ .campo { margin-bottom: 18px; } .campo label { display: block; margin-bottom: 6px; font-size: 13px; font-weight: 600; color: #374151; } .campo input { width: 100%; padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 15px; transition: border-color .2s; } .campo input:focus { outline: none; border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99,102,241,.15); } button[type="submit"] { background: #4f46e5; color: white; border: none; padding: 10px 24px; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background .2s; } button[type="submit"]:hover { background: #4338ca; } .btn-novo { background: #4f46e5; color: white; text-decoration: none; padding: 8px 18px; border-radius: 8px; font-size: 14px; font-weight: 600; } /* Pesquisa */ form[method="get"] { display: flex; gap: 8px; margin-bottom: 20px; } form[method="get"] input { flex: 1; padding: 9px 14px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; } form[method="get"] button { padding: 9px 18px; background: #64748b; border: none; color: white; border-radius: 8px; cursor: pointer; } .vazio { text-align: center; color: #94a3b8; padding: 40px; } /* Responsivo */ @media (max-width: 640px) { .container { padding: 16px; } table { font-size: 13px; } thead th:nth-child(3), thead th:nth-child(4), tbody td:nth-child(3), tbody td:nth-child(4) { display: none; } }
🔒 Segurança Essencial
Antes de colocar qualquer aplicação online, verifica estas práticas obrigatórias:
SQL Injection
✅ Sempre usar prepared statements com PDO. Nunca concatenar variáveis diretamente em queries SQL.
XSS (Cross-Site Scripting)
✅ Sempre usar htmlspecialchars() ao mostrar dados do utilizador em HTML. Nunca fazer echo $variavel diretamente.
CSRF
Adiciona tokens CSRF nos formulários para evitar ataques de submissão forjada. Verifica o token no servidor.
Palavras-passe
Se o projeto tiver autenticação: usa sempre password_hash() e password_verify(). Nunca guarda em texto simples!
// Gerar token no formulário if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } // No HTML do formulário: // <input type="hidden" name="csrf" value="<?= $_SESSION['csrf_token'] ?>"> // Verificar no POST if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf'] ?? '')) { die('Pedido inválido.'); } // Exemplo de hash de palavra-passe $hash = password_hash($_POST['password'], PASSWORD_BCRYPT); // Verificar ao fazer login: if (password_verify($_POST['password'], $hash_da_bd)) { // palavra-passe correta }
Em produção, no ficheiro database.php: Muda o utilizador MySQL de root para um utilizador com permissões mínimas (apenas SELECT, INSERT, UPDATE, DELETE na tua base de dados). Nunca expõe o root online.
-- Correr no phpMyAdmin ou terminal MySQL do servidor CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'password_forte_aqui'; GRANT SELECT, INSERT, UPDATE, DELETE ON crud_contactos.* TO 'app_user'@'localhost'; FLUSH PRIVILEGES;
🌐 Escolher um Serviço de Alojamento
Para colocar o projeto online precisas de um serviço de alojamento com suporte a PHP e MySQL. Aqui ficam as melhores opções para projetos de aprendizagem e produção:
InfinityFree
- PHP 7/8
- MySQL incluído
- phpMyAdmin
- Subdomínio grátis
- Sem publicidade
000webhost
- PHP + MySQL
- 1 GB espaço
- Painel Hepsia
- Fácil para iniciantes
Hostinger
- PHP 8.x
- MySQL 8
- SSL grátis
- Domínio incluído
- Painel hPanel
SiteGround
- PHP 8.x + MySQL
- Backups diários
- SSL grátis
- Suporte 24/7
Alojamento PT
- Servidores em Portugal
- PHP + MySQL
- Suporte em português
- cPanel incluído
Para aprendizagem: Começa com o InfinityFree ou 000webhost (gratuitos). Para projetos reais, o Hostinger tem excelente relação qualidade/preço e suporte em português.
Criar conta no Hostinger (exemplo)
-
1
Registar conta
Vai a hostinger.pt, escolhe o plano, regista e faz o pagamento. Recebes acesso ao painel hPanel.
-
2
Configurar o domínio
Se incluído, configura o teu domínio. Caso contrário, usa o subdomínio gratuito (ex:
meusite.hostingersite.com). -
3
Criar base de dados MySQL
hPanel → Bases de Dados → MySQL → Criar nova BD. Anota o nome, utilizador e palavra-passe.
-
4
Importar a estrutura SQL
hPanel → phpMyAdmin → seleciona a BD → aba Importar → carrega o ficheiro
.sqlexportado do XAMPP (phpMyAdmin local → Exportar). -
5
Anotar credenciais
Guarda o host (geralmente
localhostou um endereço específico), nome da BD, utilizador e palavra-passe — vais precisar deles nodatabase.php.
🚀 Upload dos Ficheiros — Deploy
Tens três opções para enviar os ficheiros para o servidor:
Gestor de Ficheiros Web
O painel de alojamento (cPanel/hPanel) tem um gestor de ficheiros web. Simples mas lento para muitos ficheiros.
FTP / SFTP
Usa o FileZilla (gratuito) ou a extensão SFTP do VS Code. Mais rápido e permite sincronização.
Git + CI/CD
Para projetos maiores: usa Git com GitHub Actions ou similar para deploy automático. Mais avançado.
📡 Upload com FileZilla (recomendado)
-
1
Instalar o FileZilla
Descarrega o FileZilla Client em
filezilla-project.orge instala. -
2
Obter as credenciais FTP
No painel do teu alojamento: procura FTP / Gestor de FTP. Cria uma conta FTP ou usa as credenciais fornecidas. Anota: Host, Utilizador, Palavra-passe e Porto (normalmente 21 para FTP ou 22 para SFTP).
-
3
Ligar ao servidor
FileZilla → Abrir Gestor de Sites → Novo Site. Preenche os dados FTP e clica Ligar. Aparece o sistema de ficheiros do servidor do lado direito.
-
4
Navegar para a pasta pública
No servidor (lado direito do FileZilla), navega até
public_html/— esta é a pasta raiz do teu site. -
5
Enviar os ficheiros
No lado esquerdo (local), navega até
C:\xampp\htdocs\contactos\. Seleciona todos os ficheiros e arrasta para a pastapublic_html/no servidor (ou para uma subpasta, ex:public_html/contactos/). -
6
Atualizar o database.php
Muito importante: atualiza as credenciais no ficheiro
config/database.phpcom as credenciais da base de dados do servidor de produção antes de fazer upload.
<?php // ⚠️ Credenciais do servidor de PRODUÇÃO // (diferentes das credenciais do XAMPP local!) $host = 'localhost'; // geralmente localhost $dbname = 'u123456789_crud'; // nome da BD no servidor $username = 'u123456789_app'; // utilizador MySQL criado $password = 'Pd9#xK2!mR'; // palavra-passe forte! $charset = 'utf8mb4'; $dsn = "mysql:host=$host;dbname=$dbname;charset=$charset"; $opcoes = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; try { $pdo = new PDO($dsn, $username, $password, $opcoes); } catch (PDOException $e) { error_log($e->getMessage()); // guarda no log do servidor die('Erro de ligação. Contacte o administrador.'); } ?>
🔧 Upload com SFTP do VS Code
-
1
Instalar extensão SFTP
VS Code → Extensões → pesquisa "SFTP" (autor: Natizyskunk) → instala.
-
2
Configurar o SFTP
Ctrl+Shift+P →
SFTP: Config. Edita o ficheiro criado:
{
"name": "Servidor Produção",
"host": "ftp.meudominio.pt",
"protocol": "sftp",
"port": 22,
"username": "utilizador_ftp",
"password": "palavrapasse_ftp",
"remotePath": "/public_html/contactos",
"uploadOnSave": false,
"ignore": [".vscode", ".git", "*.log"]
}
-
3
Fazer upload
Ctrl+Shift+P →
SFTP: Upload Folderpara enviar toda a pasta de uma vez. Ou clica com botão direito num ficheiro → Upload para enviar individualmente.
Projeto online! Após o upload, abre o browser em https://meudominio.pt/contactos/ e o teu CRUD PHP está a funcionar em produção. Verifica se o SSL (HTTPS) está ativo para proteger os dados dos utilizadores.
📤 Exportar a BD do XAMPP para o servidor
-
1
Exportar do XAMPP local
phpMyAdmin local (
localhost/phpmyadmin) → selecionacrud_contactos→ aba Exportar → método Rápido → formato SQL → Executar. Guarda o ficheiro.sql. -
2
Importar no servidor
phpMyAdmin do servidor → seleciona a base de dados do servidor → aba Importar → escolhe o ficheiro
.sql→ Executar. A estrutura e os dados são importados!
✅ Checklist Final de Deploy
| Item | Descrição | Estado |
|---|---|---|
| database.php atualizado | Credenciais de produção no lugar das do XAMPP | ☐ |
| BD importada no servidor | Tabela criada e dados de exemplo importados | ☐ |
| Ficheiros enviados por FTP | Todos os .php, .css em public_html/ | ☐ |
| HTTPS ativo | SSL configurado no painel do alojamento | ☐ |
| Permissões de ficheiros | PHP: 644, Pastas: 755 | ☐ |
| Erros ocultos ao utilizador | display_errors = Off em produção | ☐ |
| Utilizador MySQL limitado | Sem usar root em produção | ☐ |
| Testar todas as operações | Criar, listar, editar e apagar no servidor | ☐ |
| Backup automático ativo | Configurado no painel do alojamento | ☐ |
// No início do index.php ou num ficheiro de bootstrap: if ($_SERVER['SERVER_NAME'] !== 'localhost') { ini_set('display_errors', 0); ini_set('log_errors', 1); error_reporting(E_ALL); }