Projeto: Sistema de Blog – Parte 2
Na aula anterior, criamos a estrutura básica do blog. Esta aula traz os toques finais — edição, exclusão, comentários, paginação e reforço de segurança —, transformando-o em um projeto que está realmente pronto para ser implantado e apresentado.
1. Pós-edição
(1) Adicionando métodos ao modelo Post
<?php
// Add these methods to app/Models/Post.php
public function findByUser(int $id, int $userId): ?array
{
$stmt = $this->db->prepare(
"SELECT * FROM posts WHERE id = :id AND user_id = :userId"
);
$stmt->execute(['id' => $id, 'userId' => $userId]);
$post = $stmt->fetch();
return $post ?: null;
}
public function update(int $id, int $userId, string $title, string $content): bool
{
$stmt = $this->db->prepare(
"UPDATE posts SET title = :title, content = :content
WHERE id = :id AND user_id = :userId"
);
$stmt->execute([
'id' => $id,
'userId' => $userId,
'title' => $title,
'content' => $content,
]);
return $stmt->rowCount() > 0;
}
public function delete(int $id, int $userId): bool
{
$stmt = $this->db->prepare(
"DELETE FROM posts WHERE id = :id AND user_id = :userId"
);
$stmt->execute(['id' => $id, 'userId' => $userId]);
return $stmt->rowCount() > 0;
}
public function getByUser(int $userId): array
{
$stmt = $this->db->prepare(
"SELECT * FROM posts WHERE user_id = :userId ORDER BY created_at DESC"
);
$stmt->execute(['userId' => $userId]);
return $stmt->fetchAll();
}
(2) Métodos correspondentes do controlador
<?php
// Add these methods to PostController
/** Edit form */
public function editForm(array $params): string
{
$this->requireLogin();
$post = $this->post->findByUser((int)$params['id'], $_SESSION['user']['id']);
if (!$post) {
http_response_code(404);
return '<h2>Post not found or you do not have permission to edit it</h2>';
}
ob_start();
require __DIR__ . '/../../views/posts/edit.php';
return ob_get_clean();
}
/** Handle editing */
public function edit(array $params): string
{
$this->requireLogin();
$userId = $_SESSION['user']['id'];
$id = (int)$params['id'];
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
if ($title === '' || $content === '') {
return '<p style="color:red">Title and content cannot be empty</p><a href="javascript:history.back()">Go back</a>';
}
$success = $this->post->update($id, $userId, $title, $content);
if (!$success) {
return '<p style="color:red">Post not found or you do not have permission to edit it</p>';
}
header("Location: /posts/{$id}");
exit;
}
/** Delete a post */
public function delete(array $params): void
{
$this->requireLogin();
$userId = $_SESSION['user']['id'];
$id = (int)$params['id'];
$this->post->delete($id, $userId);
header("Location: /posts/my");
exit;
}
/** My posts */
public function myPosts(): string
{
$this->requireLogin();
$posts = $this->post->getByUser($_SESSION['user']['id']);
ob_start();
require __DIR__ . '/../../views/posts/my.php';
return ob_get_clean();
}
2. Funcionalidade de comentários
(1) Criar a tabela “Comentários”
▶ Exemplo: Implementação de comentários
CREATE TABLE comments (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
(2) Modelo de comentários
<?php
// app/Models/Comment.php
namespace App\Models;
use App\Core\Database;
use PDO;
class Comment
{
private PDO $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function getByPost(int $postId): array
{
$stmt = $this->db->prepare(
"SELECT c.*, u.username
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.post_id = :postId
ORDER BY c.created_at ASC"
);
$stmt->execute(['postId' => $postId]);
return $stmt->fetchAll();
}
public function create(int $postId, int $userId, string $content): int
{
$stmt = $this->db->prepare(
"INSERT INTO comments (post_id, user_id, content)
VALUES (:post_id, :user_id, :content)"
);
$stmt->execute([
'post_id' => $postId,
'user_id' => $userId,
'content' => $content,
]);
return (int)$this->db->lastInsertId();
}
}
(3) Página de detalhes da publicação — com visualização de comentários
<?php
// views/posts/show.php (improved version)
$commentModel = new \App\Models\Comment();
$comments = $commentModel->getByPost($post['id']);
$title = $post['title'];
ob_start();
?>
<article>
<h2><?= htmlspecialchars($post['title']) ?></h2>
<div class="post-meta">
Author: <?= htmlspecialchars($post['username']) ?> |
<?= $post['created_at'] ?>
</div>
<div style="line-height:1.8;margin:20px 0">
<?= nl2br(htmlspecialchars($post['content'])) ?>
</div>
<?php if (isset($_SESSION['user']) && $_SESSION['user']['id'] === (int)$post['user_id']): ?>
<p>
<a href="/posts/<?= $post['id'] ?>/edit">Edit</a> |
<a href="/posts/<?= $post['id'] ?>/delete" onclick="return confirm('Confirm deletion?')">Delete</a>
</p>
<?php endif; ?>
</article>
<!-- Comment List -->
<h3>Comments (<?= count($comments) ?>)</h3>
<?php foreach ($comments as $c): ?>
<div style="border-bottom:1px solid #eee;padding:10px 0">
<strong><?= htmlspecialchars($c['username']) ?></strong>
<span style="color:#999;font-size:12px">
<?= date('Y-m-d H:i', strtotime($c['created_at'])) ?>
</span>
<p><?= nl2br(htmlspecialchars($c['content'])) ?></p>
</div>
<?php endforeach; ?>
<!-- Post a Comment -->
<?php if (isset($_SESSION['user'])): ?>
<h4>Leave a Comment</h4>
<form method="POST" action="/posts/<?= $post['id'] ?>/comment">
<textarea name="content" rows="3" required placeholder="Write your comment..."></textarea>
<button type="submit" style="margin-top:8px">Submit</button>
</form>
<?php else: ?>
<p><a href="/login">Log in</a> to leave a comment</p>
<?php endif; ?>
<p style="margin-top:20px"><a href="/posts">← Back to posts</a></p>
<?php $content = ob_get_clean(); require __DIR__ . '/../layout.php'; ?>
3. Paginação
<?php
// Post model: paginated queries
public function getPaginated(int $page = 1, int $perPage = 10): array
{
$offset = ($page - 1) * $perPage;
$stmt = $this->db->prepare(
"SELECT p.*, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.status = 'published'
ORDER BY p.created_at DESC
LIMIT :limit OFFSET :offset"
);
// PDO LIMIT/OFFSET must use bindValue with explicit types
$stmt->bindValue(':limit', $perPage, \PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
public function count(): int
{
return (int)$this->db->query(
"SELECT COUNT(*) FROM posts WHERE status = 'published'"
)->fetchColumn();
}
▶ Exemplo: Componente de paginação
<?php
// Pagination HTML component
function renderPagination(int $currentPage, int $totalPages, string $baseUrl): string
{
if ($totalPages <= 1) return '';
$html = '<div style="display:flex;gap:8px;margin-top:20px">';
for ($i = 1; $i <= $totalPages; $i++) {
$active = $i === $currentPage ? 'background:#4a90d9;color:white' : '';
$html .= "<a href='{$baseUrl}?page={$i}'
style='padding:6px 12px;border:1px solid #ddd;border-radius:4px;
text-decoration:none;{$active}'>{$i}</a>";
}
$html .= '</div>';
return $html;
}
4. Lista de verificação para reforço de segurança
// Security hardening summary
// 1. Escape all output with htmlspecialchars
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// 2. Use prepared statements for all database operations
$stmt = $pdo->prepare("UPDATE posts SET title = :t WHERE id = :id AND user_id = :uid");
$stmt->execute(['t' => $title, 'id' => $id, 'uid' => $_SESSION['user']['id']]);
// 3. Verify permissions on sensitive operations (not just hiding buttons—validate on the backend)
public function delete(array $params): void
{
$this->requireLogin();
// ✅ Verify the current user is the post author
$this->post->delete((int)$params['id'], $_SESSION['user']['id']);
}
// 4. Session security configuration
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1'); // Enable when using HTTPS
ini_set('session.cookie_samesite', 'Lax');
// 5. Never store plain-text passwords
$hash = password_hash($password, PASSWORD_DEFAULT);
// 6. CSRF Token (simplified version)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_POST['_token'] !== $_SESSION['_token']) {
die('CSRF attack blocked');
}
}
// In forms: <input type="hidden" name="_token" value="<?= $_SESSION['_token'] ?>">
5. Lista de verificação para implantação
Pre-deployment checklist:
□ display_errors = Off (production)
□ error_log path is writable
□ Database password is not root/empty
□ session.cookie_secure = 1 (if using HTTPS)
□ Upload directory permissions are correct (PHP execution disallowed)
□ .env file is not web-accessible (store outside the web root)
□ SSL certificate configured (Let's Encrypt is free)
□ robots.txt and sitemap.xml generated
❓ Perguntas Frequentes
P: À medida que o sistema de blog ganha mais funcionalidades, o código está ficando confuso. E agora? R: Esse é exatamente o problema que os frameworks (Laravel/Symfony) resolvem. Você já entende os conceitos de Router, Controller, Model e View. Quando aprender Laravel na próxima lição, você vai se pegar pensando: “Isso não é apenas uma versão mais elegante do que eu construí manualmente?”
P: Como posso evitar spam nos comentários? R: Soluções progressivas: (1) exigir login para comentar (a mais simples); (2) adicionar verificação por reCAPTCHA (gratuita); (3) moderação de conteúdo (filtragem de palavras sensíveis + revisão manual); (4) limitação de frequência (o mesmo usuário não pode comentar novamente em 60 segundos).
❓ Perguntas Frequentes
P: Por que preciso de proteção contra CSRF para o painel de administração do meu blog? R: Sem proteção contra CSRF (Cross-Site Request Forgery), um invasor poderia criar um link ou formulário malicioso que realizasse ações administrativas utilizando sua sessão ativa. A solução: gerar um token aleatório armazenado na sessão, incluí-lo como um campo oculto em todos os formulários do painel de administração e verificá-lo no servidor antes de processar a solicitação.
📖 Resumo
- As operações de edição/exclusão devem verificar as permissões (
WHERE user_id = :uid) - Funcionalidade de comentários: a camada de modelo encapsula as operações CRUD → o controlador coordena → a visualização exibe
- Paginação:
LIMIT :limit OFFSET :offset, usebindValuecomPARAM_INT - Fortalecimento da segurança: escapamento de saída + instruções preparadas + verificação de permissões no backend + configuração de segurança de sessão
- Lista de verificação para implantação: desativar o
display_errors, ativar o HTTPS, verificar as permissões dos arquivos
📝 Exercícios
- Adicionar as funcionalidades “Editar postagem” e “Excluir postagem” ao sistema do blog (somente o autor pode realizar essas ações).
- Implementar o recurso de comentários: a página de detalhes da publicação exibe uma lista de comentários, e os usuários conectados podem postar comentários.
- Adicionar paginação à lista de posts (5 posts por página), com suporte à paginação tanto na página inicial quanto na página da lista de posts.



