Entenda a base.
Construa o próximo passo.
Do ambiente local ao servidor dos seus clientes. Um guia prático para desenvolver, versionar e publicar com o A7 Framework.
Exemplos baseados no código atual. Os limites de cada componente e os cuidados de produção estão indicados ao longo do guia.
Núcleo HTTP 2.0#
Erros claros, respostas consistentes e formulários protegidos com componentes próprios do A7.
Application captura exceções e avisos; Handler negocia HTML/JSON. Em APP_ENV=local, o diagnóstico mostra classe, mensagem, arquivo, linha e stack sem argumentos. Em produção, retorna página amigável e registra detalhes mascarados em storage/logs/a7.log. Personalize views/errors/404.deep.php e demais páginas sem depender de Vite.
use A7Framework\Validation\Validator;
use A7Framework\Core\Response;
$values = Validator::make($request->only(['nome', 'email']), [
'nome' => ['required', 'string', 'min:3', 'max:120'],
'email' => ['required', 'string', 'email'],
])->validate();
return Response::json(['success' => true, 'data' => $values]);Falha de validate() gera 422 com errors por campo. validate(url("contato")) habilita redirect 303 e old/errors para HTML; AJAX continua JSON 422. Campos sensíveis não são recuperados e flash dura uma requisição. Use input/all para query + corpo e body para somente corpo; o conteúdo original não é sanitizado.
use A7Framework\Security\RateLimiter;
use A7Framework\Security\SpamGuard;
RateLimiter::enforce('contato:' . $request->ip(), 5, 60);
(new SpamGuard())->check($request->all(), ['mensagem'])->enforce();<input type="hidden" name="_token" value="{{ csrf_token() }}">
{!! honeypot_fields() !!}
<input name="nome" value="{{ old('nome') }}">
@if (has_error('nome'))<p role="alert">{{ error('nome') }}</p>@endifVisão geral#
Uma base PHP própria, organizada em componentes que você pode entender e evoluir.
O A7 Framework reúne roteamento, controllers, templates DeepEngine, acesso a dados com PDO, Models e migrations. Este manual descreve a versão presente no projeto e acompanha o caminho de uma requisição até a resposta.
Navegador → public/index.php → Application → Request → Router
→ Controller → View → DeepEngine → Response → Navegador
→ Model → QueryBuilder → PDO → MySQL / MariaDBInstalação e ambiente local#
Prepare o PHP, instale as dependências e abra sua primeira página.
| Ferramenta | Uso |
|---|---|
| PHP 8.2 ou superior | Executar a aplicação e os comandos da CLI. |
| Composer | Instalar dependências PHP e gerar o autoload. |
| Node.js compatível com o Vite instalado | Gerar CSS e JavaScript; consulte engines em node_modules/vite/package.json. |
| MySQL ou MariaDB + PDO MySQL | Necessários para consultas e migrations; as páginas institucionais não exigem conexão. |
- Copie o código atualizado ou clone o repositório da sua equipe para uma pasta de trabalho.
- Abra o terminal na raiz do projeto e instale as dependências.
- O Composer cria .env sem sobrescrever configurações anteriores. Para HTTP local, ajuste APP_ENV=local, APP_URL=http://localhost:8000 e MAIL_MAILER=log.
- Inicie o PHP com public/ como raiz e execute npm run dev em outro terminal. Use npm run build para gerar a entrega de produção.
cd ./a7framework
composer install
npm ci
php -S localhost:8000 -t public
# Em outro terminal:
npm run devAbra http://localhost:8000/documentacao. No Apache, configure a raiz pública para o diretório public/ do projeto e use o endereço definido no servidor. O servidor embutido do PHP é destinado ao desenvolvimento.
Estrutura do projeto#
Encontre o lugar certo para cada mudança.
| Caminho | Responsabilidade |
|---|---|
| routes/web.php | Registrar as URLs da aplicação. |
| src/Core/ | Application, Router, Request, Response e integração das views. |
| src/Controllers/ | Coordenar os dados e a resposta de cada página. |
| src/View/ | DeepEngine e leitura do manifest do Vite. |
| src/Database/ e src/Model/ | Conexão, consultas, Model base e Models da aplicação. |
| views/ | Templates .deep.php, layouts, componentes e parciais. |
| resources/ | JavaScript, estilos e o conteúdo deste manual. |
| public/ | Única raiz pública: index.php, assets e build. |
| config/ e .env | Configurações e valores específicos do ambiente. |
| database/migrations/ | Histórico versionado da estrutura do banco. |
| docs/ | Referência técnica em Markdown. |
| a7 | Ponto de entrada da CLI: execute com php a7. |
O autoload PSR-4 mapeia A7Framework\ para src/. Por exemplo, A7Framework\Controllers\DocumentController corresponde a src/Controllers/DocumentController.php. Respeite maiúsculas e minúsculas para manter a compatibilidade com servidores Linux.
Configuração e ambiente#
Mantenha as configurações de cada instalação fora do código da aplicação.
Env carrega o .env da raiz uma vez por execução. Variáveis do processo têm prioridade sobre o arquivo. Os valores do arquivo são strings: converta booleanos e números quando precisar desses tipos.
APP_NAME="A7 Framework"
APP_ENV=local
APP_DEBUG=true
APP_LANG=pt-BR
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=a7framework
DB_USERNAME=root
DB_PASSWORD=
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci$name = config('app.name', 'A7 Framework');
$debug = config('app.debug', false);Rotas, Request e Response#
Associe caminhos a ações e controle o conteúdo da resposta HTTP.
routes/web.php retorna uma função que recebe o Router. Registre caminhos estáticos com get(). A rota /documentacao chama DocumentController::index(). HEAD utiliza a mesma ação de GET e a resposta é enviada sem corpo.
use A7Framework\Core\Router;
use A7Framework\Controllers\HomeController;
use A7Framework\Controllers\DocumentController;
return function (Router $router): void {
$router->get('/', [HomeController::class, 'index']);
$router->get('/documentacao', [DocumentController::class, 'index']);
$router->get('/sobre', [HomeController::class, 'sobre']);
};use A7Framework\Core\Request;
use A7Framework\Core\Response;
$router->get('/saudacao', function (): Response {
$request = new Request();
$name = $request->query('nome', 'Visitante');
$name = is_string($name) ? $name : 'Visitante';
return new Response(
'Olá, ' . htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
);
});| Recurso | Comportamento |
|---|---|
| Request | method(), uri(), path(), query() para GET e input()/all() para query + corpo, incluindo JSON. body() restringe ao corpo. |
| Response | Oferece html(), json() e redirect(); withHeader() retorna clone. Headers e status são validados. |
| Rota desconhecida | Retorna 404. |
| Método não permitido em rota GET | Retorna 405 com Allow: GET, HEAD. |
| POST e middleware | Use post() para escrita e passe um array de classes de middleware como terceiro argumento. A Application valida CSRF em todos os POST. |
| Limites do Router | Sem parâmetros como /users/{id}, grupos ou nomes de rotas nesta versão. |
Controllers#
Organize a lógica de cada página e envie dados para o template.
Os controllers ficam em src/Controllers e são instanciados pelo Router sem argumentos. Suas ações podem receber Request como primeiro argumento; ações antigas sem parâmetros continuam funcionando. Não há injeção automática de outras dependências.
php a7 make:controller SobreController<?php
declare(strict_types=1);
namespace A7Framework\Controllers;
use A7Framework\Core\View;
class SobreController
{
public function index(): string
{
return (new View())->render('sobre', [
'title' => 'Sobre o projeto',
'description' => 'Uma aplicação construída com A7 Framework.',
]);
}
}Registre a ação em routes/web.php após importar a classe. O gerador cria uma estrutura inicial que renderiza a view test; substitua essa referência pela view da sua página antes de acessar a nova rota.
Views e layouts#
Compartilhe a estrutura visual e mantenha o conteúdo de cada página em seu próprio arquivo.
Use A7Framework\Core\View para renderizar templates. O nome sobre aponta para views/sobre.deep.php; a notação partials.header aponta para views/partials/header.deep.php. O layout app fica em views/layouts/app.deep.php.
@extends('app')
@section('title')
{{ $title }}
@endsection
@section('content')
<section class="a7-section">
<div class="a7-container">
<h1>{{ $title }}</h1>
<p>{{ $description }}</p>
</div>
</section>
@endsectionO layout atual oferece as sections title, styles e content. Cabeçalho, rodapé, metadados e o build principal são compartilhados. Para metadados específicos, envie meta com title e description ao renderizar a view.
DeepEngine#
A sintaxe dos templates: saída de dados, composição, condições e repetição.
| Sintaxe | Uso |
|---|---|
| {{ $value }} | Exibe o valor com escape HTML. |
| {!! $html !!} | Exibe HTML sem escape, apenas para conteúdo confiável. |
| @extends('app') | Seleciona o layout. |
| @section('content') / @endsection | Define o conteúdo de uma section. |
| @yield('content') | Insere uma section no layout. |
| @include('partials.header') | Renderiza uma view parcial. |
| @if / @elseif / @else / @endif | Controla a exibição condicional. |
| @foreach / @endforeach, @for / @endfor, @while / @endwhile | Repete blocos de conteúdo. |
| @switch / @case / @break / @default / @endswitch | Seleciona um bloco conforme o valor. |
| @php / @endphp | Executa um bloco PHP no template. |
| @vite('resources/js/app.js') | Inclui o JavaScript e os estilos associados no manifest. |
@if (count($products) > 0)
<ul>
@foreach ($products as $product)
<li>{{ $product['name'] }}</li>
@endforeach
</ul>
@else
<p>Nenhum produto encontrado.</p>
@endifAssets e Vite#
Compile estilos, scripts e fontes para uma entrega reproduzível.
resources/js/app.js importa Bootstrap, Bootstrap Icons e o tema da aplicação. resources/js/document.js inclui os estilos e interações específicos desta documentação. As duas entradas são registradas em vite.config.js.
npm ci
npm run buildEm APP_ENV=local, npm run dev fornece assets e HMR; alterações em views, includes, rotas e controllers provocam full reload. Em produção, npm run build gera public/build/.vite/manifest.json e os arquivos com hash. O servidor Vite não é usado em produção. Veja UPGRADE.md para atualizar projetos existentes.
<a href="{{ url('documentacao') }}">Documentação</a>
<img src="{{ asset('imgs/icon.svg') }}" alt="A7 Framework">Banco de dados#
Uma conexão PDO compartilhada, configurada pelo ambiente.
Crie o banco no MySQL ou MariaDB e configure as variáveis DB_* antes de consultar ou migrar. Connection lê config/database.php e cria a conexão quando ela é solicitada. O PDO usa exceções, retorno associativo e prepared statements nativos.
use A7Framework\Database\DB;
$products = DB::table('products')
->select('id', 'name', 'quantity')
->orderBy('name', 'ASC')
->limit(20)
->get();A tabela products é criada por uma migration incluída no projeto. Execute as migrations no banco de desenvolvimento antes desse exemplo. Para operações que precisam de recursos do PDO, obtenha a conexão com A7Framework\Database\Connection::get().
Query Builder#
Monte consultas com valores vinculados e métodos encadeáveis.
use A7Framework\Database\DB;
$products = DB::table('products')
->where('quantity', '>', 0)
->where('name', 'LIKE', '%camiseta%')
->orderBy('id', 'DESC')
->limit(10)
->offset(0)
->get();
$product = DB::table('products')->where('id', '=', 1)->first();
$total = DB::table('products')->where('quantity', '>', 0)->count();| Método | Resultado / regra |
|---|---|
| get() / first() | Array de resultados / primeiro resultado ou null. |
| count() | Quantidade de registros que atendem ao filtro. |
| insert($data) | Insere os dados e retorna o ID. |
| update($data) / delete() | Exigem WHERE; retornam a quantidade de linhas afetadas. |
| where($column, $operator, $value) | Filtros acumulam com AND. IN e NOT IN exigem arrays não vazios. |
| limit() / offset() | Limite maior que zero; deslocamento não negativo. Use offset junto com limit. |
$affected = DB::table('products')
->where('id', '=', 1)
->update(['quantity' => 12]);Nomes de tabelas e colunas são validados como identificadores simples. Defina-os no código; use parâmetros para os valores. JOIN, OR, agrupamentos, relacionamentos e paginação pronta ainda não fazem parte desta API. Inicie um novo builder para cada consulta independente.
Models#
Represente uma tabela como objeto e controle os atributos persistidos.
<?php
declare(strict_types=1);
namespace A7Framework\Model;
use A7Framework\Database\Model;
class Product extends Model
{
protected string $table = 'products';
protected array $fillable = ['name', 'description', 'valor', 'quantity'];
}use A7Framework\Model\Product;
$product = Product::create([
'name' => 'Camiseta A7',
'description' => 'Algodão, tamanho M',
'valor' => '79.90',
'quantity' => 10,
]);
$product->quantity = 12;
$product->save();
$saved = Product::find($product->id);
$data = $saved?->toArray();- all() e where(...)->get() retornam Models hidratados; find() retorna um Model ou null.
- save() insere ou atualiza conforme o estado do objeto; refresh() recarrega o registro; delete() o exclui.
- Timestamps estão ativos por padrão e usam created_at e updated_at. Desative-os no Model se a tabela não tiver essas colunas.
- $hidden remove campos de toArray(); não altera os atributos armazenados internamente.
Schema e migrations#
Registre as mudanças de estrutura do banco junto com o código.
Cada arquivo em database/migrations retorna uma instância de Migration com up() e down(). O MigrationManager registra execuções na tabela migrations e agrupa cada execução em um batch.
php a7 make:migration create_tags_table<?php
declare(strict_types=1);
use A7Framework\Database\Schema\Blueprint;
use A7Framework\Database\Schema\Migration;
use A7Framework\Database\Schema\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tags', function (Blueprint $table): void {
$table->id();
$table->string('name', 100)->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tags');
}
};Blueprint oferece id, string, text, integer, bigInteger, boolean, dateTime e timestamps, além de nullable, unique e uniqueIndex. Schema oferece create, hasTable, drop e dropIfExists; ainda não há uma API de alteração de tabelas existentes.
php a7 migrate:status
php a7 migrateLinha de comando#
Execute tarefas do framework a partir da raiz do projeto.
php a7| Comando | Função |
|---|---|
| php a7 make:controller NomeController | Gera um controller; ajuste a view usada na ação. |
| php a7 admin:create email@dominio.com "Nome" | Cria o primeiro administrador e solicita uma senha no terminal. |
| php a7 make:migration nome_da_migration | Gera uma migration com timestamp. |
| php a7 migrate:status | Mostra migrations executadas e pendentes. |
| php a7 migrate | Aplica migrations pendentes. |
| php a7 migrate:rollback | Reverte o último batch. Pode excluir dados. |
| php a7 migrate:reset | Reverte todas as migrations registradas. Pode excluir dados. |
| php a7 migrate:refresh | Executa reset e depois migrate. Pode excluir dados. |
| php a7 new nome-do-projeto | Extrai o ZIP local ou remoto; depois execute composer install na pasta criada. |
| composer release -- 2.0.0 | Gera o ZIP sanitizado e o índice Composer em dist/composer/. |
O comando make:model ainda não está implementado. Crie os Models em src/Model seguindo o exemplo deste guia. composer release atualiza o ZIP em app/; npm run build sozinho não gera a distribuição.
Hospedagem e publicação#
Prepare o deploy de uma versão completa, com public/ como raiz web.
composer create-project --repository=https://framework.agencia7sites.com.br/app --prefer-dist --no-dev a7sites/a7framework meu-projeto 2.0.0O pacote inclui os assets compilados e não contém credenciais. O Composer instala vendor/, prepara storage/ e cria .env somente se ele não existir. Configure DB_*, APP_URL e SMTP; execute as migrations e crie o administrador pela CLI. Consulte docs/17-distribuicao.md para publicar o repositório e instalar do zero.
- Prepare uma cópia de entrega da versão revisada e instale as dependências PHP de produção.
- Gere o build do frontend nessa cópia antes do envio ao servidor.
- Configure o ambiente de destino, o banco e as credenciais fora do repositório.
- Publique o PHP, vendor/, config/, routes/, views/, resources/docs/, migrations e public/, incluindo .htaccess e public/build/.vite/manifest.json.
- Aponte o DocumentRoot para public/, configure HTTPS e confira a reescrita das URLs.
- Faça backup antes de aplicar migrations e valide as páginas, os assets e os logs após a troca de versão.
composer install --no-dev --optimize-autoloader
npm ci
npm run buildNo Apache, habilite mod_rewrite e permita as diretivas do public/.htaccess. Em hospedagem compartilhada, escolha public/ como raiz do domínio no painel; se o plano não permitir, solicite ao provedor uma configuração equivalente. Nginx e IIS precisam de regras próprias porque não interpretam .htaccess.
- Configure APP_ENV=production e APP_DEBUG=false; mantenha .env e arquivos internos fora do acesso público.
- Confirme respostas 200 para / e /documentacao, 404 para uma URL inexistente e 200 para CSS, JavaScript, fontes e imagens.
- Confira HEAD sem corpo, 405 nas rotas que não aceitam o método e 419 para POST sem token CSRF.
- Node.js não precisa estar instalado no servidor quando o build já foi gerado.
Git e trabalho em equipe#
Guarde o histórico do código e revise mudanças antes de publicar.
Se a pasta ainda não for um repositório, inicialize o Git e configure o remoto fornecido pela equipe. Se você clonou um projeto existente, comece verificando git status. Não adicione credenciais ou dados de clientes ao histórico.
git init
git statusConfira o .gitignore antes do primeiro commit. .env, vendor/ e node_modules/ devem ficar de fora; .env.example, composer.lock e package-lock.json acompanham o código. A equipe deve definir se public/build será versionado ou gerado no processo de entrega.
git switch -c docs/manual
git diff
git add views/document.deep.php resources/docs/manual.php resources/js/document.js resources/css/document.css
git diff --cached
git commit -m "Conclui o manual do framework"Adicione também quaisquer alterações relacionadas em controllers, rotas e configuração do build. Após revisar e validar, envie a branch ao remoto da equipe e abra uma solicitação de revisão. Publique somente a versão aprovada e mantenha uma referência da versão anterior para retorno.
Diagnóstico e manutenção#
Identifique as falhas mais comuns e mantenha o manual junto do código.
| Sintoma | O que conferir |
|---|---|
| Página responde 500 | Consulte storage/logs/a7.log; falhas de autoload/escrita de log têm aviso fixo no error_log do PHP. |
| Manifest ausente ou entrada não encontrada | Rode npm run build e publique toda a pasta public/build, incluindo .vite. |
| Mudanças visuais não aparecem | Em local, confira npm run dev e o cliente HMR no HTML. Em produção, recompile e publique o build. Verifique o log PHP quando a view falhar. |
| Rota retorna 404 | Confira routes/web.php, a raiz public/ e a reescrita do servidor. |
| View não encontrada | Confira o nome, o diretório views/ e as maiúsculas/minúsculas no arquivo .deep.php. |
| Falha de conexão ou tabela ausente | Confira PDO MySQL, serviço de banco, DB_* e migrations do ambiente. |
| Cópia automática indisponível | Use HTTPS ou localhost; selecione o exemplo e copie manualmente quando o navegador negar a área de transferência. |
Atualizar este guia
Edite os capítulos em resources/docs/manual.php. Cada seção possui id, group, title, summary e blocks. O controller monta o índice automaticamente. Preserve IDs já publicados para manter os links dos capítulos funcionando.
- Conteúdo: resources/docs/manual.php. Apresentação: views/document.deep.php.
- Interações: resources/js/document.js. Estilos: resources/css/document.css.
- Após alterações, confira /documentacao, busca, links, cópia, impressão e a leitura em telas pequenas.
- A pasta tests/ ainda não tem uma suíte permanente. Valide separadamente os fluxos de banco e de cada aplicação antes da entrega.
Autenticação e sessões#
Acesso restrito com senha, recuperação por e-mail e sessões com expiração.
Configure DB_* no .env, crie o banco e execute as migrations. A tabela users recebe nome, e-mail, hash de senha, perfil, status e versão da autenticação. Se já houver uma tabela users própria, adapte a migration antes de executá-la.
php a7 migrate
php a7 admin:create admin@seu-dominio.com "Nome do administrador"A CLI solicita uma senha com pelo menos 15 caracteres. Não há senha padrão nem cadastro público. Abra /login e entre no painel /admin. O perfil admin dá acesso ao painel; member não tem permissão administrativa.
- Sessões usam cookie HttpOnly e SameSite=Lax, modo estrito, expiração por inatividade e duração máxima.
- SESSION_LIFETIME define a inatividade em minutos; SESSION_ABSOLUTE_LIFETIME define a duração máxima.
- Login renova sessão e CSRF. Logout, expiração e mudanças de acesso invalidam a autenticação.
- APP_ENV=production ativa cookies Secure por padrão. Use HTTPS no ambiente publicado.
- A recuperação usa um token de uso único que expira em 30 minutos. O banco armazena somente seu hash.
Validação e proteção CSRF#
Valide os dados no servidor e vincule cada envio à sessão do visitante.
<form method="post" action="{{ url('minha-rota') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<label for="email">E-mail</label>
<input id="email" name="email" type="email" required>
<button type="submit">Enviar</button>
</form>use A7Framework\Validation\Validator;
$validator = new Validator($request->input(), [
'name' => 'required|max:120',
'email' => 'required|email|max:190',
], ['name' => 'nome', 'email' => 'e-mail']);
if ($validator->fails()) {
$errors = $validator->errors();
} else {
$values = $validator->validated();
}| Status | Significado |
|---|---|
| 303 | Sucesso seguido de redirecionamento para uma página GET. |
| 403 | Conta sem permissão para acessar o painel. |
| 419 | Token CSRF ausente ou inválido; recarregue a página. |
| 422 | Dados inválidos, com mensagens de validação. |
| 429 | Limite de tentativas atingido; respeite Retry-After. |
Validator oferece required, string, email, min, max, maxbytes, confirmed, in, slug e accepted. A Application aplica VerifyCsrfToken globalmente. RequireAdmin é informado no terceiro argumento de get() ou post() para proteger uma rota administrativa.
E-mail e SMTP#
Configure recuperação de senha, notificações e mensagens da aplicação.
O envio usa PHPMailer, instalado pelo Composer. Informe no .env os dados reais do seu provedor de e-mail. As credenciais não são exibidas no painel nem devem ser adicionadas ao repositório.
APP_URL=https://seu-dominio.com
MAIL_MAILER=smtp
MAIL_HOST=servidor-smtp-do-provedor
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=usuario-smtp
MAIL_PASSWORD="senha-do-provedor"
MAIL_FROM_ADDRESS=noreply@seu-dominio.com
MAIL_FROM_NAME="Nome do projeto"
MAIL_ADMIN_ADDRESS=contato@seu-dominio.comUse tls para STARTTLS ou ssl para TLS implícito, conforme o provedor. A verificação de certificado permanece ativa. Em /admin/email, envie um teste para o e-mail da sua própria conta e confira a caixa de entrada e o spam.
(new A7Framework\Mail\Mailer())->send(
'destinatario@seu-dominio.com',
'Assunto da mensagem',
'Conteúdo em texto simples.'
);Painel administrativo#
Gerencie acessos, formulários, respostas e a configuração do envio.
| Caminho | Função |
|---|---|
| /admin | Resumo dos usuários, formulários e respostas. |
| /admin/usuarios | Criar e editar usuários, perfis e status. |
| /admin/formularios | Criar, editar, publicar e desativar formulários. |
| /admin/respostas | Consultar respostas, marcar como lidas e excluir. |
| /admin/email | Conferir configuração e testar o envio de e-mail. |
Ao alterar acessos, confirme sua senha atual. O usuário alterado precisa entrar novamente; não é permitido retirar seu próprio acesso administrativo nem desativar o último administrador ativo. As listagens mostram 20 registros por página.
O visual usa o tema roxo do projeto. O layout fica em views/layouts/admin.deep.php, as telas em views/admin/ e as interações em resources/js/admin.js. Recompile o Vite ao alterar os estilos ou scripts.
Criar formulários e receber respostas#
Monte formulários pelo painel e compartilhe um link público.
- Entre em Formulários e escolha Novo formulário.
- Informe título, endereço curto, descrição e status de publicação.
- Adicione até 12 campos: texto, e-mail, texto longo, lista de opções ou confirmação.
- Defina títulos, identificadores únicos, opções e campos obrigatórios.
- Salve e compartilhe /formularios?slug=seu-endereco.
- Acompanhe os envios em Respostas. Configure MAIL_ADMIN_ADDRESS para receber notificações.
A validação ocorre no servidor. Cada envio usa CSRF, limite por IP e um campo antispam. As respostas são armazenadas antes da notificação, para que uma falha de e-mail não perca o contato. O e-mail inclui o link do painel, sem copiar os dados enviados.
Consulte também docs/16-autenticacao-painel.md para instalação, variáveis de ambiente e conferências antes da publicação.