From a0a5c0693db61c0c12e9669bb5cce86229a79163 Mon Sep 17 00:00:00 2001 From: Peter Elmered Date: Mon, 25 Mar 2024 15:18:20 +0100 Subject: [PATCH] First pass for domain autoloader --- config/ddd.php | 35 ++++ src/LaravelDDDServiceProvider.php | 6 + src/Support/DomainAutoloader.php | 154 ++++++++++++++++++ src/Support/Path.php | 9 + .../AutoloadServiceProviderTest.php | 8 + 5 files changed, 212 insertions(+) create mode 100644 src/Support/DomainAutoloader.php create mode 100644 tests/Autoloader/AutoloadServiceProviderTest.php diff --git a/config/ddd.php b/config/ddd.php index e7dd5bf..a763011 100644 --- a/config/ddd.php +++ b/config/ddd.php @@ -44,8 +44,43 @@ 'view_models' => 'ViewModels', 'value_objects' => 'ValueObjects', 'actions' => 'Actions', + 'factories' => 'Database\Factories', ], + + 'autoload' => [ + /* + | Autoload service providers from the domain namespace. + | By default, it loads any file that ends with 'ServiceProvider.php' inside your domain. + | For example: Domain/Invoicing/Providers/InvoicingServiceProvider.php or Domain/Invoicing/InvoicingServiceProvider.php + */ + // To customize the pattern, you can use a glob pattern like '*/Providers/*.php' + 'service_providers' => '*/*ServiceProvider.php', + + /* + | Autoload commands from the domain namespace. + | By default, it loads any file inside the /Commands folder that ends '.php', extends Illuminate\Console\Command and is not abstract. + | For example: Domain/Invoicing/Commands/CreateInvoiceCommand.php + */ + // To customize the pattern, you can use a glob pattern like '*/Commands/*.php' + 'commands' => '*/Commands/*.php', + + /* + | Autoload policies from the domain namespace. + | By default, it loads any file inside the /Policies folder that ends 'Policy.php' and is not abstract. + | For example: Domain/Invoicing/Policies/InvoicePolicy.php + */ + 'policies' => 'Policies\\{model}Policy', + + /* + | Autoload factories from the domain namespace. + | By default, it loads any file inside the /Database/Factories folder that ends 'Factory.php' and is not abstract. + | For example: Domain/Invoicing/Database/Factories/InvoiceFactory.php + */ + 'factories' => 'Database\\Factories\\{model}Factory', + ], + + /* |-------------------------------------------------------------------------- | Base Model diff --git a/src/LaravelDDDServiceProvider.php b/src/LaravelDDDServiceProvider.php index 5827049..dbebc0c 100644 --- a/src/LaravelDDDServiceProvider.php +++ b/src/LaravelDDDServiceProvider.php @@ -11,6 +11,7 @@ use Lunarstorm\LaravelDDD\Commands\MakeModel; use Lunarstorm\LaravelDDD\Commands\MakeValueObject; use Lunarstorm\LaravelDDD\Commands\MakeViewModel; +use Lunarstorm\LaravelDDD\Support\DomainAutoloader; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -45,4 +46,9 @@ public function packageBooted() $this->package->basePath('/../stubs') => resource_path("stubs/{$this->package->shortName()}"), ], "{$this->package->shortName()}-stubs"); } + + public function packageRegistered() + { + (new DomainAutoloader())->autoload(); + } } diff --git a/src/Support/DomainAutoloader.php b/src/Support/DomainAutoloader.php new file mode 100644 index 0000000..1b00472 --- /dev/null +++ b/src/Support/DomainAutoloader.php @@ -0,0 +1,154 @@ +config = config('ddd'); + } + + public function autoload(): void + { + if(isset($this->config['autoload.service_providers'])) { + $this->registerDomainServiceProviders($this->config['autoload.service_providers']); + } + if(isset($this->config['autoload.commands'])) { + $this->registerDomainCommands($this->config['autoload.commands']); + } + if(isset($this->config['autoload.policies'])) { + $this->registerPolicies($this->config['autoload.policies']); + } + if(isset($this->config['autoload.factories'])) { + $this->registerFactories($this->config['autoload.factories']); + } + } + + protected function registerDomainServiceProviders(bool|string $domainPath = null): void + { + $domainPath = is_string($domainPath) ? $domainPath : '*/*ServiceProvider.php'; + + $serviceProviders = Cache::rememberForever('ddd-domain-service-providers', static function () use ($domainPath){ + return Arr::map( + glob(base_path(DomainResolver::getConfiguredDomainPath().'/'.$domainPath)), + (static function ($serviceProvider) { + return Path::filePathToNamespace( + $serviceProvider, + DomainResolver::getConfiguredDomainPath(), + DomainResolver::getConfiguredDomainNamespace() + ); + })); + }); + + $app = app(); + foreach ($serviceProviders as $serviceProvider) { + $app->register($serviceProvider); + } + } + + protected function registerDomainCommands(bool|string $domainPath = null): void + { + $domainPath = is_string($domainPath) ? $domainPath : '*/Commands/*.php'; + $commands = Cache::rememberForever('ddd-domain-commands', static function () use ($domainPath){ + $commands = Arr::map( + glob(base_path(DomainResolver::getConfiguredDomainPath().'/'.$domainPath)), + static function ($command) { + return Path::filePathToNamespace( + $command, + DomainResolver::getConfiguredDomainPath(), + DomainResolver::getConfiguredDomainNamespace() + ); + }); + + // Filter out invalid commands (Abstract classes and classes not extending Illuminate\Console\Command) + return Arr::where($commands, static function($command) { + if (is_subclass_of($command, Command::class) && + ! (new ReflectionClass($command))->isAbstract()) { + ConsoleApplication::starting(static function ($artisan) use ($command): void { + $artisan->resolve($command); + }); + } + }); + }); + ConsoleApplication::starting(static function ($artisan) use ($commands): void { + foreach ($commands as $command) { + $artisan->resolve($command); + } + }); + } + + protected function registerPolicies(bool|string $domainPath = null): void + { + $domainPath = is_string($domainPath) ? $domainPath : 'Policies\\{model}Policy'; + + Gate::guessPolicyNamesUsing(static function (string $modelClass) use ($domainPath): ?string { + + [$domain, $model] = static::extractDomainAndModelFromModelNamespace($modelClass); + + if (is_null($domain)) { + return null; + } + + $policy = DomainResolver::getConfiguredDomainNamespace().'\\'.$domain.'\\'.str_replace('{model}', $model, $domainPath); + + return $policy; + }); + } + + protected function registerFactories(bool|string $domainPath = null): void + { + $domainPath = is_string($domainPath) ? $domainPath : 'Database\\Factories\\{model}Factory'; + + Factory::guessFactoryNamesUsing( function (string $modelClass) use ($domainPath){ + + [$domain, $model] = $this->extractDomainAndModelFromModelNamespace($modelClass); + + if (is_null($domain)) { + return null; + } + + // Look for domain model factory in \\Database\\Factories\Factory.php + $classPath = 'Domain\\'.$domain.'\\'.str_replace('{model}', $model, $domainPath); + if (class_exists($classPath)) { + return $classPath; + } + + // Look for domain factory in /database/factories//Factory.php + $classPath = 'Database\\Factories\\'.$domain.'\\'.$model.'Factory'; + if (class_exists($classPath)) { + return $classPath; + } + + // Default Laravel factory location + return 'Database\Factories\\'.class_basename($modelClass).'Factory'; + }); + } + + protected function extractDomainAndModelFromModelNamespace(string $modelName): array + { + // Matches \{domain}\\{model} and extracts domain and model + // For example: Domain\Invoicing\Models\Invoice gives ['domain' => 'Invoicing', 'model' => 'Invoice'] + $regex = '/'.DomainResolver::getConfiguredDomainNamespace().'\\\\(?.+)\\\\'.$this->config['namespaces.models'].'\\\\(?.+)/'; + + if (preg_match($regex, $modelName, $matches, PREG_OFFSET_CAPTURE, 0)) { + return [ + 'domain' => $matches['domain'][0], + 'model' => $matches['model'][0] + ]; + } + + return []; + } +} diff --git a/src/Support/Path.php b/src/Support/Path.php index c516dbd..50e9e6c 100644 --- a/src/Support/Path.php +++ b/src/Support/Path.php @@ -17,4 +17,13 @@ public static function join(...$parts) return implode(DIRECTORY_SEPARATOR, $parts); } + + public static function filePathToNamespace(string $path, string $namespacePath, string $namespace): string + { + return str_replace( + [base_path().$namespacePath, '/', '.php'], + [$namespace, '\\', ''], + $path + ); + } } diff --git a/tests/Autoloader/AutoloadServiceProviderTest.php b/tests/Autoloader/AutoloadServiceProviderTest.php new file mode 100644 index 0000000..f205325 --- /dev/null +++ b/tests/Autoloader/AutoloadServiceProviderTest.php @@ -0,0 +1,8 @@ +