Ciclo de requisição
Do public/index.php ao response: front controller único do Laravel, routes/web.php e MadAppController.
Este app não tem mais "vários arquivos de entrada PHP" — tem
um único front controller, o public/index.php padrão do
Laravel. Tudo que antes era roteado por arquivo (admin, AJAX, site público, API) hoje é
roteado por uma árvore de rotas declarada em routes/web.php,
com dois controllers especializados por cima dela: MadAppController
(telas do admin) e o par MadFrameworkDocs /
MadSiteWireController (este próprio portal de docs, que é uma página
pública reativa). A API REST vive num pipeline à parte — ver
Engine vs REST.
Front controller único
bootstrap/app.php usa Application::configure() (Laravel 11+) e
declara só um arquivo de rotas web, o agendador (console.php) e o
health-check (/up). Não existe index.php?class=,
engine.php, public.php nem rest.php como arquivos
físicos neste projeto — essa era a arquitetura do framework legado, substituída pelo
roteador nativo do Laravel.
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'embed/v1/*',
'agent-console/v1/*',
]);
$middleware->web(append: [
SetTenantConnection::class,
SetUserLocale::class,
LogRequest::class,
]);
})
->create();
O CSRF é unificado: o token que o mad-livewire.js envia no
header X-CSRF-TOKEN é o mesmo csrf_token() nativo do Laravel —
validado pelo ValidateCsrfToken de framework, sem reimplementação própria.
Só duas exceções ficam fora do CSRF: embed/v1/* e
agent-console/v1/* (chat embeds cross-origin autenticados por Bearer token,
não por sessão-cookie).
routes/web.php — allowlist, não catch-all
Não existe rota coringa. Toda tela do admin, toda rota pública e todo módulo de conteúdo
precisa de uma linha explícita — telas cujo controller ainda não foi portado simplesmente
respondem 404. O arquivo principal declara as rotas públicas (login, cadastro,
reset de senha, link público do GED, este portal de docs) e depois carrega os módulos de
conteúdo autenticado:
// routes/web.php — ALLOWLIST, sem catch-all: toda tela precisa de 1 linha.
// Publicas (sem login)
MadRoutes::expose('login', 'LoginForm'); // /app/login
Route::get('/docs/{section?}/{page?}',
[MadFrameworkDocs::class, 'showPage'])->name('docs.show');
Route::post('/public/_mad-wire',
[MadSiteWireController::class, 'handle'])->name('public.mad-wire');
// Pos-login (so Auth)
Route::middleware('mad.auth')->group(function () {
MadRoutes::screen('welcome', 'WelcomeView'); // /app/inicio
// ...
});
// Modulos de conteudo — cada um autocontido, declara mad.auth + mad.permission
require base_path('routes/modules/admin.php');
require base_path('routes/modules/builder.php');
require base_path('routes/modules/communication.php');
// ...
routes/modules/admin.php, builder.php,
communication.php etc. declaram seus próprios grupos de middleware
(mad.auth + mad.permission) — não dependem de ordem de
carregamento nem de um grupo global compartilhado.
Telas do admin — MadAppController
Os helpers de Mad\Routing\MadRoutes (screen(),
expose(), resource(), exposeClass(),
exposeService()) são todos açúcar sintático sobre a mesma rota: um
Route::get/match apontando para
[MadAppController::class, 'run'], com a classe do componente fixada via
->defaults('class', ...). URL amigável sempre — nunca
?class=X&method=Y cru.
// Mad\Routing\MadRoutes — todo helper termina em [MadAppController::class, 'run']
public static function screen(string $key, string $class): Route
{
return Route::get('/app/' . $slug, [MadAppController::class, 'run'])
->defaults('class', $class);
}
public static function expose(string $key, string $class): Route
{
return Route::match(['get', 'post'], '/app/' . $slug . '/{method?}',
[MadAppController::class, 'run'])->defaults('class', $class);
}
// MadServiceProvider — UM endpoint de wire compartilhado por TODO MadComponent
Route::middleware(['web', 'mad.auth'])
->post('/app/_mad-wire', [MadAppController::class, 'wire']);
Mad\Http\Controllers\MadAppController é o controller que centraliza o que no
framework legado eram duas classes separadas (o dispatcher de telas e o handler de wire) —
hoje é uma única classe com dois métodos:
| Método | Rota | Função |
|---|---|---|
run() |
qualquer rota registrada por MadRoutes |
Navegação direta (GET sem X-Mad-Partial) → casco completo via
ShellController. AJAX/fragmento → instancia a classe e chama
show($_REQUEST). ?static=1 num método estático →
chama o método direto, sem casco, para widgets JS (ex.: feed do
FullCalendar) — resposta application/json; charset=utf-8 +
Cache-Control: no-store, private. |
wire() |
POST /app/_mad-wire |
Valida CSRF (MadCsrf::validateWire()) e permissão
(PermissionGate::canAccessWire()), delega para
MadComponentHandler::process($_POST) e devolve JSON. Endpoint
único — compartilhado por todo MadComponent do admin. |
// Mad\Http\Controllers\MadAppController::run() — resumo do fluxo real
public function run(Request $request)
{
$class = (string) $request->route()->parameter('class');
$method = $request->route()->parameter('method');
if (!class_exists($class) || !is_subclass_of($class, MadComponent::class)) {
abort(404);
}
if (!PermissionGate::canAccess($class, $method)) {
return redirect(MadRoutes::loginUrl()); // ou 403
}
// ?static=1 + metodo PHP-estatico => widgets JS (ex: feed do FullCalendar).
// Metodo de INSTANCIA com static=1 NAO cai aqui: segue pro fluxo normal
// de fragmento (Mad.go sempre manda static=1 — heranca do engine legado,
// onde static=1 significava so' "sem template").
if ($method !== null && (string) $request->input('static') === '1'
&& method_exists($class, $method)
&& (new ReflectionMethod($class, $method))->isStatic()) {
// CSRF em profundidade: a rota aceita GET+POST e SameSite=lax deixa
// passar navegacao GET top-level cross-site. Metodo cujo nome bate a
// lista curada de verbos MUTANTES (save/store/create/update/delete/
// remove/destroy/persist/insert/import/apply/register/upload/submit/
// confirm/generate e os on* equivalentes) exige POST.
$mutates = preg_match($regexVerbosMutantes, $method); // lista curada — ver abaixo
if (session('logged') && $mutates && !$request->isMethod('post')) {
return new Response('405 — state-changing action requires POST', 405, [
'Content-Type' => 'text/plain; charset=utf-8',
'Allow' => 'POST',
]);
}
$_REQUEST = array_merge($request->query(), $request->post());
$_REQUEST['class'] = $class;
ob_start(); $class::$method($_REQUEST); $out = ob_get_clean();
return new Response($out, 200, [
'Content-Type' => 'application/json; charset=utf-8',
'Cache-Control' => 'no-store, private',
]);
}
// GET sem X-Mad-Partial => navegacao direta no browser = casco completo
if ($this->isDirectNavigation($request)) {
return $this->renderShell(); // iframe | layout | public | login
}
// Senao => fragmento: instancia o componente e deixa o show() decidir
$_REQUEST['class'] = $class;
ob_start();
(new $class())->show($_REQUEST);
$html = ob_get_clean();
return new Response($html, 200, ['Content-Type' => 'text/html; charset=utf-8']);
}
O branch static=1 roda fora do gate de wire() e a rota
aceita GET e POST — com SameSite=lax, uma navegação GET top-level
cross-site passaria com a sessão do usuário. Por isso run() aplica uma
defesa em profundidade: se o usuário está logado e o nome do método bate a lista
curada de verbos mutantes (save, store,
create, update, delete, remove,
destroy, persist, insert,
import, apply, register,
upload, submit, confirm,
generate — e os on* equivalentes, como
onSave/onDelete/onMove), a requisição só
passa por POST; um GET recebe 405 com
Allow: POST. Os on* read-only que os widgets buscam por
GET (onSearch, onFilter, onLoad,
onColFilter, onSort, onChange*) ficam de
fora da lista de propósito.
Este portal de docs — MadFrameworkDocs + MadSiteWireController
A página que você está lendo agora passa por um pipeline análogo, mas público (sem sessão
admin). MadFrameworkDocs estende MadSitePage, que por sua vez
estende o próprio MadComponent — ou seja, o portal de documentação
é um MadComponent reativo, só que servido por rota pública e com o
endpoint de wire apontando para /public/_mad-wire em vez de
/app/_mad-wire.
GET /docs/architecture/request-lifecycle
|
v
public/index.php -> Laravel HTTP kernel -> routes/web.php
|
v
Route::get('/docs/{section?}/{page?}', [MadFrameworkDocs::class, 'showPage'])
|
v
MadFrameworkDocs::showPage($request, $section, $page) <-- override estatico
- 404 antecipado se section/page nao existem no catalogo
- injeta Prism.js + CSS/JS proprios da doc (MadSiteAssets)
- $instance = new static(); $instance->boot();
- $instance->_resolveAndCall('mount', ['section'=>.., 'page'=>..])
- $componentHtml = $instance->_renderWrapped(); <-- e' um MadComponent!
- MadBlade::render('public.docs-fw.layout', [...])
|
v
Response HTML (200, text/html; charset=UTF-8)
--- navegacao seguinte (clique na sidebar, sem reload) -----------------------
POST /public/_mad-wire { mad_state, mad_id, mad_action: 'onNavigate', mad_model... }
|
v
MadSiteWireController::handle()
|
v
Mad\Component\MadComponentHandler::process($_POST) <-- MESMO pipeline do admin
|
v
JSON { partial: true, ops: [ {op:'html', target:'#docs-main-content', ...}, ... ] }
Note que a navegação reativa (clicar num item da sidebar) usa o
mesmo MadComponentHandler::process() do admin —
MadSiteWireController só troca o transporte HTTP (rota pública, sem
mad.auth); a lógica de decriptar estado, aplicar mad_model e
gerar ops é idêntica. Ver
MadWire por dentro.
Dentro de MadComponent::show()
Tanto MadAppController::run() quanto MadFrameworkDocs::showPage()
convergem para o mesmo lugar: o ciclo de vida do MadComponent. Na primeira
carga, a ordem é boot() → mount() →
_renderWrapped() (que chama rendering() →
view() → MadBlade::render() → rendered() →
dehydrate() → criptografa o estado → MadComponentWrapper::wrap()
aplica o chrome de MODAL/DRAWER, ou passa direto para INTERNAL). Detalhes completos da
classe e dos hooks em
Anatomia do MadComponent
e
Wrappers.
REST API — pipeline separado
Requisições para um Route::apiResource(...) nunca passam por
MadComponent, por estado criptografado ou por MadComponentWrapper
— vão direto de routes/web.php (ou api.php) para um controller
Mad\Rest\ApiResourceController, que lê/grava Eloquent e devolve
Illuminate\Http\JsonResponse puro. É um pipeline deliberadamente mais raso.
Ver Engine vs REST.
Middleware da requisição
| Camada | Onde | Função |
|---|---|---|
grupo web |
todas as rotas de routes/web.php |
StartSession (driver database), CSRF unificado,
SetTenantConnection, SetUserLocale,
LogRequest (grava mad_log_request no
terminate()). |
mad.auth |
rotas pós-login | Alias de Mad\Http\Middleware\MadAuthenticate — exige
session('logged'). Trata _mad-wire como caso
especial (resposta JSON 401, não redirect). |
mad.permission |
rotas de tela do admin | Alias de Mad\Http\Middleware\MadProgramPermission — checa
PermissionGate::canAccess($class, $method) por programa. |