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