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

v3.0.0 2026-01-06 00:49 UTC

This package is auto-updated.

Last update: 2026-01-06 00:49:51 UTC


README

Latest Version Total Downloads License

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 β†’ Uses fromString() (no generate())
  • category_id:int β†’ Uses fromInt() (no generate())

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 notes and paid_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-run flag 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:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

⭐ Show Your Support

If you find this package helpful, please consider giving it a ⭐ on GitHub!

Made with ❀️ by Yntech