Docs›Arquitetura›Ciclo de requisição
Arquitetura

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');
// ...
Cada módulo é autocontido

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étodoRotaFunçã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']);
}
?static=1 que MUTA estado exige POST (405 no GET)

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

CamadaOndeFunçã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.

Próximos passos