laravel-ddd/starter
Composer starter kit that turns a fresh Laravel 12/13 app into a Domain-Driven Design structure. Includes base Entity/ValueObject/Repository/Service classes, 12 generators, interactive installer (auth, docs, tests, sample module), API-ready routes, and optional AI context.
Usa nombres en singular cuando sea posible:
Users, Post, OrderUserManagement, PostCollectionLos plurales se generan automáticamente en las migraciones y rutas.
Mantén la consistencia:
Domains/
├── Users/ # Singular
│ ├── Entities/ # Contiene User.php
│ ├── Services/ # Contiene UserService.php
│ ├── Repositories/ # Contiene UserRepository.php
│ └── Http/Controllers/ # Contiene UserController.php
class User extends Entity
{
// Lógica del dominio
public function isActive(): bool
{
return $this->status === 'active';
}
public function suspend(): void
{
$this->status = 'suspended';
}
}
class User extends Entity
{
// Acceso a datos directo en la entidad
public function getOrders()
{
return Order::where('user_id', $this->id)->get();
}
}
class UserService extends Service
{
public function __construct(
protected UserRepositoryInterface $repository
) {}
public function createUser(array $data): User
{
// Validación de reglas de negocio
if ($this->userExists($data['email'])) {
throw new UserAlreadyExistsException();
}
return $this->repository->create($data);
}
private function userExists(string $email): bool
{
return $this->repository->findByEmail($email) !== null;
}
}
class UserService extends Service
{
public function createUser(array $data): void
{
// Validación incompleta
User::create($data);
// Efecto secundario sin control
Mail::send(new WelcomeEmail($data['email']));
}
}
interface UserRepositoryInterface extends RepositoryInterface
{
public function findByEmail(string $email): ?User;
public function findActive(): Collection;
}
class EloquentUserRepository implements UserRepositoryInterface
{
public function findByEmail(string $email): ?User
{
$model = User::where('email', $email)->first();
return $model ? new User($model->toArray()) : null;
}
public function findActive(): Collection
{
return User::where('status', 'active')->get()
->map(fn($m) => new User($m->toArray()));
}
}
class UserRepository
{
// Mezclar consultas con lógica de negocio
public function getPayingUsers()
{
return User::where('status', 'active')
->whereHas('subscription')
->with('orders')
->get();
}
}
class UserController extends Controller
{
public function store(
StoreUserRequest $request,
UserService $service
): JsonResponse {
$user = $service->createUser($request->validated());
return response()->json(['data' => $user], 201);
}
}
class UserController extends Controller
{
public function store(Request $request): JsonResponse
{
// Validación en el controlador
$validated = $request->validate([...]);
// Lógica de negocio en el controlador
if (User::where('email', $validated['email'])->exists()) {
return response()->json(['error' => 'exists'], 422);
}
$user = User::create($validated);
return response()->json(['data' => $user], 201);
}
}
class Email extends ValueObject
{
public function __construct(protected string $value)
{
$this->validate();
}
protected function validate(): void
{
if (!filter_var($this->value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
}
public function getValue(): mixed
{
return $this->value;
}
public function isSame(ValueObject $valueObject): bool
{
return $this->value === $valueObject->getValue();
}
public function __toString(): string
{
return $this->value;
}
}
// Strings simples sin validación
$email = "user@example.com";
class UserService extends Service
{
public function __construct(
protected UserRepositoryInterface $repository,
protected EventDispatcher $dispatcher
) {}
}
class UserService extends Service
{
public function createUser(array $data): User
{
$repo = new EloquentUserRepository();
$user = $repo->create($data);
return $user;
}
}
namespace Tests\Unit\Domains\Users\Services;
use Tests\TestCase;
use App\Domains\Users\Services\UserService;
class UserServiceTest extends TestCase
{
public function test_create_user_successfully(): void
{
$repository = $this->mock(UserRepositoryInterface::class);
$repository->expects('create')->with(['email' => 'test@example.com']);
$service = new UserService($repository);
$service->createUser(['email' => 'test@example.com']);
}
public function test_prevent_duplicate_email(): void
{
$this->expectException(UserAlreadyExistsException::class);
// ...
}
}
// database/migrations/2024_01_01_create_users_table.php
return new class extends Migration {
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('email')->unique();
$table->string('name');
$table->enum('status', ['active', 'suspended']);
$table->timestamps();
$table->softDeletes();
});
}
};
Para proyectos complejos, considera usar eventos:
class UserCreatedEvent
{
public function __construct(public User $user) {}
}
// En el servicio
public function createUser(array $data): User
{
$user = $this->repository->create($data);
event(new UserCreatedEvent($user));
return $user;
}
// En un listener
public function handle(UserCreatedEvent $event): void
{
// Enviar welcome email
Mail::send(new WelcomeEmail($event->user));
}
Request → FormRequest → Controller → Service → Repository → Entity → JsonResource → Response
// En el dominio, lanza excepciones específicas
class UserService extends Service
{
public function createUser(array $data): User
{
if ($this->userExists($data['email'])) {
throw new UserAlreadyExistsException();
}
return $this->repository->create($data);
}
}
// En el controlador, captura y maneja
class UserController extends Controller
{
public function store(
StoreUserRequest $request,
UserService $service
): JsonResponse {
try {
$user = $service->createUser($request->validated());
return response()->json(['data' => $user], 201);
} catch (UserAlreadyExistsException $e) {
return response()->json(['error' => 'Email already registered'], 422);
}
}
}
Asegúrate de que tu namespace está configurado correctamente:
{
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
}
}
Luego ejecuta:
composer dump-autoload
How can I help you explore Laravel packages today?