From 4661c3a332854fe611ad2f1e56625206871ca88a Mon Sep 17 00:00:00 2001 From: Jasper Tey Date: Wed, 13 Nov 2024 01:28:16 -0500 Subject: [PATCH] Improved config management plus test coverage. --- README.md | 17 +- composer.json | 5 +- config/ddd.php | 9 +- config/ddd.php.stub | 78 +++----- src/Commands/ConfigCommand.php | 60 ++++--- src/Commands/DomainListCommand.php | 4 +- src/Commands/InstallCommand.php | 70 ++++---- src/Commands/UpgradeCommand.php | 60 +++++-- src/ComposerManager.php | 7 + src/ConfigManager.php | 148 +++++++++++++++ src/DomainManager.php | 5 + src/Facades/DDD.php | 1 + src/LaravelDDDServiceProvider.php | 4 + tests/Command/ConfigTest.php | 180 +++++++++++++++++++ tests/Command/InstallTest.php | 16 +- tests/Command/resources/composer.sample.json | 26 +++ tests/Config/ManagerTest.php | 51 ++++++ tests/Config/resources/config.sparse.php | 25 +++ tests/Datasets/resources/config.0.10.0.php | 132 +++++++------- tests/TestCase.php | 1 + 20 files changed, 682 insertions(+), 217 deletions(-) create mode 100755 src/ConfigManager.php create mode 100644 tests/Command/ConfigTest.php create mode 100644 tests/Command/resources/composer.sample.json create mode 100644 tests/Config/ManagerTest.php create mode 100644 tests/Config/resources/config.sparse.php diff --git a/README.md b/README.md index 82c9c55..753d582 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Domain Driven Design toolkit for Laravel +# Domain Driven Design Toolkit for Laravel [![Latest Version on Packagist](https://img.shields.io/packagist/v/lunarstorm/laravel-ddd.svg?style=flat-square)](https://packagist.org/packages/lunarstorm/laravel-ddd) [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/lunarstorm/laravel-ddd/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/lunarstorm/laravel-ddd/actions?query=workflow%3Arun-tests+branch%3Amain) @@ -21,16 +21,9 @@ php artisan ddd:install ### Peer Dependencies The following additional packages are suggested (but not required) while working with this package. +- Data Transfer Objects: [spatie/laravel-data](https://github.com/spatie/laravel-data) +- Actions: [lorisleiva/laravel-actions](https://github.com/lorisleiva/laravel-actions) -Data Transfer Objects: [spatie/laravel-data](https://github.com/spatie/laravel-data) -```bash -composer require spatie/laravel-data -``` - -Actions: [lorisleiva/laravel-actions](https://github.com/lorisleiva/laravel-actions) -```bash -composer require lorisleiva/laravel-actions -``` The default DTO and Action stubs of this package reference classes from these packages. If this doesn't apply to your application, you may [customize the stubs](#publishing-stubs-advanced) accordingly. ### Deployment @@ -38,7 +31,7 @@ In production, run `ddd:optimize` during the deployment process to [optimize aut ```bash php artisan ddd:optimize ``` -Since Laravel 11.27.1, `php artisan optimize` automatically invokes `ddd:optimize`. If you already run `optimize` in production, a separate `ddd:optimize` is no longer necessary. +Since Laravel 11.27.1, `php artisan optimize` automatically invokes `ddd:optimize`. If you already run `optimize` in production, a separate `ddd:optimize` is no longer necessary. In previous versions of this package, this command was named `ddd:cache`, which will continue to work as an alias. ### Version Compatibility Laravel | LaravelDDD | | @@ -393,8 +386,8 @@ return [ | */ 'application' => [ - 'namespace' => 'App\Modules', 'path' => 'app/Modules', + 'namespace' => 'App\Modules', 'objects' => [ 'controller', 'request', diff --git a/composer.json b/composer.json index fd4f4e5..7bcfdf1 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,14 @@ "require": { "php": "^8.1|^8.2|^8.3", "illuminate/contracts": "^10.25|^11.0", + "laravel/pint": "^1.18", "laravel/prompts": "^0.1.16|^0.3.1", "lorisleiva/lody": "^0.5.0", - "spatie/laravel-package-tools": "^1.13.0" + "spatie/laravel-package-tools": "^1.13.0", + "symfony/var-exporter": "^7.1" }, "require-dev": { "larastan/larastan": "^2.0.1", - "laravel/pint": "^1.0", "nunomaduro/collision": "^7.0|^8.1", "orchestra/testbench": "^8|^9.0", "pestphp/pest": "^2.34", diff --git a/config/ddd.php b/config/ddd.php index 082196a..1a051af 100644 --- a/config/ddd.php +++ b/config/ddd.php @@ -31,8 +31,8 @@ | */ 'application' => [ - 'namespace' => 'App\Modules', 'path' => 'app/Modules', + 'namespace' => 'App\Modules', 'objects' => [ 'controller', 'request', @@ -45,14 +45,11 @@ | Custom Layers |-------------------------------------------------------------------------- | - | Mapping of additional top-level namespaces and paths that should - | be recognized as layers when generating ddd:* objects. + | Additional top-level namespaces and paths that should be recognized as + | layers when generating ddd:* objects. | | e.g., 'Infrastructure' => 'src/Infrastructure', | - | When using ddd:* generators, specifying a domain matching a key in - | this array will generate objects in that corresponding layer. - | */ 'layers' => [ 'Infrastructure' => 'src/Infrastructure', diff --git a/config/ddd.php.stub b/config/ddd.php.stub index 612b1d8..41e6874 100644 --- a/config/ddd.php.stub +++ b/config/ddd.php.stub @@ -27,23 +27,27 @@ return [ | Application Layer |-------------------------------------------------------------------------- | - | Configure objects that belong in the application layer. + | Configure domain objects in the application layer. | - | e.g., App\Modules\Invoicing\Controllers\* - | App\Modules\Invoicing\Requests\* + */ + 'application' => {{application}}, + + /* + |-------------------------------------------------------------------------- + | Custom Layers + |-------------------------------------------------------------------------- + | + | Additional top-level namespaces and paths that should + | be recognized as layers when generating ddd:* objects. + | + | e.g., 'Infrastructure' => 'src/Infrastructure', + | + | When using ddd:* generators, specifying a domain matching a key in + | this array will generate objects in that corresponding layer. | */ - 'application' => [ - 'path' => 'app/Modules', - 'namespace' => 'App\Modules', + 'layers' => {{layers}}, - // Specify which ddd:* objects belong in the application layer - 'objects' => [ - 'controller', - 'request', - 'middleware', - ], - ], /* |-------------------------------------------------------------------------- @@ -61,38 +65,7 @@ return [ | Domain\Invoicing\Actions\* | */ - 'namespaces' => [ - 'model' => {{namespaces.model}}, - 'data_transfer_object' => {{namespaces.data_transfer_object}}, - 'view_model' => {{namespaces.view_model}}, - 'value_object' => {{namespaces.value_object}}, - 'action' => {{namespaces.action}}, - 'cast' => 'Casts', - 'class' => '', - 'channel' => 'Channels', - 'command' => 'Commands', - 'controller' => 'Controllers', - 'enum' => 'Enums', - 'event' => 'Events', - 'exception' => 'Exceptions', - 'factory' => 'Database\Factories', - 'interface' => '', - 'job' => 'Jobs', - 'listener' => 'Listeners', - 'mail' => 'Mail', - 'middleware' => 'Middleware', - 'migration' => 'Database\Migrations', - 'notification' => 'Notifications', - 'observer' => 'Observers', - 'policy' => 'Policies', - 'provider' => 'Providers', - 'resource' => 'Resources', - 'request' => 'Requests', - 'rule' => 'Rules', - 'scope' => 'Scopes', - 'seeder' => 'Database\Seeders', - 'trait' => '', - ], + 'namespaces' => {{namespaces}}, /* |-------------------------------------------------------------------------- @@ -152,13 +125,7 @@ return [ | should be auto-discovered and registered. | */ - 'autoload' => [ - 'providers' => true, - 'commands' => true, - 'policies' => true, - 'factories' => true, - 'migrations' => true, - ], + 'autoload' => {{autoload}}, /* |-------------------------------------------------------------------------- @@ -175,10 +142,7 @@ return [ | the AppServiceProvider's boot method. | */ - 'autoload_ignore' => [ - 'Tests', - 'Database/Migrations', - ], + 'autoload_ignore' => {{autoload_ignore}}, /* |-------------------------------------------------------------------------- @@ -189,5 +153,5 @@ return [ | autoloading. | */ - 'cache_directory' => 'bootstrap/cache/ddd', + 'cache_directory' => {{cache_directory}}, ]; diff --git a/src/Commands/ConfigCommand.php b/src/Commands/ConfigCommand.php index 0ddda01..8cc4d52 100644 --- a/src/Commands/ConfigCommand.php +++ b/src/Commands/ConfigCommand.php @@ -3,8 +3,6 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Console\Command; -use Illuminate\Support\Collection; -use Illuminate\Support\Composer; use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; use Lunarstorm\LaravelDDD\ComposerManager; @@ -13,6 +11,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; +use function Laravel\Prompts\confirm; use function Laravel\Prompts\form; use function Laravel\Prompts\info; use function Laravel\Prompts\multiselect; @@ -54,6 +53,7 @@ public function handle(): int return match ($action) { 'wizard' => $this->wizard(), + 'update' => $this->update(), 'detect' => $this->detect(), 'composer' => $this->syncComposer(), 'layers' => $this->layers(), @@ -65,6 +65,7 @@ protected function home(): int { $action = select('Laravel-DDD Config Utility', [ 'wizard' => 'Run the configuration wizard', + 'update' => 'Update and merge ddd.php with latest package version', 'detect' => 'Detect domain namespace from composer.json', 'composer' => 'Sync composer.json from ddd.php', 'exit' => 'Exit', @@ -72,6 +73,7 @@ protected function home(): int return match ($action) { 'wizard' => $this->wizard(), + 'update' => $this->update(), 'detect' => $this->detect(), 'composer' => $this->syncComposer(), 'exit' => $this->exit(), @@ -114,18 +116,16 @@ protected function wizard(): int $applicationLayer = $possibleApplicationLayers->first(); $detected = collect([ - 'domain_namespace' => $domainLayer?->namespace, 'domain_path' => $domainLayer?->path, + 'domain_namespace' => $domainLayer?->namespace, 'application' => [ - 'namespace' => $applicationLayer?->namespace, 'path' => $applicationLayer?->path, + 'namespace' => $applicationLayer?->namespace, ], ]); $config = $detected->merge(Config::get('ddd')); - // dd($config); - info('Detected DDD configuration:'); table( @@ -185,8 +185,6 @@ protected function wizard(): int ], ]; - // dd($choices['application_namespace']); - $form = form() ->add( function ($responses) use ($choices, $detected, $config) { @@ -218,13 +216,14 @@ function ($responses) use ($choices) { label: 'Path to Application Layer', options: $choices['application_path'], hint: "For objects that don't belong in the domain layer (controllers, form requests, etc.)", - placeholder: 'Leave blank to skip and configure later', + placeholder: 'Leave blank to skip and use defaults', scroll: 10, ); }, name: 'application_path' ) - ->add( + ->addIf( + fn ($responses) => filled($responses['application_path']), function ($responses) use ($choices, $laravelAppLayer) { $applicationPath = $responses['application_path']; $laravelAppPath = $laravelAppLayer->path; @@ -240,6 +239,7 @@ function ($responses) use ($choices, $laravelAppLayer) { options: $choices['application_namespace'], default: $namespace, hint: 'The root application namespace.', + placeholder: 'Leave blank to use defaults', ); }, name: 'application_namespace' @@ -247,7 +247,7 @@ function ($responses) use ($choices, $laravelAppLayer) { ->add( function ($responses) use ($choices) { return multiselect( - label: 'Additional Layers (Optional)', + label: 'Custom Layers (Optional)', options: $choices['layers'], hint: 'Layers can be customized in the ddd.php config file at any time.', ); @@ -257,7 +257,11 @@ function ($responses) use ($choices) { $responses = $form->submit(); - // dd($responses); + $this->info('Building configuration...'); + + DDD::config()->fill($responses)->save(); + + $this->info('Configuration updated: '.config_path('ddd.php')); return self::SUCCESS; } @@ -270,8 +274,8 @@ protected function detect(): int foreach ($search as $namespace) { if ($path = $this->composer->getAutoloadPath($namespace)) { - $detected['domain_namespace'] = $namespace; $detected['domain_path'] = $path; + $detected['domain_namespace'] = $namespace; break; } } @@ -285,17 +289,33 @@ protected function detect(): int ->all() ); + if (confirm('Update configuration with these values?', true)) { + DDD::config()->fill($detected)->save(); + + $this->info('Configuration updated: '.config_path('ddd.php')); + } + return self::SUCCESS; } - protected function applyConfig(Collection $config) + protected function update(): int { - // $this->composer->update([ - // ['domain_namespace', $config['domain_namespace']], - // ['domain_path', $config['domain_path']], - // ['application.namespace', $config['application']['namespace']], - // ['application.path', $config['application']['path']], - // ]); + $config = DDD::config(); + + $confirmed = confirm('Are you sure you want to update ddd.php and merge with latest copy from the package?'); + + if (! $confirmed) { + $this->info('Configuration update aborted.'); + + return self::SUCCESS; + } + + $this->info('Merging ddd.php...'); + + $config->syncWithLatest()->save(); + + $this->info('Configuration updated: '.config_path('ddd.php')); + $this->warn('Note: Some values may require manual adjustment.'); return self::SUCCESS; } diff --git a/src/Commands/DomainListCommand.php b/src/Commands/DomainListCommand.php index 3b6adb6..1014b8a 100644 --- a/src/Commands/DomainListCommand.php +++ b/src/Commands/DomainListCommand.php @@ -7,6 +7,8 @@ use Lunarstorm\LaravelDDD\Support\DomainResolver; use Lunarstorm\LaravelDDD\Support\Path; +use function Laravel\Prompts\table; + class DomainListCommand extends Command { protected $name = 'ddd:list'; @@ -29,7 +31,7 @@ public function handle() }) ->toArray(); - $this->table($headings, $table); + table($headings, $table); $countDomains = count($table); diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index e37d186..0594133 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -3,65 +3,67 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Console\Command; +use Illuminate\Foundation\Console\InteractsWithComposerPackages; use Lunarstorm\LaravelDDD\Support\DomainResolver; use Symfony\Component\Process\Process; +use function Laravel\Prompts\confirm; + class InstallCommand extends Command { + use InteractsWithComposerPackages; + public $signature = 'ddd:install {--composer=global : Absolute path to the Composer binary which should be used}'; protected $description = 'Install and initialize Laravel-DDD'; public function handle(): int { - $this->comment('Publishing config...'); - $this->call('vendor:publish', [ - '--tag' => 'ddd-config', - ]); + $this->call('ddd:publish', ['--config' => true]); - $this->comment('Ensuring domain path is registered in composer.json...'); - $this->registerDomainAutoload(); + $this->comment('Updating composer.json...'); + $this->callSilently('ddd:config', ['action' => 'composer']); - if ($this->confirm('Would you like to publish stubs?')) { + if (confirm('Would you like to publish stubs now?', default: false, hint: 'You may do this at any time via ddd:stub')) { $this->call('ddd:stub'); } return self::SUCCESS; } - public function registerDomainAutoload() - { - $domainPath = DomainResolver::domainPath(); + // public function registerDomainAutoload() + // { + // $domainPath = DomainResolver::domainPath(); - $domainRootNamespace = str(DomainResolver::domainRootNamespace()) - ->rtrim('/\\') - ->toString(); + // $domainRootNamespace = str(DomainResolver::domainRootNamespace()) + // ->rtrim('/\\') + // ->toString(); - $this->comment("Registering domain path `{$domainPath}` in composer.json..."); + // $this->comment("Registering domain path `{$domainPath}` in composer.json..."); - $composerFile = base_path('composer.json'); - $data = json_decode(file_get_contents($composerFile), true); - data_fill($data, ['autoload', 'psr-4', $domainRootNamespace.'\\'], $domainPath); + // $composerFile = base_path('composer.json'); + // $data = json_decode(file_get_contents($composerFile), true); + // data_fill($data, ['autoload', 'psr-4', $domainRootNamespace . '\\'], $domainPath); - file_put_contents($composerFile, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + // file_put_contents($composerFile, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); - $this->composerReload(); - } + // $this->composerReload(); + // } - protected function composerReload() - { - $composer = $this->option('composer'); + // protected function composerReload() + // { + // $composer = $this->option('composer'); - if ($composer !== 'global') { - $command = ['php', $composer, 'dump-autoload']; - } else { - $command = ['composer', 'dump-autoload']; - } + // if ($composer !== 'global') { + // $command = ['php', $composer, 'dump-autoload']; + // } else { + // $command = ['composer', 'dump-autoload']; + // } - (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) - ->setTimeout(null) - ->run(function ($type, $output) { - $this->output->write($output); - }); - } + // (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) + // ->setTimeout(null) + // ->run(function ($type, $output) { + // $this->output->write($output); + // }); + // } } diff --git a/src/Commands/UpgradeCommand.php b/src/Commands/UpgradeCommand.php index e696605..ea0c3c4 100644 --- a/src/Commands/UpgradeCommand.php +++ b/src/Commands/UpgradeCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Config; class UpgradeCommand extends Command { @@ -19,36 +20,73 @@ public function handle() return; } - $replacements = [ + $legacyMapping = [ 'domain_path' => 'paths.domain', 'domain_namespace' => 'domain_namespace', - 'namespaces.model' => 'namespaces.models', - 'namespaces.data_transfer_object' => 'namespaces.data_transfer_objects', - 'namespaces.view_model' => 'namespaces.view_models', - 'namespaces.value_object' => 'namespaces.value_objects', - 'namespaces.action' => 'namespaces.actions', + 'application' => null, + 'layers' => null, + 'namespaces' => [ + 'model' => 'namespaces.models', + 'data_transfer_object' => 'namespaces.data_transfer_objects', + 'view_model' => 'namespaces.view_models', + 'value_object' => 'namespaces.value_objects', + 'action' => 'namespaces.actions', + ], 'base_model' => 'base_model', 'base_dto' => 'base_dto', 'base_view_model' => 'base_view_model', 'base_action' => 'base_action', + 'autoload' => null, + 'autoload_ignore' => null, + 'cache_directory' => null, ]; + $factoryConfig = require __DIR__.'/../../config/ddd.php'; $oldConfig = require config_path('ddd.php'); $oldConfig = Arr::dot($oldConfig); - // Grab a flesh copy of the new config - $newConfigContent = file_get_contents(__DIR__.'/../../config/ddd.php.stub'); + $replacements = []; + + $map = Arr::dot($legacyMapping); - foreach ($replacements as $dotPath => $legacyKey) { + foreach ($map as $dotPath => $legacyKey) { $value = match (true) { array_key_exists($dotPath, $oldConfig) => $oldConfig[$dotPath], array_key_exists($legacyKey, $oldConfig) => $oldConfig[$legacyKey], default => config("ddd.{$dotPath}"), }; + $replacements[$dotPath] = $value ?? data_get($factoryConfig, $dotPath); + } + + $replacements = Arr::undot($replacements); + + $freshConfig = $factoryConfig; + + // Grab a fresh copy of the new config + $newConfigContent = file_get_contents(__DIR__.'/../../config/ddd.php.stub'); + + foreach ($freshConfig as $key => $value) { + $resolved = null; + + if (is_array($value)) { + $resolved = [ + ...$value, + ...data_get($replacements, $key, []), + ]; + + if (array_is_list($resolved)) { + $resolved = array_unique($resolved); + } + } else { + $resolved = data_get($replacements, $key, $value); + } + + $freshConfig[$key] = $resolved; + $newConfigContent = str_replace( - '{{'.$dotPath.'}}', - var_export($value, true), + '{{'.$key.'}}', + var_export($resolved, true), $newConfigContent ); } diff --git a/src/ComposerManager.php b/src/ComposerManager.php index df30f14..52d9005 100755 --- a/src/ComposerManager.php +++ b/src/ComposerManager.php @@ -139,6 +139,13 @@ public function getAutoloadPath($namespace) return $this->get(['autoload', 'psr-4', $namespace]); } + public function unsetPsr4Autoload($namespace) + { + $namespace = Str::finish($namespace, '\\'); + + return $this->forget(['autoload', 'psr-4', $namespace]); + } + public function reload() { $this->output?->writeLn('Reloading composer (dump-autoload)...'); diff --git a/src/ConfigManager.php b/src/ConfigManager.php new file mode 100755 index 0000000..59591cd --- /dev/null +++ b/src/ConfigManager.php @@ -0,0 +1,148 @@ +packageConfig = require DDD::packagePath('config/ddd.php'); + + $this->config = file_exists($configPath) ? require ($configPath) : $this->packageConfig; + + $this->stub = file_get_contents(DDD::packagePath('config/ddd.php.stub')); + } + + protected function mergeArray($path, $array) + { + $path = Arr::wrap($path); + + $merged = []; + + foreach ($array as $key => $value) { + $merged[$key] = is_array($value) + ? $this->mergeArray([...$path, $key], $value, [...$path, $key]) + : $this->resolve([...$path, $key], $value); + } + + if (array_is_list($merged)) { + $merged = array_unique($merged); + } + + return $merged; + } + + public function resolve($path, $value) + { + $path = Arr::wrap($path); + + return data_get($this->config, $path, $value); + } + + public function syncWithLatest() + { + $fresh = []; + + foreach ($this->packageConfig as $key => $value) { + $resolved = is_array($value) + ? $this->mergeArray($key, $value) + : $this->resolve($key, $value); + + $fresh[$key] = $resolved; + } + + $this->config = $fresh; + + return $this; + } + + public function get($key = null) + { + if (is_null($key)) { + return $this->config; + } + + return data_get($this->config, $key); + } + + public function set($key, $value) + { + data_set($this->config, $key, $value); + + return $this; + } + + public function fill($values) + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return $this; + } + + public function save() + { + $content = $this->stub; + + // We will temporary substitute namespace slashes + // with a placeholder to avoid double exporter + // escaping them as double backslashes. + $keysWithNamespaces = [ + 'domain_namespace', + 'application.namespace', + 'layers', + 'namespaces', + 'base_model', + 'base_dto', + 'base_view_model', + 'base_action', + ]; + + foreach ($keysWithNamespaces as $key) { + $value = $this->get($key); + + if (is_string($value)) { + $value = str_replace('\\', '[[BACKSLASH]]', $value); + } + + if (is_array($value)) { + $array = $value; + foreach ($array as $k => $v) { + $array[$k] = str_replace('\\', '[[BACKSLASH]]', $v); + } + $value = $array; + } + + $this->set($key, $value); + } + + foreach ($this->config as $key => $value) { + $content = str_replace( + '{{'.$key.'}}', + VarExporter::export($value), + $content + ); + } + + // Restore namespace slashes + $content = str_replace('[[BACKSLASH]]', '\\', $content); + + file_put_contents($this->configPath, $content); + + Process::run("./vendor/bin/pint {$this->configPath}"); + + return $this; + } +} diff --git a/src/DomainManager.php b/src/DomainManager.php index 728867a..eff013d 100755 --- a/src/DomainManager.php +++ b/src/DomainManager.php @@ -45,6 +45,11 @@ public function __construct() $this->stubs = new StubManager; } + public function config(): ConfigManager + { + return app(ConfigManager::class); + } + public function composer(): ComposerManager { return app(ComposerManager::class); diff --git a/src/Facades/DDD.php b/src/Facades/DDD.php index 136de7e..a7c4afa 100644 --- a/src/Facades/DDD.php +++ b/src/Facades/DDD.php @@ -10,6 +10,7 @@ * @method static void filterAutoloadPathsUsing(callable $filter) * @method static void resolveObjectSchemaUsing(callable $resolver) * @method static string packagePath(string $path = '') + * @method static \Lunarstorm\LaravelDDD\ConfigManager config() * @method static \Lunarstorm\LaravelDDD\StubManager stubs() * @method static \Lunarstorm\LaravelDDD\ComposerManager composer() */ diff --git a/src/LaravelDDDServiceProvider.php b/src/LaravelDDDServiceProvider.php index 9060842..aeba7b1 100644 --- a/src/LaravelDDDServiceProvider.php +++ b/src/LaravelDDDServiceProvider.php @@ -15,6 +15,10 @@ public function configurePackage(Package $package): void return new DomainManager; }); + $this->app->scoped(ConfigManager::class, function () { + return new ConfigManager(config_path('ddd.php')); + }); + $this->app->scoped(ComposerManager::class, function () { return ComposerManager::make(app()->basePath('composer.json')); }); diff --git a/tests/Command/ConfigTest.php b/tests/Command/ConfigTest.php new file mode 100644 index 0000000..1b4a8c0 --- /dev/null +++ b/tests/Command/ConfigTest.php @@ -0,0 +1,180 @@ +cleanSlate(); + $this->setupTestApplication(); + Artisan::call('config:clear'); +}); + +afterEach(function () { + $this->cleanSlate(); + Artisan::call('config:clear'); +}); + +it('can run the config wizard', function () { + Artisan::call('config:cache'); + + expect(config('ddd.domain_path'))->toBe('src/Domain'); + expect(config('ddd.domain_namespace'))->toBe('Domain'); + expect(config('ddd.layers'))->toBe([ + 'Infrastructure' => 'src/Infrastructure', + ]); + + $path = config_path('ddd.php'); + + $this->artisan('ddd:config') + ->expectsQuestion('Laravel-DDD Config Utility', 'wizard') + ->expectsQuestion('Domain Path', 'src/CustomDomain') + ->expectsQuestion('Domain Namespace', 'CustomDomain') + ->expectsQuestion('Path to Application Layer', null) + ->expectsQuestion('Custom Layers (Optional)', ['Support' => 'src/Support']) + ->expectsOutput('Building configuration...') + ->expectsOutput("Configuration updated: {$path}") + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); + + Artisan::call('config:cache'); + + expect(config('ddd.domain_path'))->toBe('src/CustomDomain'); + expect(config('ddd.domain_namespace'))->toBe('CustomDomain'); + expect(config('ddd.application'))->toBe([ + 'path' => 'app/Modules', + 'namespace' => 'App\Modules', + 'objects' => [ + 'controller', + 'request', + 'middleware', + ], + ]); + expect(config('ddd.layers'))->toBe([ + 'Support' => 'src/Support', + ]); +}); + +it('can update and merge ddd.php with latest package version', function () { + $path = config_path('ddd.php'); + + $originalContents = <<<'PHP' +artisan('ddd:config') + ->expectsQuestion('Laravel-DDD Config Utility', 'update') + ->expectsQuestion('Are you sure you want to update ddd.php and merge with latest copy from the package?', true) + ->expectsOutput('Merging ddd.php...') + ->expectsOutput("Configuration updated: {$path}") + ->expectsOutput('Note: Some values may require manual adjustment.') + ->assertSuccessful() + ->execute(); + + $packageConfigContents = file_get_contents(DDD::packagePath('config/ddd.php')); + + expect($updatedContents = file_get_contents($path)) + ->not->toEqual($originalContents); + + $updatedConfigArray = include $path; + $packageConfigArray = include DDD::packagePath('config/ddd.php'); + + expect($updatedConfigArray)->toHaveKeys(array_keys($packageConfigArray)); +}); + +it('can sync composer.json from ddd.php ', function () { + $configContent = <<<'PHP' + 'src/CustomDomain', + 'domain_namespace' => 'CustomDomain', + 'application' => [ + 'path' => 'src/CustomApplication', + 'namespace' => 'CustomApplication', + 'objects' => [ + 'controller', + 'request', + 'middleware', + ], + ], + 'layers' => [ + 'Infrastructure' => 'src/Infrastructure', + 'CustomLayer' => 'src/CustomLayer', + ], +]; +PHP; + + file_put_contents(config_path('ddd.php'), $configContent); + + Artisan::call('config:cache'); + + $composerContents = file_get_contents(base_path('composer.json')); + + $fragments = [ + '"CustomDomain\\\\": "src/CustomDomain"', + '"Infrastructure\\\\": "src/Infrastructure"', + '"CustomLayer\\\\": "src/CustomLayer"', + '"CustomApplication\\\\": "src/CustomApplication"', + ]; + + expect($composerContents)->not->toContain(...$fragments); + + $this->artisan('ddd:config') + ->expectsQuestion('Laravel-DDD Config Utility', 'composer') + ->expectsOutput('Syncing composer.json from ddd.php...') + ->expectsOutputToContain(...[ + 'Namespace', + 'Path', + 'Status', + + 'CustomDomain', + 'src/CustomDomain', + 'Added', + + 'CustomApplication', + 'src/CustomApplication', + 'Added', + + 'Infrastructure', + 'src/Infrastructure', + 'Added', + ]) + ->assertSuccessful() + ->execute(); + + $composerContents = file_get_contents(base_path('composer.json')); + + expect($composerContents)->toContain(...$fragments); +}); + +it('can detect domain namespace from composer.json', function () { + $sampleComposer = file_get_contents(__DIR__.'/resources/composer.sample.json'); + + file_put_contents( + app()->basePath('composer.json'), + $sampleComposer + ); + + $this->artisan('ddd:config') + ->expectsQuestion('Laravel-DDD Config Utility', 'detect') + ->expectsOutputToContain(...[ + 'Detected configuration:', + 'domain_path', + 'lib/CustomDomain', + 'domain_namespace', + 'Domain', + ]) + ->expectsQuestion('Update configuration with these values?', true) + ->expectsOutput('Configuration updated: '.config_path('ddd.php')) + ->assertSuccessful() + ->execute(); + + $configValues = DDD::config()->get(); + + expect(data_get($configValues, 'domain_path'))->toBe('lib/CustomDomain'); + expect(data_get($configValues, 'domain_namespace'))->toBe('Domain'); +}); diff --git a/tests/Command/InstallTest.php b/tests/Command/InstallTest.php index 2be26aa..c950925 100644 --- a/tests/Command/InstallTest.php +++ b/tests/Command/InstallTest.php @@ -15,11 +15,11 @@ expect(file_exists($path))->toBeFalse(); - $command = $this->artisan('ddd:install'); - $command->expectsOutput('Publishing config...'); - $command->expectsOutput('Ensuring domain path is registered in composer.json...'); - $command->expectsConfirmation('Would you like to publish stubs?', 'no'); - $command->execute(); + $command = $this->artisan('ddd:install') + ->expectsOutput('Publishing config...') + ->expectsOutput('Updating composer.json...') + ->expectsQuestion('Would you like to publish stubs now?', false) + ->execute(); expect(file_exists($path))->toBeTrue(); expect(file_get_contents($path))->toEqual(file_get_contents(__DIR__.'/../../config/ddd.php')); @@ -42,9 +42,9 @@ $before = data_get($data, ['autoload', 'psr-4', $domainRoot.'\\']); expect($before)->toBeNull(); - $command = $this->artisan('ddd:install'); - $command->expectsConfirmation('Would you like to publish stubs?', 'no'); - $command->execute(); + $command = $this->artisan('ddd:install') + ->expectsQuestion('Would you like to publish stubs now?', false) + ->execute(); $data = json_decode(file_get_contents(base_path('composer.json')), true); $after = data_get($data, ['autoload', 'psr-4', $domainRoot.'\\']); diff --git a/tests/Command/resources/composer.sample.json b/tests/Command/resources/composer.sample.json new file mode 100644 index 0000000..61f8be5 --- /dev/null +++ b/tests/Command/resources/composer.sample.json @@ -0,0 +1,26 @@ +{ + "name": "laravel/laravel", + "description": "The Laravel Framework.", + "keywords": [ + "framework", + "laravel" + ], + "license": "MIT", + "type": "project", + "autoload": { + "classmap": [ + "database", + "tests/TestCase.php" + ], + "psr-4": { + "App\\": "app/", + "Domain\\": "lib/CustomDomain" + } + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "minimum-stability": "dev" +} diff --git a/tests/Config/ManagerTest.php b/tests/Config/ManagerTest.php new file mode 100644 index 0000000..9368f75 --- /dev/null +++ b/tests/Config/ManagerTest.php @@ -0,0 +1,51 @@ +cleanSlate(); + + $this->latestConfig = require DDD::packagePath('config/ddd.php'); +}); + +afterEach(function () { + $this->cleanSlate(); +}); + +it('can update and merge current config file with latest copy from package', function () { + $path = __DIR__.'/resources/config.sparse.php'; + + File::copy($path, config_path('ddd.php')); + + expect(file_exists($path))->toBeTrue(); + + $originalContents = file_get_contents($path); + + expect(file_get_contents(config_path('ddd.php')))->toEqual($originalContents); + + $original = include $path; + + $config = DDD::config(); + + $config->syncWithLatest()->save(); + + $updatedContents = file_get_contents(config_path('ddd.php')); + + expect($updatedContents)->not->toEqual($originalContents); + + $updatedConfig = include config_path('ddd.php'); + + // Expect original values to be retained + foreach ($original as $key => $value) { + if (is_array($value)) { + // We won't worry about arrays for now + continue; + } + + expect($updatedConfig[$key])->toEqual($value); + } + + // Expect the updated config to have all top-level keys from the latest config + expect($updatedConfig)->toHaveKeys(array_keys($this->latestConfig)); +}); diff --git a/tests/Config/resources/config.sparse.php b/tests/Config/resources/config.sparse.php new file mode 100644 index 0000000..8c61ab9 --- /dev/null +++ b/tests/Config/resources/config.sparse.php @@ -0,0 +1,25 @@ + 'src/CustomDomainFolder', + 'domain_namespace' => 'CustomDomainNamespace', + 'application' => [ + 'objects' => [ + 'keepthis', + ], + ], + 'namespaces' => [ + 'model' => 'CustomModels', + 'data_transfer_object' => 'CustomData', + 'view_model' => 'CustomViewModels', + 'value_object' => 'CustomValueObjects', + 'action' => 'CustomActions', + ], + 'base_model' => 'Domain\Shared\Models\CustomBaseModel', + 'base_dto' => 'Spatie\LaravelData\Data', + 'base_view_model' => 'Domain\Shared\ViewModels\CustomViewModel', + 'base_action' => null, + 'autoload' => [ + 'migrations' => false, + ], +]; diff --git a/tests/Datasets/resources/config.0.10.0.php b/tests/Datasets/resources/config.0.10.0.php index 59e9940..ea52a19 100644 --- a/tests/Datasets/resources/config.0.10.0.php +++ b/tests/Datasets/resources/config.0.10.0.php @@ -2,41 +2,41 @@ return [ /* - |-------------------------------------------------------------------------- - | Domain Path - |-------------------------------------------------------------------------- - | - | The path to the domain folder relative to the application root. - | - */ + |-------------------------------------------------------------------------- + | Domain Path + |-------------------------------------------------------------------------- + | + | The path to the domain folder relative to the application root. + | + */ 'domain_path' => 'src/CustomDomainFolder', /* - |-------------------------------------------------------------------------- - | Domain Namespace - |-------------------------------------------------------------------------- - | - | The root domain namespace. - | - */ + |-------------------------------------------------------------------------- + | Domain Namespace + |-------------------------------------------------------------------------- + | + | The root domain namespace. + | + */ 'domain_namespace' => 'CustomDomainNamespace', /* - |-------------------------------------------------------------------------- - | Domain Object Namespaces - |-------------------------------------------------------------------------- - | - | This value contains the default namespaces of generated domain - | objects relative to the domain namespace of which the object - | belongs to. - | - | e.g., Domain/Invoicing/Models/* - | Domain/Invoicing/Data/* - | Domain/Invoicing/ViewModels/* - | Domain/Invoicing/ValueObjects/* - | Domain/Invoicing/Actions/* - | - */ + |-------------------------------------------------------------------------- + | Domain Object Namespaces + |-------------------------------------------------------------------------- + | + | This value contains the default namespaces of generated domain + | objects relative to the domain namespace of which the object + | belongs to. + | + | e.g., Domain/Invoicing/Models/* + | Domain/Invoicing/Data/* + | Domain/Invoicing/ViewModels/* + | Domain/Invoicing/ValueObjects/* + | Domain/Invoicing/Actions/* + | + */ 'namespaces' => [ 'models' => 'CustomModels', 'data_transfer_objects' => 'CustomData', @@ -46,51 +46,51 @@ ], /* - |-------------------------------------------------------------------------- - | Base Model - |-------------------------------------------------------------------------- - | - | The base class which generated domain models should extend. By default, - | generated domain models will extend `Domain\Shared\Models\BaseModel`, - | which will be created if it doesn't already exist. - | - */ + |-------------------------------------------------------------------------- + | Base Model + |-------------------------------------------------------------------------- + | + | The base class which generated domain models should extend. By default, + | generated domain models will extend `Domain\Shared\Models\BaseModel`, + | which will be created if it doesn't already exist. + | + */ 'base_model' => 'Domain\Shared\Models\CustomBaseModel', /* - |-------------------------------------------------------------------------- - | Base DTO - |-------------------------------------------------------------------------- - | - | The base class which generated data transfer objects should extend. By - | default, generated DTOs will extend `Spatie\LaravelData\Data` from - | Spatie's Laravel-data package, a highly recommended data object - | package to work with. - | - */ + |-------------------------------------------------------------------------- + | Base DTO + |-------------------------------------------------------------------------- + | + | The base class which generated data transfer objects should extend. By + | default, generated DTOs will extend `Spatie\LaravelData\Data` from + | Spatie's Laravel-data package, a highly recommended data object + | package to work with. + | + */ 'base_dto' => 'Spatie\LaravelData\Data', /* - |-------------------------------------------------------------------------- - | Base ViewModel - |-------------------------------------------------------------------------- - | - | The base class which generated view models should extend. By default, - | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`, - | which will be created if it doesn't already exist. - | - */ + |-------------------------------------------------------------------------- + | Base ViewModel + |-------------------------------------------------------------------------- + | + | The base class which generated view models should extend. By default, + | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`, + | which will be created if it doesn't already exist. + | + */ 'base_view_model' => 'Domain\Shared\ViewModels\CustomViewModel', /* - |-------------------------------------------------------------------------- - | Base Action - |-------------------------------------------------------------------------- - | - | The base class which generated action objects should extend. By default, - | generated actions are based on the `lorisleiva/laravel-actions` package - | and do not extend anything. - | - */ + |-------------------------------------------------------------------------- + | Base Action + |-------------------------------------------------------------------------- + | + | The base class which generated action objects should extend. By default, + | generated actions are based on the `lorisleiva/laravel-actions` package + | and do not extend anything. + | + */ 'base_action' => null, ]; diff --git a/tests/TestCase.php b/tests/TestCase.php index 6dc58c5..39b3e58 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -177,6 +177,7 @@ protected function setupTestApplication() File::copyDirectory(__DIR__.'/.skeleton/database', base_path('database')); File::copyDirectory(__DIR__.'/.skeleton/src/Domain', base_path('src/Domain')); File::copy(__DIR__.'/.skeleton/bootstrap/providers.php', base_path('bootstrap/providers.php')); + File::copy(__DIR__.'/.skeleton/composer.json', base_path('composer.json')); File::ensureDirectoryExists(app_path('Models')); $this->setDomainPathInComposer('Domain', 'src/Domain');