adhenrique/laravel-domain-oriented
A Laravel package for organizing applications by domain rather than technical layers, helping group models, actions, and services into modular domain-oriented structures for clearer boundaries and more scalable codebases.
This is a thin scaffolding convention, not a framework extension, so the fastest path to understanding it is reading src/ and stubs/ rather than expecting docs. The README is essentially a two-line description plus a changelog.
composer require adhenrique/laravel-domain-oriented
php artisan list | grep -i domain # discover the generator commands
php artisan vendor:publish --tag=domain # publish config/stubs if provided
Order of inspection after install:
config/domain-oriented.php) and note the namespace root and folder layout it expects, e.g. App\Domains vs Domains. If it is not App\Domains\..., add a matching PSR-4 entry to composer.json and composer dump-autoload before scaffolding anything.make:domain, make:domain-controller, make:domain-service. Run one against a throwaway name and read what it emits; the generated files are the documentation for the intended base classes.src/ — specifically ValidateService (getRules() became public in v2.1.1, which tells you the intended call site) and the Resource base (a field-name typo was fixed in v2.1.1 — check whether your generated stubs reference the old or new name).First use case: scaffold exactly one thin vertical slice against a real endpoint — e.g. Billing/Invoices with a GET index. Generate the domain, put the route in the domain's routes file, write a single ValidateService rule set, and confirm the full request → Controller → Service → Resource path returns correct JSON. Do not generate a second domain until this one works end-to-end; a package this small is either obvious within one slice or not worth the integration cost.
Generator-driven vertical slices. The day-to-day workflow is: make:domain X to create the bounded context, then per-artifact generators to add a controller, service, repository, model, event, or resource. Resist hand-writing files into the domain tree unless you have already matched the stub output — mismatched base classes are the most common way this convention degrades.
Domain owns its routes and providers. Each domain is self-contained: a routes file, a service provider, and its own migrations. Register the domain provider explicitly in config/app.php (or rely on auto-discovery if the package scans a configured directory). Keep the domain provider doing only two things — route registration and container bindings — so the domain stays removable.
// app/Domains/Billing/Providers/BillingServiceProvider.php
public function boot(): void
{
$this->loadRoutesFrom(__DIR__.'/../Routes/api.php');
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
}
Validation lives in the domain, not in FormRequests. Use the ValidateService base: extend it, override getRules(), and call it from the controller (or inject it). Since getRules() is public as of v2.1.1, you can also compose rule sets in tests or reuse them across two entry points.
class CreateInvoiceValidateService extends ValidateService
{
public function getRules(): array
{
return [
'customer_id' => ['required', 'uuid', 'exists:customers,id'],
'amount' => ['required', 'integer', 'min:1'],
];
}
}
Cross-domain is service-to-service, never model-to-model. The whole point of the layout is bounded contexts. If Billing needs customer data, inject a Customers service (or listen to an event the Customers domain emits) rather than reaching into App\Domains\Customers\Models\Customer. This is the single discipline that makes the structure pay off; the package itself enforces nothing here.
Migrate opportunistically. When you touch an existing controller, move that slice into a domain rather than scheduling a restructure. Big-bang migration across a mature app/Http/Controllers tree is where these adoptions die.
composer.json constraints first; on Laravel 10+ expect composer require to fail or the base classes to break. Do not start an upgrade to Laravel 10 while still consuming this package upstream.composer.json PSR-4 map must agree. A generator that writes App\Domains\... into a project whose autoloader does not map it produces classes that resolve in dev (via classmap scan) and fail elsewhere. composer dump-autoload after every namespace/config change.Resource field and widened getRules() visibility. Pin the exact version ("2.1.1", not ^2.1) and avoid depending on Resource field names in serialized API output you cannot re-test.loadMigrationsFrom merges everything into one chronological run. Two domains adding migrations in the same minute can produce non-deterministic order on a fresh migrate. Keep foreign-key-creating domains' migrations named so timestamps don't collide, and prefer php artisan migrate --path=app/Domains/X/Database/Migrations when iterating on one domain.ValidateService or crossing domain boundaries. Pair adoption with deptrac or PHPStan layered rules, or the convention erodes within a quarter or two.config/app.php. Auditability is worth more than the convenience here.php artisan route:list --path=billing to confirm a domain's routes actually registered — a missing loadRoutesFrom call is silent. If routes vanish after a config change, run php artisan config:clear; the package's namespace config is often cached.app/Domain/Support/ (with attribution) removes the dependency, lets you target your PHP version, and makes upgrades a non-event. For a package in this adoption state, vendoring is usually the correct first move rather than the fallback.How can I help you explore Laravel packages today?