Mini-projeto

Mini-blog em PHP

Um blog mínimo com o fluxo completo: listar posts, ler um post inteiro e publicar novos. Um arquivo só (index.php), com PDO + prepared statements e escape anti-XSS.

⚠️ Os três pilares deste projeto: PDO prepared (? + execute(), nunca concatenar) contra SQL injection; h() em toda saída contra XSS; e PRG (Post-Redirect-Get) pra o F5 não republicar.
1

Criar a tabela

Só uma tabela: posts. O criado_em usa DEFAULT CURRENT_TIMESTAMP — o banco preenche a data/hora sozinho ao inserir, então o PHP nem manda esse campo. Rode no phpMyAdmin.

blog.sql
CREATE TABLE posts (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    titulo    VARCHAR(160) NOT NULL,
    conteudo  TEXT NOT NULL,
    -- o banco preenche a data sozinho ao inserir:
    criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
2

Conectar ao banco (PDO)

A conexão fica num arquivo separado pra reaproveitar com require. Sempre em try/catch e com o modo de exceção ligado.

conexao.php
<?php
$dsn = "mysql:host=localhost;dbname=blog;charset=utf8mb4";

try {
    $pdo = new PDO($dsn, "root", "");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
    exit("Não foi possível conectar ao banco.");
}
3

Listar posts (SELECT)

Como não há valor do usuário na query, pode usar query() direto. ORDER BY criado_em DESC mostra os mais novos primeiro. Toda saída passa por h().

listar (trecho)
<?php
require "conexao.php";

// Helper anti-XSS: escapa tudo que vai pro HTML.
function h(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
}

$posts = $pdo->query("SELECT * FROM posts ORDER BY criado_em DESC")->fetchAll();

foreach ($posts as $p) {
    echo h($p["titulo"]) . "\n";
}
4

Ver um post (SELECT com id)

Aqui o id vem da URL (valor de fora!), então obrigatoriamente prepared statement. O cast (int) é uma defesa extra. Pra exibir o conteúdo, nl2br(h(...)) nesta ordem.

ver post (trecho)
$id = (int) ($_GET["post"] ?? 0);   // cast protege contra lixo na URL

$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = ?");
$stmt->execute([$id]);
$post = $stmt->fetch();   // false se não existir

echo nl2br(h($post["conteudo"]));   // h() ANTES: escapa; nl2br DEPOIS: vira <br>
📌 Ordem importa: nl2br(h($txt)). Se invertesse — h(nl2br($txt)) — o <br> gerado seria escapado e apareceria como texto; pior, abriria brecha pra HTML malicioso passar.
5

Criar post (validar + INSERT + PRG)

No POST: ler com trim(), validar no backend, inserir com prepared statement e redirecionar (PRG). O criado_em não vai aqui — é o DEFAULT do banco.

criar (trecho)
$erros = [];

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $titulo   = trim($_POST["titulo"] ?? "");
    $conteudo = trim($_POST["conteudo"] ?? "");

    // Backend é obrigatório; o frontend é só UX.
    if ($titulo === "")   $erros[] = "O título é obrigatório.";
    if ($conteudo === "") $erros[] = "O conteúdo é obrigatório.";

    if ($erros === []) {
        $stmt = $pdo->prepare("INSERT INTO posts (titulo, conteudo) VALUES (?, ?)");
        $stmt->execute([$titulo, $conteudo]);
        header("Location: index.php?msg=publicado");   // PRG: F5 não republica
        exit;
    }
}
6

Tudo junto: index.php

Os passos num arquivo só. A mesma página tem 3 estados decididos pela requisição: ?post=ID mostra o post inteiro; senão mostra a lista + formulário; o POST publica e redireciona.

index.php
<?php
require "conexao.php";   // traz o $pdo

function h(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
}

// DATETIME (YYYY-MM-DD HH:MM:SS) -> BR (DD/MM/AAAA HH:MM).
function dataBr(string $dt): string {
    return (new DateTime($dt))->format("d/m/Y H:i");
}

// ---- CRIAR POST (POST) ----
$erros = [];
$titulo = $conteudo = "";   // sticky form

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $titulo   = trim($_POST["titulo"] ?? "");
    $conteudo = trim($_POST["conteudo"] ?? "");

    if ($titulo === "")               $erros[] = "O título é obrigatório.";
    elseif (mb_strlen($titulo) > 160) $erros[] = "Título muito longo (máx. 160).";
    if ($conteudo === "")             $erros[] = "O conteúdo é obrigatório.";

    if ($erros === []) {
        $stmt = $pdo->prepare("INSERT INTO posts (titulo, conteudo) VALUES (?, ?)");
        $stmt->execute([$titulo, $conteudo]);
        header("Location: index.php?msg=publicado");
        exit;
    }
}

// ---- VER UM POST (GET ?post=ID) ----
$post = null;
if (isset($_GET["post"])) {
    $stmt = $pdo->prepare("SELECT * FROM posts WHERE id = ?");
    $stmt->execute([(int) $_GET["post"]]);
    $post = $stmt->fetch();
}

// ---- LISTAR (só na home) ----
$posts = $post ? [] : $pdo->query("SELECT * FROM posts ORDER BY criado_em DESC")->fetchAll();

$aviso = ($_GET["msg"] ?? "") === "publicado" ? "Post publicado!" : "";
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Mini-blog</title>
    <style> /* ...CSS da página (resumido)... */ </style>
</head>
<body>

<?php if ($post): ?>
    <!-- ESTADO 1: um post inteiro -->
    <p><a href="index.php">&larr; voltar</a></p>
    <h1><?= h($post["titulo"]) ?></h1>
    <p class="meta"><?= dataBr($post["criado_em"]) ?></p>
    <div class="conteudo"><?= nl2br(h($post["conteudo"])) ?></div>

<?php else: ?>
    <!-- ESTADO 2: lista + formulário -->
    <h1>Mini-blog</h1>

    <?php if ($aviso !== ""): ?>
        <p class="aviso"><?= h($aviso) ?></p>
    <?php endif; ?>

    <?php if ($erros !== []): ?>
        <div class="erros"><ul>
        <?php foreach ($erros as $e): ?><li><?= h($e) ?></li><?php endforeach; ?>
        </ul></div>
    <?php endif; ?>

    <form method="post">
        <input type="text" name="titulo" value="<?= h($titulo) ?>" placeholder="Título">
        <textarea name="conteudo" rows="5"><?= h($conteudo) ?></textarea>
        <button type="submit">Publicar</button>
    </form>

    <?php foreach ($posts as $p): ?>
        <div class="post-item">
            <h2><a href="index.php?post=<?= (int) $p["id"] ?>"><?= h($p["titulo"]) ?></a></h2>
            <span class="meta"><?= dataBr($p["criado_em"]) ?></span>
        </div>
    <?php endforeach; ?>

<?php endif; ?>

</body>
</html>
📌 Como rodar: crie a tabela (passo 1), salve conexao.php e index.php numa pasta em htdocs/ e acesse pelo Apache (ex.: http://localhost/blog/). Como usa header(), tem que rodar no servidor — não abra como file://.
← Ver o blog rodando Voltar ao topo ↑