CRUD completo
Listagem + form + detail-form com validação e upload.
Exemplo ponta-a-ponta de um CRUD completo de produtos: listagem com
busca e exportação, formulário em drawer com validação e upload de imagem, e um
mestre/detalhe (variações do produto) com salvamento automático. Tudo com
MadDataGrid + MadComponent + MadForm — nenhuma
linha de SQL manual, nenhuma classe de banco fora do Eloquent.
2 models Eloquent (Produto + ProdutoVariacao) +
listagem (ProdutoList) + form em drawer com mestre/detalhe
(ProdutoForm) + 2 linhas de rota. Sem controller HTML manual —
o MadComponent já é o controller.
1. Models Eloquent
Produto é o mestre; ProdutoVariacao é o detalhe (1:N via
produto_id). Eloquent puro — só $connection/$table
de configuração e um rules() estático, a convenção que
MadForm::validate() espera.
// app/Models/Produto.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Produto extends Model
{
protected $connection = 'business';
protected $table = 'produto';
protected $fillable = [
'nome', 'cod_barras', 'valor', 'categoria_id', 'descricao', 'foto_url', 'ativo',
];
public function categoria(): BelongsTo
{
return $this->belongsTo(Categoria::class, 'categoria_id');
}
public function variacoes(): HasMany
{
return $this->hasMany(ProdutoVariacao::class, 'produto_id');
}
public static function rules($id = null): array
{
return [
'nome' => 'required|max:200',
'cod_barras' => 'max:50|unique:produto,cod_barras,' . ($id ?? 'NULL') . ',id',
'valor' => 'required|numeric|min:0',
'categoria_id' => 'required|exists:categoria,id',
];
}
}
// app/Models/ProdutoVariacao.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ProdutoVariacao extends Model
{
protected $connection = 'business';
protected $table = 'produto_variacao';
protected $fillable = ['produto_id', 'nome', 'valor_extra'];
}
2. Listagem (MadDataGrid)
A listagem é uma classe que estende Mad\Grid\MadDataGrid — ela já carrega,
pagina, ordena e exporta sozinha a partir do $model declarado.
O único método que escrevemos é a exclusão.
// app/control/Produto/ProdutoList.php
namespace App\Control\Produto;
use App\Models\Produto;
use Illuminate\Support\Facades\DB;
use Mad\Grid\MadDataGrid;
use Mad\Http\MadResponse;
use Mad\Ui\MadToast;
class ProdutoList extends MadDataGrid
{
protected static string $wrapper = self::INTERNAL;
protected string $model = Produto::class;
protected string $defaultSort = 'nome ASC';
public int $perPage = 20;
protected string $actionSide = 'left';
public function onExcluir(int $id): MadResponse
{
try {
DB::connection('business')->transaction(fn () => Produto::findOrFail($id)->delete());
return (new MadResponse())
->removeRow($id, static::class)
->toast('Produto excluído', 'success');
} catch (\Throwable $e) {
return MadToast::danger($e->getMessage());
}
}
protected function view(): string|array
{
return ['produto.produto-list', ['__component' => $this]];
}
}
<mad-page-container>
<mad-page-header title="Produtos" icon="package" breadcrumb="Cadastros > Produtos">
<actions>
<mad-btn navigate="ProdutoForm" variant="primary" icon="plus">Novo</mad-btn>
</actions>
</mad-page-header>
<mad-page-content>
<mad-grid self per-page="20" searchable :search-columns="['nome','cod_barras']" card-view>
<mad-columns>
<mad-col field="id" label="Cod." width="70" center sort />
<mad-col field="foto_url" label="Foto" card-role="image" hide />
<mad-col field="nome" label="Nome" sort filter card-role="title" />
<mad-col field="cod_barras" label="EAN" filter />
<mad-col field="categoria->nome" label="Categoria" sort />
<mad-col field="valor" label="Valor" right money="R$" total="sum" card-role="highlight" />
<mad-col field="ativo" label="Status"
badge="1:success:Ativo|0:danger:Inativo" card-role="badge" />
</mad-columns>
<mad-actions>
<mad-nav icon="pencil" label="Editar" target="ProdutoForm::onEdit({id})" />
<mad-act method="onExcluir" icon="trash-2" label="Excluir" danger
confirm="Excluir o produto?" />
</mad-actions>
</mad-grid>
</mad-page-content>
</mad-page-container>
3. Form em drawer (mestre/detalhe + upload)
O form roda como MadComponent com $wrapper = self::DRAWER — o
framework já abre/fecha o drawer sozinho, sem precisar declarar
<mad-drawer> na view. Repare que não há método nenhum para as
variações: o <mad-detail-form> declarado no Blade é carregado
no fill() e persistido (insert/update/delete) pelo mesmo
$this->form->save($produto) do mestre. Os hooks
$this->form->onLoadDetail($nome, $fn) /
onSaveDetail($nome, $fn) existem só para customizar esse caminho
automático (injetar campo extra na carga, carimbar coluna no salvamento) — sem eles
registrados, o smart-sync roda igual.
// app/control/Produto/ProdutoForm.php
namespace App\Control\Produto;
use App\Models\Produto;
use Illuminate\Support\Facades\DB;
use Mad\Component\MadComponent;
use Mad\Form\MadForm;
use Mad\Form\MadValidationException;
use Mad\Http\MadResponse;
use Mad\Ui\MadMessage;
class ProdutoForm extends MadComponent
{
protected static string $wrapper = self::DRAWER;
protected static string $title = 'Produto';
protected static string $size = '900px';
protected static string $side = 'right';
public MadForm $form;
public ?int $registroId = null;
public function mount(array $params = []): void
{
$this->form = new MadForm('form');
}
public function onEdit(int $id): void
{
// Leitura — sem transação.
$produto = Produto::findOrFail($id);
$this->registroId = (int) $produto->id;
$this->form->fill($produto);
}
public function onSave(): MadResponse
{
try {
$this->form->validate(Produto::rules($this->registroId));
DB::connection('business')->transaction(function () {
$produto = Produto::findOrNew($this->registroId);
// fill + save do mestre + smart-sync das variações (df = detail-form)
// num único passo — inclui o upload da foto declarado no Blade.
$this->form->save($produto);
$this->registroId = (int) $produto->id;
});
return (new MadResponse())
->toast('Produto salvo!', 'success')
->closeDrawer()
->manageRow($this->registroId, ProdutoList::class);
} catch (MadValidationException $e) {
return $e->asInline();
} catch (\Throwable $e) {
return MadMessage::error('Erro ao salvar', $e->getMessage());
}
}
protected function view(): string|array
{
return 'produto.produto-form';
}
}
<mad-form submit="onSave">
<mad-form-section title="Dados do produto" icon="package">
<mad-form-grid :cols="2">
<mad-input-field name="nome" label="Nome" required />
<mad-input-field name="cod_barras" label="EAN" />
</mad-form-grid>
<mad-form-grid :cols="3">
<mad-money-field name="valor" label="Valor" required />
<mad-dbcombo-field name="categoria_id" label="Categoria"
model="Categoria" display="nome" required />
<mad-switch-field name="ativo" label="Ativo" value-on="1" value-off="0" />
</mad-form-grid>
<mad-textarea-field name="descricao" label="Descrição" :rows="4" />
</mad-form-section>
<mad-separator />
<mad-form-section title="Foto" icon="image">
<mad-image-field name="foto_url" label="Foto do produto"
storage="disk" folder="uploads/produtos" name-column="foto_url"
crop aspect-ratio="4:3" />
</mad-form-section>
<mad-separator />
<mad-form-section title="Variações" icon="layers">
<mad-detail-form name="dfProdutoVariacao" model="ProdutoVariacao"
foreign-key="produto_id" mode="drawer" form-title="Variação">
<mad-detail-fields>
<mad-form-grid :cols="2">
<mad-input-field name="nome" label="Variação" required />
<mad-money-field name="valor_extra" label="Valor extra" />
</mad-form-grid>
</mad-detail-fields>
<mad-columns>
<mad-col field="nome" label="Variação" />
<mad-col field="valor_extra" label="Extra" right money="R$" />
</mad-columns>
</mad-detail-form>
</mad-form-section>
<mad-separator />
<mad-form-actions>
<mad-btn type="submit" variant="primary" icon="save">Salvar</mad-btn>
</mad-form-actions>
</mad-form>
4. Registrar as rotas
Telas do admin não têm catch-all — cada classe precisa de uma linha explícita em
routes/modules/*.php, atrás de mad.auth +
mad.permission:
// routes/modules/catalogo.php
use Illuminate\Support\Facades\Route;
use Mad\Routing\MadRoutes;
Route::middleware(['mad.auth', 'mad.permission'])->group(function () {
MadRoutes::resource('produtos', 'ProdutoList', 'ProdutoForm'); // /app/produtos
});
O que esse CRUD entrega
$model.MadValidationException, drawer reativo, toast.form->save().manageRow atualiza a linha do grid após salvar; removeRow some a linha após excluir.confirm antes de chamar onExcluir — sem confirm() nativo.