yntech / domain-forge
Hexagonal architecture generator for Laravel. Provides Artisan commands for structuring domains following the principles of DDD and Screaming Architecture.
Installs: 21
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/yntech/domain-forge
Requires
- php: ^8.2
- illuminate/console: ^12.2
- illuminate/support: ^12.2
README
Domain Forge is a powerful Laravel package that streamlines the creation of domain modules following Hexagonal Architecture (Ports & Adapters) and Domain-Driven Design (DDD) principles. Generate complete, production-ready domain structures with a single command.
π Features
- β Complete DDD Structure - Automatically generates Application, Domain, and Infrastructure layers
- β Value Objects - Rich domain models with validation and type safety
- β Native PHP Enums - First-class support for PHP 8.1+ enums with helper methods
- β Auto-generated IDs - Smart UUID/ULID generation for entity identifiers
- β Type Safety - Nullable types, primitives, and enum support
- β Customizable Stubs - Publish and modify templates to fit your needs
- β Automatic Mappers - Eloquent β Domain entity mappers
- β Smart Validation - Password hashing, timestamp handling, and more
- β Rollback Support - Automatic cleanup on errors
- β Permission Checks - Validates write permissions before generation
π Requirements
- PHP 8.1 or higher
- Laravel 10.x or 11.x or higher
- Composer
π Installation
Install the package via Composer:
composer require yntech/domain-forge
βοΈ Configuration
1. Configure Base Structure
Run the installation command to set up the base structure:
php artisan domain-forge:install
2. Update Composer Autoload
Add the following to your composer.json in the autoload.psr-4 section:
{
"autoload": {
"psr-4": {
"App\\": "app/",
"Src\\": "src/"
}
}
}
3. Refresh Autoload
composer dump-autoload
π Usage
Basic Domain Generation
Create a simple domain module:
php artisan domain-forge:domain User
This generates:
src/Contexts/User/
βββ Application/
β βββ Commands/
β βββ Handlers/
β βββ DTOs/
β βββ Services/
β βββ UseCases/
βββ Domain/
β βββ Entities/
β β βββ User.php
β βββ Contracts/
β β βββ UserRepositoryContract.php
β βββ Exceptions/
β βββ ValueObjects/
βββ Infrastructure/
βββ Http/
β βββ Controllers/
β βββ Requests/
β βββ Resources/
β βββ Routes/
β βββ User.php
βββ Persistence/
β βββ Mappers/
β βββ Repositories/
β βββ Eloquent/
β βββ UserRepository.php
βββ UserServiceProvider.php
Domain with Properties
Generate a domain with value objects:
php artisan domain-forge:domain Product --props="name:string,price:float,stock:int,description:?string"
Generated Value Objects:
ProductName(string)ProductPrice(float)ProductStock(int)ProductDescription(nullable string)
Entity Structure:
<?php namespace Src\Contexts\Product\Domain\Entities; use Src\Contexts\Product\Domain\ValueObjects\ProductName; use Src\Contexts\Product\Domain\ValueObjects\ProductPrice; use Src\Contexts\Product\Domain\ValueObjects\ProductStock; use Src\Contexts\Product\Domain\ValueObjects\ProductDescription; final readonly class Product { private function __construct( private ProductName $name, private ProductPrice $price, private ProductStock $stock, private ProductDescription $description, ) {} public function name(): ProductName { return $this->name; } public function price(): ProductPrice { return $this->price; } // ... other getters public static function create( ProductName $name, ProductPrice $price, ProductStock $stock, ProductDescription $description, ): static { return new self( name: $name, price: $price, stock: $stock, description: $description, ); } public static function fromPrimitives( string $name, float $price, int $stock, ?string $description, ): static { return new self( name: ProductName::fromString($name), price: ProductPrice::fromFloat($price), stock: ProductStock::fromInt($stock), description: ProductDescription::fromNullableString($description), ); } }
π― Property Types
Supported Types
| Type | Example | Value Object Method |
|---|---|---|
string |
name:string |
fromString(string) |
int |
age:int |
fromInt(int) |
float |
price:float |
fromFloat(float) |
bool |
active:bool |
fromBool(bool) |
?string |
description:?string |
fromNullableString(?string) |
?int |
quantity:?int |
fromNullableInt(?int) |
?float |
discount:?float |
fromNullableFloat(?float) |
?bool |
verified:?bool |
fromNullableBool(?bool) |
Nullable Types
Add ? prefix for nullable properties:
php artisan domain-forge:domain Post --props="title:string,content:string,excerpt:?string,published_at:?string"
Generated Value Object:
final readonly class PostExcerpt { private function __construct( private ?string $value ) { $this->validate($value); } public function value(): ?string { return $this->value; } public static function fromNullableString(?string $value): static { return new self($value); } }
π’ Enum Support
Creating Enums
Use the enum[value1|value2|value3] syntax:
php artisan domain-forge:domain Order --props="id:string,customer_name:string,status:enum[pending|processing|shipped|delivered|cancelled],total:float"
Generated Enum Structure
File: src/Contexts/Order/Domain/Enums/OrderStatus.php
<?php namespace Src\Contexts\Order\Domain\Enums; enum OrderStatus: string { case PENDING = 'pending'; case PROCESSING = 'processing'; case SHIPPED = 'shipped'; case DELIVERED = 'delivered'; case CANCELLED = 'cancelled'; /** * Get all possible enum values * * @return array<self> */ public static function all(): array { return [ self::PENDING, self::PROCESSING, self::SHIPPED, self::DELIVERED, self::CANCELLED, ]; } /** * Create enum from string value * * @throws \ValueError */ public static function fromString(string $value): self { return self::from($value); } /** * Create enum from nullable string value */ public static function fromNullableString(?string $value): ?self { return $value !== null ? self::from($value) : null; } /** * Get string representation of the enum value */ public function toString(): string { return $this->value; } }
Using Enums in Your Code
// Create entity with enum $order = Order::create( customer_name: OrderCustomerName::create('John Doe'), status: OrderStatus::PENDING, total: OrderTotal::create(99.99) ); // From primitives $order = Order::fromPrimitives( id: '123-456', customer_name: 'John Doe', status: 'pending', total: 99.99 ); // Access enum value $status = $order->status(); // OrderStatus $statusString = $order->status()->toString(); // 'pending' $statusValue = $order->status()->value; // 'pending' // Get all available statuses $allStatuses = OrderStatus::all(); // Check specific status if ($order->status() === OrderStatus::DELIVERED) { // Order delivered logic }
Enum Examples
# User roles php artisan domain-forge:domain User --props="id:string,name:string,email:string,role:enum[admin|editor|viewer]" # Product categories php artisan domain-forge:domain Product --props="name:string,category:enum[electronics|clothing|food|books]" # Ticket priority php artisan domain-forge:domain Ticket --props="title:string,priority:enum[low|medium|high|urgent],status:enum[open|in_progress|resolved|closed]"
π Auto-Generated IDs
String IDs (UUID)
When you define an id:string property, Domain Forge automatically generates UUID methods:
php artisan domain-forge:domain User --props="id:string,name:string,email:string"
Generated UserId Value Object:
final readonly class UserId { private function __construct( private string $value ) { $this->validate($value); } /** * Generate a new unique ID. * By default uses UUID v4. Override to use ULID or other strategies. */ public static function generate(): static { return new self(\Illuminate\Support\Str::uuid()->toString()); } /** * Generate using ULID (uncomment if preferred) */ // public static function generate(): static // { // return new self(\Illuminate\Support\Str::ulid()->toString()); // } public static function fromString(string $value): static { return new self($value); } public function value(): string { return $this->value; } }
Entity create() method excludes ID:
public static function create( UserName $name, UserEmail $email, ): static { return new self( id: UserId::generate(), // Auto-generated name: $name, email: $email, ); }
Integer IDs (Auto-increment)
For database auto-increment IDs:
php artisan domain-forge:domain Product --props="id:int,name:string"
Generated ProductId Value Object:
public static function generate(): static { // This is a placeholder. Usually handled by database auto-increment throw new \RuntimeException('ID generation should be handled by the database'); }
π Special Property Types
Password Fields
Properties containing "password" get special hashing methods:
php artisan domain-forge:domain User --props="id:string,email:string,password:string"
Generated UserPassword Value Object:
final readonly class UserPassword { public static function fromHashed(string $hashedPassword): static { return new self($hashedPassword); } public static function hash(string $plainPassword): static { return new self(\Illuminate\Support\Facades\Hash::make($plainPassword)); } public static function fromString(string $value): static { return new self($value); } }
Usage:
// Create with hashed password (from database) $user = User::fromPrimitives( id: '123', email: 'user@example.com', password: '$2y$10$...' // Already hashed ); // Hash plain password $password = UserPassword::hash('my-plain-password'); // Create user with hashed password $user = User::create( email: UserEmail::create('user@example.com'), password: UserPassword::hash('plain-password') );
Timestamp Fields
Properties ending with _at are treated as timestamps:
php artisan domain-forge:domain Post --props="id:string,title:string,created_at:string,published_at:?string"
These use fromString() or fromNullableString() methods automatically in fromPrimitives().
ποΈ Command Flags
--props
Define entity properties with their types:
php artisan domain-forge:domain Product --props="name:string,price:float,stock:int"
--skip-model
Skip Laravel Eloquent model and migration generation:
php artisan domain-forge:domain User --props="name:string,email:string" --skip-model
Use case: When you don't need database persistence or already have the model.
π¨ Customization with Stubs
Publishing Stubs
Publish stub templates to your project:
php artisan domain-forge:publish-stubs
This creates:
stubs/domain-forge/
βββ entity.stub
βββ entity-simple.stub
βββ value-object.stub
βββ enum.stub
βββ repository-contract.stub
βββ repository.stub
βββ mapper.stub
βββ service-provider.stub
βββ routes.stub
Customizing Stubs
Edit any stub file to change the generated code structure. For example, add timestamps to all entities:
Edit: stubs/domain-forge/entity.stub
final readonly class {{ className }} { private function __construct( {{ constructorParams }} private \DateTime $createdAt, private ?\DateTime $updatedAt = null, ) {} public function createdAt(): \DateTime { return $this->createdAt; } public function updatedAt(): ?\DateTime { return $this->updatedAt; } {{ getters }} // ... rest of the code }
Available Stub Variables
Each stub supports different replacement variables:
entity.stub:
{{ namespace }}- Full namespace{{ className }}- Class name{{ imports }}- Value Object/Enum imports{{ constructorParams }}- Constructor parameters{{ getters }}- Getter methods{{ createParams }}- create() parameters{{ createArgs }}- create() arguments{{ fromPrimitivesParams }}- fromPrimitives() parameters{{ fromPrimitivesArgs }}- fromPrimitives() arguments
value-object.stub:
{{ namespace }}{{ className }}{{ type }}- Property type{{ additionalMethods }}- Generated methods
enum.stub:
{{ namespace }}{{ className }}{{ cases }}- Enum cases{{ allMethod }}- all() method{{ fromStringMethod }}- Conversion methods
Force Overwrite Stubs
php artisan domain-forge:publish-stubs --force
πΊοΈ Generated Mappers
When generating a domain with properties (and without --skip-model), Domain Forge creates a mapper:
File: src/Contexts/User/Infrastructure/Persistence/Mappers/UserMapper.php
<?php namespace Src\Contexts\User\Infrastructure\Persistence\Mappers; use App\Models\User as UserModel; use Src\Contexts\User\Domain\Entities\User; class UserMapper { public static function toDomain(UserModel $model): User { return User::fromPrimitives( id: $model->id, name: $model->name, email: $model->email, password: $model->password, ); } public static function toEloquent(User $entity): array { return [ 'name' => $entity->name()->value(), 'email' => $entity->email()->value(), 'password' => $entity->password()->value(), ]; } }
Usage in Repository:
public function findById(UserId $id): ?User { $model = UserModel::find($id->value()); return $model ? UserMapper::toDomain($model) : null; } public function save(User $user): void { $data = UserMapper::toEloquent($user); UserModel::updateOrCreate( ['id' => $user->id()->value()], $data ); }
π Real-World Examples
E-Commerce Product
php artisan domain-forge:domain Product --props="id:string,name:string,description:?string,price:float,stock:int,sku:string,category:enum[electronics|clothing|food|books],status:enum[available|out_of_stock|discontinued],created_at:string,updated_at:?string"
User Management System
php artisan domain-forge:domain User --props="id:string,name:string,email:string,password:string,phone:?string,avatar:?string,role:enum[admin|manager|user|guest],status:enum[active|inactive|suspended|banned],email_verified_at:?string,created_at:string"
Blog Post
php artisan domain-forge:domain Post --props="id:string,title:string,slug:string,content:string,excerpt:?string,author_id:string,category_id:?string,status:enum[draft|published|archived],published_at:?string,created_at:string,updated_at:?string"
Support Ticket System
php artisan domain-forge:domain Ticket --props="id:string,title:string,description:string,user_id:string,assigned_to:?string,priority:enum[low|medium|high|critical],status:enum[open|in_progress|waiting|resolved|closed],created_at:string,updated_at:?string,closed_at:?string"
Invoice System
php artisan domain-forge:domain Invoice --props="id:string,invoice_number:string,customer_id:string,amount:float,tax:float,total:float,status:enum[draft|sent|paid|overdue|cancelled],due_date:string,paid_at:?string,created_at:string"
π‘οΈ Error Handling & Rollback
Domain Forge includes automatic rollback on errors:
php artisan domain-forge:domain User --props="invalid prop format"
Output:
β Error: Property 'invalid prop format' doesn't have correct format
π Rolling back changes...
ποΈ Deleted: src/Contexts/User/Domain/Entities/User.php
ποΈ Deleted directory: src/Contexts/User/Domain/Entities
ποΈ Deleted directory: src/Contexts/User/Domain
β
Rollback completed.
Permission Validation
Before generating, Domain Forge checks:
- Write permissions on
src/Contexts/ - Write permissions on
bootstrap/providers.php - Directory creation capabilities
Example error:
β No write permissions for: /path/to/src/Contexts
Run: chmod -R 755 /path/to/src/Contexts
π Generation Summary
After successful generation, you'll see a detailed summary:
π Creating domain module: User
π Creation Summary:
Directories:
ββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β Status β Path β
ββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β β
β src/Contexts/User/Application β
β β
β src/Contexts/User/Domain β
β β
β src/Contexts/User/Domain/Entities β
β β
β src/Contexts/User/Domain/ValueObjects β
β β
β src/Contexts/User/Domain/Enums β
β β
β src/Contexts/User/Infrastructure β
ββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
Files:
ββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Status β Path β
ββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β src/Contexts/User/Domain/Entities/User.php β
β β
β src/Contexts/User/Domain/ValueObjects/UserId.php β
β β
β src/Contexts/User/Domain/ValueObjects/UserName.php β
β β
β src/Contexts/User/Domain/ValueObjects/UserEmail.php β
β β
β src/Contexts/User/Domain/Enums/UserRole.php β
β β
β src/Contexts/User/Infrastructure/UserServiceProvider.php β
ββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Total items created: 42
β
Domain User created successfully!
π§ Advanced Usage
Foreign Keys
Use *_id properties for foreign keys (they won't auto-generate):
php artisan domain-forge:domain Post --props="id:string,title:string,author_id:string,category_id:int"
author_id:stringβ UsesfromString()(nogenerate())category_id:intβ UsesfromInt()(nogenerate())
Combining Features
php artisan domain-forge:domain Order \
--props="id:string,customer_id:string,status:enum[pending|paid|shipped],total:float,notes:?string,created_at:string,paid_at:?string" \
--skip-model
This creates:
- β
Auto-generated
id - β
Foreign key
customer_id(no generation) - β
Enum
status - β
Nullable
notesandpaid_at - β
Timestamp
created_at - β No Eloquent model
π Best Practices
1. Property Naming
# β GOOD - snake_case --props="first_name:string,created_at:string" # β BAD - camelCase or PascalCase --props="firstName:string,CreatedAt:string"
2. Enum Values
# β GOOD - lowercase, snake_case --props="status:enum[pending|in_progress|completed]" # β BAD - uppercase or mixed case --props="status:enum[PENDING|InProgress]"
3. Nullable Usage
Use nullable only when truly optional:
# User email is required, phone is optional --props="email:string,phone:?string"
4. ID Strategy
Choose consistent ID strategy per project:
# UUID for distributed systems --props="id:string" # Auto-increment for traditional apps --props="id:int"
π― Domain-Driven Design Tips
Aggregates
Create separate domains for aggregates:
# Order aggregate php artisan domain-forge:domain Order --props="id:string,customer_id:string,total:float,status:enum[pending|paid]" # OrderItem is part of Order aggregate php artisan domain-forge:domain OrderItem --props="id:string,order_id:string,product_id:string,quantity:int,price:float"
Value Objects
Rich domain models emerge from proper value objects:
// Instead of primitive obsession $user->email = 'invalid-email'; // No validation! // Use Value Objects $user->updateEmail(UserEmail::create('new@example.com')); // Validated!
Domain Events
Add events to your domain entities (manual step):
final readonly class Order { private array $domainEvents = []; public function markAsPaid(): void { $this->status = OrderStatus::PAID; $this->domainEvents[] = new OrderPaidEvent($this); } public function pullDomainEvents(): array { $events = $this->domainEvents; $this->domainEvents = []; return $events; } }
π Troubleshooting
Error: "No write permissions"
chmod -R 755 src/Contexts chmod 755 bootstrap/providers.php
Error: "Class not found"
composer dump-autoload
Enum ValueError
// β Wrong OrderStatus::fromString('PENDING'); // ValueError // β Correct OrderStatus::fromString('pending');
Property validation errors
Ensure property names follow rules:
- Start with lowercase letter
- Only alphanumeric and underscores
- No spaces or special characters
π§ Roadmap
v3.1 (Planned)
-
--dry-runflag for preview - Duplicate module detection
- Controller generation
- DTO generation
- Smart validations in ValueObjects
v3.2 (Planned)
- Test generation
- Event generation
- Command/Query handlers
- GraphQL support
Future
- Multi-language support
- Custom templates per project
- Migration generator from props
- Interactive mode
π€ Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
git clone https://github.com/yntech/domain-forge.git
cd domain-forge
composer install
Running Tests
composer test
π License
This project is licensed under the MIT License. See the LICENSE file for details.
π Credits
Created and maintained by Yntech.
π Support
- π§ Email: support@yntech.com
- π Issues: GitHub Issues
- π¬ Discussions: GitHub Discussions
β Show Your Support
If you find this package helpful, please consider giving it a β on GitHub!
Made with β€οΈ by Yntech