diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 3855a08..c1a2fa8 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -16,7 +16,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.2' coverage: none - name: Install composer dependencies diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f14c12e..afe477a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: [main, next] jobs: test: @@ -14,18 +14,15 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] php: [8.3, 8.2, 8.1] - laravel: [11.*, 10.*, 9.*] + laravel: [11.*, 10.25.*] stability: [prefer-lowest, prefer-stable] include: - laravel: 11.* testbench: 9.* carbon: ^3.0 - - laravel: 10.* + - laravel: 10.25.* testbench: 8.* - carbon: ^2.63 - - laravel: 9.* - testbench: 7.* - carbon: ^2.63 + carbon: 2.* exclude: - laravel: 11.* php: 8.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bad98b..6a8d6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to `laravel-ddd` will be documented in this file. +## [Unversioned] +### Added +- `ddd:list` to show a summary of current domains in the domain folder. +- Additional generators that extend Laravel's generators and funnel the generated objects into the domain layer: + - `ddd:cast {domain}:{name}` + - `ddd:channel {domain}:{name}` + - `ddd:command {domain}:{name}` + - `ddd:enum {domain}:{name}` (Laravel 11 only) + - `ddd:event {domain}:{name}` + - `ddd:exception {domain}:{name}` + - `ddd:job {domain}:{name}` + - `ddd:listener {domain}:{name}` + - `ddd:mail {domain}:{name}` + - `ddd:notification {domain}:{name}` + - `ddd:observer {domain}:{name}` + - `ddd:policy {domain}:{name}` + - `ddd:provider {domain}:{name}` + - `ddd:resource {domain}:{name}` + - `ddd:rule {domain}:{name}` + - `ddd:scope {domain}:{name}` +- For all `ddd:*` generator commands, if a domain wasn't specified, prompt for the domain with auto-completion (based on current domains in the domain folder). + +### Changed +- (BREAKING) `ddd:*` commands no longer receive a dedicated domain argument. Example: `ddd:action Invoicing CreateInvoice` can be one of: + - `ddd:action CreateInvoice --domain=Invoicing` (this takes precedence). + - Shorthand syntax: `ddd:action Invoicing:CreateInvoice`. + - Or simply `ddd:action CreateInvoice` to be prompted for the domain. + +### Chore +- Dropped Laravel 9 support. + ## [0.10.0] - 2024-03-23 ### Added - Add `ddd.domain_path` and `ddd.domain_namespace` to config, to specify the path to the domain layer and root domain namespace more explicitly (replaces the previous `ddd.paths.domains` config). diff --git a/README.md b/README.md index da8c48d..6310442 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,15 @@ [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/lunarstorm/laravel-ddd/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/lunarstorm/laravel-ddd/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/lunarstorm/laravel-ddd.svg?style=flat-square)](https://packagist.org/packages/lunarstorm/laravel-ddd) -Laravel-DDD is a toolkit to support domain driven design (DDD) patterns in Laravel applications. One of the pain points when adopting DDD is the inability to use Laravel's native `make:model` artisan command to properly generate domain models, since domain models are not intended to be stored in the `App/Models/*` namespace. This package aims to fill the gaps by providing an equivalent command, `ddd:model`, plus a few more. +Laravel-DDD is a toolkit to support domain driven design (DDD) patterns in Laravel applications. One of the pain points when adopting DDD is the inability to use Laravel's native `make:model` artisan command to properly generate domain models, since domain models are not intended to be stored in the `App/Models/*` namespace. This package aims to fill the gaps by providing an equivalent command, `ddd:model`, plus many more. -> :warning: **Disclaimer**: This package is subject to frequent design changes as it evolves towards a stable v1.0 release. It is currently being tested and fine tuned within Lunarstorm's client projects. +## Version Compatibility + Laravel | LaravelDDD +:-----------|:----------- + 9.x | 0.x + <10.25 | 0.x + 10.25+ | 1.x + 11.x | 1.x ## Installation @@ -24,56 +30,94 @@ php artisan ddd:install ## Usage -The following generator commands are currently available: +Command syntax: +```bash +# Specifying the domain as an option +php artisan ddd:{object} {name} --domain={domain} + +# Specifying the domain as part of the name (short-hand syntax) +php artisan ddd:{object} {domain}:{name} + +# Not specifying the domain at all, which will then prompt +# you to enter the domain name (with auto-completion) +php artisan ddd:{object} {name} +``` +The following generators are currently available, shown using short-hand syntax: ```bash # Generate a domain model -php artisan ddd:model {domain} {name} +php artisan ddd:model {domain}:{name} # Generate a domain model with factory -php artisan ddd:model {domain} {name} -f -php artisan ddd:model {domain} {name} --factory +php artisan ddd:model {domain}:{name} -f +php artisan ddd:model {domain}:{name} --factory # Generate a domain factory -php artisan ddd:factory {domain} {name} [--model={model}] +php artisan ddd:factory {domain}:{name} [--model={model}] # Generate a data transfer object -php artisan ddd:dto {domain} {name} +php artisan ddd:dto {domain}:{name} # Generates a value object -php artisan ddd:value {domain} {name} +php artisan ddd:value {domain}:{name} # Generates a view model -php artisan ddd:view-model {domain} {name} +php artisan ddd:view-model {domain}:{name} # Generates an action -php artisan ddd:action {domain} {name} +php artisan ddd:action {domain}:{name} + +# Extended Commands +# (extends Laravel's make:* generators and funnels the objects into the domain layer) +php artisan ddd:cast {domain}:{name} +php artisan ddd:channel {domain}:{name} +php artisan ddd:command {domain}:{name} +php artisan ddd:enum {domain}:{name} # Requires Laravel 11+ +php artisan ddd:event {domain}:{name} +php artisan ddd:exception {domain}:{name} +php artisan ddd:job {domain}:{name} +php artisan ddd:listener {domain}:{name} +php artisan ddd:mail {domain}:{name} +php artisan ddd:notification {domain}:{name} +php artisan ddd:observer {domain}:{name} +php artisan ddd:policy {domain}:{name} +php artisan ddd:provider {domain}:{name} +php artisan ddd:resource {domain}:{name} +php artisan ddd:rule {domain}:{name} +php artisan ddd:scope {domain}:{name} ``` Examples: ```bash -php artisan ddd:model Invoicing LineItem # Domain/Invoicing/Models/LineItem -php artisan ddd:model Invoicing LineItem -f # Domain/Invoicing/Models/LineItem + Database/Factories/Invoicing/LineItemFactory -php artisan ddd:factory Invoicing LineItemFactory # Database/Factories/Invoicing/LineItemFactory -php artisan ddd:dto Invoicing LinePayload # Domain/Invoicing/Data/LinePayload -php artisan ddd:value Shared Percentage # Domain/Shared/ValueObjects/Percentage -php artisan ddd:view-model Invoicing ShowInvoiceViewModel # Domain/Invoicing/ViewModels/ShowInvoiceViewModel -php artisan ddd:action Invoicing SendInvoiceToCustomer # Domain/Invoicing/Actions/SendInvoiceToCustomer +php artisan ddd:model Invoicing:LineItem # Domain/Invoicing/Models/LineItem +php artisan ddd:model Invoicing:LineItem -f # Domain/Invoicing/Models/LineItem + Database/Factories/Invoicing/LineItemFactory +php artisan ddd:factory Invoicing:LineItemFactory # Database/Factories/Invoicing/LineItemFactory +php artisan ddd:dto Invoicing:LinePayload # Domain/Invoicing/Data/LinePayload +php artisan ddd:value Shared:Percentage # Domain/Shared/ValueObjects/Percentage +php artisan ddd:view-model Invoicing:ShowInvoiceViewModel # Domain/Invoicing/ViewModels/ShowInvoiceViewModel +php artisan ddd:action Invoicing:SendInvoiceToCustomer # Domain/Invoicing/Actions/SendInvoiceToCustomer ``` Subdomains (nested domains) can be specified with dot notation: ```bash -php artisan ddd:model Invoicing.Customer CustomerInvoice # Domain/Invoicing/Customer/Models/CustomerInvoice -php artisan ddd:factory Invoicing.Customer CustomerInvoice # Database/Factories/Invoicing/Customer/CustomerInvoiceFactory +php artisan ddd:model Invoicing.Customer:CustomerInvoice # Domain/Invoicing/Customer/Models/CustomerInvoice +php artisan ddd:factory Invoicing.Customer:CustomerInvoice # Database/Factories/Invoicing/Customer/CustomerInvoiceFactory # (supported by all generator commands) ``` +### Other Commands +```bash +# Show a summary of current domains in the domain folder +php artisan ddd:list +``` + This package ships with opinionated (but sensible) configuration defaults. If you need to customize, you may do so by publishing the config file and generator stubs as needed: ```bash php artisan vendor:publish --tag="ddd-config" php artisan vendor:publish --tag="ddd-stubs" ``` +Note that the extended commands do not publish ddd-specific stubs, and inherit the respective application-level stubs published by Laravel. This is the content of the published config file (`ddd.php`): @@ -117,11 +161,27 @@ return [ | */ 'namespaces' => [ - 'models' => 'Models', - 'data_transfer_objects' => 'Data', - 'view_models' => 'ViewModels', - 'value_objects' => 'ValueObjects', - 'actions' => 'Actions', + 'model' => 'Models', + 'data_transfer_object' => 'Data', + 'view_model' => 'ViewModels', + 'value_object' => 'ValueObjects', + 'action' => 'Actions', + 'cast' => 'Casts', + 'channel' => 'Channels', + 'command' => 'Commands', + 'enum' => 'Enums', + 'event' => 'Events', + 'exception' => 'Exceptions', + 'job' => 'Jobs', + 'listener' => 'Listeners', + 'mail' => 'Mail', + 'notification' => 'Notifications', + 'observer' => 'Observers', + 'policy' => 'Policies', + 'provider' => 'Providers', + 'resource' => 'Resources', + 'rule' => 'Rules', + 'scope' => 'Scopes', ], /* diff --git a/composer.json b/composer.json index 2f83313..51cfca0 100644 --- a/composer.json +++ b/composer.json @@ -19,20 +19,20 @@ ], "require": { "php": "^8.1|^8.2|^8.3", - "spatie/laravel-package-tools": "^1.13.0", - "illuminate/contracts": "^9.0|^10.0|^11.0" + "illuminate/contracts": "^10.25|^11.0", + "laravel/prompts": "^0.1.16", + "spatie/laravel-package-tools": "^1.13.0" }, "require-dev": { "laravel/pint": "^1.0", - "nunomaduro/collision": "^6.0|^7.0|^8.1", + "nunomaduro/collision": "^7.0|^8.1", "larastan/larastan": "^2.0.1", - "orchestra/testbench": "^7|^8|^9.0", - "pestphp/pest": "^1.22|^2.0", - "pestphp/pest-plugin-laravel": "^1.1|^2.0", + "orchestra/testbench": "^8|^9.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5|^10" + "phpstan/phpstan-phpunit": "^1.0" }, "autoload": { "psr-4": { diff --git a/config/ddd.php b/config/ddd.php index a763011..b818743 100644 --- a/config/ddd.php +++ b/config/ddd.php @@ -39,11 +39,27 @@ | */ 'namespaces' => [ - 'models' => 'Models', - 'data_transfer_objects' => 'Data', - 'view_models' => 'ViewModels', - 'value_objects' => 'ValueObjects', - 'actions' => 'Actions', + 'model' => 'Models', + 'data_transfer_object' => 'Data', + 'view_model' => 'ViewModels', + 'value_object' => 'ValueObjects', + 'action' => 'Actions', + 'cast' => 'Casts', + 'channel' => 'Channels', + 'command' => 'Commands', + 'enum' => 'Enums', + 'event' => 'Events', + 'exception' => 'Exceptions', + 'job' => 'Jobs', + 'listener' => 'Listeners', + 'mail' => 'Mail', + 'notification' => 'Notifications', + 'observer' => 'Observers', + 'policy' => 'Policies', + 'provider' => 'Providers', + 'resource' => 'Resources', + 'rule' => 'Rules', + 'scope' => 'Scopes', 'factories' => 'Database\Factories', ], diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0ea972c..c646074 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,23 +1,23 @@ - - - tests - - - - - - - - - - - - - - - ./src - - + + + tests + + + + + + + + + + + + + + + ./src + + diff --git a/src/Commands/Concerns/CanPromptForDomain.php b/src/Commands/Concerns/CanPromptForDomain.php new file mode 100644 index 0000000..f4d95e9 --- /dev/null +++ b/src/Commands/Concerns/CanPromptForDomain.php @@ -0,0 +1,35 @@ +mapWithKeys(fn ($name) => [Str::lower($name) => $name]); + + // Prompt for the domain + $domainName = suggest( + label: 'What is the domain?', + options: fn ($value) => collect($choices) + ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true)) + ->toArray(), + placeholder: 'Start typing to search...', + required: true + ); + + // Normalize the case of the domain name + // if it is an existing domain. + if ($match = $choices->get(Str::lower($domainName))) { + $domainName = $match; + } + + return $domainName; + } +} diff --git a/src/Commands/Concerns/ResolvesDomainFromInput.php b/src/Commands/Concerns/ResolvesDomainFromInput.php new file mode 100644 index 0000000..33e1587 --- /dev/null +++ b/src/Commands/Concerns/ResolvesDomainFromInput.php @@ -0,0 +1,91 @@ +name) { + 'ddd:base-view-model' => 'view_model', + 'ddd:base-model' => 'model', + 'ddd:value' => 'value_object', + 'ddd:dto' => 'data_transfer_object', + default => str($this->name)->after(':')->snake()->toString(), + }; + } + + protected function getDefaultNamespace($rootNamespace) + { + if ($this->domain) { + return $this->domain->namespaceFor($this->guessObjectType()); + } + + return parent::getDefaultNamespace($rootNamespace); + } + + protected function getPath($name) + { + if ($this->domain) { + return Path::normalize($this->laravel->basePath( + $this->domain->object($this->guessObjectType(), class_basename($name))->path + )); + } + + return parent::getPath($name); + } + + public function handle() + { + $nameInput = $this->getNameInput(); + + // If the name contains a domain prefix, extract it + // and strip it from the name argument. + $domainExtractedFromName = null; + + if (Str::contains($nameInput, ':')) { + $domainExtractedFromName = Str::before($nameInput, ':'); + $this->input->setArgument('name', Str::after($nameInput, ':')); + } + + $this->domain = match (true) { + // Domain was specified explicitly via option (priority) + filled($this->option('domain')) => new Domain($this->option('domain')), + + // Domain was specified as a prefix in the name + filled($domainExtractedFromName) => new Domain($domainExtractedFromName), + + default => null, + }; + + // If the domain is not set, prompt for it + if (! $this->domain) { + $this->domain = new Domain($this->promptForDomainName()); + } + + parent::handle(); + } +} diff --git a/src/Commands/MakeAction.php b/src/Commands/DomainActionMakeCommand.php similarity index 53% rename from src/Commands/MakeAction.php rename to src/Commands/DomainActionMakeCommand.php index eb4b6e8..c1a14f2 100644 --- a/src/Commands/MakeAction.php +++ b/src/Commands/DomainActionMakeCommand.php @@ -2,9 +2,7 @@ namespace Lunarstorm\LaravelDDD\Commands; -use Symfony\Component\Console\Input\InputArgument; - -class MakeAction extends DomainGeneratorCommand +class DomainActionMakeCommand extends DomainGeneratorCommand { protected $name = 'ddd:action'; @@ -17,29 +15,11 @@ class MakeAction extends DomainGeneratorCommand protected $type = 'Action'; - protected function getArguments() - { - return [ - ...parent::getArguments(), - - new InputArgument( - 'name', - InputArgument::REQUIRED, - 'The name of the Action', - ), - ]; - } - protected function getStub() { return $this->resolveStubPath('action.php.stub'); } - protected function getRelativeDomainNamespace(): string - { - return config('ddd.namespaces.actions', 'Actions'); - } - protected function preparePlaceholders(): array { $baseClass = config('ddd.base_action'); diff --git a/src/Commands/MakeBaseModel.php b/src/Commands/DomainBaseModelMakeCommand.php similarity index 83% rename from src/Commands/MakeBaseModel.php rename to src/Commands/DomainBaseModelMakeCommand.php index 7b4062e..559cd59 100644 --- a/src/Commands/MakeBaseModel.php +++ b/src/Commands/DomainBaseModelMakeCommand.php @@ -4,7 +4,7 @@ use Symfony\Component\Console\Input\InputArgument; -class MakeBaseModel extends DomainGeneratorCommand +class DomainBaseModelMakeCommand extends DomainGeneratorCommand { protected $name = 'ddd:base-model'; @@ -20,8 +20,6 @@ class MakeBaseModel extends DomainGeneratorCommand protected function getArguments() { return [ - ...parent::getArguments(), - new InputArgument( 'name', InputArgument::OPTIONAL, @@ -38,6 +36,6 @@ protected function getStub() protected function getRelativeDomainNamespace(): string { - return config('ddd.namespaces.models', 'Models'); + return config('ddd.namespaces.model', 'Models'); } } diff --git a/src/Commands/MakeBaseViewModel.php b/src/Commands/DomainBaseViewModelMakeCommand.php similarity index 82% rename from src/Commands/MakeBaseViewModel.php rename to src/Commands/DomainBaseViewModelMakeCommand.php index 5966277..afac4f7 100644 --- a/src/Commands/MakeBaseViewModel.php +++ b/src/Commands/DomainBaseViewModelMakeCommand.php @@ -4,7 +4,7 @@ use Symfony\Component\Console\Input\InputArgument; -class MakeBaseViewModel extends DomainGeneratorCommand +class DomainBaseViewModelMakeCommand extends DomainGeneratorCommand { protected $name = 'ddd:base-view-model'; @@ -20,8 +20,6 @@ class MakeBaseViewModel extends DomainGeneratorCommand protected function getArguments() { return [ - ...parent::getArguments(), - new InputArgument( 'name', InputArgument::OPTIONAL, @@ -38,6 +36,6 @@ protected function getStub() protected function getRelativeDomainNamespace(): string { - return config('ddd.namespaces.view_models', 'ViewModels'); + return config('ddd.namespaces.view_model', 'ViewModels'); } } diff --git a/src/Commands/DomainCastMakeCommand.php b/src/Commands/DomainCastMakeCommand.php new file mode 100644 index 0000000..30531e4 --- /dev/null +++ b/src/Commands/DomainCastMakeCommand.php @@ -0,0 +1,13 @@ +resolveStubPath('dto.php.stub'); @@ -37,7 +22,7 @@ protected function getStub() protected function getRelativeDomainNamespace(): string { - return config('ddd.namespaces.data_transfer_objects', 'Data'); + return config('ddd.namespaces.data_transfer_object', 'Data'); } protected function preparePlaceholders(): array diff --git a/src/Commands/DomainEnumMakeCommand.php b/src/Commands/DomainEnumMakeCommand.php new file mode 100644 index 0000000..d3110b7 --- /dev/null +++ b/src/Commands/DomainEnumMakeCommand.php @@ -0,0 +1,13 @@ +getDomain(); + $domain = $this->domain?->domainWithSubdomain; return $rootNamespace.'\\'.$domain; } @@ -89,7 +75,7 @@ protected function getFactoryName() protected function preparePlaceholders(): array { - $domain = new Domain($this->getDomain()); + $domain = $this->domain; $name = $this->getNameInput(); @@ -99,6 +85,12 @@ protected function preparePlaceholders(): array $domainFactory = $domain->factory($name); + // dump('preparing placeholders', [ + // 'name' => $name, + // 'modelName' => $modelName, + // 'domainFactory' => $domainFactory, + // ]); + return [ 'namespacedModel' => $domainModel->fqn, 'model' => class_basename($domainModel->fqn), @@ -113,6 +105,6 @@ protected function guessModelName($name) $name = substr($name, 0, -7); } - return (new Domain($this->getDomain()))->model($name)->name; + return $this->domain->model($name)->name; } } diff --git a/src/Commands/DomainGeneratorCommand.php b/src/Commands/DomainGeneratorCommand.php index 016c417..6421409 100644 --- a/src/Commands/DomainGeneratorCommand.php +++ b/src/Commands/DomainGeneratorCommand.php @@ -4,78 +4,23 @@ use Illuminate\Console\GeneratorCommand; use Illuminate\Support\Str; +use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; use Lunarstorm\LaravelDDD\Support\DomainResolver; -use Lunarstorm\LaravelDDD\Support\Path; -use Symfony\Component\Console\Input\InputArgument; abstract class DomainGeneratorCommand extends GeneratorCommand { - protected function getArguments() - { - return [ - new InputArgument( - 'domain', - InputArgument::REQUIRED, - 'The domain' - ), - ]; - } - - protected function rootNamespace() - { - return str(DomainResolver::getConfiguredDomainNamespace()) - ->rtrim('/\\') - ->toString(); - } + use ResolvesDomainFromInput; - protected function getDefaultNamespace($rootNamespace) + protected function getRelativeDomainNamespace(): string { - $domain = $this->getDomain(); - - return $rootNamespace.'\\'.$domain.'\\'.$this->getRelativeDomainNamespace(); + return DomainResolver::getRelativeObjectNamespace($this->guessObjectType()); } - abstract protected function getRelativeDomainNamespace(): string; - protected function getNameInput() { return Str::studly($this->argument('name')); } - protected function getDomainInput() - { - return $this->argument('domain'); - } - - protected function getDomain() - { - return str($this->getDomainInput()) - ->trim() - ->replace(['.', '/'], '\\') - ->studly() - ->toString(); - } - - protected function getDomainBasePath() - { - return Path::normalize($this->laravel->basePath( - DomainResolver::getConfiguredDomainPath() ?? 'src/Domain' - )); - } - - protected function getPath($name) - { - $path = str($name) - ->replaceFirst($this->rootNamespace(), '') - ->replace('\\', '/') - ->ltrim('/') - ->append('.php') - ->prepend($this->getDomainBasePath().DIRECTORY_SEPARATOR) - ->toString(); - - return Path::normalize($path); - } - protected function resolveStubPath($path) { $path = ltrim($path, '/\\'); diff --git a/src/Commands/DomainJobMakeCommand.php b/src/Commands/DomainJobMakeCommand.php new file mode 100644 index 0000000..7fc5e37 --- /dev/null +++ b/src/Commands/DomainJobMakeCommand.php @@ -0,0 +1,13 @@ +map(function (string $name) { + $domain = new Domain($name); + + return [ + $domain->domain, + $domain->namespace->root, + Path::normalize($domain->path), + ]; + }) + ->toArray(); + + $this->table($headings, $table); + + $countDomains = count($table); + + $this->info(trans_choice("{$countDomains} domain|{$countDomains} domains", $countDomains)); + } +} diff --git a/src/Commands/DomainListenerMakeCommand.php b/src/Commands/DomainListenerMakeCommand.php new file mode 100644 index 0000000..9726d20 --- /dev/null +++ b/src/Commands/DomainListenerMakeCommand.php @@ -0,0 +1,13 @@ +resolveStubPath('model.php.stub'); } - protected function getRelativeDomainNamespace(): string - { - return config('ddd.namespaces.models', 'Models'); - } - protected function preparePlaceholders(): array { $baseClass = config('ddd.base_model'); @@ -103,8 +85,8 @@ protected function createBaseModelIfNeeded() if (! file_exists($baseModelPath)) { $this->info("Generating {$baseModel}..."); - $this->call(MakeBaseModel::class, [ - 'domain' => $domain, + $this->call(DomainBaseModelMakeCommand::class, [ + '--domain' => $domain, 'name' => $baseModelName, ]); } @@ -112,9 +94,9 @@ protected function createBaseModelIfNeeded() protected function createFactory() { - $this->call(MakeFactory::class, [ - 'domain' => $this->getDomain(), + $this->call(DomainFactoryMakeCommand::class, [ 'name' => $this->getNameInput().'Factory', + '--domain' => $this->domain->dotName, '--model' => $this->qualifyClass($this->getNameInput()), ]); } diff --git a/src/Commands/DomainNotificationMakeCommand.php b/src/Commands/DomainNotificationMakeCommand.php new file mode 100644 index 0000000..04de8ab --- /dev/null +++ b/src/Commands/DomainNotificationMakeCommand.php @@ -0,0 +1,13 @@ +resolveStubPath('value-object.php.stub'); + } +} diff --git a/src/Commands/MakeViewModel.php b/src/Commands/DomainViewModelMakeCommand.php similarity index 57% rename from src/Commands/MakeViewModel.php rename to src/Commands/DomainViewModelMakeCommand.php index 16ebd16..3006518 100644 --- a/src/Commands/MakeViewModel.php +++ b/src/Commands/DomainViewModelMakeCommand.php @@ -2,9 +2,7 @@ namespace Lunarstorm\LaravelDDD\Commands; -use Symfony\Component\Console\Input\InputArgument; - -class MakeViewModel extends DomainGeneratorCommand +class DomainViewModelMakeCommand extends DomainGeneratorCommand { protected $name = 'ddd:view-model'; @@ -17,24 +15,6 @@ class MakeViewModel extends DomainGeneratorCommand protected $type = 'View Model'; - protected function getArguments() - { - return [ - ...parent::getArguments(), - - new InputArgument( - 'name', - InputArgument::REQUIRED, - 'The name of the view model', - ), - ]; - } - - protected function getRelativeDomainNamespace(): string - { - return config('ddd.namespaces.view_models', 'ViewModels'); - } - protected function getStub() { return $this->resolveStubPath('view-model.php.stub'); @@ -51,8 +31,8 @@ public function handle() if (! file_exists($basePath)) { $this->warn("Base view model {$baseViewModel} doesn't exist, generating..."); - $this->call(MakeBaseViewModel::class, [ - 'domain' => 'Shared', + $this->call(DomainBaseViewModelMakeCommand::class, [ + '--domain' => 'Shared', 'name' => $baseName, ]); } diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index b566358..1ad031e 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -35,9 +35,9 @@ public function handle(): int public function registerDomainAutoload() { - $domainPath = DomainResolver::getConfiguredDomainPath(); + $domainPath = DomainResolver::domainPath(); - $domainRootNamespace = str(DomainResolver::getConfiguredDomainNamespace()) + $domainRootNamespace = str(DomainResolver::domainRootNamespace()) ->rtrim('/\\') ->toString(); diff --git a/src/Commands/MakeValueObject.php b/src/Commands/MakeValueObject.php deleted file mode 100644 index ad54ea5..0000000 --- a/src/Commands/MakeValueObject.php +++ /dev/null @@ -1,42 +0,0 @@ -resolveStubPath('value-object.php.stub'); - } -} diff --git a/src/Factories/DomainFactory.php b/src/Factories/DomainFactory.php index 8767e49..6518d0d 100644 --- a/src/Factories/DomainFactory.php +++ b/src/Factories/DomainFactory.php @@ -15,7 +15,7 @@ abstract class DomainFactory extends Factory */ protected static function domainNamespace() { - return Str::finish(DomainResolver::getConfiguredDomainNamespace(), '\\'); + return Str::finish(DomainResolver::domainRootNamespace(), '\\'); } /** @@ -28,7 +28,7 @@ public static function resolveFactoryName(string $modelName) { $resolver = function (string $modelName) { $domainNamespace = static::domainNamespace(); - $modelNamespace = config('ddd.namespaces.models'); + $modelNamespace = config('ddd.namespaces.model'); // Expected domain model FQN: // {DomainNamespace}\{Domain}\{ModelNamespace}\{Model} diff --git a/src/LaravelDDDServiceProvider.php b/src/LaravelDDDServiceProvider.php index dbebc0c..fe8b301 100644 --- a/src/LaravelDDDServiceProvider.php +++ b/src/LaravelDDDServiceProvider.php @@ -28,16 +28,37 @@ public function configurePackage(Package $package): void ->name('laravel-ddd') ->hasConfigFile() ->hasCommands([ - InstallCommand::class, - MakeModel::class, - MakeFactory::class, - MakeBaseModel::class, - MakeDTO::class, - MakeValueObject::class, - MakeViewModel::class, - MakeBaseViewModel::class, - MakeAction::class, + Commands\InstallCommand::class, + Commands\DomainListCommand::class, + Commands\DomainModelMakeCommand::class, + Commands\DomainFactoryMakeCommand::class, + Commands\DomainBaseModelMakeCommand::class, + Commands\DomainDtoMakeCommand::class, + Commands\DomainValueObjectMakeCommand::class, + Commands\DomainViewModelMakeCommand::class, + Commands\DomainBaseViewModelMakeCommand::class, + Commands\DomainActionMakeCommand::class, + Commands\DomainCastMakeCommand::class, + Commands\DomainChannelMakeCommand::class, + Commands\DomainConsoleMakeCommand::class, + Commands\DomainEventMakeCommand::class, + Commands\DomainExceptionMakeCommand::class, + Commands\DomainJobMakeCommand::class, + Commands\DomainListenerMakeCommand::class, + Commands\DomainMailMakeCommand::class, + Commands\DomainNotificationMakeCommand::class, + Commands\DomainObserverMakeCommand::class, + Commands\DomainPolicyMakeCommand::class, + Commands\DomainProviderMakeCommand::class, + Commands\DomainResourceMakeCommand::class, + Commands\DomainRuleMakeCommand::class, + Commands\DomainScopeMakeCommand::class, ]); + + // Enum generator only in Laravel 11 + if (app()->version() >= 11) { + $package->hasCommand(\Lunarstorm\LaravelDDD\Commands\DomainEnumMakeCommand::class); + } } public function packageBooted() diff --git a/src/Support/Domain.php b/src/Support/Domain.php index c539313..a0967e2 100644 --- a/src/Support/Domain.php +++ b/src/Support/Domain.php @@ -19,6 +19,8 @@ class Domain public readonly DomainNamespaces $namespace; + public static array $objects = []; + public function __construct(string $domain, ?string $subdomain = null) { if (is_null($subdomain)) { @@ -54,12 +56,20 @@ public function __construct(string $domain, ?string $subdomain = null) $this->namespace = DomainNamespaces::from($this->domain, $this->subdomain); - $this->path = Path::join(DomainResolver::getConfiguredDomainPath(), $this->domainWithSubdomain); + $this->path = Path::join(DomainResolver::domainPath(), $this->domainWithSubdomain); + } + + protected function registerDomainObjects() + { + } + + protected function registerDomainObject() + { } protected function getDomainBasePath() { - return app()->basePath(DomainResolver::getConfiguredDomainPath()); + return app()->basePath(DomainResolver::domainPath()); } public function path(?string $path = null): string @@ -82,21 +92,33 @@ public function relativePath(string $path = ''): string return collect([$this->domain, $path])->filter()->implode(DIRECTORY_SEPARATOR); } - public function model(string $name): DomainObject + public function namespaceFor(string $type): string { - $name = str_replace($this->namespace->models.'\\', '', $name); + return DomainResolver::getDomainObjectNamespace($this->domainWithSubdomain, $type); + } + + public function object(string $type, string $name): DomainObject + { + $namespace = $this->namespaceFor($type); + + $name = str($name)->replace("{$namespace}\\", '')->toString(); return new DomainObject( name: $name, - namespace: $this->namespace->models, - fqn: $this->namespace->models.'\\'.$name, - path: $this->path($this->namespace->models.'\\'.$name), + namespace: $namespace, + fqn: $namespace.'\\'.$name, + path: $this->path($namespace.'\\'.$name), ); } + public function model(string $name): DomainObject + { + return $this->object('model', $name); + } + public function factory(string $name): DomainObject { - $name = str_replace($this->namespace->factories.'\\', '', $name); + $name = str($name)->replace($this->namespace->root, '')->toString(); return new DomainObject( name: $name, @@ -110,14 +132,7 @@ public function factory(string $name): DomainObject public function dataTransferObject(string $name): DomainObject { - $name = str_replace($this->namespace->dataTransferObjects.'\\', '', $name); - - return new DomainObject( - name: $name, - namespace: $this->namespace->dataTransferObjects, - fqn: $this->namespace->dataTransferObjects.'\\'.$name, - path: $this->path($this->namespace->dataTransferObjects.'\\'.$name), - ); + return $this->object('data_transfer_object', $name); } public function dto(string $name): DomainObject @@ -127,37 +142,66 @@ public function dto(string $name): DomainObject public function viewModel(string $name): DomainObject { - $name = str_replace($this->namespace->viewModels.'\\', '', $name); - - return new DomainObject( - name: $name, - namespace: $this->namespace->viewModels, - fqn: $this->namespace->viewModels.'\\'.$name, - path: $this->path($this->namespace->viewModels.'\\'.$name), - ); + return $this->object('view_model', $name); } public function valueObject(string $name): DomainObject { - $name = str_replace($this->namespace->valueObjects.'\\', '', $name); - - return new DomainObject( - name: $name, - namespace: $this->namespace->valueObjects, - fqn: $this->namespace->valueObjects.'\\'.$name, - path: $this->path($this->namespace->valueObjects.'\\'.$name), - ); + return $this->object('value_object', $name); } public function action(string $name): DomainObject { - $name = str_replace($this->namespace->actions.'\\', '', $name); + return $this->object('action', $name); + } - return new DomainObject( - name: $name, - namespace: $this->namespace->actions, - fqn: $this->namespace->actions.'\\'.$name, - path: $this->path($this->namespace->actions.'\\'.$name), - ); + public function cast(string $name): DomainObject + { + return $this->object('cast', $name); + } + + public function command(string $name): DomainObject + { + return $this->object('command', $name); + } + + public function enum(string $name): DomainObject + { + return $this->object('enum', $name); + } + + public function job(string $name): DomainObject + { + return $this->object('job', $name); + } + + public function mail(string $name): DomainObject + { + return $this->object('mail', $name); + } + + public function notification(string $name): DomainObject + { + return $this->object('notification', $name); + } + + public function resource(string $name): DomainObject + { + return $this->object('resource', $name); + } + + public function rule(string $name): DomainObject + { + return $this->object('rule', $name); + } + + public function event(string $name): DomainObject + { + return $this->object('event', $name); + } + + public function exception(string $name): DomainObject + { + return $this->object('exception', $name); } } diff --git a/src/Support/DomainResolver.php b/src/Support/DomainResolver.php index 470849c..d4e3ead 100644 --- a/src/Support/DomainResolver.php +++ b/src/Support/DomainResolver.php @@ -2,37 +2,48 @@ namespace Lunarstorm\LaravelDDD\Support; -use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; class DomainResolver { - public static function getConfiguredDomainPath(): ?string + public static function domainChoices(): array { - if (Config::has('ddd.paths.domains')) { - // Deprecated - return config('ddd.paths.domains'); - } + $folders = glob(app()->basePath(static::domainPath().'/*'), GLOB_ONLYDIR); + + return collect($folders) + ->map(function ($folder) { + return basename($folder); + }) + ->sort() + ->toArray(); + } + public static function domainPath(): ?string + { return config('ddd.domain_path'); } - public static function getConfiguredDomainNamespace(): ?string + public static function domainRootNamespace(): ?string { - if (Config::has('ddd.paths.domains')) { - // Deprecated - return basename(config('ddd.paths.domains')); - } - return config('ddd.domain_namespace'); } + public static function getRelativeObjectNamespace(string $type): string + { + return config("ddd.namespaces.{$type}", str($type)->plural()->studly()->toString()); + } + + public static function getDomainObjectNamespace(string $domain, string $type): string + { + return implode('\\', [static::domainRootNamespace(), $domain, static::getRelativeObjectNamespace($type)]); + } + public static function guessDomainFromClass(string $class): ?string { - $domainNamespace = Str::finish(DomainResolver::getConfiguredDomainNamespace(), '\\'); + $domainNamespace = Str::finish(DomainResolver::domainRootNamespace(), '\\'); if (! str($class)->startsWith($domainNamespace)) { - // Not a domain model + // Not a domain object return null; } diff --git a/src/ValueObjects/DomainNamespaces.php b/src/ValueObjects/DomainNamespaces.php index 44f9780..3f1d9d9 100644 --- a/src/ValueObjects/DomainNamespaces.php +++ b/src/ValueObjects/DomainNamespaces.php @@ -14,6 +14,16 @@ public function __construct( public readonly string $viewModels, public readonly string $valueObjects, public readonly string $actions, + public readonly string $casts, + public readonly string $commands, + public readonly string $enums, + public readonly string $events, + public readonly string $exceptions, + public readonly string $jobs, + public readonly string $mail, + public readonly string $notifications, + public readonly string $resources, + public readonly string $rules, ) { } @@ -23,18 +33,28 @@ public static function from(string $domain, ?string $subdomain = null): self ->when($subdomain, fn ($domain) => $domain->append("\\{$subdomain}")) ->toString(); - $root = DomainResolver::getConfiguredDomainNamespace(); + $root = DomainResolver::domainRootNamespace(); $domainNamespace = implode('\\', [$root, $domainWithSubdomain]); return new self( root: $domainNamespace, - models: "{$domainNamespace}\\".config('ddd.namespaces.models'), + models: "{$domainNamespace}\\".config('ddd.namespaces.model', 'Models'), factories: "Database\\Factories\\{$domainWithSubdomain}", - dataTransferObjects: "{$domainNamespace}\\".config('ddd.namespaces.data_transfer_objects'), - viewModels: "{$domainNamespace}\\".config('ddd.namespaces.view_models'), - valueObjects: "{$domainNamespace}\\".config('ddd.namespaces.value_objects'), - actions: "{$domainNamespace}\\".config('ddd.namespaces.actions'), + dataTransferObjects: "{$domainNamespace}\\".config('ddd.namespaces.data_transfer_object', 'Data'), + viewModels: "{$domainNamespace}\\".config('ddd.namespaces.view_model', 'ViewModels'), + valueObjects: "{$domainNamespace}\\".config('ddd.namespaces.value_object', 'ValueObjects'), + actions: "{$domainNamespace}\\".config('ddd.namespaces.action', 'Actions'), + enums: "{$domainNamespace}\\".config('ddd.namespaces.enums', 'Enums'), + events: "{$domainNamespace}\\".config('ddd.namespaces.event', 'Events'), + casts: "{$domainNamespace}\\".config('ddd.namespaces.cast', 'Casts'), + commands: "{$domainNamespace}\\".config('ddd.namespaces.command', 'Commands'), + exceptions: "{$domainNamespace}\\".config('ddd.namespaces.exception', 'Exceptions'), + jobs: "{$domainNamespace}\\".config('ddd.namespaces.job', 'Jobs'), + mail: "{$domainNamespace}\\".config('ddd.namespaces.mail', 'Mail'), + notifications: "{$domainNamespace}\\".config('ddd.namespaces.notification', 'Notifications'), + resources: "{$domainNamespace}\\".config('ddd.namespaces.resource', 'Resources'), + rules: "{$domainNamespace}\\".config('ddd.namespaces.rule', 'Rules'), ); } } diff --git a/src/ValueObjects/DomainObjectNamespace.php b/src/ValueObjects/DomainObjectNamespace.php new file mode 100644 index 0000000..0681f8d --- /dev/null +++ b/src/ValueObjects/DomainObjectNamespace.php @@ -0,0 +1,30 @@ +when($subdomain, fn ($domain) => $domain->append("\\{$subdomain}")) + ->toString(); + + $root = DomainResolver::domainRootNamespace(); + + $domainNamespace = implode('\\', [$root, $domainWithSubdomain]); + + $namespace = "{$domainNamespace}\\".config("ddd.namespaces.{$key}", Str::studly($key)); + + return new self(key: $key, namespace: $namespace); + } +} diff --git a/tests/Command/ListTest.php b/tests/Command/ListTest.php new file mode 100644 index 0000000..36afcee --- /dev/null +++ b/tests/Command/ListTest.php @@ -0,0 +1,41 @@ +artisan('ddd:model', [ + 'name' => 'Invoice', + '--domain' => 'Invoicing', + ]); + + $this->artisan('ddd:dto', [ + 'name' => 'CustomerProfile', + '--domain' => 'Customer', + ]); + + $this->expectedDomains = [ + 'Customer', + 'Invoicing', + 'Shared', + ]; +}); + +it('can list domains', function () { + $expectedTableContent = collect($this->expectedDomains) + ->map(function (string $name) { + return [ + $name, + "Domain\\{$name}", + Path::normalize("src/Domain/{$name}"), + ]; + }) + ->toArray(); + + $this + ->artisan('ddd:list') + ->expectsTable([ + 'Domain', + 'Namespace', + 'Path', + ], $expectedTableContent); +}); diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index c42fd63..526fb88 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -8,11 +8,11 @@ Config::set('ddd.domain_path', $path); - expect(DomainResolver::getConfiguredDomainPath())->toEqual($path); + expect(DomainResolver::domainPath())->toEqual($path); }); it('can customize the domain root namespace via ddd.domain_namespace', function () { Config::set('ddd.domain_namespace', 'Doughmain'); - expect(DomainResolver::getConfiguredDomainNamespace())->toEqual('Doughmain'); + expect(DomainResolver::domainRootNamespace())->toEqual('Doughmain'); }); diff --git a/tests/Fixtures/Enums/Feature.php b/tests/Fixtures/Enums/Feature.php index 97de399..21c2aa1 100644 --- a/tests/Fixtures/Enums/Feature.php +++ b/tests/Fixtures/Enums/Feature.php @@ -6,6 +6,7 @@ enum Feature: string { case PromptForMissingInput = '9.49.0'; case IncludeFilepathInGeneratorCommandOutput = '9.32.0'; + case LaravelPromptsPackage = '10.17'; public function exists(): bool { diff --git a/tests/Generator/ExtendedCommandsTest.php b/tests/Generator/ExtendedCommandsTest.php new file mode 100644 index 0000000..2678e2d --- /dev/null +++ b/tests/Generator/ExtendedCommandsTest.php @@ -0,0 +1,54 @@ +object($type, $objectName); + + $relativePath = $domainObject->path; + $expectedNamespace = $domainObject->namespace; + $expectedPath = base_path($relativePath); + + if (file_exists($expectedPath)) { + unlink($expectedPath); + } + + expect(file_exists($expectedPath))->toBeFalse(); + + $command = "ddd:{$type} {$domain->domain}:{$objectName}"; + + Artisan::call($command); + + expect(Artisan::output())->toContainFilepath($relativePath); + + expect(file_exists($expectedPath))->toBeTrue(); + + expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};"); +})->with([ + 'cast' => ['cast', 'SomeCast'], + 'channel' => ['channel', 'SomeChannel'], + 'command' => ['command', 'SomeCommand'], + 'enum' => ['enum', 'SomeEnum'], + 'event' => ['event', 'SomeEvent'], + 'exception' => ['exception', 'SomeException'], + 'job' => ['job', 'SomeJob'], + 'listener' => ['listener', 'SomeListener'], + 'mail' => ['mail', 'SomeMail'], + 'notification' => ['notification', 'SomeNotification'], + 'observer' => ['observer', 'SomeObserver'], + 'policy' => ['policy', 'SomePolicy'], + 'provider' => ['provider', 'SomeProvider'], + 'resource' => ['resource', 'SomeResource'], + 'rule' => ['rule', 'SomeRule'], + 'scope' => ['scope', 'SomeScope'], +])->with('domainPaths'); diff --git a/tests/Generator/MakeActionTest.php b/tests/Generator/MakeActionTest.php index d80cb6f..2bdb1f2 100644 --- a/tests/Generator/MakeActionTest.php +++ b/tests/Generator/MakeActionTest.php @@ -15,7 +15,7 @@ $relativePath = implode('/', [ $domainPath, $domain, - config('ddd.namespaces.actions'), + config('ddd.namespaces.action'), "{$name}.php", ]); @@ -27,7 +27,7 @@ expect(file_exists($expectedPath))->toBeFalse(); - Artisan::call("ddd:action {$domain} {$name}"); + Artisan::call("ddd:action {$domain}:{$name}"); expect(Artisan::output())->when( Feature::IncludeFilepathInGeneratorCommandOutput->exists(), @@ -39,7 +39,7 @@ $expectedNamespace = implode('\\', [ $domainRoot, $domain, - config('ddd.namespaces.actions'), + config('ddd.namespaces.action'), ]); expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};"); @@ -51,22 +51,15 @@ $expectedPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.actions'), + config('ddd.namespaces.action'), "{$normalized}.php", ])); - Artisan::call("ddd:action {$domain} {$given}"); + Artisan::call("ddd:action {$domain}:{$given}"); expect(file_exists($expectedPath))->toBeTrue(); })->with('makeActionInputs'); -it('shows meaningful hints when prompting for missing input', function () { - $this->artisan('ddd:action') - ->expectsQuestion('What is the domain?', 'Utility') - ->expectsQuestion('What should the action be named?', 'DoThatThing') - ->assertExitCode(0); -})->ifSupportsPromptForMissingInput(); - it('extends a base action if specified in config', function ($baseAction) { Config::set('ddd.base_action', $baseAction); @@ -76,7 +69,7 @@ $expectedPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.actions'), + config('ddd.namespaces.action'), "{$name}.php", ])); @@ -84,7 +77,7 @@ unlink($expectedPath); } - Artisan::call("ddd:action {$domain} {$name}"); + Artisan::call("ddd:action {$domain}:{$name}"); expect(file_exists($expectedPath))->toBeTrue(); @@ -103,7 +96,7 @@ $expectedPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.actions'), + config('ddd.namespaces.action'), "{$name}.php", ])); @@ -111,7 +104,7 @@ unlink($expectedPath); } - Artisan::call("ddd:action {$domain} {$name}"); + Artisan::call("ddd:action {$domain}:{$name}"); expect(file_exists($expectedPath))->toBeTrue(); expect(file_get_contents($expectedPath))->toContain("class {$name}".PHP_EOL.'{'); diff --git a/tests/Generator/MakeBaseModelTest.php b/tests/Generator/MakeBaseModelTest.php index f7507e8..7a30076 100644 --- a/tests/Generator/MakeBaseModelTest.php +++ b/tests/Generator/MakeBaseModelTest.php @@ -14,7 +14,7 @@ $relativePath = implode('/', [ $domainPath, $domain, - config('ddd.namespaces.models'), + config('ddd.namespaces.model'), "{$modelName}.php", ]); @@ -26,7 +26,7 @@ expect(file_exists($expectedPath))->toBeFalse(); - Artisan::call("ddd:base-model {$domain} {$modelName}"); + Artisan::call("ddd:base-model {$domain}:{$modelName}"); expect(Artisan::output())->when( Feature::IncludeFilepathInGeneratorCommandOutput->exists(), @@ -38,14 +38,8 @@ $expectedNamespace = implode('\\', [ $domainRoot, $domain, - config('ddd.namespaces.models'), + config('ddd.namespaces.model'), ]); expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};"); })->with('domainPaths'); - -it('shows meaningful hints when prompting for missing input', function () { - $this->artisan('ddd:base-model') - ->expectsQuestion('What is the domain?', 'Shared') - ->assertExitCode(0); -})->ifSupportsPromptForMissingInput(); diff --git a/tests/Generator/MakeBaseViewModelTest.php b/tests/Generator/MakeBaseViewModelTest.php index 9b85555..2527ce2 100644 --- a/tests/Generator/MakeBaseViewModelTest.php +++ b/tests/Generator/MakeBaseViewModelTest.php @@ -2,7 +2,6 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Config; -use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature; it('can generate base view model', function ($domainPath, $domainRoot) { Config::set('ddd.domain_path', $domainPath); @@ -14,7 +13,7 @@ $relativePath = implode('/', [ $domainPath, $domain, - config('ddd.namespaces.view_models'), + config('ddd.namespaces.view_model'), "{$className}.php", ]); @@ -26,26 +25,17 @@ expect(file_exists($expectedPath))->toBeFalse(); - Artisan::call("ddd:base-view-model {$domain} {$className}"); + Artisan::call("ddd:base-view-model {$domain}:{$className}"); - expect(Artisan::output())->when( - Feature::IncludeFilepathInGeneratorCommandOutput->exists(), - fn ($output) => $output->toContainFilepath($relativePath), - ); + expect(Artisan::output())->toContainFilepath($relativePath); expect(file_exists($expectedPath))->toBeTrue(); $expectedNamespace = implode('\\', [ $domainRoot, $domain, - config('ddd.namespaces.view_models'), + config('ddd.namespaces.view_model'), ]); expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};"); })->with('domainPaths'); - -it('shows meaningful hints when prompting for missing input', function () { - $this->artisan('ddd:base-view-model') - ->expectsQuestion('What is the domain?', 'Shared') - ->assertExitCode(0); -})->ifSupportsPromptForMissingInput(); diff --git a/tests/Generator/MakeDataTransferObjectTest.php b/tests/Generator/MakeDataTransferObjectTest.php index 5cfd786..34c69df 100644 --- a/tests/Generator/MakeDataTransferObjectTest.php +++ b/tests/Generator/MakeDataTransferObjectTest.php @@ -15,7 +15,7 @@ $relativePath = implode('/', [ $domainPath, $domain, - config('ddd.namespaces.data_transfer_objects'), + config('ddd.namespaces.data_transfer_object'), "{$dtoName}.php", ]); @@ -27,7 +27,7 @@ expect(file_exists($expectedPath))->toBeFalse(); - Artisan::call("ddd:dto {$domain} {$dtoName}"); + Artisan::call("ddd:dto {$domain}:{$dtoName}"); expect(Artisan::output())->when( Feature::IncludeFilepathInGeneratorCommandOutput->exists(), @@ -39,7 +39,7 @@ $expectedNamespace = implode('\\', [ $domainRoot, $domain, - config('ddd.namespaces.data_transfer_objects'), + config('ddd.namespaces.data_transfer_object'), ]); expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};"); @@ -51,18 +51,11 @@ $expectedPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.data_transfer_objects'), + config('ddd.namespaces.data_transfer_object'), "{$normalized}.php", ])); - Artisan::call("ddd:dto {$domain} {$given}"); + Artisan::call("ddd:dto {$domain}:{$given}"); expect(file_exists($expectedPath))->toBeTrue(); })->with('makeDtoInputs'); - -it('shows meaningful hints when prompting for missing input', function () { - $this->artisan('ddd:dto') - ->expectsQuestion('What is the domain?', 'Utility') - ->expectsQuestion('What should the data transfer object be named?', 'Belt') - ->assertExitCode(0); -})->ifSupportsPromptForMissingInput(); diff --git a/tests/Generator/MakeFactoryTest.php b/tests/Generator/MakeFactoryTest.php index 2af1625..75173e8 100644 --- a/tests/Generator/MakeFactoryTest.php +++ b/tests/Generator/MakeFactoryTest.php @@ -33,7 +33,7 @@ expect(file_exists($expectedFactoryPath))->toBeFalse(); - Artisan::call("ddd:factory {$domain->dotName} {$modelName}"); + Artisan::call("ddd:factory {$domain->dotName}:{$modelName}"); expect(Artisan::output())->when( Feature::IncludeFilepathInGeneratorCommandOutput->exists(), diff --git a/tests/Generator/MakeModelTest.php b/tests/Generator/MakeModelTest.php index d4abde0..50076a7 100644 --- a/tests/Generator/MakeModelTest.php +++ b/tests/Generator/MakeModelTest.php @@ -2,7 +2,6 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Str; use Lunarstorm\LaravelDDD\Support\Domain; use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature; @@ -10,13 +9,13 @@ Config::set('ddd.domain_path', $domainPath); Config::set('ddd.domain_namespace', $domainRoot); - $modelName = Str::studly(fake()->word()); - $domain = Str::studly(fake()->word()); + $modelName = 'Record'; + $domain = 'World'; $relativePath = implode('/', [ $domainPath, $domain, - config('ddd.namespaces.models'), + config('ddd.namespaces.model'), "{$modelName}.php", ]); @@ -28,7 +27,7 @@ expect(file_exists($expectedModelPath))->toBeFalse(); - Artisan::call("ddd:model {$domain} {$modelName}"); + Artisan::call("ddd:model {$domain}:{$modelName}"); expect(Artisan::output())->when( Feature::IncludeFilepathInGeneratorCommandOutput->exists(), @@ -40,7 +39,7 @@ $expectedNamespace = implode('\\', [ $domainRoot, $domain, - config('ddd.namespaces.models'), + config('ddd.namespaces.model'), ]); expect(file_get_contents($expectedModelPath))->toContain("namespace {$expectedNamespace};"); @@ -49,7 +48,7 @@ it('can generate a domain model with factory', function ($domainPath, $domainRoot, $domainName, $subdomain) { Config::set('ddd.domain_path', $domainPath); - $modelName = Str::studly(fake()->word()); + $modelName = 'Record'; $domain = new Domain($domainName, $subdomain); @@ -72,18 +71,17 @@ } Artisan::call('ddd:model', [ - 'domain' => $domain->dotName, 'name' => $modelName, + '--domain' => $domain->dotName, '--factory' => true, ]); - expect(Artisan::output())->when( - Feature::IncludeFilepathInGeneratorCommandOutput->exists(), - fn ($output) => $output->toContainFilepath($domainModel->path), - ); + $output = Artisan::output(); - expect(file_exists($expectedModelPath))->toBeTrue(); - expect(file_exists($expectedFactoryPath))->toBeTrue(); + expect($output)->toContainFilepath($domainModel->path); + + expect(file_exists($expectedModelPath))->toBeTrue("Expecting model file to be generated at {$expectedModelPath}"); + expect(file_exists($expectedFactoryPath))->toBeTrue("Expecting factory file to be generated at {$expectedFactoryPath}"); expect(file_get_contents($expectedFactoryPath)) ->toContain("use {$domainModel->fqn};") @@ -91,37 +89,37 @@ })->with('domainPaths')->with('domainSubdomain'); it('normalizes generated model to pascal case', function ($given, $normalized) { - $domain = Str::studly(fake()->word()); + $domain = 'World'; $expectedModelPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.models'), + config('ddd.namespaces.model'), "{$normalized}.php", ])); - Artisan::call("ddd:model {$domain} {$given}"); + Artisan::call("ddd:model {$domain}:{$given}"); expect(file_exists($expectedModelPath))->toBeTrue(); })->with('makeModelInputs'); it('generates the base model when possible', function ($baseModelClass, $baseModelPath) { - $modelName = Str::studly(fake()->word()); - $domain = Str::studly(fake()->word()); + $modelName = 'Record'; + $domain = 'World'; Config::set('ddd.base_model', $baseModelClass); $expectedModelPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.models'), + config('ddd.namespaces.model'), "{$modelName}.php", ])); $expectedModelClass = implode('\\', [ basename(config('ddd.domain_path')), $domain, - config('ddd.namespaces.models'), + config('ddd.namespaces.model'), $modelName, ]); @@ -141,7 +139,7 @@ expect(file_exists($expectedBaseModelPath))->toBeFalse("{$baseModelPath} expected not to exist."); - Artisan::call("ddd:model {$domain} {$modelName}"); + Artisan::call("ddd:model {$domain}:{$modelName}"); expect(file_exists($expectedBaseModelPath))->toBeTrue("Expecting base model file to be generated at {$baseModelPath}"); @@ -158,7 +156,7 @@ expect(class_exists($baseModel))->toBeFalse(); - Artisan::call('ddd:model Fruits Lemon'); + Artisan::call('ddd:model Fruits:Lemon'); expect(Artisan::output()) ->toContain("Configured base model {$baseModel} doesn't exist.") @@ -175,7 +173,7 @@ expect(class_exists($baseModel))->toBeTrue(); - Artisan::call('ddd:model Fruits Lemon'); + Artisan::call('ddd:model Fruits:Lemon'); expect(Artisan::output()) ->not->toContain("Configured base model {$baseModel} doesn't exist.") @@ -184,10 +182,3 @@ ['Illuminate\Database\Eloquent\Model'], ['Lunarstorm\LaravelDDD\Models\DomainModel'], ]); - -it('shows meaningful hints when prompting for missing input', function () { - $this->artisan('ddd:model') - ->expectsQuestion('What is the domain?', 'Utility') - ->expectsQuestion('What should the model be named?', 'Belt') - ->assertExitCode(0); -})->ifSupportsPromptForMissingInput(); diff --git a/tests/Generator/MakeValueObjectTest.php b/tests/Generator/MakeValueObjectTest.php index 92d8029..5b0ffa9 100644 --- a/tests/Generator/MakeValueObjectTest.php +++ b/tests/Generator/MakeValueObjectTest.php @@ -9,13 +9,13 @@ Config::set('ddd.domain_path', $domainPath); Config::set('ddd.domain_namespace', $domainRoot); - $valueObjectName = Str::studly(fake()->word()); - $domain = Str::studly(fake()->word()); + $domain = 'Mission'; + $valueObjectName = 'ImpossibleValue'; $relativePath = implode('/', [ $domainPath, $domain, - config('ddd.namespaces.value_objects'), + config('ddd.namespaces.value_object'), "{$valueObjectName}.php", ]); @@ -27,7 +27,7 @@ expect(file_exists($expectedPath))->toBeFalse(); - Artisan::call("ddd:value {$domain} {$valueObjectName}"); + Artisan::call("ddd:value {$domain}:{$valueObjectName}"); expect(Artisan::output())->when( Feature::IncludeFilepathInGeneratorCommandOutput->exists(), @@ -39,7 +39,7 @@ $expectedNamespace = implode('\\', [ $domainRoot, $domain, - config('ddd.namespaces.value_objects'), + config('ddd.namespaces.value_object'), ]); expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};"); @@ -51,11 +51,11 @@ $expectedPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.value_objects'), + config('ddd.namespaces.value_object'), "{$normalized}.php", ])); - Artisan::call("ddd:value {$domain} {$given}"); + Artisan::call("ddd:value {$domain}:{$given}"); expect(file_exists($expectedPath))->toBeTrue(); })->with([ @@ -65,10 +65,3 @@ 'LargeNumber' => ['LargeNumber', 'LargeNumber'], 'large-number' => ['large-number', 'LargeNumber'], ]); - -it('shows meaningful hints when prompting for missing input', function () { - $this->artisan('ddd:value') - ->expectsQuestion('What is the domain?', 'Utility') - ->expectsQuestion('What should the value object be named?', 'Belt') - ->assertExitCode(0); -})->ifSupportsPromptForMissingInput(); diff --git a/tests/Generator/MakeViewModelTest.php b/tests/Generator/MakeViewModelTest.php index 2120023..482b6da 100644 --- a/tests/Generator/MakeViewModelTest.php +++ b/tests/Generator/MakeViewModelTest.php @@ -3,7 +3,6 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; -use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature; it('can generate view models', function ($domainPath, $domainRoot) { Config::set('ddd.domain_path', $domainPath); @@ -15,7 +14,7 @@ $relativePath = implode('/', [ $domainPath, $domain, - config('ddd.namespaces.view_models'), + config('ddd.namespaces.view_model'), "{$viewModelName}.php", ]); @@ -27,19 +26,16 @@ expect(file_exists($expectedPath))->toBeFalse(); - Artisan::call("ddd:view-model {$domain} {$viewModelName}"); + Artisan::call("ddd:view-model {$domain}:{$viewModelName}"); - expect(Artisan::output())->when( - Feature::IncludeFilepathInGeneratorCommandOutput->exists(), - fn ($output) => $output->toContainFilepath($relativePath), - ); + expect(Artisan::output())->toContainFilepath($relativePath); expect(file_exists($expectedPath))->toBeTrue(); $expectedNamespace = implode('\\', [ $domainRoot, $domain, - config('ddd.namespaces.view_models'), + config('ddd.namespaces.view_model'), ]); expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};"); @@ -51,11 +47,11 @@ $expectedPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.view_models'), + config('ddd.namespaces.view_model'), "{$normalized}.php", ])); - Artisan::call("ddd:view-model {$domain} {$given}"); + Artisan::call("ddd:view-model {$domain}:{$given}"); expect(file_exists($expectedPath))->toBeTrue(); })->with('makeViewModelInputs'); @@ -67,7 +63,7 @@ $expectedPath = base_path(implode('/', [ config('ddd.domain_path'), $domain, - config('ddd.namespaces.view_models'), + config('ddd.namespaces.view_model'), "{$className}.php", ])); @@ -86,14 +82,7 @@ expect(file_exists($expectedBaseViewModelPath))->toBeFalse(); - Artisan::call("ddd:view-model {$domain} {$className}"); + Artisan::call("ddd:view-model {$domain}:{$className}"); expect(file_exists($expectedBaseViewModelPath))->toBeTrue(); }); - -it('shows meaningful hints when prompting for missing input', function () { - $this->artisan('ddd:view-model') - ->expectsQuestion('What is the domain?', 'Utility') - ->expectsQuestion('What should the view model be named?', 'Belt') - ->assertExitCode(0); -})->ifSupportsPromptForMissingInput(); diff --git a/tests/Generator/PromptTest.php b/tests/Generator/PromptTest.php new file mode 100644 index 0000000..3dea905 --- /dev/null +++ b/tests/Generator/PromptTest.php @@ -0,0 +1,48 @@ +artisan('ddd:action') + ->expectsQuestion('What should the action be named?', 'DoThatThing') + ->expectsQuestion('What is the domain?', 'Utility') + ->assertExitCode(0); +}); + +it('[view model] prompts for missing input', function () { + $this->artisan('ddd:view-model') + ->expectsQuestion('What should the view model be named?', 'Belt') + ->expectsQuestion('What is the domain?', 'Utility') + ->assertExitCode(0); +}); + +it('[base view model] prompts for missing input', function () { + $this->artisan('ddd:base-view-model') + ->expectsQuestion('What is the domain?', 'Shared') + ->assertExitCode(0); +}); + +it('[model] prompts for missing input', function () { + $this->artisan('ddd:model') + ->expectsQuestion('What should the model be named?', 'Belt') + ->expectsQuestion('What is the domain?', 'Utility') + ->assertExitCode(0); +}); + +it('[base model] prompts for missing input', function () { + $this->artisan('ddd:base-model') + ->expectsQuestion('What is the domain?', 'Shared') + ->assertExitCode(0); +}); + +it('[value object] prompts for missing input', function () { + $this->artisan('ddd:value') + ->expectsQuestion('What should the value object be named?', 'Belt') + ->expectsQuestion('What is the domain?', 'Utility') + ->assertExitCode(0); +}); + +it('[data transfer object] prompts for missing input', function () { + $this->artisan('ddd:dto') + ->expectsQuestion('What should the data transfer object be named?', 'Belt') + ->expectsQuestion('What is the domain?', 'Utility') + ->assertExitCode(0); +}); diff --git a/tests/Model/FactoryTest.php b/tests/Model/FactoryTest.php index afe9368..eda5d9e 100644 --- a/tests/Model/FactoryTest.php +++ b/tests/Model/FactoryTest.php @@ -15,7 +15,7 @@ it('can instantiate a domain model factory', function ($domainParameter, $modelName, $modelClass) { Config::set('ddd.base_model', 'Lunarstorm\LaravelDDD\Models\DomainModel'); - Artisan::call("ddd:model -f {$domainParameter} {$modelName}"); + Artisan::call("ddd:model -f {$domainParameter}:{$modelName}"); expect(class_exists($modelClass))->toBeTrue(); expect($modelClass::factory())->toBeInstanceOf(Factory::class); })->with([ diff --git a/tests/Pest.php b/tests/Pest.php index d9fde5f..1363be1 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -12,13 +12,3 @@ function skipOnLaravelVersionsBelow($minimumVersion) test()->markTestSkipped("Only relevant from Laravel {$minimumVersion} onwards (Current version: {$version})."); } } - -function ifSupportsPromptForMissingInput() -{ - return skipOnLaravelVersionsBelow('9.49.0'); -} - -function ifGeneratorCommandsOutputFilePath() -{ - return skipOnLaravelVersionsBelow('9.32.0'); -} diff --git a/tests/Support/DomainResolverTest.php b/tests/Support/DomainResolverTest.php new file mode 100644 index 0000000..37b63b9 --- /dev/null +++ b/tests/Support/DomainResolverTest.php @@ -0,0 +1,25 @@ +artisan('ddd:model', [ + 'name' => 'Invoice', + '--domain' => 'Invoicing', + ]); + + $this->artisan('ddd:dto', [ + 'name' => 'CustomerProfile', + '--domain' => 'Customer', + ]); + + $this->expectedDomains = [ + 'Customer', + 'Invoicing', + 'Shared', + ]; +}); + +it('can get the current domains', function () { + expect(DomainResolver::domainChoices())->toEqualCanonicalizing($this->expectedDomains); +}); diff --git a/tests/Support/DomainTest.php b/tests/Support/DomainTest.php index 5b20189..5e3d760 100644 --- a/tests/Support/DomainTest.php +++ b/tests/Support/DomainTest.php @@ -79,3 +79,13 @@ ['Reporting', 'SendInvoiceReport', 'Domain\\Reporting\\Actions\\SendInvoiceReport', 'src/Domain/Reporting/Actions/SendInvoiceReport.php'], ['Reporting.Internal', 'SendInvoiceReport', 'Domain\\Reporting\\Internal\\Actions\\SendInvoiceReport', 'src/Domain/Reporting/Internal/Actions/SendInvoiceReport.php'], ]); + +it('can describe an anonymous domain object', function ($domainName, $objectType, $objectName, $expectedFQN, $expectedPath) { + expect((new Domain($domainName))->object($objectType, $objectName)) + ->name->toBe($objectName) + ->fqn->toBe($expectedFQN) + ->path->toBe(Path::normalize($expectedPath)); +})->with([ + ['Invoicing', 'rule', 'SomeRule', 'Domain\\Invoicing\\Rules\\SomeRule', 'src/Domain/Invoicing/Rules/SomeRule.php'], + ['Other', 'thing', 'Something', 'Domain\\Other\\Things\\Something', 'src/Domain/Other/Things/Something.php'], +]);