Anatomia do MadComponent
Props públicas, hooks, view(), mount(), boot().
MadComponent (Mad\Component\MadComponent) é a classe base de
todo componente reativo do framework — formulários (DRAWER/MODAL), listagens
(MadDataGrid extends MadComponent), dashboards, páginas internas
(INTERNAL), e até este portal de documentação (MadSitePage extends
MadComponent). Entender a anatomia dela é entender a unidade fundamental de UI
reativa do MAD.
Estrutura mínima
namespace App\Components\Admin;
use Mad\Component\MadComponent;
use Mad\Form\MadForm;
use Mad\Http\MadResponse;
class PedidoForm extends MadComponent
{
// Como o framework embrulha o componente quando ele e' aberto
protected static string $wrapper = self::DRAWER; // INTERNAL | MODAL | DRAWER
protected static string $title = 'Pedido';
protected static string $size = 'lg'; // sm | md | lg | xl | CSS direto
protected static string $side = 'right'; // right | left (so' DRAWER)
// Props publicas = state reativo (serializado + criptografado a cada response)
public int $pedidoId = 0;
public MadForm $form;
public function mount(int $pedidoId = 0): void
{
$this->pedidoId = $pedidoId;
$this->form = new MadForm('form');
// carregado so' na 1a carga (GET) — nao roda de novo em actions AJAX
}
public function onSave(): MadResponse
{
$pedido = Pedido::updateOrCreate(['id' => $this->pedidoId], $this->form->fields);
return MadToast::success('Pedido salvo!');
}
protected function view(): string|array
{
return 'admin.pedido-form';
}
}
Quatro peças compõem qualquer MadComponent: configuração estática do
wrapper (como ele é apresentado), props públicas (o
state reativo), hooks de lifecycle (mount() e companhia) e o
método abstrato view(), que decide qual template Blade renderizar.
Wrapper — como o componente é apresentado
Quatro props estáticas controlam o "chrome" ao redor do componente — INTERNAL (sem chrome, renderiza direto no main), MODAL (centralizado) ou DRAWER (painel lateral). Cobertura completa, com tamanhos e exemplos de cada um, em Wrappers.
| Prop estática | Tipo | Default | Função |
|---|---|---|---|
$wrapper | string | self::MODAL | self::INTERNAL / self::MODAL / self::DRAWER. |
$title | string | '' | Header do modal/drawer. |
$size | string | 'lg' | sm/md/lg/xl ou CSS direto ("900px", "60vw"). |
$side | string | 'right' | Lado do drawer — right/left. Ignorado em MODAL/INTERNAL. |
Lifecycle hooks
Ordem real de execução, idêntica entre 1ª carga e ciclo AJAX exceto pelo que dispara antes da action:
Primeira carga (GET, via show()):
boot() -> mount($params)
-> [se o request trouxe `method` != 'show': chama esse metodo]
-> rendering() -> render() -> rendered() -> dehydrate()
Requisicoes AJAX (mad:click, mad:model, mad:submit -> MadComponentHandler::process()):
boot() -> hydrate() -> updating()/updated() (por prop alterada via mad:model)
-> a action chamada -> rendering() -> render() -> rendered() -> dehydrate()
public function boot(): void
{
// Roda em TODA requisicao (1a carga E cada action AJAX).
// Setup compartilhado: ler config, resolver tenant, abrir algo que toda
// execucao precisa — nunca operacao que deva rodar so' uma vez.
}
public function mount(MadRequest $req): void
{
// Roda APENAS na 1a carga (GET). NAO roda de novo em actions subsequentes.
$this->categoriaId = $req->int('categoria_id');
$this->produtos = Produto::where('categoria_id', $this->categoriaId)->get();
}
public function hydrate(): void
{
// Roda apos restaurar o estado em requisicoes AJAX (nao na 1a carga).
// Recarregue aqui o que NAO foi serializado no mad_state (relations, etc).
}
public function updating(string $prop, mixed $old, mixed $new): void
{
// ANTES de uma prop publica mudar via mad:model. Normalize/valide o valor.
}
public function updated(string $prop, mixed $value): void
{
// APOS a prop mudar. Efeitos colaterais: recalcular total, buscar dados.
if ($prop === 'quantidade' || $prop === 'valorUnitario') {
$this->total = $this->quantidade * $this->valorUnitario;
}
}
public function rendering(): void { /* antes de render() — prepara dados de ultima hora */ }
public function rendered(string $html): void { /* recebe o HTML ja' renderizado */ }
public function dehydrate(): void { /* antes de serializar o estado — limpe props pesadas */ }
public function exception(\Throwable $e, callable $stopPropagation): void
{
// Capture uma excecao da action atual. Chame $stopPropagation() para
// tratar o erro localmente em vez de deixar subir como 500/erro generico.
if ($e instanceof \App\Exceptions\EstoqueInsuficiente) {
$this->erro = $e->getMessage();
$stopPropagation();
}
}
| Hook | Quando |
|---|---|
boot() | Toda requisição (inicial e AJAX) — primeiro hook a rodar. |
mount(...) | Apenas na primeira carga. Aceita array $params, MadRequest $request ou nenhum argumento — o framework injeta o tipo certo. |
hydrate() | Após restaurar o estado em requisições AJAX (não roda na 1ª carga). |
updating() / updatingProp() | Antes de uma prop pública mudar via mad:model. |
updated() / updatedProp() | Depois que a prop mudou via mad:model. |
rendering() | Imediatamente antes de render(). |
rendered(string $html) | Imediatamente depois de render(), recebe o HTML gerado. |
dehydrate() | Antes de serializar o estado para o mad_state — último hook do ciclo. |
exception(Throwable, callable) | Captura exceção de uma action. Chame o callback para impedir que ela suba. |
Actions
Métodos públicos chamáveis via mad:click, mad:submit ou
mad:change. O framework resolve os argumentos automaticamente por Reflection —
por nome, por tipo, ou passando o array inteiro:
// Sem argumentos
public function onSave(): MadResponse { /* ... */ }
// Com argumentos — resolvidos por Reflection (nome do parametro OU tipo)
public function onEdit(int $id): void
{
$this->registroId = $id;
}
// Tipo array => recebe o payload inteiro (forma legada, ainda suportada)
public function onProcessar(array $params): void { /* ... */ }
// Type-hint MadRequest => injetado automaticamente com os dados da action
public function onFiltrar(MadRequest $req): MadResponse
{
$this->busca = $req->string('busca');
return new MadResponse();
}
// Array posicional (ex: linha de field-list) => mapeado por posicao
public function onRowEdit(array $rowData, int $editIndex): void { /* ... */ }
MadComponentHandler usa uma blacklist, não whitelist:
qualquer método público é chamável via mad_action, exceto
métodos que começam com _ e os hooks/método de render declarados na
própria base MadComponent (render, rendering,
rendered, boot, mount, hydrate,
dehydrate, updating, updated,
exception, show, fill,
forceFullRender). Isso bloqueia automaticamente qualquer método novo
que a base venha a ganhar no futuro — uma subclasse precisa sobrescrever
explicitamente para tornar algo chamável.
Uma exceção explícita: as 4 actions do
MadDbBlocksTrait (usadas por <mad-comments> e
<mad-attachments>). O trait foi içado para a própria base, e
método de trait reporta getDeclaringClass() = a classe que o usa — ou
seja MadComponent —, então a regra acima passou a barrar exatamente o
caso que o içamento veio resolver (blockAdd respondia "Método não
permitido"). Não afrouxa nada: essas actions exigem o mad_state
criptografado que o componente emitiu (model, FK e colunas viajam assinados) e
passam pelo PermissionGate como qualquer outra.
onShow() — nome reservado, não é hook de lifecycle
MadComponent declara public function onShow(): void na base como
um no-op vazio. Ele não é chamado automaticamente por
show(), mount() ou pelo ciclo AJAX — existe apenas como
ponto de entrada nomeado convencional, para que
navigate="ClienteForm::onShow({id})" e o parâmetro method do
request resolvam sempre para um método existente, em vez de estourar em classes que não o
definem.
Quem chama onShow | Como |
|---|---|
show() | Só se o request trouxer method=onShow — e sempre depois de boot()/mount(). |
navigate / click-target | "ClienteForm::onShow({id})" — MadAction tira os parênteses e monta a URL /app/{slug}/onShow; os args declarados viram query params, nunca segmento de URL. |
MadFiltersTrait | Sobrescreve onShow() para re-renderizar com os filtros aplicados (onFiltrar() só delega para ele) — é o submit dos dash-filters-*. |
MadResponse::teleport() | Aceita o método a chamar após o mount — teleport('#painel', DocDetail::class, 'onShow', ['id' => 42]). |
Carregar dados, ler o request e preencher props é trabalho de mount():
ele roda sempre na primeira carga, recebe os parâmetros já resolvidos e não depende
de o chamador ter passado method=onShow. Enquanto a subclasse não
sobrescreve onShow(), ele continua declarado em
MadComponent e o MadComponentHandler o recusa como
action AJAX pela regra da classe declarante — sobrescrever é o que o torna
chamável via mad_action.
Variáveis injetadas na view
Toda view de um MadComponent recebe automaticamente:
| Variável | Tipo | Descrição |
|---|---|---|
$that | MadComponent | A própria instância — acesse props públicas via $that->prop. |
$_component / $__component | MadComponent | Alias de $that. |
$mad | helper | Helper de contexto de render (MadRenderContext::madHelper()). |
| props públicas | — | Cada prop pública também fica disponível direto pelo nome (flattening de contexto) — MadForm é achatado em seus fields. |
<h1>{{ $that->titulo }}</h1>
<mad-input-field name="nome" :value="$that->nome" />
<mad-btn mad:click="onSalvar">Salvar</mad-btn>
Defesa contra mass-assignment em mad:model
Valores que chegam via mad:model passam por _isModelAssignable()
antes de tocar qualquer prop. Chaves começadas com _ são sempre recusadas; para
props públicas, uma blacklist fixa bloqueia nomes sensíveis mesmo que a subclasse declare
uma prop pública com esse nome — password, senha,
permissions, role, is_admin, tenant_id,
created_by/updated_by/deleted_by,
_token/csrf_token, entre outras. Props públicas já inicializadas
como objeto (ex.: um MadForm) também não podem ser sobrescritas por um
escalar vindo do cliente. Subclasses podem reforçar essa regra sobrescrevendo
_isModelAssignable().
Subclasses na base do framework
/public/_mad-wire, INTERNAL forçado.