Skip to content

Arquitetura de Formulários

Resumo Inicial

Essa é a maneira padrão de organizar o funcionamento de formulários na nova Tech.


Visão Geral

Esta arquitetura segue o princípio de Separação de Responsabilidades (SoC), onde cada camada tem uma função específica e bem definida:

  • Formulário Livewire: Responsáveis apenas pela validação de dados e interação com o usuário
  • DTOs (Data Transfer Objects): Estruturas de dados tipadas para transferência de informações entre camadas
  • Actions: Responsáveis pela lógica de negócio e persistência de dados no banco de dados
┌─────────────────────┐
│ Formulário Livewire │ ← Valida dados do formulário
└──────────┬──────────┘
           │ cria
┌─────────────────────┐
│        DTO          │ ← Transfere dados tipados
└──────────┬──────────┘
           │ passa para
┌─────────────────────┐
│       Action        │ ← Persiste no banco de dados
└─────────────────────┘

Camadas da Arquitetura

1. Livewire Component (Camada de Apresentação)

Responsabilidades:

  • Receber entrada do usuário
  • Validar dados do formulário
  • Criar instância do DTO
  • Chamar a Action apropriada
  • Exibir mensagens de sucesso/erro
  • Redirecionar o usuário

NÃO deve:

  • ❌ Manipular diretamente o banco de dados
  • ❌ Conter lógica de negócio complexa
  • ❌ Fazer cálculos ou transformações de dados além da validação

2. DTO (Camada de Transferência)

Responsabilidades:

  • Encapsular dados em uma estrutura tipada
  • Garantir type safety entre camadas
    • Dados que podem possuir formatos variados devem utilizar uma Interface única.
    • Ex: datas devem ser tipadas como Carbon ao invés de apenas string
  • Facilitar refatoração e manutenção
  • Documentar a estrutura de dados esperada

Características:

  • Classes simples com propriedades públicas
  • Tipagem forte (PHP 8.x)
  • Sem lógica de negócio
  • Imutáveis após criação (quando possível)

3. Action (Camada de Negócio)

Responsabilidades:

  • Executar lógica de negócio
  • Persistir dados no banco de dados
  • Gerenciar transações
  • Lançar eventos quando necessário
  • Retornar resultado padronizado

Características:

  • Uma classe por operação (CreateX, UpdateX, DeleteX)
  • Método execute() como ponto de entrada
  • Uso de transactions quando necessário
  • Retorna array com success e dados relevantes

Fluxo de Dados

<?php
// 1. Usuário preenche formulário
// 2. Livewire valida os dados
public function save()
{
    $dados = $this->validate([
        'campo1' => 'required|string',
        'campo2' => 'required|integer',
    ]);

    // 3. Cria DTO com dados validados
    $dto = new CreateExemploDTO(
        campo1: $dados['campo1'],
        campo2: $dados['campo2'],
        created_by: auth()->id(),
    );

    // 4. Chama Action passando o DTO
    $action = new CreateExemplo();
    $resultado = $action->execute($dto);

    // 5. Trata o resultado
    if (!$resultado['success']) {
        $this->notifyError('Erro ao salvar!');
        return;
    }

    // 6. Sucesso - redireciona
    $this->notifySuccess('Salvo com sucesso!');
    return redirect()->route('exemplo.index');
}

Componentes da Arquitetura

DTO (Data Transfer Object)

Localização: app/DTOs/

Estrutura básica:

<?php

namespace App\DTOs;

use App\Traits\DTOHelper;
use Carbon\Carbon;

class CreateExemploDTO
{
    use DTOHelper;

    public function __construct(
        public string $nome,
        public int $categoria_id,
        public Carbon $data,
        public string|null $observacoes = null,
        public int|null $created_by = null,
    ) {}
}

Action

Localização: app/Actions/

Estrutura básica:

<?php

namespace App\Actions\Exemplo;

use App\DTOs\CreateExemploDTO;
use App\Models\Exemplo;
use Illuminate\Support\Facades\DB;

class CreateExemplo
{
    public function execute(CreateExemploDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $exemplo = Exemplo::query()->create([
                    'nome' => $dto->nome,
                    'categoria_id' => $dto->categoria_id,
                    'data' => $dto->data,
                    'observacoes' => $dto->observacoes,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                // Lógica adicional (ex: relacionamentos, históricos, etc)
                // ...

                return [
                    'success' => true,
                    'exemplo' => $exemplo,
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

Livewire Component

Localização: app/Livewire/

Estrutura básica:

<?php

namespace App\Livewire\Exemplo;

use App\Actions\Exemplo\CreateExemplo;
use App\DTOs\CreateExemploDTO;
use App\Models\Exemplo;
use Carbon\Carbon;
use Livewire\Component;
use W2oUtils\W2oUtils\Traits\HasNotifications;

class Create extends Component
{
    use HasNotifications;

    // Propriedades do formulário
    public ?string $nome = null;
    public ?int $categoria_id = null;
    public ?string $data = null;
    public ?string $observacoes = null;

    // Dados auxiliares
    public array $categorias = [];
    public array $breadcrumb = [];

    public function mount(): void
    {
        $this->data = Carbon::now()->format('d/m/Y');

        $this->breadcrumb = [
            __('exemplo.breadcrumbs.index') => route('exemplo.index'),
            __('exemplo.breadcrumbs.create') => null,
        ];
    }

    public function save()
    {
        // Verifica permissão
        if (auth()->user()->cant('criar', Exemplo::class)) {
            abort(403);
        }

        // Valida os dados
        $dados = $this->validate([
            'nome' => 'required|string|max:255',
            'categoria_id' => 'required|integer|exists:categorias,id',
            'data' => 'required|date_format:d/m/Y',
            'observacoes' => 'nullable|string',
        ]);

        // Cria o DTO
        $dto = new CreateExemploDTO(
            nome: $dados['nome'],
            categoria_id: $dados['categoria_id'],
            data: Carbon::createFromFormat('d/m/Y', $dados['data']),
            observacoes: $dados['observacoes'] ?? null,
            created_by: auth()->id(),
        );

        // Executa a Action
        $action = new CreateExemplo();
        $resultado = $action->execute($dto);

        // Trata o resultado
        if (!$resultado['success']) {
            $this->notifyError(__('exemplo.mensagens.erro_criar'));
            return;
        }

        // Sucesso
        $this->notifySuccess(__('exemplo.mensagens.sucesso_criar'));
        return redirect()->route('exemplo.edit', ['exemplo' => $resultado['exemplo']->id]);
    }

    public function render()
    {
        return view('livewire.exemplo.create');
    }
}

Implementação Passo a Passo

Passo 1: Criar o DTO

# Use o comando artisan dentro do container Docker
php artisan make:dto CreateExemploDTO"

Edite o arquivo gerado em app/DTOs/CreateExemploDTO.php:

<?php

namespace App\DTOs;

use App\Traits\DTOHelper;

class CreateExemploDTO
{
    use DTOHelper;

    public function __construct(
        public string $nome,
        public int $categoria_id,
        public ?string $descricao = null,
        public ?int $created_by = null,
    ) {}
}

Passo 2: Criar a Action

# Use o comando artisan dentro do container Docker
php artisan make:action Exemplo/CreateExemplo"

Edite o arquivo gerado em app/Actions/Exemplo/CreateExemplo.php:

<?php

namespace App\Actions\Exemplo;

use App\DTOs\CreateExemploDTO;
use App\Models\Exemplo;
use Illuminate\Support\Facades\DB;

class CreateExemplo
{
    public function execute(CreateExemploDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $exemplo = Exemplo::query()->create([
                    'nome' => $dto->nome,
                    'categoria_id' => $dto->categoria_id,
                    'descricao' => $dto->descricao,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                return [
                    'success' => true,
                    'exemplo' => $exemplo,
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

Passo 3: Criar o Livewire Component

# Use o comando artisan dentro do container Docker
php artisan make:livewire Exemplo/Create"

Implemente o método save() conforme o exemplo mostrado anteriormente.


Boas Práticas

1. Nomenclatura Consistente

1
2
3
4
5
✅ CreateExemploDTO        (DTO de criação)
✅ EditExemploDTO          (DTO de edição)
✅ CreateExemplo           (Action de criação)
✅ UpdateExemplo           (Action de atualização)
✅ DeleteExemplo           (Action de exclusão)

2. Validação no Livewire

  • ✅ Use as regras definidas no Model quando possível
  • ✅ Combine regras do Model com validações específicas da view
1
2
3
4
5
6
<?php

$dados = $this->validate([
    ...Exemplo::createRules(),
    'campo_extra' => 'required|string',
]);

3. DTOs Específicos para Create e Edit

Crie DTOs separados para criação e edição, mesmo que sejam similares:

<?php

// CreateExemploDTO.php
public function __construct(
    public string $nome,
    public int $categoria_id,
    public ?int $created_by = null,
) {}

// EditExemploDTO.php
public function __construct(
    public int $id,
    public string $nome,
    public int $categoria_id,
    public ?int $updated_by = null,
) {}

4. Retorno Padronizado das Actions

Sempre retorne um array com a estrutura:

<?php

// Sucesso
return [
    'success' => true,
    'modelo' => $modelo,
    // outros dados relevantes
];

// Erro
return [
    'success' => false,
    'exception' => $e,
    'message' => 'Mensagem opcional',
];

5. Uso de Transactions

Sempre use DB::transaction() quando houver múltiplas operações:

<?php

return DB::transaction(function () use ($dto) {
    // Operação principal
    $exemplo = Exemplo::create([...]);

    // Operações relacionadas
    $exemplo->tags()->attach($dto->tag_ids);

    // Eventos ou jobs
    event(new ExemploCreated($exemplo));

    return ['success' => true, 'exemplo' => $exemplo];
});

6. Tratamento de Erros nas actions

<?php

catch (\Exception $e) {
    report($e);

    return [
        'success' => false,
        'exception' => $e,
    ];
}

7. Type Hints e Type Safety

Use type hints rigorosamente:

<?php

// DTO
public function __construct(
    public string $nome,              // Sempre string
    public int $categoria_id,         // Sempre int
    public Carbon $data,              // Sempre Carbon
    public ?array $tags = null,       // Nullable array
) {}

// Action
/**
 * @returns array{'sucesso': boolean}
 */
public function execute(CreateExemploDTO $dto): array
{
    // Sempre retorna array
}

8. Separação de Lógica Complexa

Se a Action ficar muito complexa, extraia lógica para Services:

<?php

class CreateExemplo
{
    public function __construct(
        private ExemploService $exemploService
    ) {}

    public function execute(CreateExemploDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            $exemplo = Exemplo::create([...]);

            // Usa service para lógica complexa
            $this->exemploService->processarRelacionamentos($exemplo, $dto);

            return ['success' => true, 'exemplo' => $exemplo];
        });
    }
}

Exemplos Práticos

Exemplo 1: Create Simples

DTO:

<?php

class CreateProdutoDTO
{
    use DTOHelper;

    public function __construct(
        public string $nome,
        public float $preco,
        public int $categoria_id,
        public ?int $created_by = null,
    ) {}
}

Action:

<?php

class CreateProduto
{
    public function execute(CreateProdutoDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $produto = Produto::query()->create([
                    'nome' => $dto->nome,
                    'preco' => $dto->preco,
                    'categoria_id' => $dto->categoria_id,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                return [
                    'success' => true,
                    'produto' => $produto,
                ];
            } catch (\Exception $e) {
                report($e);

                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

Livewire:

<?php

public function save()
{
    $dados = $this->validate([
        'nome' => 'required|string|max:255',
        'preco' => 'required|numeric|min:0',
        'categoria_id' => 'required|exists:categorias,id',
    ]);

    $dto = new CreateProdutoDTO(
        nome: $dados['nome'],
        preco: $dados['preco'],
        categoria_id: $dados['categoria_id'],
        created_by: auth()->id(),
    );

    $resultado = (new CreateProduto())->execute($dto);

    if (!$resultado['success']) {
        $this->notifyError('Erro ao criar produto!');
        return;
    }

    $this->notifySuccess('Produto criado com sucesso!');
    return redirect()->route('produto.index');
}

Exemplo 2: Create com Relacionamentos

DTO:

<?php

class CreateLancamentoDTO
{
    use DTOHelper;

    public function __construct(
        public Carbon|string $data_hora,
        public int $setor_id,
        public ?int $tema_id = null,
        public ?string $observacoes = null,
        public ?int $created_by = null,
    ) {}
}

Action:

<?php

class CreateLancamento
{
    public function execute(
        CreateLancamentoDTO $dto,
        array $participantes = [],
        array $presencas = []
    ): array {
        return DB::transaction(function () use ($dto, $participantes, $presencas) {
            try {
                // Cria o lançamento
                $lancamento = Lancamento::query()->create([
                    'data_hora' => $dto->data_hora,
                    'setor_id' => $dto->setor_id,
                    'tema_id' => $dto->tema_id,
                    'observacoes' => $dto->observacoes,
                    'created_by' => $dto->created_by ?? auth()->id(),
                ]);

                // Salva traduções se necessário
                $lancamento->saveTranslation(config('app.default_locale'), [
                    'observacoes' => $dto->observacoes,
                ]);

                // Cria registros de presença
                foreach ($participantes as $participante) {
                    $usuarioId = $participante['usuario_id'];
                    LancamentoPresenca::create([
                        'lancamento_id' => $lancamento->id,
                        'usuario_id' => $usuarioId,
                        'presente' => $presencas[$usuarioId] ?? false,
                    ]);
                }

                return [
                    'success' => true,
                    'lancamento' => $lancamento,
                ];
            } catch (\Exception $e) {
                report($e);

                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

Livewire:

<?php

public function save()
{
    $dados = $this->validate([
        ...Lancamento::createRules(),
    ]);

    $dataHora = Carbon::createFromFormat('d/m/Y H:i', $this->data_dds . ' ' . $this->horario_dds);

    $dto = new CreateLancamentoDTO(
        data_hora: $dataHora,
        setor_id: $dados['setor_id'],
        tema_id: $this->tema_id,
        observacoes: $this->observacoes,
        created_by: auth()->id(),
    );

    $action = new CreateLancamento();
    $resultado = $action->execute($dto, $this->participantes, $this->presencas);

    if (!$resultado['success']) {
        $this->notifyError('Erro ao criar lançamento!');
        return;
    }

    $this->notifySuccess('Lançamento criado com sucesso!');
    return redirect()->route('lancamento.edit', ['lancamento' => $resultado['lancamento']->id]);
}

Exemplo 3: Update/Edit

DTO:

<?php

class EditProdutoDTO
{
    use DTOHelper;

    public function __construct(
        public int $id,
        public string $nome,
        public float $preco,
        public int $categoria_id,
        public ?int $updated_by = null,
    ) {}
}

Action:

<?php

class UpdateProduto
{
    public function execute(EditProdutoDTO $dto): array
    {
        return DB::transaction(function () use ($dto) {
            try {
                $produto = Produto::query()->findOrFail($dto->id);

                $produto->update([
                    'nome' => $dto->nome,
                    'preco' => $dto->preco,
                    'categoria_id' => $dto->categoria_id,
                    'updated_by' => $dto->updated_by ?? auth()->id(),
                ]);

                return [
                    'success' => true,
                    'produto' => $produto->fresh(),
                ];
            } catch (\Exception $e) {
                return [
                    'success' => false,
                    'exception' => $e,
                ];
            }
        });
    }
}

Livewire:

<?php

public function update()
{
    $dados = $this->validate([
        'nome' => 'required|string|max:255',
        'preco' => 'required|numeric|min:0',
        'categoria_id' => 'required|exists:categorias,id',
    ]);

    $dto = new EditProdutoDTO(
        id: $this->produto->id,
        nome: $dados['nome'],
        preco: $dados['preco'],
        categoria_id: $dados['categoria_id'],
        updated_by: auth()->id(),
    );

    $resultado = (new UpdateProduto())->execute($dto);

    if (!$resultado['success']) {
        $this->notifyError('Erro ao atualizar produto!');
        return;
    }

    $this->notifySuccess('Produto atualizado com sucesso!');
    return redirect()->route('produto.index');
}


Vantagens da Arquitetura

1. Separação de Responsabilidades

  • Cada camada tem uma função clara e bem definida
  • Facilita manutenção e evolução do código
  • Reduz acoplamento entre componentes

2. Testabilidade

  • Actions podem ser testadas isoladamente
  • DTOs garantem contratos bem definidos
  • Mocks e stubs facilitados
<?php

// Teste de Action
public function test_create_produto()
{
    $dto = new CreateProdutoDTO(
        nome: 'Produto Teste',
        preco: 100.00,
        categoria_id: 1,
    );

    $action = new CreateProduto();
    $resultado = $action->execute($dto);

    $this->assertTrue($resultado['success']);
    $this->assertInstanceOf(Produto::class, $resultado['produto']);
}

3. Reutilização

  • Actions podem ser usadas em diferentes contextos (API, CLI, Jobs, etc.)
  • DTOs facilitam conversão entre formatos
<?php

// Usando a mesma Action em uma API
class ProdutoController extends Controller
{
    public function store(Request $request)
    {
        $dados = $request->validate([...]);

        $dto = new CreateProdutoDTO(...$dados);
        $resultado = (new CreateProduto())->execute($dto);

        return response()->json($resultado);
    }
}

4. Type Safety

  • Erros detectados em tempo de desenvolvimento
  • IDE autocomplete e análise estática
  • Menos bugs em produção

5. Refatoração Segura

  • Mudanças em uma camada não afetam outras
  • Contratos bem definidos facilitam mudanças
  • PHPStan/Larastan podem verificar tipos

6. Documentação Viva

  • DTOs documentam a estrutura de dados
  • Actions documentam operações disponíveis
  • Código mais legível e autodocumentado

7. Transações Centralizadas

  • Lógica de transação em um único lugar
  • Rollback automático em caso de erro
  • Consistência de dados garantida

8. Logs e Auditoria

  • Ponto único para adicionar logs
  • Rastreamento de operações facilitado
  • Auditoria centralizada
<?php

public function execute(CreateProdutoDTO $dto): array
{
    return DB::transaction(function () use ($dto) {
        try {
            $produto = Produto::create([...]);

            // Log centralizado
            activity()
                ->performedOn($produto)
                ->causedBy(auth()->user())
                ->log('Produto criado');

            return ['success' => true, 'produto' => $produto];
        } catch (\Exception $e) {
            logger()->error('Erro ao criar produto', [
                'dto' => $dto->toArray(),
                'exception' => $e->getMessage(),
            ]);

            return ['success' => false, 'exception' => $e];
        }
    });
}

Quando NÃO Usar Esta Arquitetura

Embora esta arquitetura seja poderosa, há casos onde ela pode ser excessiva:

  • CRUDs extremamente simples (apenas um campo, sem lógica)
  • Protótipos rápidos onde a velocidade é mais importante
  • Formulários de configuração muito simples

Nestes casos, pode-se usar diretamente o Livewire com Eloquent:

1
2
3
4
5
6
7
8
<?php

public function save()
{
    $dados = $this->validate(['nome' => 'required']);
    Config::create($dados);
    $this->notifySuccess('Salvo!');
}

Conclusão

Esta arquitetura oferece:

Código mais limpo e organizado
Facilidade de manutenção
Testabilidade superior
Reutilização de código
Type safety e menos bugs
Escalabilidade

Ao seguir este padrão, você garante que o projeto W2O Safety mantenha alta qualidade de código e seja fácil de manter e evoluir.