20 changed files with 460 additions and 90 deletions
@ -0,0 +1,130 @@ |
|||
<?php |
|||
|
|||
namespace App\Console\Commands; |
|||
|
|||
use Illuminate\Console\Command; |
|||
use Illuminate\Filesystem\Filesystem; |
|||
|
|||
class DomainNewCommand extends Command |
|||
{ |
|||
protected $signature = 'domain:new |
|||
{domain : Domain name, e.g. Order, Payment, Billing}'; |
|||
|
|||
protected $description = 'Scaffold a new domain with a service provider and a relationship provider'; |
|||
|
|||
public function __construct(protected Filesystem $files) |
|||
{ |
|||
parent::__construct(); |
|||
} |
|||
|
|||
public function handle(): int |
|||
{ |
|||
$domain = ucfirst($this->argument('domain')); |
|||
$namespace = "App\\Domains\\{$domain}\\Providers"; |
|||
$providerDir = app_path("Domains/{$domain}/Providers"); |
|||
|
|||
if ($this->files->isDirectory($providerDir) && count($this->files->files($providerDir)) > 0) { |
|||
$this->components->error( |
|||
"Domain [{$domain}] already has providers. ". |
|||
"Use [domain:make provider] to add individual providers." |
|||
); |
|||
|
|||
return self::FAILURE; |
|||
} |
|||
|
|||
$this->files->ensureDirectoryExists($providerDir); |
|||
|
|||
$domainProviderClass = "{$domain}ServiceProvider"; |
|||
|
|||
if (! $this->createDomainProvider($providerDir, $namespace, $domainProviderClass)) { |
|||
return self::FAILURE; |
|||
} |
|||
|
|||
if (! $this->createRelationshipProvider($providerDir, $namespace)) { |
|||
return self::FAILURE; |
|||
} |
|||
|
|||
$this->registerInBootstrap($domain, $namespace, $domainProviderClass); |
|||
|
|||
$this->components->twoColumnDetail( |
|||
"Domain <fg=green;options=bold>{$domain}</> scaffolded", |
|||
'<fg=green;options=bold>DONE</>' |
|||
); |
|||
|
|||
return self::SUCCESS; |
|||
} |
|||
|
|||
// ─── Generators ────────────────────────────────────────────────────────── |
|||
|
|||
protected function createDomainProvider(string $dir, string $namespace, string $class): bool |
|||
{ |
|||
$path = "{$dir}/{$class}.php"; |
|||
$content = $this->buildFromStub('domain-provider', $namespace, $class); |
|||
|
|||
if ($content === '') { |
|||
return false; |
|||
} |
|||
|
|||
$this->files->put($path, $content); |
|||
$this->components->info("Provider [{$path}] created successfully."); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
protected function createRelationshipProvider(string $dir, string $namespace): bool |
|||
{ |
|||
$path = "{$dir}/RelationshipServiceProvider.php"; |
|||
$content = $this->buildFromStub('relationship-provider', $namespace, 'RelationshipServiceProvider'); |
|||
|
|||
if ($content === '') { |
|||
return false; |
|||
} |
|||
|
|||
$this->files->put($path, $content); |
|||
$this->components->info("Provider [{$path}] created successfully."); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
// ─── Bootstrap Registration ─────────────────────────────────────────────── |
|||
|
|||
/** |
|||
* Append the new domain service provider to bootstrap/providers.php. |
|||
* |
|||
* Inserts a commented domain block immediately before the closing ]; |
|||
* so the file stays readable and grouped by domain. |
|||
*/ |
|||
protected function registerInBootstrap(string $domain, string $namespace, string $class): void |
|||
{ |
|||
$bootstrapPath = base_path('bootstrap/providers.php'); |
|||
$content = $this->files->get($bootstrapPath); |
|||
|
|||
$entry = "\n // Domain: {$domain}\n {$namespace}\\{$class}::class,\n"; |
|||
|
|||
// Anchor on the closing ]; — safe because providers.php has exactly one |
|||
$content = preg_replace('/(\];)(\s*)$/', "{$entry}];$2", rtrim($content))."\n"; |
|||
|
|||
$this->files->put($bootstrapPath, $content); |
|||
|
|||
$this->components->info("Domain [{$domain}] registered in [bootstrap/providers.php]."); |
|||
} |
|||
|
|||
// ─── Stubs ──────────────────────────────────────────────────────────────── |
|||
|
|||
protected function buildFromStub(string $stubName, string $namespace, string $class): string |
|||
{ |
|||
$stubPath = app_path("Console/stubs/domain-make/{$stubName}.stub"); |
|||
|
|||
if (! $this->files->exists($stubPath)) { |
|||
$this->components->error("Stub file not found: [{$stubPath}]"); |
|||
|
|||
return ''; |
|||
} |
|||
|
|||
return str_replace( |
|||
['{{ namespace }}', '{{ class }}'], |
|||
[$namespace, $class], |
|||
$this->files->get($stubPath), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
namespace {{ namespace }}; |
|||
|
|||
use App\Domains\System\Traits\Provider\RegistersDomainEvents; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class {{ class }} extends ServiceProvider |
|||
{ |
|||
use RegistersDomainEvents; |
|||
|
|||
protected array $listen = []; |
|||
|
|||
public function register(): void |
|||
{ |
|||
$this->app->register(RelationshipServiceProvider::class); |
|||
} |
|||
|
|||
public function boot(): void |
|||
{ |
|||
$this->registerEvents(); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
namespace {{ namespace }}; |
|||
|
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class {{ class }} extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Boot cross-domain runtime relationships. |
|||
* |
|||
* This provider is strictly dedicated to runtime, decoupled cross-domain |
|||
* relationship definitions. Use Model::resolveRelationUsing() here to wire |
|||
* explicit polymorphic or foreign-key macros between models that live in |
|||
* different domains — without introducing a hard compile-time dependency |
|||
* between those domains. |
|||
* |
|||
* Do NOT add route registrations, bindings, event listeners, or any other |
|||
* bootstrapping logic here. Keep this file focused on relationships only. |
|||
* |
|||
* Example: |
|||
* |
|||
* Order::resolveRelationUsing('customer', fn (Order $order) => |
|||
* $order->belongsTo(Customer::class, 'customer_id') |
|||
* ); |
|||
*/ |
|||
public function boot(): void |
|||
{ |
|||
// Register cross-domain relationships below. |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
namespace {{ namespace }}; |
|||
|
|||
use Illuminate\Support\Facades\View; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class {{ class }} extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Bootstrap any application services. |
|||
*/ |
|||
public function boot(): void |
|||
{ |
|||
/** |
|||
* Register a view composer for presentation glue. |
|||
* |
|||
* This binds data to specific layout assets (e.g., sidebar) using |
|||
* a decoupled query pattern, keeping the domain's root provider pristine. |
|||
*/ |
|||
View::composer('components.layouts.sidebar', function ($view) { |
|||
// Instantiate the internal domain model query dynamically or via DI container |
|||
// $query = $this->app->make(GetSidebarNavigationItems::class); |
|||
// $view->with('navigationItems', $query->execute()); |
|||
|
|||
$view->with('navigationItems', [ |
|||
[ |
|||
'label' => 'Dashboard', |
|||
'route' => '{domain}.dashboard', |
|||
'icon' => 'home', |
|||
], |
|||
]); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Account\Providers; |
|||
|
|||
use App\Domains\Account\Models\Profile; |
|||
use App\Domains\Identity\Models\User; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class RelationshipServiceProvider extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Boot cross-domain runtime relationships. |
|||
* |
|||
* This provider is strictly dedicated to runtime, decoupled cross-domain |
|||
* relationship definitions. Use Model::resolveRelationUsing() here to wire |
|||
* explicit polymorphic or foreign-key macros between models that live in |
|||
* different domains — without introducing a hard compile-time dependency |
|||
* between those domains. |
|||
* |
|||
* Do NOT add route registrations, bindings, event listeners, or any other |
|||
* bootstrapping logic here. Keep this file focused on relationships only. |
|||
* |
|||
* Example: |
|||
* |
|||
* Order::resolveRelationUsing('customer', fn (Order $order) => |
|||
* $order->belongsTo(Customer::class, 'customer_id') |
|||
* ); |
|||
*/ |
|||
public function boot(): void |
|||
{ |
|||
User::resolveRelationUsing('profile', fn (User $user) => $user->hasOne( |
|||
related: Profile::class, |
|||
foreignKey: 'user_id' |
|||
)); |
|||
Profile::resolveRelationUsing('user', fn (Profile $profile) => $profile->belongsTo( |
|||
related: User::class, |
|||
foreignKey: 'user_id', |
|||
ownerKey: 'id', |
|||
)); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\Identity\Providers; |
|||
|
|||
use App\Domains\Identity\Models\User; |
|||
use App\Domains\System\Models\File; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class RelationshipServiceProvider extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Boot cross-domain runtime relationships. |
|||
* |
|||
* This provider is strictly dedicated to runtime, decoupled cross-domain |
|||
* relationship definitions. Use Model::resolveRelationUsing() here to wire |
|||
* explicit polymorphic or foreign-key macros between models that live in |
|||
* different domains — without introducing a hard compile-time dependency |
|||
* between those domains. |
|||
* |
|||
* Do NOT add route registrations, bindings, event listeners, or any other |
|||
* bootstrapping logic here. Keep this file focused on relationships only. |
|||
*/ |
|||
public function boot(): void |
|||
{ |
|||
File::resolveRelationUsing( |
|||
'uploader', |
|||
fn (File $file) => $file->belongsTo(User::class, 'uploader_id'), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\System\Providers; |
|||
|
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class RelationshipServiceProvider extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Boot cross-domain runtime relationships. |
|||
* |
|||
* This provider is strictly dedicated to runtime, decoupled cross-domain |
|||
* relationship definitions. Use Model::resolveRelationUsing() here to wire |
|||
* explicit polymorphic or foreign-key macros between models that live in |
|||
* different domains — without introducing a hard compile-time dependency |
|||
* between those domains. |
|||
* |
|||
* Do NOT add route registrations, bindings, event listeners, or any other |
|||
* bootstrapping logic here. Keep this file focused on relationships only. |
|||
* |
|||
* Example: |
|||
* |
|||
* Order::resolveRelationUsing('customer', fn (Order $order) => |
|||
* $order->belongsTo(Customer::class, 'customer_id') |
|||
* ); |
|||
*/ |
|||
public function boot(): void |
|||
{ |
|||
// Register cross-domain relationships below. |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\System\Providers; |
|||
|
|||
use App\Domains\System\Enums\SystemSettingKey; |
|||
use App\Domains\System\Queries\GetSystemSettings; |
|||
use Illuminate\Support\Facades\View; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class ViewServiceProvider extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Bootstrap any application services. |
|||
*/ |
|||
public function boot(GetSystemSettings $getSystemSettings): void |
|||
{ |
|||
/** |
|||
* Register a view composer for presentation glue. |
|||
* |
|||
* This binds data to specific layout assets (e.g., sidebar) using |
|||
* a decoupled query pattern, keeping the domain's root provider pristine. |
|||
*/ |
|||
View::composer(['components.layouts.*'], function ($view) use ($getSystemSettings) { |
|||
$view->with('logo', $getSystemSettings->get(SystemSettingKey::WEB_LOGO)); |
|||
$view->with('favicon', $getSystemSettings->get(SystemSettingKey::WEB_FAVICON)); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<?php |
|||
|
|||
namespace App\Domains\System\Traits\Provider; |
|||
|
|||
use Illuminate\Support\Facades\Event; |
|||
|
|||
/** |
|||
* Provides self-contained event registration for domain service providers. |
|||
* |
|||
* Usage: define a protected (or public) $listen array on the using class, |
|||
* mapping event class-strings to arrays of listener class-strings, then call |
|||
* registerEvents() — typically inside boot(). |
|||
* |
|||
* Example: |
|||
* |
|||
* protected array $listen = [ |
|||
* OrderPlaced::class => [ |
|||
* SendOrderConfirmation::class, |
|||
* UpdateInventory::class, |
|||
* ], |
|||
* ]; |
|||
*/ |
|||
trait RegistersDomainEvents |
|||
{ |
|||
/** |
|||
* Register all events declared in the $listen property. |
|||
* |
|||
* Iterates over $this->listen (which may be declared as public, protected, |
|||
* or static on the consuming class) and delegates each event–listener pair |
|||
* to Laravel's Event facade. No filesystem scanning is performed. |
|||
*/ |
|||
public function registerEvents(): void |
|||
{ |
|||
/** @var array<class-string, list<class-string>> $listen */ |
|||
$listen = $this->listen ?? []; |
|||
|
|||
foreach ($listen as $event => $listeners) { |
|||
foreach ($listeners as $listener) { |
|||
Event::listen($event, $listener); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Providers; |
|||
|
|||
use Illuminate\Support\Facades\Event; |
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class EventServiceProvider extends ServiceProvider |
|||
{ |
|||
/** |
|||
* Bootstrap services. |
|||
*/ |
|||
public function boot(): void |
|||
{ |
|||
$this->buildDomainEvent(); |
|||
} |
|||
|
|||
public function buildDomainEvent(): void |
|||
{ |
|||
$domainProviders = glob(app_path('Domains/*/Providers/*ServiceProvider.php')); |
|||
|
|||
foreach ($domainProviders as $file) { |
|||
// Convert file path directly to its full PHP Namespace |
|||
// Example: /var/www/app/Domains/Identity/Providers/IdentityServiceProvider.php |
|||
// Becomes: App\Domains\Identity\Providers\IdentityServiceProvider |
|||
$relativePath = str_replace(app_path(), 'App', $file); |
|||
$className = str_replace(['/', '.php'], ['\\', ''], $relativePath); |
|||
|
|||
// If the class exists and contains our static $listen array, register it |
|||
if (class_exists($className) && property_exists($className, 'listen')) { |
|||
foreach ($className::$listen as $event => $listeners) { |
|||
foreach ((array) $listeners as $listener) { |
|||
Event::listen($event, $listener); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue