From 97495f99910499baec0e44390ed7adf1ab12af82 Mon Sep 17 00:00:00 2001 From: Syarif Ubaidillah Date: Tue, 7 Jul 2026 10:50:36 +0700 Subject: [PATCH] feat: scaffold domain-specific providers, add runtime relationship support, and improve event registration logic. --- app/Console/Commands/DomainMakeCommand.php | 34 ++--- app/Console/Commands/DomainNewCommand.php | 130 ++++++++++++++++++ .../stubs/domain-make/domain-provider.stub | 23 ++++ .../domain-make/relationship-provider.stub | 31 +++++ .../stubs/domain-make/view-provider.stub | 35 +++++ .../Providers/AccountServiceProvider.php | 15 +- .../Providers/RelationshipServiceProvider.php | 41 ++++++ app/Domains/Identity/Models/User.php | 5 - .../Providers/IdentityServiceProvider.php | 13 +- .../Providers/RelationshipServiceProvider.php | 30 ++++ .../Providers/RelationshipServiceProvider.php | 31 +++++ .../Providers/SystemServiceProvider.php | 24 ++-- .../System/Providers/ViewServiceProvider.php | 28 ++++ .../Traits/Provider/RegistersDomainEvents.php | 43 ++++++ app/Providers/EventServiceProvider.php | 39 ------ bootstrap/providers.php | 21 ++- config/audit.php | 2 +- lang/en/domains/identity/messages.php | 2 +- lang/id/domains/identity/messages.php | 2 +- routes/api.php | 1 - 20 files changed, 460 insertions(+), 90 deletions(-) create mode 100644 app/Console/Commands/DomainNewCommand.php create mode 100644 app/Console/stubs/domain-make/domain-provider.stub create mode 100644 app/Console/stubs/domain-make/relationship-provider.stub create mode 100644 app/Console/stubs/domain-make/view-provider.stub create mode 100644 app/Domains/Account/Providers/RelationshipServiceProvider.php create mode 100644 app/Domains/Identity/Providers/RelationshipServiceProvider.php create mode 100644 app/Domains/System/Providers/RelationshipServiceProvider.php create mode 100644 app/Domains/System/Providers/ViewServiceProvider.php create mode 100644 app/Domains/System/Traits/Provider/RegistersDomainEvents.php delete mode 100644 app/Providers/EventServiceProvider.php diff --git a/app/Console/Commands/DomainMakeCommand.php b/app/Console/Commands/DomainMakeCommand.php index 144be3b..c5552ab 100644 --- a/app/Console/Commands/DomainMakeCommand.php +++ b/app/Console/Commands/DomainMakeCommand.php @@ -9,7 +9,7 @@ use Illuminate\Support\Str; class DomainMakeCommand extends Command { protected $signature = 'domain:make - {type : Type to generate: model, action, dto, enum, event, listener, notification, policy, query, provider, export, mapper, scope, mailable} + {type : Type to generate: model, action, dto, enum, event, listener, notification, policy, query, provider, relationship-provider, export, mapper, scope, mailable} {domain : Domain name, e.g. Identity, Account, System} {name : Class name, supports sub-paths e.g. Backup/DeleteBackup} {--factory : Also generate a factory (model only)} @@ -20,22 +20,24 @@ class DomainMakeCommand extends Command /** @var array type → subdirectory */ protected array $types = [ - 'model' => 'Models', - 'action' => 'Actions', - 'dto' => 'DTOs', - 'enum' => 'Enums', - 'event' => 'Events', - 'listener' => 'Listeners', - 'notification' => 'Notifications', - 'policy' => 'Policies', - 'scope' => 'Scopes', - 'trait' => 'Traits', - 'query' => 'Queries', - 'provider' => 'Providers', - 'export' => 'Exports', + 'model' => 'Models', + 'action' => 'Actions', + 'dto' => 'DTOs', + 'enum' => 'Enums', + 'event' => 'Events', + 'listener' => 'Listeners', + 'notification' => 'Notifications', + 'policy' => 'Policies', + 'scope' => 'Scopes', + 'trait' => 'Traits', + 'query' => 'Queries', + 'provider' => 'Providers', + 'relationship-provider' => 'Providers', + 'view-provider' => 'Providers', + 'export' => 'Exports', // Integration layer — files live under Integration// - 'mapper' => 'Integration/Mappers', - 'mailable' => 'Mail', + 'mapper' => 'Integration/Mappers', + 'mailable' => 'Mail', ]; public function __construct(protected Filesystem $files) diff --git a/app/Console/Commands/DomainNewCommand.php b/app/Console/Commands/DomainNewCommand.php new file mode 100644 index 0000000..622a9fb --- /dev/null +++ b/app/Console/Commands/DomainNewCommand.php @@ -0,0 +1,130 @@ +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 {$domain} scaffolded", + '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), + ); + } +} diff --git a/app/Console/stubs/domain-make/domain-provider.stub b/app/Console/stubs/domain-make/domain-provider.stub new file mode 100644 index 0000000..068332f --- /dev/null +++ b/app/Console/stubs/domain-make/domain-provider.stub @@ -0,0 +1,23 @@ +app->register(RelationshipServiceProvider::class); + } + + public function boot(): void + { + $this->registerEvents(); + } +} diff --git a/app/Console/stubs/domain-make/relationship-provider.stub b/app/Console/stubs/domain-make/relationship-provider.stub new file mode 100644 index 0000000..c3b0e30 --- /dev/null +++ b/app/Console/stubs/domain-make/relationship-provider.stub @@ -0,0 +1,31 @@ + + * $order->belongsTo(Customer::class, 'customer_id') + * ); + */ + public function boot(): void + { + // Register cross-domain relationships below. + } +} diff --git a/app/Console/stubs/domain-make/view-provider.stub b/app/Console/stubs/domain-make/view-provider.stub new file mode 100644 index 0000000..1abe324 --- /dev/null +++ b/app/Console/stubs/domain-make/view-provider.stub @@ -0,0 +1,35 @@ +app->make(GetSidebarNavigationItems::class); + // $view->with('navigationItems', $query->execute()); + + $view->with('navigationItems', [ + [ + 'label' => 'Dashboard', + 'route' => '{domain}.dashboard', + 'icon' => 'home', + ], + ]); + }); + } +} diff --git a/app/Domains/Account/Providers/AccountServiceProvider.php b/app/Domains/Account/Providers/AccountServiceProvider.php index f790107..3988741 100644 --- a/app/Domains/Account/Providers/AccountServiceProvider.php +++ b/app/Domains/Account/Providers/AccountServiceProvider.php @@ -4,19 +4,26 @@ namespace App\Domains\Account\Providers; use App\Domains\Account\Listeners\SyncImportedUserProfile; use App\Domains\Identity\Events\Integration\UserImportWasProcessed; -use Illuminate\Support\Facades\Event; +use App\Domains\System\Traits\Provider\RegistersDomainEvents; use Illuminate\Support\ServiceProvider; class AccountServiceProvider extends ServiceProvider { - public static array $listen = [ + use RegistersDomainEvents; + + protected array $listen = [ UserImportWasProcessed::class => [ SyncImportedUserProfile::class, - ] + ], ]; + public function register(): void + { + $this->app->register(RelationshipServiceProvider::class); + } + public function boot(): void { - // + $this->registerEvents(); } } diff --git a/app/Domains/Account/Providers/RelationshipServiceProvider.php b/app/Domains/Account/Providers/RelationshipServiceProvider.php new file mode 100644 index 0000000..da85b67 --- /dev/null +++ b/app/Domains/Account/Providers/RelationshipServiceProvider.php @@ -0,0 +1,41 @@ + + * $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', + )); + } +} diff --git a/app/Domains/Identity/Models/User.php b/app/Domains/Identity/Models/User.php index 4450a9d..6989edd 100644 --- a/app/Domains/Identity/Models/User.php +++ b/app/Domains/Identity/Models/User.php @@ -90,9 +90,4 @@ class User extends Authenticatable implements Auditable, MustVerifyEmail { return $this->hasSingleFile('avatar'); } - - public function profile(): HasOne - { - return $this->hasOne(Profile::class); - } } diff --git a/app/Domains/Identity/Providers/IdentityServiceProvider.php b/app/Domains/Identity/Providers/IdentityServiceProvider.php index b44cb47..9e5e687 100644 --- a/app/Domains/Identity/Providers/IdentityServiceProvider.php +++ b/app/Domains/Identity/Providers/IdentityServiceProvider.php @@ -10,15 +10,15 @@ use App\Domains\Identity\Listeners\Authentication\SendSignInActivityNotification use App\Domains\Identity\Listeners\Governance\SendUserActivatedNotification; use App\Domains\Identity\Listeners\Governance\SendUserPurgedNotification; use App\Domains\Identity\Listeners\Governance\SendUserSuspendedNotification; -use App\Domains\Identity\Models\User; use App\Domains\Identity\Queries\GetAuthenticatedUserContext; -use App\Domains\System\Models\File; -use Illuminate\Support\Facades\Event; +use App\Domains\System\Traits\Provider\RegistersDomainEvents; use Illuminate\Support\ServiceProvider; class IdentityServiceProvider extends ServiceProvider { - public static array $listen = [ + use RegistersDomainEvents; + + protected array $listen = [ UserLoggedIn::class => [ SendSignInActivityNotification::class, ], @@ -30,16 +30,17 @@ class IdentityServiceProvider extends ServiceProvider ], UserWasSuspended::class => [ SendUserSuspendedNotification::class, - ] + ], ]; public function register(): void { + $this->app->register(RelationshipServiceProvider::class); $this->app->singleton(GetAuthenticatedUserContext::class, fn () => new GetAuthenticatedUserContext); } public function boot(): void { - File::resolveRelationUsing('uploader', fn (File $file) => $file->belongsTo(User::class, 'uploader_id')); + $this->registerEvents(); } } diff --git a/app/Domains/Identity/Providers/RelationshipServiceProvider.php b/app/Domains/Identity/Providers/RelationshipServiceProvider.php new file mode 100644 index 0000000..5694329 --- /dev/null +++ b/app/Domains/Identity/Providers/RelationshipServiceProvider.php @@ -0,0 +1,30 @@ + $file->belongsTo(User::class, 'uploader_id'), + ); + } +} diff --git a/app/Domains/System/Providers/RelationshipServiceProvider.php b/app/Domains/System/Providers/RelationshipServiceProvider.php new file mode 100644 index 0000000..8b4ce40 --- /dev/null +++ b/app/Domains/System/Providers/RelationshipServiceProvider.php @@ -0,0 +1,31 @@ + + * $order->belongsTo(Customer::class, 'customer_id') + * ); + */ + public function boot(): void + { + // Register cross-domain relationships below. + } +} diff --git a/app/Domains/System/Providers/SystemServiceProvider.php b/app/Domains/System/Providers/SystemServiceProvider.php index 374f613..cd6c08f 100644 --- a/app/Domains/System/Providers/SystemServiceProvider.php +++ b/app/Domains/System/Providers/SystemServiceProvider.php @@ -10,15 +10,17 @@ use App\Domains\System\Listeners\Excel\SendExportReportEmail; use App\Domains\System\Listeners\Excel\SendImportReportEmail; use App\Domains\System\Listeners\Files\RemoveUserFiles; use App\Domains\System\Queries\GetSystemSettings; +use App\Domains\System\Traits\Provider\RegistersDomainEvents; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; use OwenIt\Auditing\Models\Audit; use View; class SystemServiceProvider extends ServiceProvider { - public static array $listen = [ + use RegistersDomainEvents; + + protected array $listen = [ ExportCompleted::class => [ SendExportReportEmail::class, ], @@ -27,11 +29,13 @@ class SystemServiceProvider extends ServiceProvider ], UserWasPurged::class => [ RemoveUserFiles::class, - ] + ], ]; public function register(): void { + $this->app->register(RelationshipServiceProvider::class); + // Tell Laravel: "Whenever someone asks for GetSystemSettings, // give them the exact same object instance for the entire request." $this->app->singleton(GetSystemSettings::class, function ($app) { @@ -39,19 +43,9 @@ class SystemServiceProvider extends ServiceProvider }); } - /** @noinspection PhpInconsistentReturnPointsInspection */ - public function boot(GetSystemSettings $getSystemSettings): void + public function boot(): void { - View::composer(['components.layouts.*'], function ($view) use ($getSystemSettings) { - $view->with('logo', $getSystemSettings->get(SystemSettingKey::WEB_LOGO)); - $view->with('favicon', $getSystemSettings->get(SystemSettingKey::WEB_FAVICON)); - }); - - Audit::creating(function (Audit $model) { - if (empty($model->old_values) && empty($model->new_values)) { - return false; - } - }); + $this->registerEvents(); Carbon::macro('toUserTz', fn () => $this->copy() ->tz(config('app.display_timezone', 'UTC'))); diff --git a/app/Domains/System/Providers/ViewServiceProvider.php b/app/Domains/System/Providers/ViewServiceProvider.php new file mode 100644 index 0000000..c69f6ca --- /dev/null +++ b/app/Domains/System/Providers/ViewServiceProvider.php @@ -0,0 +1,28 @@ +with('logo', $getSystemSettings->get(SystemSettingKey::WEB_LOGO)); + $view->with('favicon', $getSystemSettings->get(SystemSettingKey::WEB_FAVICON)); + }); + } +} diff --git a/app/Domains/System/Traits/Provider/RegistersDomainEvents.php b/app/Domains/System/Traits/Provider/RegistersDomainEvents.php new file mode 100644 index 0000000..c0e8390 --- /dev/null +++ b/app/Domains/System/Traits/Provider/RegistersDomainEvents.php @@ -0,0 +1,43 @@ + [ + * 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> $listen */ + $listen = $this->listen ?? []; + + foreach ($listen as $event => $listeners) { + foreach ($listeners as $listener) { + Event::listen($event, $listener); + } + } + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php deleted file mode 100644 index fafe0dd..0000000 --- a/app/Providers/EventServiceProvider.php +++ /dev/null @@ -1,39 +0,0 @@ -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); - } - } - } - } - } -} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index cd6427b..406473f 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,8 +3,27 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\UiServiceProvider::class, - App\Providers\EventServiceProvider::class, OwenIt\Auditing\AuditingServiceProvider::class, + + // Domain: Account + App\Domains\Account\Providers\AccountServiceProvider::class, + + // Domain: Identity App\Domains\Identity\Providers\IdentityServiceProvider::class, + + // Domain: System App\Domains\System\Providers\SystemServiceProvider::class, + + + // Domain: Finance + App\Domains\Finance\Providers\FinanceServiceProvider::class, + + // Domain: Channel + App\Domains\Channel\Providers\ChannelServiceProvider::class, + + // Domain: Admission + App\Domains\Admission\Providers\AdmissionServiceProvider::class, + + // Domain: Academic + App\Domains\Academic\Providers\AcademicServiceProvider::class, ]; diff --git a/config/audit.php b/config/audit.php index 46f6dd1..0f9f0cf 100644 --- a/config/audit.php +++ b/config/audit.php @@ -107,7 +107,7 @@ return [ | */ - 'empty_values' => true, + 'empty_values' => false, 'allowed_empty_values' => [ 'retrieved', ], diff --git a/lang/en/domains/identity/messages.php b/lang/en/domains/identity/messages.php index d983cd3..7871640 100644 --- a/lang/en/domains/identity/messages.php +++ b/lang/en/domains/identity/messages.php @@ -4,7 +4,7 @@ return [ 'success' => [ 'password_reset_link_sent' => 'Password reset link has been sent.', 'user_role_updated' => 'User role has been updated.', - 'user_status_updated' => 'User status has been updated.' + 'user_status_updated' => 'User status has been updated.', ], 'exceptions' => [ 'failed_to_send_password_reset_link' => 'Failed to send password reset link.', diff --git a/lang/id/domains/identity/messages.php b/lang/id/domains/identity/messages.php index daed0ce..845d1b9 100644 --- a/lang/id/domains/identity/messages.php +++ b/lang/id/domains/identity/messages.php @@ -4,7 +4,7 @@ return [ 'success' => [ 'password_reset_link_sent' => 'Tautan atur ulang kata sandi telah dikirim.', 'user_role_updated' => 'Peran pengguna telah diperbarui.', - 'user_status_updated' => 'Status pengguna telah diperbarui.' + 'user_status_updated' => 'Status pengguna telah diperbarui.', ], 'exceptions' => [ 'failed_to_send_password_reset_link' => 'Gagal mengirim tautan atur ulang kata sandi.', diff --git a/routes/api.php b/routes/api.php index 880c6d4..f535302 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,5 @@