DDD

This document explains the domain-first structure used by the starter kit. It is a DDD-style organization centered around domains instead of controllers.

Goal

The main goal is to keep business rules out of large controllers and move them into predictable domain folders.

Domain Structure

Typical structure after installation:

app/Domain/
├── Auth/
├── Role/
│   └── BulkActions/
└── User/
    └── BulkActions/

Domains that are fully managed by the package and run from vendor are not scaffolded into app/ on a fresh install. See Vendor-resident domains below.

Vendor-resident domains

The following domains have their runtime layer (Actions, DTOs, Queries, Events, Listeners, Services) inside the package (src/Domain/, Lvntr\StarterKit\Domain\). They are not copied into your app on install.

Domain Vendor namespace
FileManager Lvntr\StarterKit\Domain\FileManager\
Shared Lvntr\StarterKit\Domain\Shared\
ActivityLog Lvntr\StarterKit\Domain\ActivityLog\
Logs Lvntr\StarterKit\Domain\Logs\
Session Lvntr\StarterKit\Domain\Session\
Media Lvntr\StarterKit\Domain\Media\
ApiClient Lvntr\StarterKit\Domain\ApiClient\
ApiRoute Lvntr\StarterKit\Domain\ApiRoute\
Role Lvntr\StarterKit\Domain\Role\
Setting Lvntr\StarterKit\Domain\Setting\
User Lvntr\StarterKit\Domain\User\

Import compatibility: controllers and providers that use App\Domain\<Module>\... import paths continue to work — StarterKitServiceProvider registers class_alias entries that resolve them to the vendor namespace. A local app/Domain/<Module>/ copy, if present, always takes precedence (the guard skips the alias when the file exists on disk).

Surface ownership is split per module. Models always stay app-owned (so policy discovery and route-model binding keep working). For user-facing modules (User, Role, Dashboard, Auth, Profile) the Controllers, FormRequests, Vue pages, and route files are scaffolded into your app. For vendor-first behaviour modules (Files, Logs, Activity Logs, API Routes, Settings, …) the entire HTTP + Vue surface runs from the package — only their Models live in app/; run sk:eject <Module> to take ownership. See the Module Ownership table in the README.

Existing app copies: if your project was installed before a domain moved to vendor, your existing app/Domain/<Module>/ files are preserved and continue to work. Removing them is optional — see UPGRADE.md for reconcile steps.

Inside a domain you will usually see these layers:

  • Actions for write operations and use-case orchestration
  • DTOs for carrying validated data
  • Queries for list and datatable query logic
  • Events for domain events
  • Listeners for side effects such as logging
  • Repositories or Contracts when the domain needs abstraction

Request Flow

Typical flow:

  1. Controller receives the request.
  2. Form Request validates it.
  3. DTO normalizes the payload.
  4. Action performs the business operation.
  5. Event is fired if side effects are needed.
  6. Listener reacts without bloating the controller.
  7. Response returns through to_api() or an Inertia redirect.

Rules Of Thumb

  • keep controllers thin
  • keep validation in Form Requests
  • keep complex writes in Actions
  • keep reusable list logic in Queries
  • keep side effects in Listeners
  • keep kit-level shared cross-domain code under src/Domain/Shared; use app/Domain/Shared only for project-owned or ejected code

Why It Helps

  • better readability in large admin projects
  • easier testing of business operations
  • safer refactoring over time
  • less duplication between web and API controllers

The domain structure is supported by scaffolding commands, but the command reference lives in artisan-commands.md. This file exists specifically to keep DDD guidance separate from command documentation.

make:sk-domain core flags

Beyond the domain name and --fields=, the wizard's layer/ID/Vue choices can all be passed non-interactively:

Flag What it does
--fields="name:string,age:integer" Comma-separated field:type pairs. Available types: string, integer, bigInteger, unsignedBigInteger, float, decimal, boolean, text, longText, json, date, dateTime, timestamp. Omit to be prompted field-by-field.
--id-type=id|uuid|ulid Primary key strategy. id (default) is an auto-increment bigint; uuid/ulid add the matching HasUuids/HasUlids concern and switch the migration's id column. Prompts interactively when omitted — skipped entirely with --from-migration (detected from the file).
--api / --no-api Force-generate or force-skip the API controller + routes. Prompts (default: yes) when neither is passed.
--admin / --no-admin Force-generate or force-skip the Admin controller + routes. Prompts (default: yes) when neither is passed.
--events / --no-events Force-generate or force-skip the Created/Updated/Deleted events and their logging listeners. Prompts (default: yes) when neither is passed.
--soft-deletes / --no-soft-deletes Force-enable or force-disable SoftDeletes on the model and migration. Prompts (default: yes) when neither is passed — skipped entirely with --from-migration (detected from the file).
--vue=none|empty|full Vue page generation mode; only applies when the Admin layer is generated (forced to none otherwise). full scaffolds Index (DataTable) + Create/Edit (FormBuilder); empty scaffolds an empty Index page only; none skips Vue generation. Prompts interactively (default: full) when omitted.
--vue-fields / --no-vue-fields Only relevant with --vue=full. Include the model's fields in the generated DataTable columns and FormBuilder, or generate an id-only skeleton. Prompts (default: yes) when neither is passed and fields exist.
--from-migration=<filename> Parse fields, ID type, and soft-deletes from an existing migration file instead of --fields/--id-type/prompts, e.g. --from-migration=2026_03_21_create_products_table.php. Accepts a full or partial filename (glob-matched under database/migrations/).

make:sk-domain v2 opt-in flags

Calling the command without any flags preserves the v13.5.x behavior (backward compatible).

Individual flags:

Flag What it generates
--with-policy Policy class
--with-factory Factory
--with-seeder Seeder
--with-test Feature test
--with-relations Relation scaffold (use together with --relations)

Bulk syntax — pass any combination of policy, factory, seeder, test, relations in one flag (individual --with-* flags are additive on top of it):

php artisan make:sk-domain Article --with=policy,factory,test

Relations syntax:

php artisan make:sk-domain Article --with-relations --relations="belongsTo:User,hasMany:Comment,morphTo:commentable"

Supported relation types: belongsTo, hasMany, morphTo. Passing --relations= implies --with-relations.

Examples:

# Domain only — v13.5.x behavior, backward compatible
php artisan make:sk-domain Article

# Policy and factory
php artisan make:sk-domain Article --with-policy --with-factory

# Bulk syntax
php artisan make:sk-domain Article --with=policy,factory,test

# With relations
php artisan make:sk-domain Article --with-relations --relations="belongsTo:User,hasMany:Comment"

# Full
php artisan make:sk-domain Article --with=policy,factory,seeder,test,relations --relations="belongsTo:User,morphTo:commentable"