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).
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.