Docs›Exemplos›CRUD completo
Exemplos

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.

Estrutura entregue

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

Listagem
Busca, ordenação, filtro inline, paginação, export e card-view — tudo automático a partir do $model.
Form
Validação client+server via MadValidationException, drawer reativo, toast.
Upload
Foto com crop 4:3, salva em disco e vinculada ao registro — zero código manual de arquivo.
Mestre/detalhe
Variações 1:N — auto load no edit, smart-sync (insert/update/delete) num único form->save().
Reload sem F5
manageRow atualiza a linha do grid após salvar; removeRow some a linha após excluir.
Confirmação destrutiva
Exclusão pede confirm antes de chamar onExcluir — sem confirm() nativo.

Próximos