Documentação Técnica

Guias e referências sobre arquitetura, segurança e boas práticas

Voltar ao Blog
Novidade 18 de novembro de 2025

Home Completamente Renovada

A página inicial do projeto recebeu melhorias significativas em design, performance e experiência do usuário.

✨ Principais Melhorias Implementadas:
  • ✨ Hero centralizada e moderna com gradientes animados

    Layout redesenhado com gradientes flutuantes, efeitos glassmorphism e animações CSS suaves.

  • 🎬 Animações AOS progressivas em todo o conteúdo

    Biblioteca AOS integrada com delays progressivos para criar movimento natural ao fazer scroll.

  • 📱 Layout totalmente responsivo (até 290px de largura)

    Otimizado para qualquer dispositivo, incluindo smartwatches e telas extremamente pequenas.

  • 🎯 Conteúdo minimalista focado em conversão

    Cards simplificados, CTAs proeminentes e remoção de informações técnicas excessivas da home.

  • 📚 Conteúdo técnico reorganizado em /docs/tech

    Artigos sobre stack tecnológica, fluxo de requisição e arquitetura MVC movidos para documentação técnica.

  • 🚀 Experiência visual fluida e profissional

    Tipografia consistente, espaçamentos harmônicos e transições suaves em todos os elementos interativos.

Tecnologias Utilizadas na Renovação:

AOS 2.3.1 CSS Animations Glassmorphism Responsive Grid
💡 Acesse: Visite a página inicial para ver todas as melhorias em ação!
Introdução 18 de novembro de 2025

O que é o System?

System é um projeto educacional open-source construído em PHP 8.3+ que demonstra conceitos modernos de desenvolvimento web através de um sistema multiusuário funcional.

Diferente de tutoriais convencionais, este projeto se explica enquanto funciona. Cada página que você visita, cada funcionalidade que você usa, está documentada no próprio código-fonte e no README do repositório.

💡 Filosofia: O código é didático, com comentários explicativos e estrutura clara que serve como referência para desenvolvedores iniciantes e intermediários.
Stack 18 de novembro de 2025

Tecnologias Utilizadas

Stack moderna e robusta para aplicações web profissionais.

Backend
  • PHP 8.3+ — Linguagem moderna com typed properties e match expressions
  • PDO + MySQL 8.0 — Banco de dados relacional com prepared statements
  • Twig 3.0 — Motor de templates seguro e expressivo
  • phpdotenv — Gerenciamento de variáveis de ambiente
  • Composer — Autoloading PSR-4 e gerenciamento de dependências
Frontend
  • Bootstrap 5.3 — Framework CSS responsivo com variáveis CSS
  • Bootstrap Icons — Biblioteca de ícones vetoriais
  • HTML5 Semântico — Estrutura acessível e moderna
  • CSS Custom Properties — Temas dinâmicos claro/escuro
  • JavaScript Vanilla — Sem dependências de frameworks frontend
Arquitetura 18 de novembro de 2025

Como Funciona uma Requisição?

Entenda o ciclo completo desde a URL digitada até o HTML renderizado.

Fluxo de Requisição HTTP

  1. Servidor Web (Apache/Nginx) recebe a requisição (ex: GET /blog)
  2. public/index.php (front controller) é executado via rewrite rules
  3. Bootstrap carrega configurações, conecta ao banco e inicializa sessão
  4. Router encontra a rota correspondente em routes/web.php
  5. Controller processa a lógica (ex: HomeController@blog)
  6. Model consulta o banco de dados MySQL (se necessário)
  7. Twig renderiza o template com os dados retornados
  8. HTML final é enviado de volta ao navegador do usuário
🎯 Padrão MVC: Model (dados) → Controller (lógica) → View (apresentação)
Observabilidade 23 de novembro de 2025

Monitor de Performance no Painel

Instrumentamos o ciclo completo de requisições do painel para registrar tempo total, segmentos (roteador, middlewares, dispatch) e quantidade de queries executadas pelo PDO, tudo sem depender de frameworks externos.

Como ativar: defina as variáveis abaixo no .env (já presentes no projeto):
PERF_MONITOR=true
PERF_SLOW_MS=600
PERF_LOG_PATH="/var/www/system/storage/logs/perf.log"
PERF_QUERY_SAMPLES=5
O que é coletado?
  • Duração total da requisição e marcação de requests "lentas" (> PERF_SLOW_MS).
  • Segmentos detalhados: router.match, router.middleware, router.dispatch e outros que adicionarmos.
  • Pico de memória (MB) e contagem total de queries executadas via PDO.
  • Até PERF_QUERY_SAMPLES SQLs completas com duração em milissegundos.
Onde visualizar?

Os eventos são gravados em storage/logs/perf.log como JSON lines. Cada linha contém:

  • metadata: URI normalizada, método HTTP, rota atendida e status final.
  • segments: tempos por etapa em milissegundos.
  • query_samples: SQL compactado + duração para troubleshooting rápido.

Exemplo de entrada (abreviado)

{
  "timestamp": "2025-11-23T14:05:10Z",
  "duration_ms": 742.18,
  "is_slow": true,
  "query_count": 38,
  "segments": [
    {"name": "router.match", "ms": 4.1},
    {"name": "router.middleware", "ms": 12.3},
    {"name": "router.dispatch", "ms": 698.4}
  ],
  "query_samples": [
    {"sql": "SELECT * FROM posts WHERE status = 'published' ORDER BY...", "ms": 83.4}
  ]
}
Dica: reduza PERF_SLOW_MS em ambientes de desenvolvimento para pegar gargalos menores e desative o monitor em produção se não precisar dos logs contínuos.
PHP Arquitetura 15 de novembro de 2025

Como Funciona o Sistema de Roteamento

Entenda como o Router custom mapeia URLs para Controllers e métodos específicos.

O roteamento é o coração de qualquer framework web. No System, usamos um Router simples mas poderoso que mapeia requisições HTTP para ações de controllers.

Exemplo de Rota em routes/web.php:

[
  'path' => '/login',
  'method' => 'POST',
  'handler' => 'AuthController@login'
]

Quando você envia um POST /login, o Router:

  1. Itera sobre todas as rotas definidas
  2. Compara $route['path'] e $route['method'] com a requisição
  3. Quando encontra match, chama dispatch('AuthController@login')
  4. Dispatch instancia AuthController e executa login()
💡 Dica: Veja o código completo em app/Core/Router.php
Arquitetura Painel Admin 21 de novembro de 2025

Painel Administrativo Modularizado

O antigo AdminController monolítico foi fatiado em módulos menores. Agora cada área (usuários, posts e galeria) possui seu próprio controller especializado, enquanto as regras compartilhadas vivem em um AdminBaseController reutilizável.

Nova árvore em app/Controllers/Admin/

Admin/
├── AdminBaseController.php   # CSRF, flashes, slug helpers, preferências
├── UsersAdminController.php  # CRUD e aprovação de usuários
├── PostsAdminController.php  # Posts, categorias, tags e notificações
└── GalleryAdminController.php# Imagens e categorias da galeria
AdminBaseController

Centraliza helpers como enforceCsrf(), flashes de sessão, normalização de status, geração de slugs e preferências do usuário. O
AdminController e todos os submódulos o estendem diretamente.

AdminController enxuto

Foca no dashboard, conta do usuário logado, preferências e widgets de servidor. Ele instancia os controllers especializados e delega os fluxos CRUD.

UsersAdminController
  • CRUD completo de usuários
  • Uploads/remoção de avatar
  • Atualiza sessões quando o usuário edita o próprio perfil
PostsAdminController
  • Posts, categorias e tags
  • Controle de status + notificações internas
  • Upload/substituição de covers com PostCoverUploader
GalleryAdminController
  • Uploads via GalleryImageUploader
  • Gerência de categorias da galeria
  • Validações de formulários e flashes dedicados
Como o Router enxerga

As rotas existentes continuam apontando para AdminController. Ele atua como façade, validando permissões e repassando a chamada para o módulo correto. Assim não foi necessário alterar routes/web.php ou o JavaScript do dashboard.

Quando útil, podemos expor rotas diretas (ex: 'handler' => 'Admin\\PostsAdminController@postsStore') sem quebrar nada.

Benefícios imediatos:
  • Cada controller ficou com ~200-300 linhas, facilitando manutenção
  • Helper únicos (CSRF, flashes, slugs) agora têm uma fonte da verdade
  • Fica mais simples testar e mover cada módulo para rotas/menus separados
  • Prepara o terreno para dividir dashboard.twig em seções menores
Atualização 23 de novembro de 2025 Dashboard 100% modularizado

Refatoração Completa do Dashboard

O arquivo views/dashboard.twig foi completamente modularizado. Todas as seções foram extraídas para partials Twig reutilizáveis, macros foram criados para reduzir duplicação, e o JavaScript foi consolidado em um bundle otimizado.

Seções Extraídas

11 partials

views/dashboard/sections/

Navbar Modular

4 partials

views/dashboard/navbar/

Macros Twig

3 arquivos

tables, flash, badges

JS Bundle

2 tags script

config + bundle.js

🧭 Modularização do Navbar

O navbar do dashboard (~180 linhas) foi dividido em componentes independentes para facilitar manutenção e testes isolados.

Estrutura Modular
navbar.twig Orquestrador (33 linhas)
navbar/breadcrumb.twig Navegação hierárquica (5 linhas)
navbar/theme-toggle.twig Botão alternar tema (3 linhas)
navbar/notifications.twig Dropdown de notificações (57 linhas)
navbar/user-menu.twig Menu do usuário logado (70 linhas)
Benefícios
  • Redução drástica: Dashboard.twig perdeu ~180 linhas, agora apenas {% include 'dashboard/navbar.twig' %}
  • Manutenção isolada: Alterar layout de notificações não afeta breadcrumb ou menu do usuário
  • Reusabilidade: User-menu pode ser reutilizado em outras áreas admin
  • Testes independentes: Cada componente pode ser testado visualmente sem carregar dashboard completo
  • AB Testing: Facilita testar novos layouts de notificações ou menus
  • Uso de macros: User-menu já utiliza badges.role() em vez de dicionário duplicado

🧩 Modularização da Sidebar

A sidebar do dashboard (~145 linhas) foi modularizada em uma arquitetura granular avançada para facilitar customizações por role (admin, editor, etc.) e manutenção independente de cada grupo de itens do menu.

Estrutura Granular
sidebar.twig Orquestrador principal (4 linhas)
Componentes Estruturais:
sidebar/header.twig Brand + mini variant (12 linhas)
sidebar/nav-items.twig Orquestrador de itens (7 linhas)
sidebar/footer.twig Badge env + links docs (12 linhas)
Itens de Menu (Customizáveis):
items/overview.twig Dashboard home (6 linhas)
items/admin-section.twig Menu admin-only (30 linhas)
items/posts-dropdown.twig Dropdown gestão posts (27 linhas)
items/gallery-dropdown.twig Dropdown gestão galeria (33 linhas)
items/user-section.twig Menu conta/docs (20 linhas)
Benefícios
  • Evolução estrutural: Dashboard passou de arquivo monolítico (~2.600 linhas) para orquestrador modular (atualmente ~323 linhas + partials)
  • Menus personalizados por role: Fácil criar sidebar específica para moderadores, revisores, etc.
  • Granularidade avançada: Cada grupo lógico de itens em arquivo separado (admin, posts, gallery, user)
  • Manutenção isolada: Alterar menu de galeria não afeta menu de posts ou admin
  • AB Testing: Testar novos layouts de dropdown sem afetar outros itens
  • Facilita expansão: Adicionar nova seção = criar novo arquivo em items/
  • Estrutura consistente: Todos os componentes seguem o mesmo padrão de classes CSS e JavaScript
Customização por Role: Para criar menu específico por role, basta:
  1. Criar novo arquivo em sidebar/items/moderator-section.twig
  2. Adicionar condição em nav-items.twig: {% if isModerator(me) %}{% include 'dashboard/sidebar/items/moderator-section.twig' %}{% endif %}
  3. Utilizar mesma estrutura <li class="sidebar-nav-item"><a href="#section" class="sidebar-nav-link"...>

📁 Estrutura de Partials Twig

Cada seção do dashboard foi extraída para um partial dedicado em views/dashboard/sections/. Isso reduziu drasticamente o tamanho do arquivo principal e facilitou manutenção isolada.

Partial Responsabilidade Dados Utilizados
overview.twig Dashboard inicial com KPIs e resumo users, posts, pending_posts, gallery_images
users.twig Tabela de usuários + modais criar/editar users, flash_users, csrf_token
posts.twig Lista, criar e editar posts (3 seções consolidadas) posts, categories, tags, post_tags_map, flash_posts
categories.twig Gerenciamento de categorias do blog categories, flash_categories
gallery.twig Upload e lista de imagens da galeria gallery_images, gallery_categories, flash_gallery
system.twig Informações do ambiente PHP e servidor session, funções PHP nativas
database.twig Schema do banco (users, posts, categories, tags, gallery) Nenhum (documentação estática)
routes.twig Listagem de todas as rotas registradas Nenhum (documentação estática)
logs.twig Debug de sessão e variáveis de servidor session, server
account.twig Perfil do usuário logado + modal editar me, flash_account
docs.twig Portal de documentação interna Nenhum (links estáticos)

🧩 Macros Twig Reutilizáveis

Três arquivos de macros foram criados para eliminar duplicação de código e padronizar componentes visuais.

tables.twig

Macro: table(id, headers, rows, classes)

Renderiza tabelas com thead/tbody padrão. Usado em users, posts e categories.

ID Nome Email
flash.twig

Macro: render(flash, attr='')

Centraliza alertas de flash messages com suporte a tipos: success, danger, warning, info.

  
badges.twig

Macro: role(role)

Renderiza badges de papéis de usuário (admin, editor, author, subscriber) com ícones e cores consistentes.

    Subscriber

⚡ Modularização JavaScript

O JavaScript do dashboard foi consolidado em um sistema de bundle que carrega todos os módulos na ordem correta, reduzindo de 9 tags <script> para apenas 2.

Arquivos Criados
config.js Define window.dashboardConfig global
bundle.js Carrega sequencialmente todos os módulos JS
Módulos Carregados (bundle, em ordem):
  1. config.js — inicialização
  2. theme.js — modo escuro/claro
  3. sidebar.js — navegação entre seções
  4. tables.js — busca e paginação
  5. modals.js — pré-preenchimento
  6. posts.js — editor Quill
  7. notifications.js — polling
  8. notifications-center.js — dropdown
  9. account.js — avatar preview
  10. main.js — orquestrador final

Módulo adicional: dogs-modals.js é carregado separadamente via defer no template do dashboard.

bundle.js

Carregador dinâmico e sequencial:

(function() {
  function withBase(path) { /* usa meta app-base-url */ }
  const modules = [
    'js/dashboard/config.js',
    'js/dashboard/theme.js',
    'js/dashboard/sidebar.js',
    'js/dashboard/tables.js',
    'js/dashboard/modals.js',
    'js/dashboard/posts.js',
    'js/dashboard/notifications.js',
    'js/dashboard/notifications-center.js',
    'js/dashboard/account.js',
    'js/dashboard/main.js'
  ];

  modules.forEach((src) => {
    const script = document.createElement('script');
    script.src = withBase(src);
    script.async = false; // Preserva ordem
    document.head.appendChild(script);
  });
})();

📊 Uso no Template

Antes (9 scripts inline):
<script>
  var dashboardConfig = { ... };
</script>
<script src="/js/dashboard/sidebar.js"></script>
<script src="/js/dashboard/tables.js"></script>
<script src="/js/dashboard/theme.js"></script>
<script src="/js/dashboard/posts.js"></script>
<script src="/js/dashboard/notifications.js"></script>
<script src="/js/dashboard/notifications-center.js"></script>
<script src="/js/dashboard/account.js"></script>
<script src="/js/dashboard/modals.js"></script>
<script src="/js/dashboard/main.js"></script>
Depois (2 scripts otimizados):
<script>
  // Injeção server-side de configuração
  window.dashboardConfig = {
    isAdmin: false,
    canManagePosts: false,
    isEditor: false,
    currentUserId: null,
    csrfToken: 'a818bc1a1b4e78dd9cabd972ee276dc13c11e463f01c0e78e4bdee6ca3f881bf'
  };
</script>
<script src="/js/dashboard/bundle.js" defer></script>
<script src="/js/dashboard/dogs-modals.js" defer></script>
✅ Benefícios da Modularização Completa:
  • Manutenibilidade: Cada partial/macro tem responsabilidade única e pode ser testado isoladamente
  • Reusabilidade: Macros de tabela, flash e badges podem ser usados em outros templates
  • Performance: Bundle.js com defer não bloqueia renderização; módulos carregam em paralelo
  • DRY: Eliminou duplicação de role badges (users/account) e flash messages (6 seções)
  • Escalabilidade: Adicionar nova seção exige apenas criar partial e incluir no dashboard.twig
  • Cache: Navegador pode cachear bundle.js e macros independentemente do template principal
📦 Estrutura Final do Projeto:
views/
├── dashboard.twig (orquestrador principal - ~323 linhas)
└── dashboard/
    ├── navbar.twig (navbar modular)
    ├── navbar/
    │   ├── breadcrumb.twig
    │   ├── theme-toggle.twig
    │   ├── notifications.twig
    │   └── user-menu.twig
    ├── sidebar.twig (sidebar modular)
    ├── sidebar/
    │   ├── header.twig
    │   ├── footer.twig
    │   ├── nav-items.twig
    │   └── items/
    │       ├── overview.twig
    │       ├── admin-section.twig
    │       ├── users-dropdown.twig
    │       ├── posts-dropdown.twig
    │       ├── dogs-dropdown.twig
    │       ├── gallery-dropdown.twig
    │       └── user-section.twig
    └── sections/
        ├── overview.twig
        ├── users.twig
        ├── posts.twig
        ├── categories.twig
        ├── gallery.twig
        ├── dogs.twig
        ├── dog-breeds.twig
        ├── dog-form.twig
        ├── system.twig
        ├── database.twig
        ├── routes.twig
        ├── logs.twig
        ├── account.twig
        ├── docs.twig
        ├── tables.twig (macro)
        ├── flash.twig (macro)
        └── badges.twig (macro)

public/js/dashboard/
├── bundle.js (loader)
├── config.js (global state)
├── theme.js
├── sidebar.js
├── tables.js
├── modals.js
├── posts.js
├── notifications.js
├── notifications-center.js
├── account.js
├── dogs-modals.js
├── navbar-height.js
└── main.js
Arquitetura 22 de novembro de 2025 Single Page Application Lite

Arquitetura SPA-Lite: Uma Rota para Todo o Dashboard

O painel administrativo do sistema (GET /dashboard) funciona como uma Single Page Application (SPA) leve: toda a interface — Gerenciar Usuários, Posts, Categorias, Galeria — vive em uma única rota. A troca entre seções é 100% client-side via JavaScript, sem recarregamento de página.

🏗️ Como Funciona

Renderização Inicial

O endpoint GET /dashboard renderiza o orquestrador views/dashboard.twig (atualmente ~323 linhas), que inclui os partials com todas as seções no mesmo HTML final:

  • <div id="overview-section" class="content-section">
  • <div id="users-section" class="content-section" style="display:none;">
  • <div id="posts-section" class="content-section" style="display:none;">
  • <div id="gallery-list-section" class="content-section" style="display:none;">
  • ...e assim por diante para todas as telas
Navegação Client-Side

O script public/js/dashboard/sidebar.js alterna seções e sincroniza hash amigável:

function showSection(sectionId, linkElement) {
  document.querySelectorAll('.content-section')
    .forEach(s => s.style.display = 'none');
  const target = document.getElementById(sectionId + '-section');
  if (target) target.style.display = 'block';

  const hashKey = sectionIdToHash(sectionId); // ex.: users -> usuarios
  const cleanUrl = window.location.pathname + window.location.search;
  const nextUrl = sectionId === 'overview' ? cleanUrl : (cleanUrl + '#' + hashKey);
  window.history.replaceState(null, '', nextUrl);
}

🔗 Fluxo Completo de Navegação

  1. Usuário acessa: GET /dashboard → servidor renderiza dashboard.twig completo (overview visível, demais seções com display:none)
  2. Clique na sidebar: Link "👥 Usuários" tem href="#users" + onclick="showSection('users', this)"
  3. JavaScript executa: showSection('users') esconde todas as divs e exibe apenas #users-section
  4. Hash muda: URL vira /dashboard#usuarios (mas sem requisição HTTP ao servidor)
  5. Estado preservado: Filtros de tabela, busca, scroll permanecem na memória enquanto você navega
  6. Ações POST: Criar/editar usuário → POST /admin/users/create → redireciona para /dashboard#users (compatível legado) → JS detecta a seção e normaliza URL para /dashboard#usuarios

⚙️ Rotas Backend Auxiliares

Tipo Rota Finalidade Redirect Padrão
GET /dashboard Renderiza template completo
POST /admin/users/create Salva novo usuário /dashboard#users (normalizado para #usuarios)
POST /admin/posts/update Atualiza post existente /dashboard#posts (normalizado para #blog-posts)
POST /admin/gallery/delete Remove imagem da galeria /dashboard#gallery-list (normalizado para #galeria-imagens)
GET /notifications API JSON para bell dropdown

Resumo: É literalmente "uma rota para tudo" do ponto de vista de navegação visual, com vários handlers POST/GET auxiliares para processar formulários e retornar para /dashboard#seção.

✅ Vantagens da Abordagem SPA-Lite

Performance e UX
  • Menos requisições HTTP: Uma vez carregado /dashboard, trocar entre Usuários → Posts → Galeria não recarrega CSS/JS
  • Transições instantâneas: JS apenas esconde/mostra divs (display:none), então a troca visual é <100ms vs. ~500ms+ de reload completo
  • Estado preservado: Filtros de tabela, campos de busca, scroll position permanecem na memória durante navegação
  • Cache browser: Scripts, imagens e estilos ficam em cache após o primeiro GET; rotas subsequentes só consomem markup mínimo
Simplicidade de Implementação
  • Zero setup SPA: Não exige Webpack, Vite, npm build ou frameworks JS complexos
  • Backend familiar: Continua sendo PHP/Twig; não precisa reescrever em React/Vue
  • Debugging fácil: View Source mostra todo o HTML; inspecionar DOM é direto
  • SEO não é problema: Dashboard é área autenticada; não precisa indexar Google

⚠️ Desvantagens e Trade-offs

Limitações Técnicas
  • HTML inicial robusto: o dashboard inclui muitas seções no primeiro render. Mesmo com o orquestrador menor (~323 linhas), o HTML final continua amplo por carregar todos os partials
  • Memória no browser: Todas as tabelas/modals ficam no DOM mesmo invisíveis. Com centenas de linhas pode pesar em dispositivos modestos
  • Deep-linking limitado: O hash (#users/#usuarios) não é rota real; se compartilhar /dashboard#posts, o backend não valida/redireciona caso sessão expire
Manutenibilidade
  • Template monolítico: Um único arquivo dificulta refatoração; precisa dividir em partials Twig (views/dashboard/sections/)
  • Testes unitários JS: Módulos estão separados mas DOM ainda é acoplado; testar showSection exige mock de elementos
  • Escalabilidade: Se crescer para 50+ seções, o modelo atual não escala sem lazy-loading

🎯 É a Forma "Correta"?

Depende do Contexto
✅ Dashboards Pequenos/Médios

5–10 seções, <50 rotas: Perfeitamente válido e comum em CMSs. WordPress admin usa abas hash, Laravel Nova tem abordagem similar. Você ganha simplicidade sem setup complexo de SPA.

⚡ Apps Grandes (Escala)

Netflix admin, Shopify: SPA real (React/Next.js, Vue/Nuxt) com code-splitting. Cada rota carrega só o JS/HTML necessário sob demanda. Escala melhor mas exige bundlers, estado global (Redux/Pinia), APIs REST/GraphQL.

🔄 Híbridos Modernos

Inertia.js, Hotwire Turbo: Entrega SSR + transições SPA sem reescrever tudo em React. É o "meio-termo profissional" atual — mantém backend PHP/Laravel com UX de SPA.

📊 Resumo da Arquitetura Atual

A abordagem é pragmática e eficiente para o escopo atual (dezenas de usuários/posts simultâneos). Não é "errado" nem "amador" — é server-rendered SPA-lite, igual a muitos painéis admin PHP/Laravel modernos.

  • Equilíbrio perfeito: Performance vs. complexidade sem over-engineering
  • Pronto para crescer: Se precisar escalar (100+ seções, milhares de registros), migre para SPA modular com lazy-loading
  • Manutenível: Com a refatoração recente (CSS/JS externos, helpers no controller), está preparado para dividir em partials

🔮 Próximas Evoluções Possíveis

Melhoria Benefício Esforço
Dividir dashboard.twig em partials Reduz complexidade do template principal Baixo
Lazy-load seções via AJAX Carrega HTML sob demanda, reduz payload inicial Médio
Migrar para Inertia.js Mantém PHP backend, ganha componentes Vue/React Médio
SPA completo (Next.js + API) Code-splitting automático, estado global robusto Alto

Leitura recomendada: Inertia.jsHotwire Turbohtmx (hypermedia-driven)

Segurança PHP 15 de novembro de 2025

Autenticação Segura com Bcrypt

Como protegemos senhas usando hashing bcrypt e validação de sessões.

Nunca armazene senhas em texto puro! No System, usamos as funções nativas do PHP para hash seguro de senhas.

Ao Registrar
$hash = password_hash(
  $_POST['password'],
  PASSWORD_DEFAULT
);
// Gera: $2y$10$abc123...
Ao Logar
$valid = password_verify(
  $_POST['password'],
  $user['password']
);
// Retorna true/false

O algoritmo bcrypt é projetado para ser computacionalmente caro, dificultando ataques de força bruta. Cada hash é único mesmo para senhas idênticas (salt aleatório).

⚠️ Importante: Nunca use md5() ou sha1() para senhas! Use sempre password_hash() e password_verify().
Banco de Dados PDO 15 de novembro de 2025

Prepared Statements e SQL Injection

Por que usamos PDO com prepared statements e como isso previne SQL injection.

SQL injection é uma das vulnerabilidades mais comuns em aplicações web. Veja a diferença:

❌ INSEGURO
$sql = "SELECT * FROM users 
  WHERE email = '{$_POST['email']}'";  
$result = $db->query($sql);

Vulnerável a: '; DROP TABLE users; --

✅ SEGURO
$stmt = $db->prepare(
  "SELECT * FROM users WHERE email = ?"
);
$stmt->execute([$_POST['email']]);

PDO escapa automático! Input malicioso é tratado como string.

Todos os métodos em app/Models/User.php usam prepared statements. Nunca concatenamos input de usuário diretamente em queries SQL.

Templates Twig 15 de novembro de 2025

Por Que Usar Twig ao Invés de PHP Puro?

Twig oferece segurança, legibilidade e separação de responsabilidades.

Recurso PHP Puro Twig
Auto-escape HTML ❌ Manual (htmlspecialchars()) ✅ Automático
Herança de Templates ⚠️ include() complexo {% extends %}
Cache de Templates ❌ Manual ✅ Integrado
Sintaxe Limpa ⚠️ {{ variavel }}

Esta página que você está lendo foi renderizada com Twig! Veja o código em views/docs-tech.twig.

🎓 Aprenda Mais: Documentação Oficial do Twig
Formulários Validação Atualizado em 19 de novembro de 2025

Formulário de Contato

Página: /contact — Formulário público com validação client/server, persistência no banco e layout consistente com outras páginas.

Estrutura Visual

  • Wrapper: .blog-page para largura consistente
  • Header: Título + subtítulo + botões de navegação (Blog, Docs)
  • Layout em 2 colunas:
    • Coluna esquerda (col-lg-7): Formulário principal
    • Coluna direita (col-lg-5): Outros canais (endereço, telefone, redes sociais, mapa)
  • Footer callout: Links para Docs e About

Campos do Formulário

Campo Tipo Validação
name text required
email email required, formato válido
subject select required, opções pré-definidas no controller
message textarea required, 5 linhas
_token hidden CSRF validation

Fluxo Backend

Rota POST em routes/web.php:
['path' => '/contact', 'method' => 'POST', 'handler' => 'HomeController@submitContact']
Controller HomeController::submitContact():
// 1. Valida CSRF token
// 2. Valida campos obrigatórios
// 3. Persiste no banco via ContactMessage::create()
// 4. Define flash message de sucesso/erro
// 5. Redireciona para /contact (PRG pattern)
Model ContactMessage:
// Tabela: contact_messages
// Campos: id, name, email, subject, message, status, created_at
// Método: create($data) insere via prepared statement

Validação Client-Side (Bootstrap)

JavaScript adiciona classe .was-validated no submit para feedback visual instantâneo:

<script>
  var forms = document.querySelectorAll('.needs-validation');
  Array.prototype.slice.call(forms).forEach(function(form) {
    form.addEventListener('submit', function(event) {
      if (!form.checkValidity()) {
        event.preventDefault();
        event.stopPropagation();
      }
      form.classList.add('was-validated');
    }, false);
  });
</script>

Flash Messages

Após o POST, mensagens são armazenadas na sessão e exibidas na próxima renderização:

{% set alert = flash_contact ?? null %}
{% if alert %}
  <div class="alert alert-{{ alert.type }}">
    {% for msg in alert.messages %}
      <li>{{ msg }}</li>
    {% endfor %}
  </div>
{% endif %}

Outros Canais

A coluna lateral exibe informações complementares de contato:

  • Endereço físico com nota sobre agendamento
  • Telefone e WhatsApp com horário de atendimento
  • E-mails diretos (suporte e comercial)
  • Redes sociais (GitHub, LinkedIn, Twitter)
  • Mapa Google Maps embarcado via iframe (ratio 16x9)

Responsividade Mobile

  • Botões de ação empilham verticalmente em telas pequenas (flex-column flex-sm-row)
  • Botões ocupam largura total em mobile (w-100 w-sm-auto)
  • Layout de 2 colunas vira 1 coluna empilhada em <992px
  • Mapa mantém proporção 16:9 via .ratio do Bootstrap
✓ Benefícios:
  • Proteção CSRF contra ataques de falsificação
  • Validação dupla (client + server) para melhor UX e segurança
  • Persistência confiável com prepared statements (SQL injection prevention)
  • Feedback imediato via flash messages
  • Layout consistente com Blog, Gallery, About
  • Múltiplos canais de contato para diferentes necessidades
UX JavaScript Atualizado em 19 de novembro de 2025

Botão "Voltar ao Topo"

Implementação: Botão flutuante que aparece após 300px de scroll, permitindo retorno suave ao topo da página.

Características

  • Aparição condicional: Visível apenas após scroll > 300px
  • Posição fixa: Canto inferior direito (bottom: 2rem, right: 2rem)
  • Design: Botão quadrado com cantos arredondados (2.5rem × 2.5rem)
  • Ícone: Bootstrap chevron-up (simples e minimalista)
  • Animação suave: Fade in/out com transition 0.3s
  • Scroll suave: behavior: 'smooth' no scrollTo
  • Acessibilidade: aria-label e title descritivos

Implementação JavaScript

Criação dinâmica do botão:
const backToTopBtn = document.createElement('button');
backToTopBtn.innerHTML = '<i class="bi bi-chevron-up"></i>';
backToTopBtn.className = 'btn btn-primary position-fixed shadow';
backToTopBtn.style.cssText = 'bottom: 2rem; right: 2rem; width: 2.5rem; height: 2.5rem; border-radius: 0.375rem;';
document.body.appendChild(backToTopBtn);
Controle de visibilidade baseado em scroll:
window.addEventListener('scroll', function() {
  if (window.pageYOffset > 300) {
    backToTopBtn.style.opacity = '1';
    backToTopBtn.style.visibility = 'visible';
  } else {
    backToTopBtn.style.opacity = '0';
    backToTopBtn.style.visibility = 'hidden';
  }
});
Scroll suave ao clicar:
backToTopBtn.addEventListener('click', function() {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  });
});

Customização CSS

Propriedade Valor Função
position fixed Mantém botão visível durante scroll
width / height 2.5rem × 2.5rem Tamanho compacto similar ao navbar-toggler
border-radius 0.375rem Cantos arredondados (padrão Bootstrap)
z-index 1000 Garante sobreposição sobre outros elementos
opacity 0 → 1 Fade in/out suave
visibility hidden → visible Remove do fluxo quando oculto
transition all 0.3s ease Animação fluida de todas as mudanças

Design Pattern

O botão segue o mesmo padrão visual do navbar-toggler do Bootstrap:

  • Forma: Quadrado com cantos levemente arredondados
  • Ícone: bi-chevron-up (simples e minimalista)
  • Cores: btn-primary com shadow padrão
  • Tamanho: Proporcional ao toggler (2.5rem vs 3rem)

Onde está implementado

  • docs-tech.twig: Única página com implementação atual
  • Razão: Conteúdo extenso (2700+ linhas) justifica a funcionalidade
  • Gatilho: 300px de scroll vertical
  • Posição: Bottom right (2rem de margem)
✓ Benefícios:
  • Visual consistente com navbar-toggler do Bootstrap
  • Melhora navegação em páginas longas
  • Reduz esforço do usuário (sem scroll manual)
  • Implementação leve (puro JavaScript vanilla)
  • Sem dependências externas
  • Responsivo e acessível
  • Animação suave compatível com todos navegadores modernos
💡 Dica: Experimente rolar esta página para baixo e veja o botão aparecer no canto inferior direito com design similar ao menu mobile!
Novo 18 de novembro de 2025

Sistema de Permissões (RBAC)

Implementação completa de Role-Based Access Control com 4 níveis hierárquicos e workflow editorial.

Níveis de Acesso

Role Badge Permissões
Subscriber Subscriber Acesso básico de visualização, sem permissões de criação
Author Author Criar posts com status draft ou pending_review, editar seus próprios posts
Editor Editor Publicar posts, gerenciar categorias, aprovar conteúdo pendente, ver todos os posts
Admin Admin Controle total: usuários, configurações do sistema, banco de dados, logs, rotas

Workflow Editorial

📝 Processo de Publicação:
  1. Authors criam posts com status draft (rascunho) ou pending_review (pendente)
  2. Editors visualizam queue de posts pendentes na seção Posts do dashboard
  3. Editors revisam e aprovam posts, alterando status para published
  4. Authors veem apenas seus próprios posts no dashboard
  5. Editors/Admins veem todos os posts do sistema

Navegação Condicional

O dashboard adapta a sidebar baseado no role do usuário:

  • Visão Geral: Todos os usuários autenticados
  • Gerenciar Usuários: Apenas Admins
  • Posts do Blog: Authors, Editors e Admins (Authors veem apenas seus posts)
  • Categorias: Apenas Editors e Admins (dentro do dropdown de Posts)
  • Sistema/Database/Logs/Rotas: Apenas Admins

Implementação Técnica

Database Schema
Tabela: users
role ENUM('subscriber', 'author', 'editor', 'admin') 
DEFAULT 'subscriber'
Tabela: posts
status ENUM('draft', 'pending_review', 'published')
DEFAULT 'draft'
Helper Methods (User Model)
// Verificar roles
User::isAdmin($user)     // Apenas admin
User::isEditor($user)    // Editor ou Admin
User::isAuthor($user)    // Author, Editor ou Admin

// Verificar permissões específicas
User::canPublish($user)  // Pode publicar diretamente
User::canEditPost($user, $post)   // Pode editar post específico
User::canDeletePost($user, $post) // Pode deletar post específico
Funções Twig
{% if isAdmin(me) %}
  <!-- Conteúdo apenas para admins -->
{% endif %}

{% if isEditor(me) %}
  <!-- Conteúdo para editors e admins -->
{% endif %}

{% if isAuthor(me) %}
  <!-- Conteúdo para authors, editors e admins -->
{% endif %}

{% if canPublish(me) %}
  <option value="published">Publicado</option>
{% else %}
  <option value="pending_review">Enviar para Revisão</option>
{% endif %}
✅ Benefícios do Sistema RBAC:
  • Separação clara de responsabilidades entre equipe
  • Workflow editorial profissional com aprovação de conteúdo
  • Segurança aprimorada com least privilege principle
  • Escalabilidade para equipes de qualquer tamanho
  • Auditoria facilitada (cada usuário vê apenas o que pode acessar)
Atualização 16 de fevereiro de 2026

Matriz de Capacidades Centralizada

O dashboard agora usa uma matriz única para controlar seções e ações permitidas por perfil, evitando divergência entre backend, Twig e JavaScript.

Fonte Única de Verdade

  • app/Controllers/AdminController.php → método resolveDashboardPermissions()
  • Retorno inclui dashboard_capabilities.sections e dashboard_capabilities.actions
  • As flags legadas (can_manage_*, can_view_*) continuam expostas para compatibilidade gradual
Shape da matriz
'dashboard_capabilities' => [
  'sections' => ['overview', 'users', 'dogs', 'account'],
  'actions' => [
    'manage_users' => false,
    'view_users_list' => true,
    'manage_dogs' => false,
    'view_dogs_list' => true,
  ],
]

Fluxo de Consumo

  1. Backend: calcula capacidades por usuário autenticado
  2. Twig: injeta matriz em window.dashboardConfig.capabilities no views/dashboard.twig
  3. Frontend: public/js/dashboard/main.js usa capabilities.sections para whitelist de seções/hash
  4. Segurança: endpoints críticos também validam permissão no controller (nunca confiar só na UI)

Padrão para Novos Módulos

Sempre aplicar em duas camadas:

  • Camada de navegação/render: sidebar, seções e botões
  • Camada de endpoint: middleware + validação no método do controller
Exemplo aplicado: módulo de cães em modo leitura para subscriber com lista visível, sem ações de gestão (editar/exportar/excluir).
Novo 18 de novembro de 2025

Workflow Editorial Completo

Entenda o fluxo de criação, revisão e publicação de posts com o sistema de aprovação hierárquico.

Conceito Fundamental:

O workflow editorial garante que Authors não possam publicar diretamente. Todo conteúdo passa por aprovação de um Editor ou Admin antes de ir ao ar, garantindo qualidade e controle editorial.

🎯 Status de Posts Explicados

Status Badge Significado Quem Vê
draft Rascunho Post em elaboração - NÃO MEXER Apenas o Author que criou
pending_review Pendente Post PRONTO aguardando aprovação Author criador + Editors/Admins
published Publicado Post aprovado e visível publicamente Todos os visitantes do site

🔄 Fluxo Completo (Passo a Passo)

Fase 1 Criação (Author)
  1. Author acessa Dashboard → Posts do Blog → Novo Post
  2. Preenche título, conteúdo, categoria, tags, etc.
  3. Define status como draft (rascunho)
  4. Clica em "Salvar Post"
Resultado: Post criado com author_id do usuário logado. Visível apenas para o Author na lista de posts.
Fase 2 Edição Livre (Author)
  1. Enquanto estiver em draft, Author pode editar livremente
  2. Fazer ajustes no texto, adicionar imagens, alterar categoria
  3. Salvar quantas vezes necessário
Proteção: Editors/Admins veem que o post existe, mas não devem editar posts em draft - significa que o Author ainda está trabalhando.
Fase 3 Submissão para Revisão (Author)
  1. Author termina o conteúdo
  2. Revisa título, ortografia, formatação
  3. Edita o post e altera status para pending_review
  4. Salva o post
Resultado: Post entra na fila de revisão. Editors/Admins veem alerta: "Posts Aguardando Revisão: X"
⚠️ Importante: Author NÃO deve editar posts em pending_review. Aguarde feedback do Editor antes de fazer alterações.
Fase 4 Revisão e Aprovação (Editor/Admin)
  1. Editor acessa Dashboard → Posts do Blog
  2. Vê alerta de posts pendentes
  3. Abre o post em pending_review
  4. Revisa conteúdo, gramática, adequação ao estilo editorial
  5. Faz ajustes se necessário (pode editar diretamente)
  6. Opção A - Aprovar: Altera status para published → Post vai ao ar
    Opção B - Recusar: Volta status para draft + envia feedback ao Author
Resultado: Se aprovado, post fica visível em /blog para todos os visitantes.
Fase 5 Pós-Publicação
  1. Post publicado pode ser editado por Editors/Admins a qualquer momento
  2. Authors podem sugerir edições criando um novo draft ou enviando feedback
  3. Editor pode despublicar voltando status para draft

📊 Resumo de Permissões por Role

Author
Pode:
  • Criar novos posts
  • Editar seus próprios posts em draft
  • Enviar posts para revisão (pending_review)
  • Ver apenas seus posts na lista
  • Categorizar e adicionar tags
NÃO Pode:
  • Publicar diretamente (published)
  • Editar posts de outros Authors
  • Ver posts de outros Authors
  • Alterar o autor de um post
Editor/Admin
Pode:
  • Ver todos os posts do sistema
  • Editar qualquer post (inclusive de outros)
  • Publicar posts diretamente (published)
  • Aprovar ou recusar posts pendentes
  • Alterar o autor de um post
  • Gerenciar categorias e tags
  • Despublicar posts
Responsabilidade:
  • Revisar fila de posts pendentes regularmente
  • Dar feedback construtivo aos Authors
  • Garantir qualidade editorial

💡 Boas Práticas Recomendadas

Para Authors:
  • ✅ Use draft enquanto estiver escrevendo e revisando
  • ✅ Só mude para pending_review quando o post estiver 100% pronto
  • ✅ Revise ortografia, gramática e formatação antes de enviar
  • ✅ Adicione categoria e tags relevantes
  • ✅ Inclua uma imagem de capa atraente
  • ❌ Não edite posts em pending_review - aguarde feedback
  • ❌ Não envie posts incompletos para revisão
Para Editors:
  • ✅ Revise a fila de posts pendentes pelo menos 1x por dia
  • ✅ Dê feedback construtivo quando recusar um post
  • ✅ Respeite o estilo do Author, faça apenas ajustes necessários
  • ✅ Verifique SEO (título, excerpt, tags) antes de publicar
  • ✅ Se recusar um post, explique o motivo ao Author
  • ❌ Não edite posts em draft sem avisar o Author
  • ❌ Não publique posts com erros ou conteúdo inadequado

🔍 Exemplo Prático Completo

Cenário: João (Author) quer publicar um artigo sobre PHP 8.3
  1. Dia 1 - Criação:
    João cria o post "Novidades do PHP 8.3", status draft.
    Escreve 500 palavras, adiciona código, salva.
  2. Dia 2 - Edição:
    João adiciona mais 1000 palavras, imagens, exemplos práticos.
    Post ainda em draft, visível só para ele.
  3. Dia 3 - Finalização:
    João revisa o texto, adiciona categoria "PHP", tags "php8, novidades".
    Altera status para pending_review, salva.
  4. Dia 3 (tarde) - Revisão:
    Maria (Editor) vê alerta "Posts Aguardando Revisão: 1".
    Abre o post de João, lê atentamente, corrige 3 erros de digitação.
  5. Dia 3 (noite) - Aprovação:
    Maria altera status para published, define data de publicação como "agora".
    Post vai ao ar em /blog/novidades-do-php-83.
  6. Resultado:
    Post publicado com sucesso! João vê seu artigo no site, recebe crédito como autor. Maria garantiu qualidade editorial antes da publicação.
✅ Por que esse workflow é importante:
  • Qualidade: Todo conteúdo passa por revisão profissional
  • Consistência: Mantém padrão editorial em todos os posts
  • Proteção: Evita publicação acidental de rascunhos incompletos
  • Responsabilidade: Clara separação entre criação e aprovação
  • Rastreabilidade: Histórico completo de quem criou e quem aprovou
Novo 18 de novembro de 2025

Sistema de Notificações em Tempo Real

Sistema completo de notificações integrado ao workflow editorial, com AJAX, auto-refresh e interface responsiva.

Funcionalidade Principal:

O sistema notifica automaticamente Editors quando Authors enviam posts para revisão e Authors quando seus posts são aprovados ou rejeitados, garantindo comunicação eficiente entre a equipe editorial.

🎯 Gatilhos de Notificação Automática

Evento Tipo Destinatário Mensagem
Author envia post para revisão post_pending Todos os Editors/Admins "[Author] enviou um post para revisão"
Editor aprova e publica post post_published Author do post "Seu post '[Título]' foi publicado!"
Editor rejeita post (volta para draft) post_returned Author do post "Seu post '[Título]' foi devolvido para revisão"
Admin cria/edita/remove usuário user_created, user_updated, user_deleted Admins "[Admin] atualizou o perfil de [Usuário]"
Fluxo de aprovação de usuário user_approved, user_pending_approval Usuário alvo + Admins "Conta aprovada" / "Cadastro aguardando aprovação"
Admin cria/edita/remove cão dog_created, dog_updated, dog_deleted Admins "[Admin] atualizou os dados de [Cão]"

⚙️ Arquitetura Backend

📁 Banco de Dados
-- Tabela notifications
CREATE TABLE IF NOT EXISTS notifications (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id INT UNSIGNED NOT NULL,
  type VARCHAR(50) NOT NULL,
  title VARCHAR(255) NOT NULL,
  message TEXT,
  link VARCHAR(255),
  is_read TINYINT(1) DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_user_read (user_id, is_read),
  INDEX idx_created (created_at DESC)
);

Índices otimizados: (user_id, is_read) para buscar não lidas rapidamente, created_at DESC para ordenação temporal eficiente.

📦 Model: Notification.php

Métodos principais:

  • create($userId, $type, $title, $message, $link) - Cria nova notificação
  • getUnreadCount($userId) - Retorna quantidade de não lidas
  • getRecent($userId, $limit) - Busca últimas N notificações
  • markAsRead($id, $userId) - Marca individual como lida
  • markAllAsRead($userId) - Marca todas como lidas
  • notifyEditors($title, $message, $link) - Notifica todos Editors/Admins
  • notifyUser($userId, $type, $title, $message, $link) - Notifica usuário específico
🛣️ Rotas API
  • GET /notifications - Retorna JSON com notificações + contador de não lidas
  • POST /notifications/mark-read/{id} - Marca notificação específica como lida
  • POST /notifications/mark-all-read - Marca todas como lidas em batch

Todas as rotas protegidas com middleware 'auth' - apenas usuários logados.

🎬 Integração com PostsAdminController

Gatilhos automáticos no método postsUpdate():

// Quando status muda de draft → pending_review
if ($oldStatus === 'draft' && $newStatus === 'pending_review') {
    Notification::notifyEditors(
        'Novo post aguardando revisão',
        "{$author['name']} enviou um post para revisão",
        '/dashboard#posts'
    );
}

// Quando post é publicado por Editor/Admin
if ($newStatus === 'published' && $oldStatus !== 'published') {
    Notification::notifyUser(
        $authorId,
        'post_published',
        'Post publicado com sucesso!',
        "Seu post '{$title}' está no ar!",
        "/blog/{$slug}"
    );
}

// Quando post é rejeitado (volta para draft)
if ($oldStatus === 'pending_review' && $newStatus === 'draft') {
    Notification::notifyUser(
        $authorId,
        'post_returned',
        'Post devolvido para revisão',
        "Seu post '{$title}' precisa de ajustes",
        '/dashboard#posts-edit-' . $postId
    );
}

Os links acima usam hashes legados internos (#posts, #posts-edit-*). No frontend atual, o dashboard aceita esses hashes e normaliza para os aliases visíveis na URL (ex.: #blog-posts).

🎨 Interface Frontend

🔔 Dropdown de Notificações

Componentes visuais:

  • Sino animado: Ícone Bootstrap bi-bell-fill com efeito hover
  • Badge dinâmico: Contador vermelho sobreposto (oculto quando 0)
  • Dropdown responsivo: 350px de largura em desktop, 100vw em mobile
  • Altura máxima: 500px com scroll automático
  • Header fixo: "Notificações" + botão "Marcar todas como lidas"
  • Lista scrollável: Container #notificationsList com itens dinâmicos

Ajustes para mobile: Dropdown ocupa largura total da tela (min-width: 100vw), alinhado à direita para facilitar acesso com polegar.

⚡ JavaScript: Gerenciamento de Notificações

Funções implementadas:

  • loadNotifications() - Carrega via AJAX (GET /notifications)
    • Chama renderNotifications() e updateBadge()
  • renderNotifications(notifications) - Renderiza lista HTML dinâmica
    • Mostra ícones coloridos por tipo (getNotificationIcon())
    • Formata timestamp relativo (formatTimeAgo())
    • Destaca não lidas com fundo claro e dot azul
    • Adiciona links clicáveis quando disponíveis
  • getNotificationIcon(type) - Retorna ícone Bootstrap específico
    • post_pendingbi-clock-history (amarelo)
    • post_publishedbi-check-circle (verde)
    • post_returnedbi-arrow-return-left (azul)
    • user_* → ícones de usuário (criação, aprovação, atualização, remoção)
    • dog_* → ícones de gestão de cães (cadastro, atualização, remoção)
  • formatTimeAgo(timestamp) - Formata em português
    • "Agora mesmo" (< 1min) → "5 min atrás" → "2h atrás" → "3d atrás"
  • updateBadge(count) - Atualiza contador dinâmico
    • Mostra número exato até 99, depois "99+"
    • Oculta badge quando count = 0
  • markNotificationRead(id) - Marca individual via POST
    • Recarrega lista após 300ms para feedback visual
  • markAllNotificationsRead() - Marca todas via POST
    • Atualiza badge e lista imediatamente
🔄 Auto-Refresh e Eventos

Carregamento inteligente:

  • Na inicialização: loadNotifications() ao carregar página
  • Ao abrir dropdown: Recarrega automaticamente (evento click)
  • Auto-refresh: Atualiza a cada 30 segundos (setInterval(loadNotifications, 30000))
  • Após marcar lida: Recarrega com delay de 300ms para animação suave

📱 Responsividade Mobile

Ajustes para telas pequenas:

@media (max-width: 576px) {
  .dropdown-menu.notification-dropdown {
    min-width: 100vw !important;
    max-width: 100vw !important;
    left: auto !important;
    right: 0 !important;
    margin-right: -1rem;
  }
  
  .dropdown-menu.notification-dropdown .notification-item {
    padding: 1rem 0.75rem !important;
  }
  
  .notification-badge {
    font-size: 0.65rem !important;
    padding: 0.2rem 0.35rem !important;
  }
}

Otimizações mobile:

  • Dropdown ocupa largura total (100vw) para facilitar leitura
  • Padding reduzido nos itens para mais conteúdo visível
  • Badge menor para não obstruir ícone do sino
  • Touch-friendly: áreas clicáveis amplas (min 44x44px)

🔍 Exemplo de Fluxo Completo

Cenário: João (Author) envia post, Maria (Editor) aprova
  1. T+0s - João envia post:
    João clica em "Salvar Post" com status pending_review.
    PostsAdminController::postsUpdate() detecta mudança de status.
    Notification::notifyEditors() cria notificações para todos Editors.
  2. T+5s - Maria recebe notificação:
    Sistema auto-refresh (30s) ou Maria clica no sino.
    AJAX busca notificações, renderiza item com ícone amarelo.
    Badge mostra "1" em vermelho.
  3. T+10s - Maria clica na notificação:
    markNotificationRead(id) envia POST.
    Notificação marcada como lida no banco.
    Maria é redirecionada para /dashboard#posts (em seguida normalizado para /dashboard#blog-posts).
  4. T+15s - Maria aprova post:
    Maria edita post, altera status para published.
    PostsAdminController::postsUpdate() detecta aprovação.
    Notification::notifyUser() cria notificação para João.
  5. T+20s - João recebe confirmação:
    João vê badge "1", abre dropdown.
    Notificação verde: "Seu post 'Título' foi publicado!"
    Clica, vai para /blog/titulo-do-post.
✅ Benefícios do Sistema de Notificações:
  • Comunicação instantânea: Equipe sempre atualizada sobre status dos posts
  • Redução de e-mails: Notificações in-app substituem alertas por e-mail
  • UX aprimorada: Feedback visual imediato com ícones e cores
  • Performance: Auto-refresh em 30s evita sobrecarga no servidor
  • Responsivo: Interface adaptada para desktop, tablet e mobile
  • Escalável: Fácil adicionar novos tipos de notificação (comentários, menções, etc.)
Próximas Melhorias Sugeridas:
  • Adicionar notificação sonora ao receber nova notificação
  • Implementar WebSockets para push real-time (sem polling)
  • Criar página dedicada "Ver todas as notificações" com paginação
  • Notificar Admins sobre novos registros de usuários
  • Permitir usuários configurarem preferências de notificação
Novo 19 de novembro de 2025

Blog com Busca Backend e API JSON

Sistema de blog com busca/ordenação server-side, layout horizontal responsivo e endpoints JSON para consumo via API.

Funcionalidade Principal:

O blog possui busca e ordenação backend (evitando manipulação client-side), layout horizontal full-width para melhor escaneabilidade, e botões JSON que expõem dados estruturados via API REST.

🔍 Busca e Ordenação Backend

📊 Parâmetros GET Suportados
Parâmetro Valores Função Exemplo
q String Busca em título, excerpt, conteúdo e categoria /blog?q=php
filter '' (padrão)
oldest
az
za
Ordenação dos posts /blog?filter=az
page Integer Paginação (10 posts/página) /blog?page=2
⚙️ Implementação em HomeController::blog()
// 1. Busca via mb_stripos (case-insensitive, multibyte-safe)
$searchTerm = $_GET['q'] ?? '';
if ($searchTerm) {
    $posts = array_filter($posts, function($post) use ($searchTerm) {
        return mb_stripos($post['title'], $searchTerm) !== false
            || mb_stripos($post['excerpt'], $searchTerm) !== false
            || mb_stripos($post['content'], $searchTerm) !== false
            || mb_stripos($post['category_name'], $searchTerm) !== false;
    });
}

// 2. Ordenação via usort
$filter = $_GET['filter'] ?? '';
switch ($filter) {
    case 'oldest':
        usort($posts, fn($a, $b) => 
            strtotime($a['published_at']) <=> strtotime($b['published_at'])
        );
        break;
    case 'az':
        usort($posts, fn($a, $b) => strcasecmp($a['title'], $b['title']));
        break;
    case 'za':
        usort($posts, fn($a, $b) => strcasecmp($b['title'], $a['title']));
        break;
    default: // 'recent'
        usort($posts, fn($a, $b) => 
            strtotime($b['published_at']) <=> strtotime($a['published_at'])
        );
}

// 3. Paginação (10 posts/página)
$perPage = 10;
$totalPosts = count($posts);
$totalPages = ceil($totalPosts / $perPage);
$currentPage = max(1, min($totalPages ?: 1, $_GET['page'] ?? 1));
$posts = array_slice($posts, ($currentPage - 1) * $perPage, $perPage);
✅ Vantagens da Busca Backend
  • SEO-friendly: URLs compartilháveis e indexáveis (/blog?q=php&filter=az)
  • Performance: Carrega apenas 10 posts por vez (não todo o dataset)
  • Segurança: Validação server-side, sem manipulação DOM no cliente
  • Compatibilidade: Funciona sem JavaScript habilitado
  • Estado preservado: Parâmetros mantidos na paginação

🎨 Layout Horizontal Full-Width

🖼️ Estrutura de Card
<article class="card sys-surface shadow-sm mb-4">
  <div class="row g-0">
    <!-- Imagem à esquerda (col-md-4) -->
    <div class="col-md-4">
      <div class="blog-card-cover-wrapper h-100">
        <img src="" class="blog-card-cover h-100 w-100">
      </div>
    </div>
    
    <!-- Conteúdo à direita (col-md-8) -->
    <div class="col-md-8">
      <div class="card-body d-flex flex-column h-100">
        <!-- Metadados: categoria, data, tempo leitura, autor -->
        <div class="d-flex flex-wrap align-items-center gap-2 mb-2">
          <a href="/blog/category/" 
             class="badge" 
             style="background-color: ">
            
          </a>
          <small>📅 09/04/2026</small>
          <small>⏱️  min</small>
          <small>👤 <a href="/blog/author/">
            
          </a></small>
        </div>
        
        <!-- Título -->
        <h3 class="h4 mb-2">
          <a href="/blog/"></a>
        </h3>
        
        <!-- Resumo truncado (200 chars) -->
        <p class="text-muted mb-3">
          
        </p>
        
        <!-- Botões (alinhados ao fim com mt-auto) -->
        <div class="mt-auto d-flex flex-wrap gap-2">
          <a href="/blog/" class="btn btn-sm btn-primary">
            📖 Ler post completo
          </a>
          <a href="/api/posts/" 
             class="btn btn-sm btn-outline-secondary" 
             target="_blank">
            {} JSON
          </a>
        </div>
      </div>
    </div>
  </div>
</article>
✅ Benefícios do Layout Horizontal
  • Escaneabilidade: Padrão F-pattern facilita leitura rápida de títulos e resumos
  • Densidade de informação: Mais metadados visíveis sem scroll
  • Responsivo: Colapsa para vertical (col-12) em mobile
  • Acessibilidade: Estrutura semântica com <article> e headings adequados
  • Consistência: Imagens sempre proporcionais (object-fit: cover)

🔗 Botão JSON e API REST

🎯 Finalidade do Endpoint JSON

Casos de uso principais:

  1. Consumo por APIs externas:
    • Aplicativos móveis podem buscar posts via GET /api/posts/{slug}
    • Integrações com plataformas de terceiros (Medium, Dev.to, etc.)
    • Agregadores de conteúdo RSS/JSON
  2. Depuração e desenvolvimento:
    • Desenvolvedores visualizam estrutura completa dos dados
    • Teste rápido de campos, tipos e relacionamentos
    • Validação de dados antes de renderizar templates
  3. Integrações headless CMS:
    • Frontend React/Vue/Angular pode consumir JSON puro
    • SSG (Static Site Generators) como Next.js, Gatsby
    • Implementação de PWA com cache offline
  4. SEO e dados estruturados:
    • Base para implementar JSON-LD (Schema.org)
    • Rich snippets para Google Search
    • Open Graph tags dinâmicas
  5. Exportação e backup:
    • Usuários avançados podem copiar/exportar dados brutos
    • Migração de conteúdo para outras plataformas
    • Arquivamento em formato estruturado
📋 Estrutura de Resposta JSON Esperada
{
  "id": 1,
  "title": "Arquitetura MVC em PHP Moderno",
  "slug": "arquitetura-mvc-php-moderno",
  "excerpt": "Entenda o padrão Model-View-Controller...",
  "content": "<p>Conteúdo HTML completo do post...</p>",
  "cover": "/uploads/posts/mvc-cover.jpg",
  "status": "published",
  "published_at": "2025-11-15T10:30:00Z",
  "created_at": "2025-11-10T14:20:00Z",
  "updated_at": "2025-11-15T10:30:00Z",
  "reading_time": 8,
  "views": 1547,
  "category": {
    "id": 3,
    "name": "PHP",
    "slug": "php",
    "color": "#777bb4",
    "description": "Artigos sobre PHP e frameworks"
  },
  "author": {
    "id": 5,
    "name": "João Silva",
    "email": "joao@example.com",
    "role": "author",
    "avatar": "/uploads/avatars/joao.jpg"
  },
  "tags": ["php", "mvc", "arquitetura", "boas-praticas"],
  "meta": {
    "seo_title": "MVC em PHP: Guia Completo",
    "seo_description": "Aprenda MVC com exemplos práticos",
    "og_image": "/uploads/posts/mvc-og.jpg"
  }
}

Nota: A estrutura exata depende da implementação do endpoint /api/posts/{slug} no HomeController ou ApiController.

🔐 Considerações de Segurança
  • CORS: Configurar headers adequados se permitir consumo cross-origin
  • Rate limiting: Limitar requisições por IP para evitar abuso
  • Filtro de status: API deve retornar apenas posts published para público
  • Sanitização: Remover dados sensíveis (IPs, drafts, emails privados)
  • Cache: Implementar cache HTTP (ETag, Last-Modified) para performance

🎯 Formulário de Busca e UX

🔍 Componentes do Formulário
<form class="row g-3 mb-4" method="get" action="/blog">
  <!-- Campo de busca (col-md-8) -->
  <div class="col-md-8">
    <label for="blogSearch" class="form-label small text-uppercase">
      Buscar posts
    </label>
    <div class="input-group">
      <span class="input-group-text">🔍</span>
      <input type="text" 
             class="form-control" 
             id="blogSearch" 
             name="q" 
             placeholder="Título, conteúdo, categoria..." 
             value="">
      
          </div>
  </div>
  
  <!-- Select de ordenação (col-md-4) -->
  <div class="col-md-4">
    <label for="blogFilter" class="form-label small text-uppercase">
      Ordenar
    </label>
    <select class="form-select" 
            id="blogFilter" 
            name="filter" 
            onchange="this.form.submit()">
      <option value="" selected>
        Mais recentes
      </option>
      <option value="oldest" >
        Mais antigos
      </option>
      <option value="az" >
        A-Z (título)
      </option>
      <option value="za" >
        Z-A (título)
      </option>
    </select>
  </div>
</form>

<!-- Alert de resultados -->
✅ Melhorias de UX Implementadas
  • Botão "Limpar": Aparece apenas quando há busca ativa, reseta filtros
  • Auto-submit: Select de ordenação submete form automaticamente (onchange)
  • Preservação de estado: Valores preenchidos mantidos após submit
  • Feedback visual: Alert mostra termo buscado e quantidade de resultados
  • Placeholder descritivo: "Título, conteúdo, categoria..." orienta usuário
  • Responsivo: col-md-8/4 em desktop, col-12 em mobile

📄 Paginação com Preservação de Parâmetros

🔗 Geração Dinâmica de Links

<!-- Exemplo de uso na paginação -->
<a class="page-link" href="/blog?page=">
  
</a>

<!-- URL gerada: /blog?page=2&q=php&filter=az -->
✅ Vantagens da Preservação
  • Experiência fluida: Usuário não perde filtros ao navegar páginas
  • Bookmarkable: URLs completas podem ser salvas/compartilhadas
  • Navegação do browser: Botões voltar/avançar funcionam corretamente
  • Analytics: Google Analytics rastreia busca + paginação juntos
✅ Benefícios Gerais do Sistema de Blog:
  • Performance: Paginação reduz carga, busca backend evita JS desnecessário
  • SEO: URLs semânticas, conteúdo renderizado server-side, meta tags adequadas
  • API-first: Botões JSON permitem headless CMS e integrações externas
  • Acessibilidade: Form funciona sem JS, estrutura semântica (article, nav)
  • Responsividade: Layout horizontal adapta para vertical em mobile
  • Manutenibilidade: Lógica de busca/ordenação centralizada no Controller
Próximas Melhorias Sugeridas:
  • Adicionar filtros por categoria e autor (checkboxes/multi-select)
  • Implementar busca full-text com MySQL MATCH...AGAINST
  • Cache de resultados de busca com Redis/Memcached
  • Sugestões de busca (autocomplete) via AJAX
  • Estatísticas de busca (termos mais buscados, zero results)
  • API GraphQL como alternativa ao REST JSON
Novo 19 de novembro de 2025

Artigo em Destaque no Blog

Sistema de destaque visual para o post mais recente, com card expandido e maior visibilidade na página inicial do blog.

Funcionalidade Principal:

O primeiro post publicado aparece em destaque na página 1 do blog (sem busca ativa), com layout expandido, imagem maior e conteúdo destacado para atrair atenção do leitor.

🎯 Lógica de Exibição

📋 Condições para Exibir Featured Post
  • Apenas na página 1: Não aparece em páginas 2, 3, etc.
  • Sem busca ativa: Se houver termo de busca, featured é ocultado
  • Post mais recente: Primeiro item do array ordenado por published_at DESC
  • Removido da lista normal: Usa array_shift() para evitar duplicação
⚙️ Implementação Backend
public function blog()
{
    $postModel = new Post($this->config);
    $allPosts = $postModel->allPublished();
    
    // Extrai post em destaque (primeiro) apenas na página 1 e sem busca
    $featuredPost = null;
    $searchTerm = trim($_GET['q'] ?? '');
    $page = max(1, (int)($_GET['page'] ?? 1));
    
    if ($page === 1 && $searchTerm === '' && !empty($allPosts)) {
        $featuredPost = array_shift($allPosts); // Remove primeiro post
    }
    
    // ... restante da lógica de busca e paginação
    
    // Paginação ajustada: 9 posts normais + 1 featured = 10 total
    $perPage = 9;
    
    $this->render('blog.twig', [
        'featured_post' => $featuredPost,
        'posts' => $posts,
        // ...
    ]);
}

🎨 Design e Layout

🖼️ Estrutura Visual do Featured Post

Componentes:

  • Badge "Artigo em Destaque": bg-warning text-dark com ícone bi-star-fill
  • Card grande: shadow-lg (vs shadow-sm dos normais)
  • Layout 50/50: col-lg-6 imagem + col-lg-6 conteúdo
  • Imagem ampliada: min-height: 400px (vs 250px dos cards normais)
  • Título maior: h2 (vs h4 nos posts normais)
  • Excerpt expandido: 300 caracteres (vs 200 dos normais)
  • Badge de categoria maior: fs-6 para destaque
  • Botões padrão: btn (vs btn-sm nos cards pequenos)
🎭 Estrutura HTML
<section class="featured-post mb-5">
  <div class="badge bg-warning text-dark mb-3">
    ⭐ Artigo em Destaque
  </div>
  
  <article class="card sys-surface shadow-lg border-0">
    <div class="row g-0">
      <!-- Imagem (col-lg-6) -->
      <div class="col-lg-6">
        <div class="featured-cover-wrapper h-100">
          <img src="" 
               class="featured-cover h-100 w-100">
        </div>
      </div>
      
      <!-- Conteúdo (col-lg-6) -->
      <div class="col-lg-6">
        <div class="card-body d-flex flex-column h-100 p-4">
          <!-- Metadados -->
          <div class="d-flex gap-2 mb-3">
            <a class="badge fs-6" 
               style="background-color: ">
              
            </a>
            <small>📅 09/04/2026</small>
            <small>⏱️  min</small>
          </div>
          
          <!-- Título H2 -->
          <h2 class="h2 mb-3">
            <a href="/blog/"></a>
          </h2>
          
          <!-- Excerpt expandido (300 chars) -->
          <p class="lead text-muted mb-4">
            ...
          </p>
          
          <!-- Autor -->
          <div class="d-flex align-items-center gap-2 mb-4">
            👤 Por <a href="/blog/author/">
              
            </a>
          </div>
          
          <!-- Botões -->
          <div class="mt-auto d-flex gap-2">
            <a href="/blog/" class="btn btn-primary">
              📖 Ler artigo completo
            </a>
            <a href="/api/posts/" 
               class="btn btn-outline-secondary">
              {} JSON
            </a>
          </div>
        </div>
      </div>
    </div>
  </article>
</section>

<hr class="my-5">

<h3 class="h5 text-muted text-uppercase fw-semibold mb-4">
  📋 Outros Artigos
</h3>

✨ Efeitos Visuais e Interatividade

🎬 Animações CSS
/* Card hover effect */
.featured-post .card {
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.featured-post .card:hover {
  transform: translateY(-5px);
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
}

/* Image zoom on hover */
.featured-cover {
  object-fit: cover;
  transition: transform 0.5s ease;
}

.featured-post .card:hover .featured-cover {
  transform: scale(1.05);
}

/* Gradient placeholder when no cover */
.featured-cover-wrapper {
  min-height: 400px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.featured-cover-wrapper.blog-card-cover-placeholder {
  background: linear-gradient(135deg, 
    rgba(var(--bs-primary-rgb), 0.1) 0%, 
    rgba(var(--bs-secondary-rgb), 0.1) 100%);
}
📱 Responsividade
  • Desktop (≥992px): Layout horizontal 50/50 (col-lg-6 + col-lg-6)
  • Tablet (768-991px): Imagem no topo, conteúdo embaixo (altura 300px)
  • Mobile (≤767px): Stack vertical completo (altura 250px), título h2 → 1.5rem
@media (max-width: 991px) {
  .featured-cover-wrapper {
    min-height: 300px;
  }
  .featured-post .card-body {
    padding: 2rem !important;
  }
}

@media (max-width: 576px) {
  .featured-cover-wrapper {
    min-height: 250px;
  }
  .featured-post h2 {
    font-size: 1.5rem;
  }
}

🌗 Suporte a Dark Theme

[data-theme='dark'] .featured-post .card {
  background: rgba(255, 255, 255, 0.05) !important;
}

[data-theme='dark'] .featured-post .card:hover {
  background: rgba(255, 255, 255, 0.08) !important;
}

O card em destaque recebe fundo semi-transparente no tema escuro, ficando mais brilhante no hover para feedback visual.

🔄 Fluxo de Usuário

Cenário: Visitante acessa o blog
  1. Landing inicial: Visitante acessa /blog (página 1, sem busca)
    Sistema exibe badge "Artigo em Destaque" + card grande do post mais recente
  2. Scroll down: Após o featured, vê separador HR + heading "Outros Artigos"
    Lista horizontal mostra 9 posts seguintes (total 10 com o featured)
  3. Busca ativa: Visitante digita termo no campo "Buscar posts"
    Featured desaparece, mostra apenas resultados filtrados (evita confusão)
  4. Ordenação diferente: Visitante seleciona "Mais antigos" ou "A-Z"
    Featured continua visível (ainda página 1), mas mostra o primeiro após ordenação
  5. Página 2+: Visitante clica em paginação
    Featured desaparece, mostra apenas os 9 posts da página atual

📊 Impacto na Paginação

Situação Posts por Página Featured Visível? Total Exibido
Página 1, sem busca 9 normais ✅ Sim 10 posts (1 featured + 9 normais)
Página 2+, sem busca 9 normais ❌ Não 9 posts
Qualquer página com busca 9 normais ❌ Não 9 posts filtrados
✅ Benefícios do Featured Post:
  • Hierarquia visual clara: Conteúdo mais importante em destaque
  • Maior CTR: Card grande com imagem atraente aumenta cliques
  • SEO interno: Posts recentes ganham visibilidade imediata
  • UX aprimorada: Visitantes identificam rapidamente o último artigo
  • Responsivo: Adapta layout para mobile sem perder impacto
  • Performance: Não afeta paginação (apenas remove 1 post da lista)
Próximas Melhorias Sugeridas:
  • Permitir admin marcar manualmente qual post deve ser featured (flag is_featured no DB)
  • Adicionar campo "featured_until" para controlar duração do destaque
  • Implementar carrossel de múltiplos featured posts (2-3 rotacionando)
  • A/B testing: medir impacto do featured vs lista uniforme
  • Meta tag Open Graph específica para featured (preview social aprimorado)