Neste tutorial vamos construir uma arquitetura profissional usando:
- Laravel + Docker
- Actions
- Events
- Listeners
- Strategy Pattern
- Boas práticas de organização
- Separação de responsabilidades
A ideia será criar um sistema simples de “teste de produto”, onde cada produto pode ser validado de maneiras diferentes usando estratégias independentes.
Como usar Strategy Pattern no Laravel com Actions e Events para organizar regras de negócio
Exemplo:
- Produto digital → valida licença
- Produto físico → valida estoque
- Produto premium → valida assinatura
Tudo desacoplado usando:
- Strategy Pattern para trocar regras
- Actions para centralizar casos de uso
- Events para desacoplar efeitos colaterais
O que vamos aprender
- Como subir Laravel com Docker
- Como estruturar Actions no Laravel
- Como usar Events e Listeners
- Como aplicar Strategy Pattern corretamente
- Como evitar Controllers gigantes
- Como organizar um projeto Laravel de forma profissional
Criando o projeto Laravel com Docker
Estrutura que iremos criar
app/
├── Actions/
├── Events/
├── Listeners/
├── Strategies/
│ └── ProductValidation/
├── Contracts/
├── DTOs/
└── Services/
O problema da regra de negócio no Controller
Muita gente faz isso:
public function store(Request $request)
{
if ($request->type === 'digital') {
// valida digital
}
if ($request->type === 'physical') {
// valida físico
}
if ($request->type === 'premium') {
// valida premium
}
}
O problema:
- difícil manutenção
- alto acoplamento
- crescimento caótico
- impossível testar direito
Vamos resolver isso usando Strategy + Actions + Events.
Criando nossa interface Strategy
Contrato das estratégias
mkdir -p app/Contracts
ProductValidationStrategyInterface.php
<?php
namespace App\Contracts;
interface ProductValidationStrategyInterface
{
public function validate(array $product): bool;
}
Criando as Strategies
Produto Digital
<?php
namespace App\Strategies\ProductValidation;
use App\Contracts\ProductValidationStrategyInterface;
class DigitalProductStrategy implements ProductValidationStrategyInterface
{
public function validate(array $product): bool
{
return isset($product['license']);
}
}
Produto Físico
<?php
namespace App\Strategies\ProductValidation;
use App\Contracts\ProductValidationStrategyInterface;
class PhysicalProductStrategy implements ProductValidationStrategyInterface
{
public function validate(array $product): bool
{
return $product['stock'] > 0;
}
}
Produto Premium
<?php
namespace App\Strategies\ProductValidation;
use App\Contracts\ProductValidationStrategyInterface;
class PremiumProductStrategy implements ProductValidationStrategyInterface
{
public function validate(array $product): bool
{
return $product['subscription_active'] === true;
}
}
Criando a Action
Actions ajudam a encapsular regras de negócio.
Estrutura
mkdir app/Actions
ValidateProductAction.php
<?php
namespace App\Actions;
use App\Events\ProductValidated;
use App\Strategies\ProductValidation\DigitalProductStrategy;
use App\Strategies\ProductValidation\PhysicalProductStrategy;
use App\Strategies\ProductValidation\PremiumProductStrategy;
class ValidateProductAction
{
public function execute(array $product): bool
{
$strategy = match ($product['type']) {
'digital' => new DigitalProductStrategy(),
'physical' => new PhysicalProductStrategy(),
'premium' => new PremiumProductStrategy(),
default => throw new \Exception('Tipo inválido')
};
$validated = $strategy->validate($product);
event(new ProductValidated($product, $validated));
return $validated;
}
}
Por que isso ficou melhor?
Agora:
- cada regra fica isolada
- adicionar novos tipos é fácil
- código testável
- baixo acoplamento
- mais legível
Criando Event
Gerando event
sail artisan make:event ProductValidated
ProductValidated.php
<?php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProductValidated
{
use Dispatchable, SerializesModels;
public function __construct(
public array $product,
public bool $validated
) {}
}
Criando Listener
Gerando listener
sail artisan make:listener LogValidatedProductListener
LogValidatedProductListener.php
<?php
namespace App\Listeners;
use App\Events\ProductValidated;
use Illuminate\Support\Facades\Log;
class LogValidatedProductListener
{
public function handle(ProductValidated $event): void
{
Log::info('Produto validado', [
'product' => $event->product,
'validated' => $event->validated
]);
}
}
Registrando Event e Listener
AppServiceProvider
<?php
namespace App\Providers;
use App\Events\ProductValidated;
use App\Listeners\LogValidatedProductListener;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Event::listen(
ProductValidated::class,
LogValidatedProductListener::class
);
}
}
Criando Controller
Gerando
sail artisan make:controller ProductController
ProductController.php
<?php
namespace App\Http\Controllers;
use App\Actions\ValidateProductAction;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function validateProduct(
Request $request,
ValidateProductAction $action
) {
$validated = $action->execute($request->all());
return response()->json([
'validated' => $validated
]);
}
}
Criando rota
routes/api.php
use App\Http\Controllers\ProductController;
Route::post('/products/validate', [
ProductController::class,
'validateProduct'
]);
Testando a API
Produto físico
{
"type": "physical",
"stock": 10
}
Produto digital
{
"type": "digital",
"license": "ABC-123"
}
Produto premium
{
"type": "premium",
"subscription_active": true
}
Melhorando ainda mais com Factory
O próximo passo profissional seria remover o match.
Exemplo:
$strategy = ProductStrategyFactory::make($product['type']);
Isso evita:
- crescimento infinito de condicionais
- dependências diretas
- violação do Open/Closed Principle
Onde essa arquitetura brilha?
Esse padrão funciona MUITO bem para:
- gateways de pagamento
- cálculo de frete
- notificações
- importadores
- ERPs
- antifraude
- regras fiscais
- workflows
Benefícios reais dessa arquitetura
Actions
- centralizam casos de uso
- deixam controller limpo
Events
- desacoplam processos
- facilitam filas e escalabilidade
Strategy
- elimina if/switch gigantes
- facilita manutenção
Estrutura final do projeto
app/
├── Actions/
│ └── ValidateProductAction.php
│
├── Contracts/
│ └── ProductValidationStrategyInterface.php
│
├── Events/
│ └── ProductValidated.php
│
├── Listeners/
│ └── LogValidatedProductListener.php
│
├── Strategies/
│ └── ProductValidation/
│ ├── DigitalProductStrategy.php
│ ├── PhysicalProductStrategy.php
│ └── PremiumProductStrategy.php
│
└── Http/
└── Controllers/
└── ProductController.php
Conclusão
Usar Laravel apenas com Controllers e Services resolve problemas pequenos.
Mas quando o sistema cresce:
- regras explodem
- manutenção vira pesadelo
- testes ficam difíceis
Com:
- Actions
- Events
- Strategy Pattern
você cria uma arquitetura:
- escalável
- desacoplada
- profissional
- fácil de testar
- preparada para crescimento
Essa é uma abordagem muito usada em sistemas grandes e times maduros de Laravel.