creativecrafts/laravel-domain-driven-design-lite 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

creativecrafts/laravel-domain-driven-design-lite

最新稳定版本:0.0.5

Composer 安装命令:

composer require creativecrafts/laravel-domain-driven-design-lite

包简介

Domain‑Driven Design (DDD)‑lite scaffolding for Laravel. This package generates a lightweight, opinionated module structure under Modules/[Module] and provides Artisan commands to scaffold common artifacts (Actions, Queries, DTOs, Repositories, Models, Controllers, Requests, Mappers, Migrations, Fac

README 文档

README

Latest Version on Packagist GitHub Tests Action Status Code Style Total Downloads

Pragmatic, Laravel-native DDD modules with generators, safety rails, and CI helpers – without drowning you in ceremony.

✅ Quick Start (60s)

composer require creativecrafts/laravel-domain-driven-design-lite --dev
php artisan ddd-lite:module Planner
php artisan ddd-lite:make:dto Planner CreateTripData --props="id:Ulid|nullable,title:string"
php artisan ddd-lite:publish:quality --target=all

📚 Contents

🧩 Start Here

If you only read three sections:

  • Getting Started (QuickStart) – a 60‑second setup.
  • Common Workflows – the most practical end‑to‑end examples.
  • Command Reference – every command and flag in one place.

🧭 What is DDD-Lite?

DDD-Lite is a developer-tooling package that helps you organise your Laravel 12+ application into modular, domain-oriented boundaries.

It gives you:

  • A module structure (modules/<ModuleName>) with a clear split between:
    • App/ – adapters & Laravel-specific glue (controllers, requests, models, providers, repositories)
    • Domain/ – pure PHP domain code (DTOs, actions, contracts, queries, value objects)
  • Generators for:
    • DTOs, Actions, Contracts, Repositories, Value Objects
    • Queries and Query Builders
    • Aggregate Roots
    • Controllers, Requests, Models, Migrations, Providers, Routes
  • A conversion engine to move existing app/* code into modules:
    • Discovers move candidates (controllers, models, requests, actions, DTOs, contracts)
    • Applies moves with AST-based namespace rewrites
  • Safety rails for all file operations:
    • Every run produces a Manifest (with creates/updates/deletes/moves/mkdirs)
    • --dry-run on everything
    • Rollback by manifest id
  • Quality & CI tooling:
    • Publishable PHPStan & Deptrac configs
    • Optional Pest architecture tests
    • ddd-lite:doctor / ddd-lite:doctor:domain / ddd-lite:doctor-ci to keep modules healthy

The goal is: clean seams, safer refactors, better testability – without requiring you to rewrite your entire app in one go.

🧱 Architecture Overview

A DDD-Lite module lives under modules/<ModuleName>:

modules/<ModuleName>/
├─ App/
│  ├─ Http/
│  │  ├─ Controllers/
│  │  └─ Requests/
│  ├─ Models/
│  ├─ Providers/
│  └─ Repositories/
├─ Domain/
│  ├─ Actions/
│  ├─ Contracts/
│  ├─ DTO/
│  ├─ Queries/
│  └─ ValueObjects/
├─ database/
│  └─ migrations/
├─ Routes/
│  ├─ api.php
│  └─ web.php
└─ tests/
   ├─ Feature/
   └─ Unit/

✅ Rules of thumb

Domain:

  • Pure PHP, no hard dependency on Laravel.
  • Orchestrates use cases with Actions (e.g. CreateTripAction).
  • Talks to the outside world through Contracts (e.g. TripRepositoryContract).
  • Uses DTOs and Value Objects for data.

App:

  • Typical Laravel adapters (controllers, form requests, models).
  • Implements contracts using Eloquent (e.g. TripRepository).
  • Wires things together using module service providers.

⚙️ Requirements

  • PHP: ^8.3
  • Laravel (Illuminate components): ^12.0
  • Composer

Recommended dev dependencies in your app (for quality tooling integration):

  • larastan/larastan
  • deptrac/deptrac
  • pestphp/pest
  • pestphp/pest-plugin-laravel
  • pestphp/pest-plugin-arch

⚙️ Installation

Require the package in your Laravel app (usually as a dev dependency):

composer require creativecrafts/laravel-domain-driven-design-lite --dev

This package is primarily a developer tool (scaffolding, conversion, quality helpers), so installing under --dev is recommended. Laravel’s package discovery will automatically register the service provider.

📦 Provider & Publishing

DDD-Lite ships with stubs and quality configs you can copy into your app.

Stubs (module scaffolding & generators)

To publish the stubs:

php artisan vendor:publish --tag=ddd-lite
php artisan vendor:publish --tag=ddd-lite-stubs

This will create:

  • stubs/ddd-lite/*.stub – templates for:
    • DTOs, Actions, Contracts, Repositories, Value Objects, Aggregates
    • Controllers (including an --inertia variant)
    • Requests
    • Models & migrations
    • Module providers and route/event providers
    • Routes (web & api)

You typically don’t need to touch these unless you want to customise the generated code style.

Quality tooling

To seed PHPStan, Deptrac and Pest architecture tests into your application:

php artisan ddd-lite:publish:quality --target=all

This will (in your app):

  • Create phpstan.app.neon with sensible defaults for app/ + modules/
  • Create deptrac.app.yaml describing layer boundaries (Http, Models, Domain, Modules, etc.)
  • Add tests/ArchitectureTest.php with baseline rules:
    • No debug helpers (dd, dump, var_dump, …)
    • No stray env() calls
    • Enforce strict types

Safe publishing tip

  • Run once, then commit the generated files so changes are visible in code review.

You can also publish selectively:

php artisan ddd-lite:publish:quality --target=phpstan
php artisan ddd-lite:publish:quality --target=deptrac
php artisan ddd-lite:publish:quality --target=pest-arch

Customising stubs (recommended workflow)

  1. Publish the stubs once:
php artisan vendor:publish --tag=ddd-lite-stubs
  1. Edit the generated files under stubs/ddd-lite/ in your app (e.g. tweak DTO or controller templates).
  2. Re-run the generator command. Your customised stubs are now the source of truth.

Tip: keep your stub changes small and version-controlled so upgrades are easy to diff.

✅ Enforcing Strict Architecture with Deptrac + PHPStan

This package ships publishable templates you can wire into your app to enforce module boundaries and strict layered architecture.

1) Publish the configs

php artisan ddd-lite:publish:quality --target=deptrac
php artisan ddd-lite:publish:quality --target=phpstan

This creates (in your app):

  • deptrac.app.yaml – layer rules for app/ and modules/
  • phpstan.app.neon – analysis paths + defaults for modules/

2) Deptrac: strict module boundaries

The template already defines layers like Http, Models, Repositories, Domain, Providers, and ModulesAll. To enforce stricter module boundaries, extend the ruleset to constrain Modules\*\Domain and Modules\*\App explicitly.

Example (add to deptrac.app.yaml):

layers:
  - name: ModuleDomain
    collectors:
      - type: classNameRegex
        value: '#^Modules\\[^\\]+\\Domain\\.*$#'

  - name: ModuleApp
    collectors:
      - type: classNameRegex
        value: '#^Modules\\[^\\]+\\App\\.*$#'

ruleset:
  ModuleDomain:
    - Framework
  ModuleApp:
    - ModuleDomain
    - Framework

For cross‑module rules, add a shared kernel (e.g. Modules\Shared) and only allow ModuleDomain to depend on Modules\Shared (not other modules).

3) PHPStan: include modules and tighten strictness

The published phpstan.app.neon already includes:

  • paths: [app, modules]
  • Larastan extension include

To go stricter, set level: max and enable stricter checks:

parameters:
  level: max
  checkMissingIterableValueType: true
  checkGenericClasses: true

4) CI integration

Use the existing Composer scripts:

composer run deptrac:app-template
composer run stan

⚠️ Limitations: Circular Dependencies at Scale

  • Deptrac detects circular dependencies at the layer graph level. If you do not model modules as distinct layers, cycles across modules may be invisible.
  • Dynamic resolution (service container bindings, facades, late static calls) can hide cycles that Deptrac cannot infer statically.
  • Large modular systems can produce noisy or slow analysis. You may need to scope rules (paths, exclude_files) or split configs per module to keep feedback fast.
  • PHPStan does not detect architectural cycles; it only validates types and static correctness. Use Deptrac (or Pest Arch tests) for structure enforcement.

🚀 Getting Started (QuickStart)

We’ll build a simple Planner module with a Trip aggregate.

1) Scaffold a module

php artisan ddd-lite:module Planner

This creates modules/Planner with:

  • Providers (module, route, event) under App/Providers
  • Routes in Routes/api.php and Routes/web.php
  • Domain folders (Actions, DTOs, Contracts, Queries, ValueObjects)
  • Tests folders

Core flags:

  • --dry-run – preview actions without writing
  • --fix-psr4 – auto-rename lowercased module folders to proper PascalCase
  • --shared – scaffold a Shared Kernel (domain‑only) module
  • --rollback= – undo a previous scaffold
  • --force – overwrite content when needed (with backups)

For full details see: docs/module-scaffold.md

2) Generate a DTO

php artisan ddd-lite:make:dto Planner CreateTripData \
  --props="id:Ulid|nullable,title:string,startsAt:CarbonImmutable,endsAt:CarbonImmutable"

This generates modules/Planner/Domain/DTO/CreateTripData.php:

  • Properly typed constructor
  • readonly by default
  • Optional unit test (unless --no-test is passed)

3) Generate a domain Action

php artisan ddd-lite:make:action Planner CreateTrip \
  --in=Trip \
  --input=FQCN --param=data \
  --returns=ulid

This creates Domain/Actions/Trip/CreateTripAction.php similar to:

namespace Modules\Planner\Domain\Actions\Trip;

use Modules\Planner\Domain\Contracts\TripRepositoryContract;
use Modules\Planner\Domain\DTO\CreateTripData;

final class CreateTripAction
{
    public function __construct(
        private TripRepositoryContract $repo,
    ) {}

    public function __invoke(CreateTripData $data): string
    {
        // Domain invariants live here
        return $this->repo->create($data);
    }
}

4) Implement the repository & bind it Create an Eloquent repository in modules/Planner/App/Repositories/TripRepository.php (or let ddd-lite:make:repository scaffold it):

php artisan ddd-lite:make:repository Planner Trip

Then wire the contract to the implementation:

php artisan ddd-lite:bind Planner TripRepositoryContract TripRepository

ddd-lite:bind edits your module provider so that:

$this->app->bind(
    TripRepositoryContract::class,
    TripRepository::class,
);

is registered.

5) Expose via HTTP Generate a controller + request:

php artisan ddd-lite:make:controller Planner Trip --resource
php artisan ddd-lite:make:request Planner StoreTrip

✅ Recipes

Recipes index

  • Controller orchestrates a Domain action
  • Payments module walkthrough
  • Orders module creation + structure

Controller Orchestrates a Domain Action (HTTP stays in App)

This is the missing piece most people want: the controller only adapts HTTP input and delegates to the Domain action. The action stays HTTP‑free.

1) Generate the pieces

php artisan ddd-lite:make:action Billing CreateInvoice --in=Invoice --input=FQCN
php artisan ddd-lite:make:controller Billing Invoice
php artisan ddd-lite:make:request Billing StoreInvoice

2) Domain action (pure business logic)

<?php

namespace Modules\Billing\Domain\Actions;

use Modules\Billing\Domain\Contracts\InvoiceRepositoryContract;
use Modules\Billing\Domain\DTO\CreateInvoiceData;

final class CreateInvoiceAction
{
    public function __construct(private InvoiceRepositoryContract $repository)
    {
    }

    public function execute(CreateInvoiceData $data): string
    {
        return $this->repository->create($data);
    }
}

3) Form request (HTTP concerns only)

<?php

namespace Modules\Billing\App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

final class StoreInvoiceRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:120'],
            'amount' => ['required', 'integer', 'min:1'],
        ];
    }
}

4) Controller (orchestrates, no business logic)

<?php

namespace Modules\Billing\App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Modules\Billing\App\Http\Requests\StoreInvoiceRequest;
use Modules\Billing\Domain\Actions\CreateInvoiceAction;
use Modules\Billing\Domain\DTO\CreateInvoiceData;

final class InvoiceController
{
    public function __construct(private CreateInvoiceAction $action)
    {
    }

    public function store(StoreInvoiceRequest $request): JsonResponse
    {
        $data = new CreateInvoiceData(
            $request->string('title')->toString(),
            $request->integer('amount'),
        );

        $id = $this->action->execute($data);

        return response()->json(['id' => $id], 201);
    }
}

5) Minimal feature test (verifies orchestration)

<?php

use Modules\Billing\Domain\Actions\CreateInvoiceAction;

it('orchestrates the domain action from the controller', function (): void {
    $action = Mockery::mock(CreateInvoiceAction::class);
    $action->shouldReceive('execute')->once()->andReturn('inv_123');
    $this->instance(CreateInvoiceAction::class, $action);

    $response = $this->postJson('/billing/invoices', [
        'title' => 'Pro Plan',
        'amount' => 1200,
    ]);

    $response->assertCreated()->assertJson(['id' => 'inv_123']);
});

Boundary rule of thumb: App layer knows HTTP; Domain layer never does.

Payments Module Walkthrough (DTO → Contract → Repository → Action)

This compact slice ties the pieces together with real code, while keeping Domain pure and App infrastructural.

1) Generate the artifacts

php artisan ddd-lite:make:dto Payments CreatePaymentData --props="reference:string,amount:int"
php artisan ddd-lite:make:contract Payments PaymentRepository
php artisan ddd-lite:make:repository Payments Payment --contract=PaymentRepositoryContract
php artisan ddd-lite:make:action Payments CreatePayment --in=Payment --input=FQCN
php artisan ddd-lite:bind Payments PaymentRepositoryContract PaymentRepository

2) DTO (Domain)

<?php

namespace Modules\Payments\Domain\DTO;

final class CreatePaymentData
{
    public function __construct(
        public string $reference,
        public int $amount,
    ) {
    }
}

3) Contract (Domain)

<?php

namespace Modules\Payments\Domain\Contracts;

use Modules\Payments\Domain\DTO\CreatePaymentData;

interface PaymentRepositoryContract
{
    public function create(CreatePaymentData $data): string;
}

4) Repository (App, Eloquent adapter)

<?php

namespace Modules\Payments\App\Repositories;

use Modules\Payments\App\Models\Payment;
use Modules\Payments\Domain\Contracts\PaymentRepositoryContract;
use Modules\Payments\Domain\DTO\CreatePaymentData;

final class PaymentRepository implements PaymentRepositoryContract
{
    public function create(CreatePaymentData $data): string
    {
        $payment = Payment::query()->create([
            'reference' => $data->reference,
            'amount' => $data->amount,
        ]);

        return (string) $payment->getKey();
    }
}

5) Action (Domain, orchestrates business rules)

<?php

namespace Modules\Payments\Domain\Actions;

use Modules\Payments\Domain\Contracts\PaymentRepositoryContract;
use Modules\Payments\Domain\DTO\CreatePaymentData;

final class CreatePaymentAction
{
    public function __construct(private PaymentRepositoryContract $repository)
    {
    }

    public function execute(CreatePaymentData $data): string
    {
        return $this->repository->create($data);
    }
}

Boundary rule of thumb: Domain owns the contract and action; App wires the implementation and persistence.

🧠 DDD-Lite in Practice: Example Flow

A typical “vertical slice” in a module:

  • DTO: CreateTripData – validated input from HTTP or CLI.
  • Action: CreateTripAction – orchestrates creation, enforces invariants.
  • Contract: TripRepositoryContract – interface for persistence.
  • Repository: TripRepository (Eloquent) – implements contract.
  • Controller: TripController@store – adapts HTTP to the action.

Generators help you keep this shape consistent across modules without hand-rolling boilerplate every time.

✅ Advanced Workflows

Multi‑Tenant SaaS Structure + Boundary Enforcement (Deptrac + PHPStan)

This section shows a concrete, enforceable layout for multi‑tenant apps, with shared domain concepts and strict module boundaries.

1) Recommended module layout

modules/
  Shared/                 # Shared Kernel (domain-only)
    Domain/
      ValueObjects/
      Contracts/
      Exceptions/
      Enums/
  Tenancy/                # Tenant resolution + context
    App/
    Domain/
  Billing/
    App/
    Domain/
  Projects/
    App/
    Domain/
  Users/
    App/
    Domain/

2) Shared Kernel (domain‑only) Use --shared to keep it pure (no HTTP/Routes):

php artisan ddd-lite:module Shared --shared

3) Shared domain concepts (example)

<?php

namespace Modules\Shared\Domain\ValueObjects;

final class TenantId
{
    public function __construct(public string $value)
    {
    }
}

4) Tenancy boundary (example contract)

<?php

namespace Modules\Tenancy\Domain\Contracts;

use Modules\Shared\Domain\ValueObjects\TenantId;

interface CurrentTenantResolverContract
{
    public function resolve(): TenantId;
}

5) App layer adapter uses tenancy, Domain stays pure

<?php

namespace Modules\Projects\App\Actions;

use Modules\Shared\Domain\ValueObjects\TenantId;
use Modules\Tenancy\Domain\Contracts\CurrentTenantResolverContract;

final class BuildTenantContext
{
    public function __construct(private CurrentTenantResolverContract $resolver)
    {
    }

    public function handle(): TenantId
    {
        return $this->resolver->resolve();
    }
}

✅ Deptrac: Enforce Module Boundaries

Create a deptrac.yaml in your host app (this repo ships a package example, but apps should define their own rules):

parameters:
  paths:
    - modules

  layers:
    - name: SharedDomain
      collectors:
        - type: directory
          value: modules/Shared/Domain

    - name: TenancyDomain
      collectors:
        - type: directory
          value: modules/Tenancy/Domain

    - name: BillingDomain
      collectors:
        - type: directory
          value: modules/Billing/Domain

    - name: ProjectsDomain
      collectors:
        - type: directory
          value: modules/Projects/Domain

    - name: UsersDomain
      collectors:
        - type: directory
          value: modules/Users/Domain

    - name: AppLayer
      collectors:
        - type: directory
          value: modules/.*/App

  ruleset:
    SharedDomain: [SharedDomain]
    TenancyDomain: [TenancyDomain, SharedDomain]
    BillingDomain: [BillingDomain, SharedDomain, TenancyDomain]
    ProjectsDomain: [ProjectsDomain, SharedDomain, TenancyDomain]
    UsersDomain: [UsersDomain, SharedDomain]
    AppLayer: [SharedDomain, TenancyDomain, BillingDomain, ProjectsDomain, UsersDomain]

6) Run Deptrac with DDD‑Lite

php artisan ddd-lite:doctor:domain --config=deptrac.yaml

✅ PHPStan: Block Cross‑Module Dependencies

Add module‑level ban rules in your app’s phpstan.neon to prevent accidental imports:

parameters:
  forbiddenSymbols:
    - 'Modules\\Billing\\Domain\\.*'
  ignoreErrors:
    - '#^Access to an undefined property#'

Then override per‑module configs (example for Projects):

includes:
  - phpstan.neon

parameters:
  forbiddenSymbols:
    - 'Modules\\Billing\\Domain\\.*'
    - 'Modules\\Users\\Domain\\.*'

Boundary rule of thumb: Domains depend only on Shared + their own contracts. App layer can orchestrate across modules via contracts and adapters.

Limitations & Mitigations (circular dependencies)

  • Deptrac can detect cycles but won’t tell you how to break them. When a cycle appears, move shared contracts/value objects into Shared/Domain and depend on those instead of cross‑module concrete classes.
  • Avoid App ↔ Domain cycles by keeping infrastructure in App and interfaces in Domain.
  • If you must allow a dependency for a transition period, document it explicitly and remove it once the migration is complete.

Shared concepts + cross‑module communication (tenant‑safe patterns)

  • Put common value objects and contracts in Shared/Domain (e.g., TenantId, Money, Period).
  • Expose inter‑module capabilities via Domain contracts (e.g., Billing\Domain\Contracts\Invoices), and bind App implementations in the owning module.
  • Prefer application services in App for orchestration across modules; Domain actions should stay within their module boundary.

Tenant isolation guidelines

  • Always pass TenantId into Domain actions or repositories; never infer tenancy inside Domain from HTTP or globals.
  • In App repositories, scope every query by tenant (e.g., ->where('tenant_id', $tenantId->value)).
  • For cross‑module reads, use contracts that accept TenantId explicitly to prevent accidental leakage.

Safe Refactor Workflow with --dry-run + Rollback

Use dry‑run previews and manifests to refactor large features into modules safely.

1) Plan the move (no files written)

php artisan ddd-lite:convert Billing \
  --plan-moves \
  --paths=app/Models,app/Http/Controllers,app/Http/Requests \
  --dry-run

Syntax reminders

  • --paths is a comma‑separated list of directories (no spaces).
  • --plan-moves discovers candidates only (no writes).

2) Apply moves with review (writes manifest)

php artisan ddd-lite:convert Billing --apply-moves --review

The output includes a manifest id:

Manifest: 55f96156eac4ea63

3) Inspect what was changed

php artisan ddd-lite:manifest:show 55f96156eac4ea63

Each action records the file path and the type (create/update/move).

How to read dry‑run output

  • move indicates a file will be relocated into the module.
  • update indicates a namespace or import rewrite.
  • If you see unexpected paths, narrow --paths before applying moves.

4) Rollback if needed

php artisan ddd-lite:convert Billing --rollback=55f96156eac4ea63

5) Refactor with dry‑run safety (scaffold + bind)

php artisan ddd-lite:module Billing --dry-run
php artisan ddd-lite:make:contract Billing BillingRepository --dry-run
php artisan ddd-lite:make:repository Billing Billing --dry-run

6) Verification checklist

  • Review dry‑run output for unexpected paths.
  • Use ddd-lite:manifest:show to confirm changes before committing.
  • If anything looks off, rollback immediately and re‑run with a narrower --paths scope.

Error handling tips

  • If a command fails after writing files, rollback using the manifest id printed in the output.
  • For repeated refactors, keep the manifest id in your PR notes.
  • If you hit unexpected rewrites, run ddd-lite:convert <Module> --report to review planned namespace changes before applying.

Before/After tree (concrete example)

Before (legacy feature in app/):

app/
  Models/
    BillingAccount.php
  Http/
    Controllers/
      Billing/
        BillingAccountController.php
    Requests/
      Billing/
        StoreBillingAccountRequest.php

After (moved into modules/Billing):

modules/
  Billing/
    App/
      Http/
        Controllers/
          BillingAccountController.php
        Requests/
          StoreBillingAccountRequest.php
      Models/
        BillingAccount.php
    Domain/
      Actions/
      Contracts/
      DTO/

Convert Monolith app/ → Invoicing Module (with namespace rewrites)

This is the complete, safe workflow: scaffold the module first, then plan + apply moves.

1) Scaffold the module (required)

php artisan ddd-lite:module Invoicing

2) Plan the conversion (no files written)

php artisan ddd-lite:convert Invoicing \
  --plan-moves \
  --paths=app/Models,app/Http/Controllers,app/Http/Requests \
  --dry-run

3) Review and apply moves (writes manifest)

php artisan ddd-lite:convert Invoicing --apply-moves --review

4) Confirm namespace rewrites

php artisan ddd-lite:convert Invoicing --report

5) Rollback if needed

php artisan ddd-lite:convert Invoicing --rollback=<manifest-id>

Notes

  • The conversion engine rewrites namespaces to Modules\\Invoicing\\... during moves.
  • Use --paths to keep scope tight and predictable for large refactors.

Create an Orders Module + Typical Structure

1) Create the module

php artisan ddd-lite:module Orders

2) Typical directory structure (Domain vs App)

modules/
  Orders/
    App/
      Http/
      Models/
      Providers/
      Repositories/
    Domain/
      Actions/
      Contracts/
      DTO/
      Queries/
      ValueObjects/
    Database/
      migrations/
    Routes/
    tests/

3) Register + adapter example The module provider is created and registered automatically during ddd-lite:module. To connect App infrastructure to Domain, bind a contract in the module provider:

$this->app->bind(
    OrderRepositoryContract::class,
    EloquentOrderRepository::class,
);

Then use the contract inside a Domain action, while App implementations remain Eloquent‑based.

4) Minimal flow (Controller → Action → Repository)

<?php

namespace Modules\Orders\App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Modules\Orders\App\Http\Requests\StoreOrderRequest;
use Modules\Orders\Domain\Actions\CreateOrderAction;
use Modules\Orders\Domain\DTO\CreateOrderData;

final class OrderController
{
    public function __construct(private CreateOrderAction $action)
    {
    }

    public function store(StoreOrderRequest $request): JsonResponse
    {
        $data = new CreateOrderData(
            $request->integer('customer_id'),
            $request->integer('total'),
        );

        $id = $this->action->execute($data);

        return response()->json(['id' => $id], 201);
    }
}
<?php

namespace Modules\Orders\Domain\Actions;

use Modules\Orders\Domain\Contracts\OrderRepositoryContract;
use Modules\Orders\Domain\DTO\CreateOrderData;

final class CreateOrderAction
{
    public function __construct(private OrderRepositoryContract $repository)
    {
    }

    public function execute(CreateOrderData $data): string
    {
        return $this->repository->create($data);
    }
}

🧭 Project Initialization

For a guided, one‑shot setup (stubs, quality configs, optional module, and CI snippet):

php artisan ddd-lite:init --module=Core --publish=all --ci=show

Common flags:

  • --module= – scaffold a starter module (e.g. Core)
  • --no-module – skip module scaffolding
  • --publish=all|quality|stubs|none
  • --ci=show|write|none
  • --ci-path=... – write workflow to a custom path
  • --yes – non‑interactive defaults

🧰 Command Reference

All commands share a consistent UX:

  • --dry-run – print what would happen; no files written; no manifest saved.
  • --force – overwrite when content changes (backups are tracked in manifests).
  • --rollback= – revert a previous run (see Manifest section below).

Command Index

  • Bootstrap: ddd-lite:init
  • Scaffolding: ddd-lite:module
  • Generators: ddd-lite:make:*
  • Wiring: ddd-lite:bind
  • Conversion: ddd-lite:convert
  • Quality: ddd-lite:publish:quality, ddd-lite:doctor, ddd-lite:doctor:domain, ddd-lite:doctor-ci, ddd-lite:boundaries
  • Manifests: ddd-lite:manifest:list, ddd-lite:manifest:show
  • Stubs: ddd-lite:stubs:diff, ddd-lite:stubs:sync
  • Modules: ddd-lite:modules:list

Cheat Sheet

Goal Command
Initialize a project php artisan ddd-lite:init --module=Core --publish=all --ci=show
Create a module skeleton php artisan ddd-lite:module Billing
Generate a DTO php artisan ddd-lite:make:dto Billing CreateInvoiceData --props="id:Ulid,title:string"
Create an Action php artisan ddd-lite:make:action Billing CreateInvoice --in=Invoice --input=FQCN
Bind a contract php artisan ddd-lite:bind Billing InvoiceRepositoryContract InvoiceRepository
Plan legacy moves php artisan ddd-lite:convert Billing --plan-moves --paths=app/Models
Apply moves safely php artisan ddd-lite:convert Billing --apply-moves --review
Suggest contracts php artisan ddd-lite:convert Billing --plan-moves --suggest-contracts
Publish quality tooling php artisan ddd-lite:publish:quality --target=all
Run structural checks php artisan ddd-lite:doctor --module=Billing
List modules + health php artisan ddd-lite:modules:list --with-health

1. Module scaffolding & conversion

ddd-lite:module Scaffold a new module skeleton:

php artisan ddd-lite:module Planner

Key flags:

  • name (required) – module name in PascalCase.
  • --dry-run – preview only.
  • --force – overwrite files if they exist.
  • --shared – scaffold a Shared Kernel (domain‑only) module.
  • --fix-psr4 – rename existing lowercased module folders to PSR-4 PascalCase.
  • --rollback= – rollback a previous scaffold.

See docs/module-scaffold.md for details.

ddd-lite:convert Discover and optionally apply moves from app/* into modules:

php artisan ddd-lite:convert Planner \
  --plan-moves \
  --paths=app/Http/Controllers,app/Models

To use the namespace rewriting and AST-based moves, install nikic/php-parser:

composer require --dev nikic/php-parser

Important options:

  • module – target module name.
  • --plan-moves – discover move candidates and print a plan (no writes).
  • --apply-moves – actually apply moves (AST-safe namespace rewrites).
  • --review – interactive confirmation per move (with --apply-moves).
  • --all – apply all moves without prompts.
  • --only=controllers,models,requests,actions,dto,contracts – include kinds.
  • --except=... – exclude kinds.
  • --paths=... – comma-separated paths to scan.
  • --with-shims – include shim suggestions in the printed plan.
  • --suggest-contracts – print suggested contracts/bindings for moved models/actions.
  • --export-plan=path.json – write discovered move plan to JSON.
  • --dry-run, --force, --rollback=.

Use this to gradually migrate a legacy app into DDD-Lite modules.

Safe conversion workflow

  • Start with --plan-moves and export the plan (--export-plan=...) for review.
  • Apply with --review before --all.
  • Keep the manifest id; roll back with --rollback=<id> if needed.

2. Domain generators

ddd-lite:make:dto Generate a DTO under Domain/DTO:

php artisan ddd-lite:make:dto Planner CreateTripData \
  --in=Trip \
  --props="id:Ulid|nullable,title:string,startsAt:CarbonImmutable"
  • module – module name.
  • name – DTO class name.
  • --in= – optional subnamespace inside Domain/DTO.
  • --props= – name:type[|nullable] comma-separated.
  • --readonly – enforce readonly class (default: true).
  • --no-test – skip generating a test.

ddd-lite:make:action Generate a domain action in Domain/Actions:

php artisan ddd-lite:make:action Planner CreateTrip \
  --in=Trip \
  --input=FQCN --param=data \
  --returns=ulid
  • --in= – optional subnamespace.
  • --method= – method name (default __invoke).
  • --input= – parameter type preset: none|ulid|FQCN.
  • --param= – parameter variable name.
  • --returns= – void|ulid|FQCN.
  • --no-test – skip test.

ddd-lite:make:contract Generate a domain contract:

php artisan ddd-lite:make:contract Planner TripRepository \
  --in=Trip \
  --methods="find:TripData|null(id:Ulid); create:TripData(data:TripCreateData)"
  • --methods= – semi-colon separated: name:ReturnType(args...).
  • --with-fake – generate a Fake implementation under tests/Unit/fakes.
  • --no-test – skip the contract test.

ddd-lite:make:repository Generate an Eloquent repository for an aggregate:

php artisan ddd-lite:make:repository Planner Trip

Creates:

  • App/Repositories/TripRepository.php
  • Optional tests (unless --no-test).

ddd-lite:make:value-object Generate a value object:

php artisan ddd-lite:make:value-object Planner Email \
  --scalar=string
  • --scalar= – backing scalar type: string|int|float|bool.
  • --no-test – skip generating a test.
  • --with-validation-test – include a validation test that expects InvalidArgumentException.

By default, a test is created at: modules/<Module>/tests/Unit/Domain/ValueObjects/<Name>Test.php

Example: Email value object with validation (egulias/email-validator)

  1. Install the validator in your app:
composer require egulias/email-validator
  1. Generate the value object:
php artisan ddd-lite:make:value-object Users Email --scalar=string
  1. Update the generated file:

Path: modules/Users/Domain/ValueObjects/Email.php

<?php

declare(strict_types=1);

namespace Modules\Users\Domain\ValueObjects;

use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\RFCValidation;
use InvalidArgumentException;

final readonly class Email
{
    public function __construct(private string $value)
    {
        $validator = new EmailValidator();
        if (!$validator->isValid($value, new RFCValidation())) {
            throw new InvalidArgumentException('Invalid email address.');
        }
    }

    public function value(): string
    {
        return $this->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}
  1. Instantiate in your Domain layer (e.g. in an Action):
use Modules\Users\Domain\ValueObjects\Email;

$email = new Email($data->email);
  1. Add a minimal test (Pest):

Path: modules/Users/tests/Unit/EmailTest.php

<?php

declare(strict_types=1);

use Modules\Users\Domain\ValueObjects\Email;

it('accepts valid email', function (): void {
    $email = new Email('user@example.com');

    expect((string)$email)->toBe('user@example.com');
});

it('rejects invalid email', function (): void {
    new Email('not-an-email');
})->throws(InvalidArgumentException::class);

ddd-lite:make:aggregate-root Generate an aggregate root base:

php artisan ddd-lite:make:aggregate-root Planner Trip

Useful for richer domain modelling around key aggregates.

Query side

  • ddd-lite:make:query – generate a Query class in Domain/Queries.
  • ddd-lite:make:query-builder – generate a QueryBuilder helper.
  • ddd-lite:make:aggregator – generate an Aggregator to combine queries.
    • add --no-test to skip generating tests for each of these.

Example:

php artisan ddd-lite:make:query Planner TripIndexQuery
php artisan ddd-lite:make:query-builder Planner Trip
php artisan ddd-lite:make:aggregator Planner TripIndexAggregator

3. App-layer generators

ddd-lite:make:model Generate an Eloquent model in App/Models:

php artisan ddd-lite:make:model Planner Trip \
  --table=trips \
  --fillable="title,starts_at,ends_at"

Options:

  • --table=, --fillable=, --guarded=
  • --soft-deletes
  • --no-timestamps

ddd-lite:make:migration Generate a migration under Database/migrations:

php artisan ddd-lite:make:migration Planner create_trips_table --create=trips
  • module? – module name (optional).
  • name? – migration base name.
  • --table= – table name.
  • --create= – shortcut for table creation.
  • --path= – override path (defaults to module migrations).
  • --force, --dry-run, --rollback=.

ddd-lite:make:controller Generate a controller:

php artisan ddd-lite:make:controller Planner Trip --resource
  • --resource – standard Laravel resource methods.
  • --inertia – generate methods that return Inertia pages.
  • --suffix= – class suffix (default Controller).

ddd-lite:make:request Generate a form request:

php artisan ddd-lite:make:request Planner StoreTrip
  • --suffix= – class suffix (default Request).

4. Binding & wiring

ddd-lite:bind Bind a domain contract to an implementation in the module provider:

php artisan ddd-lite:bind Planner TripRepositoryContract TripRepository
  • module – module name.
  • contract – contract short name or FQCN.
  • implementation – implementation short name or FQCN.
  • --force – skip class existence checks (e.g. when generating ahead of time).

5. Manifest commands Every write operation (scaffolding, generate, convert, publish, doctor fixes) is tracked via a Manifest.

ddd-lite:manifest:list List manifests:

php artisan ddd-lite:manifest:list --module=Planner --type=create --json

Options:

  • --module= – filter by module.
  • --type= – mkdir|create|update|delete|move.
  • --after=, --before= – ISO8601 bounds for created_at.
  • --json – machine-readable output.

ddd-lite:manifest:show Inspect a single manifest:

php artisan ddd-lite:manifest:show 2025-11-24-13-54-01 --json

Shows the tracked operations for that run (created files, backups, moves, deletions, etc.).

6. Doctor & Quality commands

ddd-lite:publish:quality (Described earlier) – publishes PHPStan, Deptrac, and Pest Arch configuration/stubs into your app.

ddd-lite:doctor Run structural checks on your modules and wiring:

php artisan ddd-lite:doctor --module=Planner --json

Checks things like:

  • Module provider registration
  • Route/service provider wiring
  • PSR-4 inconsistencies
  • Missing or mis-wired module components

Flags:

  • --module= – limit to a specific module.
  • --fix – attempt automatic fixes (provider edits, PSR-4 renames, etc.).
  • --json – JSON report for tooling.
  • --deep – run doctor:domain and doctor-ci after base checks.
  • --prefer=file|class – strategy when class and filename mismatch.
  • --rollback= – undo fixes.

ddd-lite:doctor:domain Run domain purity checks via Deptrac:

php artisan ddd-lite:doctor:domain \
  --config=deptrac.app.yaml \
  --bin=vendor/bin/deptrac \
  --json \
  --fail-on=violations

Options:

  • --config= – Deptrac YAML config.
  • --bin= – path to deptrac executable.
  • --json – JSON summary.
  • --strict – treat uncovered as failure.
  • --stdin-report= – use pre-generated Deptrac JSON report.
  • --fail-on= – violations|errors|uncovered|any.

ddd-lite:doctor-ci Run both structural and domain checks in CI:

php artisan ddd-lite:doctor-ci --json --fail-on=error
  • --paths= – paths to scan (defaults to modules/ and bootstrap/app.php).
  • --fail-on=none|any|error – CI failure policy.
  • --json – CI-friendly JSON result.

Use this in your CI pipeline to enforce module health.

ddd-lite:boundaries Alias for ddd-lite:doctor:domain to run Deptrac boundary checks with a friendlier name.

ddd-lite:modules:list List modules and (optionally) show health:

php artisan ddd-lite:modules:list --with-health

ddd-lite:stubs:diff Compare package stubs to your customized stubs:

php artisan ddd-lite:stubs:diff --json

ddd-lite:stubs:sync Sync missing or changed stubs into your app:

php artisan ddd-lite:stubs:sync --mode=missing

🧪 Safety Rails: Manifest & Rollback

DDD-Lite never silently edits your app.

For each command run that changes files:

  • A Manifest is written with:
    • mkdir, create, update, delete, move records
    • Backups of overwritten files
  • You can inspect manifests with:
    • ddd-lite:manifest:list
    • ddd-lite:manifest:show {id}
  • You can revert a run by passing --rollback=<manifest-id> to the original command (e.g. ddd-lite:module, ddd-lite:convert, ddd-lite:publish:quality, ddd-lite:doctor).

This makes DDD-Lite safe to use on large, existing codebases.

🧮 Package Quality: PHPStan, Deptrac & Pest

Inside this package:

  • phpstan.neon.dist – strict rules for the package itself.
  • deptrac.package.yaml – package-level dependency rules.
  • tests/ArchTest.php – baseline architecture checks via Pest.

In your application, use:

php artisan ddd-lite:publish:quality --target=all

and then:

# In your app
vendor/bin/phpstan analyse -c phpstan.app.neon
vendor/bin/deptrac --config=deptrac.app.yaml
php artisan test tests/ArchitectureTest.php

Combine this with ddd-lite:doctor-ci in CI for a tight feedback loop.

🧩 Common Workflows

Greenfield project

  • Install DDD-Lite.
  • Scaffold your first module: ddd-lite:module.
  • Generate DTOs, Actions, Contracts, Repositories, Controllers, Requests.
  • Set up quality tooling with ddd-lite:publish:quality.
  • Wire ddd-lite:doctor-ci into your CI.

Migrating a legacy app

  • Install DDD-Lite.
  • Scaffold a module for a coherent slice (e.g. Planner, Billing, Users).
  • Use ddd-lite:convert with --plan-moves on a subset of app/*.
  • Iterate with --apply-moves and --review, keeping an eye on manifests.
  • Introduce contracts + repositories for areas you want to harden.
  • Run ddd-lite:doctor and ddd-lite:doctor:domain regularly during the migration.

🧪 Testing Philosophy

The package itself is tested with:

  • Pest for:
    • Feature tests of console commands
    • Unit tests for internals (filesystem, manifests, planners)
  • Architecture tests to protect boundaries.

You’re encouraged to:

  • Keep module tests close to modules (under modules//tests).
  • Use the provided stubs for DTO / Action / Contract / Repository tests to keep patterns consistent.

🔒 Design Principles

  • Domain purity – Domain/ should know nothing about Laravel.
  • Explicit boundaries – Domain <-> App contracts are interfaces, not facades.
  • Safety first – manifests, backups, --dry-run, --rollback.
  • Deterministic generators – running a command twice should be safe and idempotent.
  • CI-friendly – all checks and reports can be consumed by automation via JSON / exit codes.

🧰 Troubleshooting

  • “Nothing seems to happen when I run a command”
    • Check if you passed --dry-run.
    • Inspect manifests using ddd-lite:manifest:list.
  • “I messed up my module structure”
    • Find the relevant manifest id: ddd-lite:manifest:list.
    • Rerun the original command with --rollback=.
  • “Deptrac or PHPStan fail after publishing quality configs”
    • Make sure you installed the suggested dev dependencies in your app.
    • Tweak phpstan.app.neon / deptrac.app.yaml to match your project’s structure.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Security

Please review our security policy on how to report security vulnerabilities.

🙌 Credits

📄 License

The MIT License (MIT). Please see License File for more information.

统计信息

  • 总下载量: 353
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-11-25

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固