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
| ✅ 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
| <?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');
}
|
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:
| <?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.