Exemplo Completo: Produto
v1.1.0Este guia demonstra o fluxo completo de utilização do Laravel Domain Generator utilizando um domínio chamado Product.
Ao final você entenderá exatamente como uma requisição percorre todas as camadas geradas, desde o endpoint da API até o Repository.
Este é o exemplo mais completo da documentação e representa o fluxo recomendado para novos projetos.
Resultado Final
Com um único comando Artisan é possível gerar uma arquitetura completa pronta para produção.
php artisan make:domain ProductEstrutura gerada.
app/
├── Domain/
│ └── Product/
│ ├── DTO/
│ │ ├── ProductDTO.php
│ │ └── ProductUpdateDTO.php
│ ├── Repositories/
│ │ └── ProductRepository.php
│ └── Services/
│ └── ProductService.php
├── Http/
│ ├── Controllers/
│ │ └── ProductController.php
│ ├── Requests/
│ │ ├── ProductRequest.php
│ │ └── ProductUpdateRequest.php
│ └── Resources/
│ └── ProductResource.php
├── Models/
│ └── Product.php
└── database/
└── migrations/Cada arquivo possui uma única responsabilidade.
Fluxo da Requisição
Toda requisição segue sempre o mesmo pipeline.
O Controller nunca conversa diretamente com o banco de dados.
Migration
A migration gerada cria automaticamente a estrutura da tabela.
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('public_id')->unique();
$table->string('nome');
$table->text('descricao')->nullable();
$table->decimal('preco',10,2);
$table->integer('estoque')->default(0);
$table->boolean('ativo')->default(true);
$table->timestamps();
$table->softDeletes();
});Execute normalmente.
php artisan migrateModel
O Model permanece extremamente simples.
class Product extends Model
{
use HasFactory;
use SoftDeletes;
use HasHash;
protected $fillable = [
'nome',
'descricao',
'preco',
'estoque',
'ativo',
];
}Observe que toda lógica permanece fora do Model.
Controller
O Controller apenas conecta as camadas geradas.
class ProductController extends AbstractController
{
protected mixed $service;
protected ?string $requestValidate = ProductRequest::class;
protected ?string $requestValidateUpdate = ProductUpdateRequest::class;
protected ?string $requestDto = ProductDTO::class;
protected ?string $requestDtoUpdate = ProductUpdateDTO::class;
protected ?string $resource = ProductResource::class;
public function __construct(ProductService $service)
{
$this->service = $service;
}
}Nenhuma lógica CRUD precisa ser escrita manualmente.
ProductRequest
A validação acontece antes de chegar ao Service.
class ProductRequest extends FormRequest
{
public function rules(): array
{
return [
'nome' => ['required','string','max:255'],
'descricao' => ['nullable','string'],
'preco' => ['required','numeric'],
'estoque' => ['required','integer'],
'ativo' => ['boolean'],
];
}
}Erros de validação retornam automaticamente HTTP 422.
ProductUpdateRequest
A atualização possui regras independentes.
class ProductUpdateRequest extends FormRequest
{
public function rules(): array
{
return [
'nome' => ['sometimes','string'],
'descricao' => ['nullable','string'],
'preco' => ['sometimes','numeric'],
'estoque' => ['sometimes','integer'],
'ativo' => ['boolean'],
];
}
}Isso permite regras diferentes entre criação e edição.
ProductDTO
Após a validação, o Request é convertido para um objeto tipado.
final class ProductDTO
{
public function __construct(
public readonly string $nome,
public readonly ?string $descricao,
public readonly float $preco,
public readonly int $estoque,
public readonly bool $ativo
) {}
public static function fromRequest(Request $request): self
{
return new self(
nome: $request->nome,
descricao: $request->descricao,
preco: (float) $request->preco,
estoque: (int) $request->estoque,
ativo: (bool) $request->ativo,
);
}
}DTOs eliminam a dependência direta do Request dentro do domínio.
ProductUpdateDTO
A atualização possui seu próprio DTO.
final class ProductUpdateDTO
{
public function __construct(
public readonly ?string $nome,
public readonly ?string $descricao,
public readonly ?float $preco,
public readonly ?int $estoque,
public readonly ?bool $ativo
) {}
}Isso mantém a tipagem consistente.
ProductService
As regras de negócio pertencem ao Service.
O Service recebe o Repository através de injeção de dependência.
class ProductService extends AbstractService
{
public function __construct(ProductRepository $repository)
{
$this->repository = $repository;
}
public function create(ProductDTO $dto)
{
if ($dto->preco <= 0) {
throw new DomainException(
'Preço inválido.'
);
}
return $this->repository->create(
$dto->toArray()
);
}
}O Service permanece totalmente desacoplado da camada HTTP.
ProductRepository
O Repository é responsável pela persistência.
class ProductRepository extends AbstractRepository
{
public function __construct(Product $model)
{
parent::__construct($model);
}
}Diversos métodos já estão disponíveis automaticamente.
Criar
$this->repository->create(
$dto->toArray()
);Buscar
$this->repository->findByPublicId(
$publicId
);Paginar
$this->repository
->where('ativo', true)
->orderBy('nome')
->paginate();Sem necessidade de escrever consultas repetitivas.
ProductResource
O Resource controla exatamente o que será retornado pela API.
class ProductResource extends JsonResource
{
public function toArray($request): array
{
return [
'public_id' => $this->public_id,
'nome' => $this->nome,
'descricao' => $this->descricao,
'preco' => $this->preco,
'estoque' => $this->estoque,
'ativo' => $this->ativo,
];
}
}Isso evita expor atributos internos.
Rotas Geradas
As rotas seguem o padrão REST.
| Método | Endpoint |
|---|---|
| GET | /api/products |
| GET | /api/products/{public_id} |
| POST | /api/products |
| PUT | /api/products/{public_id} |
| DELETE | /api/products/{public_id} |
Criando um Produto
POST /api/products
Request.
{
"nome": "Notebook Gamer",
"descricao": "RTX 4070",
"preco": 7999.90,
"estoque": 12,
"ativo": true
}Resposta (201 Created).
{
"type": "success",
"status": 201,
"data": {
"public_id": "01JXYZABCDEF123456",
"nome": "Notebook Gamer",
"preco": 7999.90,
"estoque": 12,
"ativo": true
}
}Listando Produtos
GET /api/products
Resposta.
{
"data": [
{
"public_id": "01JXYZABCDEF123456",
"nome": "Notebook Gamer"
}
],
"links": {},
"meta": {}
}A paginação já vem pronta.
Buscando um Produto
GET /api/products/{public_id}
Exemplo.
GET /api/products/01JXYZABCDEF123456O Repository resolve automaticamente o identificador público.
Atualizando um Produto
PUT /api/products/{public_id}
Request.
{
"preco": 7499.90
}Resposta.
{
"type": "success",
"status": 200,
"data": {
"public_id": "01JXYZABCDEF123456",
"preco": 7499.90
}
}O fluxo utiliza automaticamente ProductUpdateRequest e ProductUpdateDTO.
Removendo um Produto
DELETE /api/products/{public_id}
Resposta.
{
"type": "success",
"status": 200,
"message": "Produto removido com sucesso."
}Quando SoftDeletes estiver habilitado, o registro será arquivado automaticamente.
Fluxo Completo da Execução
POST /api/products
│
▼
ProductRequest
│
▼
ProductDTO
│
▼
ProductService
│
▼
ProductRepository
│
▼
Product Model
│
▼
ProductResource
│
▼
JSON ResponseCada camada executa apenas sua responsabilidade.
Boas Práticas
- mantenha Controllers enxutos;
- coloque regras de negócio no Service;
- utilize DTOs;
- retorne Resources;
- trabalhe com identificadores públicos;
- deixe o Repository responsável apenas pela persistência.
Este exemplo demonstra exatamente como um desenvolvedor utilizaria o Laravel Domain Generator em um projeto real.