Docs›Arquitetura›MadWire por dentro
Arquitetura

MadWire por dentro

state criptografado, mad_action, partial render.

Visão geral do MadWire explica o conceito e o "hello world". Esta página entra no pipeline interno de Mad\Component\MadComponentHandler::process() — o código que realmente roda a cada mad:click/mad:model/mad:submit, chamado tanto por /app/_mad-wire quanto por /public/_mad-wire.

O pipeline passo a passo

MadComponentHandler::process($_POST)

1. Decripta mad_state (Mad\Http\MadStateCrypt::decrypt) -> { class, state }
   - token invalido/corrompido -> { error: 'mad_state invalido ou expirado' }
2. Valida a classe: existe e e' subclasse de MadComponent?
3. Hidrata: new $class() -> boot() -> _setState($state) -> hydrate() -> _setId($mad_id)
   - restaura _forwardParams persistidos no state (MadAction::setForwardParams)
4. Aplica mad_model[] (se houver) -> _applyModelValues()
   - filtra por _isModelAssignable() (mass-assignment defense)
   - dispara updating()/updatingProp() ANTES, fill() no meio, updated()/updatedProp() DEPOIS
4b. Aplica mad_field_lists / mad_detail_forms (JSON) -> _setFieldListData()
5. Se veio mad_action:
   - bloqueia se _isAllowedMethod() recusar (blacklist — ver Anatomia do MadComponent)
   - caso especial: upload via action -> move $_FILES pro tmp, injeta o path como 1o param
   - chama via _resolveAndCall($action, $params)
   - excecao? -> component->exception($e, $stop) decide se trata local ou propaga
6. snapshot ANTES (passo 3) vs DEPOIS (passo 5) do estado -> algoritmo de auto-bind (abaixo)
7. Responde JSON: partial (so' ops) OU full (html + ops)

O algoritmo de auto-bind

Quando uma action retorna void (ou um valor diferente de MadResponse), o handler não sabe o que mudou — então ele compara o estado antes e depois da chamada, prop por prop, e decide sozinho como atualizar o DOM sem reenviar o HTML inteiro:

Para cada prop publica que mudou entre o snapshot ANTES e DEPOIS da action:

  valor e' OBJETO?              -> full re-render (precisaRender = true)

  valor e' MadForm serializado? -> diff granular por bucket, sem full render:
      fields    (valores)        -> op 'val'              [name="campo"]
      hidden    (visibilidade)   -> op 'mad_hide' / 'mad_show'
      readonly                   -> op 'mad_readonly'
      disabled  (botoes)         -> op 'mad_disabled'
      items     (opcoes de       -> op 'reload_combo' / 'reload_radio' /
                  combo/radio/...)  'reload_checkbox_group' / 'reload_multi_entry' /
                                     'reload_sort_list' / 'reload_checklist'
                                  (tipo desconhecido no schema -> full re-render)

  valor e' ARRAY simples?       -> registrado como autocomplete no MadVarRegistry?
                                     sim -> op 'reload_completion'
                                     nao -> full re-render

  valor e' ESCALAR?              -> op 'bind' (spans @madBind) + op 'val' se
                                     houver input registrado com esse name

O diff de um MadForm é o caso mais elaborado: ele não vira um bind genérico — é desmembrado em até cinco buckets independentes (fields, hidden, readonly, disabled, items), cada um virando o tipo de op mais barato possível. Um combo que troca de opções, por exemplo, gera reload_combo (substitui só as <option>), nunca um re-render da tela inteira.

Forma da resposta

// Partial — quando nao precisa full render e ha' pelo menos 1 op
{
  "partial": true,
  "id": "mc_abc123",
  "mad_state": "v2:NOVO_TOKEN...",
  "ops": [
    { "op": "bind", "prop": "total", "content": "159.80" },
    { "op": "val", "target": "[name=\"total\"]", "content": "159.80" },
    { "op": "toast", "message": "Salvo!", "type": "success" }
  ]
}

// Full — quando precisaRender = true (objeto mudou, array sem registro, etc)
{
  "id": "mc_abc123",
  "html": "<div mad-component=\"...\" mad-state=\"...\">...</div>",
  "ops": [ { "op": "toast", "message": "Salvo!", "type": "success" } ]
}

Ops explícitas de um MadResponse retornado pela action, ops de auto-bind, dump_modal pendente (dd()/dump() capturado durante a action) e ops de combo pendentes de form->setItems() são todas mescladas na mesma lista ops, nessa ordem — o client aplica tudo em sequência.

MadStateCrypt — o token mad_state

MadStateCrypt::encrypt(['class' => static::class, 'state' => $this->_getState()])

Formato v2 (unico aceito hoje):
  payload  = JSON { ...state, _iat: timestamp, _nonce: 8 bytes hex }
  chave    = sha256(MadFormRegistry::getSecret())          — 32 bytes
  cifra    = AES-256-GCM(payload, chave, iv=12 bytes random)
  token    = "v2:" + base64(iv || tag(16 bytes) || ciphertext)

decrypt(): valida a TAG de autenticacao GCM antes de devolver qualquer coisa.
  bit-flip / forge  -> openssl_decrypt() falha -> retorna null -> handler
                       responde 'mad_state invalido ou expirado' (re-hidrata do zero)
  maxAge opcional   -> rejeita _iat fora da janela (anti-replay)

Formato v1 (AES-256-CBC sem MAC) foi REMOVIDO — superficie de padding-oracle/bit-flip.
Token v1 antigo = invalido (mesmo tratamento de token corrompido).
Por que isso dispensa CSRF próprio (mas não o nativo)

AES-256-GCM é criptografia autenticada: o conteúdo não é só ilegível para o cliente, qualquer bit alterado quebra a tag e o decrypt falha — não há como o cliente forjar ou adulterar o estado sem a chave do servidor. Isso substitui um mecanismo próprio de proteção do estado, mas não substitui o token CSRF da rota: /app/_mad-wire e /public/_mad-wire continuam atrás do ValidateCsrfToken nativo do Laravel, validado pelo header X-CSRF-TOKEN que o mad-livewire.js envia.

Resiliência a output acidental

Antes de chamar process(), MadComponentHandler::handle() descarta qualquer buffer de output pendente e abre um buffer próprio — captura warnings, notices ou echos soltos que vazariam para o meio do JSON e o corromperiam. Se algo vazar, o handler separa <script> legítimo (executado normalmente como op script) do restante (convertido em op alert com o texto do output), em vez de simplesmente quebrar a resposta.

Próximos passos