From c944c790c9f9569ca2f5b7f9338df027d6cadd77 Mon Sep 17 00:00:00 2001 From: Jasper Tey Date: Tue, 22 Oct 2024 08:43:17 -0400 Subject: [PATCH 1/5] [1.2] ddd:model options, migrations, application layer (#69) * WIP experimentation * Fix styling * Refactor OverridesHandle to HandleHooks * Fix styling * WIP * WIP: Refactoring ddd:model, generate application layer objects, and more. * Fix styling * Replace HasFactory with HasDomainFactory. * Fix styling * Normalize paths for tests on Windows. * Keep phpstan happy. * Fix styling * Add header block. * Remove stray methods. * Update readme. * Further refinements and WIP. * Update change notes. * Fix phpstan issue. * Fix test. * Normalize paths for windows. * Support application layer paths outside app folder. * Add peer dependencies section. * Document the Application Layer. * Update wording. * Minor wording. * Support optimize commands in Laravel 11.27.1 * Update cache tests. * Ensure optimize:clear after each test. * Update internals and implement custom namespace resolving. * Apply namespace resolver to all types. * Refactor cache/optimize commands. * Minor changes. * Wrap command column with backticks. * Simplify ddd.application_layer to ddd.application * Fix styling * Update readme/config wordings. --------- Co-authored-by: JasperTey --- CHANGELOG.md | 38 +++ README.md | 218 +++++++++++------- composer.json | 3 +- config/ddd.php | 66 +++--- config/ddd.php.stub | 48 ++-- src/Commands/CacheCommand.php | 24 -- .../Concerns/ForwardsToDomainCommands.php | 62 +++++ src/Commands/Concerns/HandleHooks.php | 25 ++ .../Concerns/QualifiesDomainModels.php | 17 ++ .../Concerns/ResolvesDomainFromInput.php | 13 +- src/Commands/DomainControllerMakeCommand.php | 82 +++++++ src/Commands/DomainMiddlewareMakeCommand.php | 13 ++ src/Commands/DomainModelMakeCommand.php | 91 ++++---- src/Commands/DomainModelMakeLegacyCommand.php | 108 +++++++++ src/Commands/DomainRequestMakeCommand.php | 27 +++ src/Commands/DomainSeederMakeCommand.php | 13 ++ .../Migration/BaseMigrateMakeCommand.php | 42 ++++ .../Migration/DomainMigrateMakeCommand.php | 27 +++ ...arCommand.php => OptimizeClearCommand.php} | 11 +- src/Commands/OptimizeCommand.php | 34 +++ src/DomainManager.php | 53 +++++ src/Facades/DDD.php | 1 + src/LaravelDDDServiceProvider.php | 35 ++- src/Support/Domain.php | 48 +++- src/Support/DomainAutoloader.php | 4 +- src/Support/DomainMigration.php | 81 +++++++ src/Support/DomainResolver.php | 99 +++++++- src/Support/Path.php | 5 + src/ValueObjects/DomainCommandContext.php | 51 ++++ .../2024_10_14_215911_do_nothing.php | 24 ++ .../NamespaceResolverTest.php | 44 ++++ tests/Command/CacheTest.php | 70 +++++- tests/Command/ListTest.php | 5 + tests/Fixtures/Enums/Feature.php | 1 + ...{MakeActionTest.php => ActionMakeTest.php} | 0 ...aseModelTest.php => BaseModelMakeTest.php} | 0 ...odelTest.php => BaseViewModelMakeTest.php} | 0 tests/Generator/ControllerMakeTest.php | 177 ++++++++++++++ ...sferObjectTest.php => DtoMakeTestTest.php} | 0 tests/Generator/ExtendedCommandsTest.php | 4 + ...akeFactoryTest.php => FactoryMakeTest.php} | 0 tests/Generator/MigrationMakeTest.php | 84 +++++++ .../{MakeModelTest.php => Model/MakeTest.php} | 34 ++- .../Model/MakeWithControllerTest.php | 90 ++++++++ tests/Generator/Model/MakeWithOptionsTest.php | 109 +++++++++ tests/Generator/PromptTest.php | 1 + tests/Generator/RequestMakeTest.php | 54 +++++ ...ObjectTest.php => ValueObjectMakeTest.php} | 0 ...iewModelTest.php => ViewModelMakeTest.php} | 0 tests/Support/DomainResolverTest.php | 5 + tests/Support/DomainTest.php | 33 +++ tests/TestCase.php | 1 + 52 files changed, 1841 insertions(+), 234 deletions(-) delete mode 100644 src/Commands/CacheCommand.php create mode 100644 src/Commands/Concerns/ForwardsToDomainCommands.php create mode 100644 src/Commands/Concerns/HandleHooks.php create mode 100644 src/Commands/Concerns/QualifiesDomainModels.php create mode 100644 src/Commands/DomainControllerMakeCommand.php create mode 100644 src/Commands/DomainMiddlewareMakeCommand.php create mode 100644 src/Commands/DomainModelMakeLegacyCommand.php create mode 100644 src/Commands/DomainRequestMakeCommand.php create mode 100644 src/Commands/DomainSeederMakeCommand.php create mode 100644 src/Commands/Migration/BaseMigrateMakeCommand.php create mode 100644 src/Commands/Migration/DomainMigrateMakeCommand.php rename src/Commands/{CacheClearCommand.php => OptimizeClearCommand.php} (66%) create mode 100644 src/Commands/OptimizeCommand.php create mode 100644 src/Support/DomainMigration.php create mode 100644 src/ValueObjects/DomainCommandContext.php create mode 100644 tests/.skeleton/src/Domain/Invoicing/Database/Migrations/2024_10_14_215911_do_nothing.php create mode 100644 tests/ApplicationLayer/NamespaceResolverTest.php rename tests/Generator/{MakeActionTest.php => ActionMakeTest.php} (100%) rename tests/Generator/{MakeBaseModelTest.php => BaseModelMakeTest.php} (100%) rename tests/Generator/{MakeBaseViewModelTest.php => BaseViewModelMakeTest.php} (100%) create mode 100644 tests/Generator/ControllerMakeTest.php rename tests/Generator/{MakeDataTransferObjectTest.php => DtoMakeTestTest.php} (100%) rename tests/Generator/{MakeFactoryTest.php => FactoryMakeTest.php} (100%) create mode 100644 tests/Generator/MigrationMakeTest.php rename tests/Generator/{MakeModelTest.php => Model/MakeTest.php} (83%) create mode 100644 tests/Generator/Model/MakeWithControllerTest.php create mode 100644 tests/Generator/Model/MakeWithOptionsTest.php create mode 100644 tests/Generator/RequestMakeTest.php rename tests/Generator/{MakeValueObjectTest.php => ValueObjectMakeTest.php} (100%) rename tests/Generator/{MakeViewModelTest.php => ViewModelMakeTest.php} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2926890..c2fab9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ All notable changes to `laravel-ddd` will be documented in this file. +## [Unreleased] +### Added +- Experimental: Ability to configure the Application Layer, to generate domain objects that don't typically belong inside the domain layer. + ```php + // In config/ddd.php + 'application' => [ + 'path' => 'app/Modules', + 'namespace' => 'App\Modules', + 'objects' => [ + 'controller', + 'request', + 'middleware', + ], + ], + ``` +- Added `ddd:controller` to generate domain-specific controllers in the application layer. +- Added `ddd:request` to generate domain-spefic requests in the application layer. +- Added `ddd:middleware` to generate domain-specific middleware in the application layer. +- Added `ddd:migration` to generate domain migrations. +- Added `ddd:seeder` to generate domain seeders. +- Migration folders across domains will be registered and scanned when running `php artisan migrate`, in addition to the standard application `database/migrations` path. + +### Changed +- `ddd:model` now internally extends Laravel's native `make:model` and inherits all standard options: + - `--migration|-m` + - `--factory|-f` + - `--seed|-s` + - `--controller --resource --requests|-crR` + - `--policy` + - `-mfsc` + - `--all|-a` + - `--pivot|-p` +- `ddd:cache` is now `ddd:optimize` (`ddd:cache` is still available as an alias). +- For Laravel 11.27.1+, the framework's `optimize` and `optimize:clear` commands will automatically invoke `ddd:optimize` and `ddd:clear` respectively. + +### Deprecated +- Domain base models are no longer required by default, and `config('ddd.base_model')` is now `null` by default. + ## [1.1.2] - 2024-09-02 ### Fixed - During domain factory autoloading, ensure that `guessFactoryNamesUsing` returns a string when a domain factory is resolved. diff --git a/README.md b/README.md index fd1c833..beb7412 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,26 @@ You may initialize the package using the `ddd:install` artisan command. This wil 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) +```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 accordingly. + ### Deployment -In production, run `ddd:cache` during the deployment process to [optimize autoloading](#autoloading-in-production). +In production, run `ddd:optimize` during the deployment process to [optimize autoloading](#autoloading-in-production). ```bash -php artisan ddd:cache +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. ### Version Compatibility Laravel | LaravelDDD | | @@ -53,55 +68,40 @@ php artisan ddd:{object} {name} ## Available Commands ### Generators -The following generators are currently available, shown using short-hand syntax: -```bash -# Generate a domain model -php artisan ddd:model Invoicing:Invoice - -# Generate a domain model with factory -php artisan ddd:model Invoicing:Invoice -f -php artisan ddd:model Invoicing:Invoice --factory - -# Generate a domain factory -php artisan ddd:factory Invoicing:InvoiceFactory -php artisan ddd:factory Invoicing:InvoiceFactory --model=Invoice # optionally specifying the model - -# Generate a data transfer object -php artisan ddd:dto Invoicing:LineItemPayload - -# Generates a value object -php artisan ddd:value Shared:DollarAmount - -# Generates a view model -php artisan ddd:view-model Invoicing:ShowInvoiceViewModel - -# Generates an action -php artisan ddd:action Invoicing:SendInvoiceToCustomer - -# Extended Commands -# These extend Laravel's respective make:* commands and places the objects into the domain layer -php artisan ddd:cast Invoicing:MoneyCast -php artisan ddd:channel Invoicing:InvoiceChannel -php artisan ddd:command Invoicing:InvoiceDeliver -php artisan ddd:event Invoicing:PaymentWasReceived -php artisan ddd:exception Invoicing:InvoiceNotFoundException -php artisan ddd:job Invoicing:GenerateInvoicePdf -php artisan ddd:listener Invoicing:HandlePaymentReceived -php artisan ddd:mail Invoicing:OverduePaymentReminderEmail -php artisan ddd:notification Invoicing:YourPaymentWasReceived -php artisan ddd:observer Invoicing:InvoiceObserver -php artisan ddd:policy Invoicing:InvoicePolicy -php artisan ddd:provider Invoicing:InvoiceServiceProvider -php artisan ddd:resource Invoicing:InvoiceResource -php artisan ddd:rule Invoicing:ValidPaymentMethod -php artisan ddd:scope Invoicing:ArchivedInvoicesScope - -# Laravel 11+ only -php artisan ddd:class Invoicing:Support/InvoiceBuilder -php artisan ddd:enum Customer:CustomerType -php artisan ddd:interface Customer:Contracts/Invoiceable -php artisan ddd:trait Customer:Concerns/HasInvoices -``` +The following generators are currently available: +| Command | Description | Usage | +|---|---|---| +| `ddd:model` | Generate a domain model | `php artisan ddd:model Invoicing:Invoice`

Options:
`--migration\|-m`
`--factory\|-f`
`--seed\|-s`
`--controller --resource --requests\|-crR`
`--policy`
`-mfsc`
`--all\|-a`
`--pivot\|-p`
| +| `ddd:factory` | Generate a domain factory | `php artisan ddd:factory Invoicing:InvoiceFactory` | +| `ddd:dto` | Generate a data transfer object | `php artisan ddd:dto Invoicing:LineItemPayload` | +| `ddd:value` | Generate a value object | `php artisan ddd:value Shared:DollarAmount` | +| `ddd:view-model` | Generate a view model | `php artisan ddd:view-model Invoicing:ShowInvoiceViewModel` | +| `ddd:action` | Generate an action | `php artisan ddd:action Invoicing:SendInvoiceToCustomer` | +| `ddd:cast` | Generate a cast | `php artisan ddd:cast Invoicing:MoneyCast` | +| `ddd:channel` | Generate a channel | `php artisan ddd:channel Invoicing:InvoiceChannel` | +| `ddd:command` | Generate a command | `php artisan ddd:command Invoicing:InvoiceDeliver` | +| `ddd:controller` | Generate a controller | `php artisan ddd:controller Invoicing:InvoiceController`

Options: inherits options from *make:controller* | +| `ddd:event` | Generate an event | `php artisan ddd:event Invoicing:PaymentWasReceived` | +| `ddd:exception` | Generate an exception | `php artisan ddd:exception Invoicing:InvoiceNotFoundException` | +| `ddd:job` | Generate a job | `php artisan ddd:job Invoicing:GenerateInvoicePdf` | +| `ddd:listener` | Generate a listener | `php artisan ddd:listener Invoicing:HandlePaymentReceived` | +| `ddd:mail` | Generate a mail | `php artisan ddd:mail Invoicing:OverduePaymentReminderEmail` | +| `ddd:middleware` | Generate a middleware | `php artisan ddd:middleware Invoicing:VerifiedCustomerMiddleware` | +| `ddd:migration` | Generate a migration | `php artisan ddd:migration Invoicing:CreateInvoicesTable` | +| `ddd:notification` | Generate a notification | `php artisan ddd:notification Invoicing:YourPaymentWasReceived` | +| `ddd:observer` | Generate an observer | `php artisan ddd:observer Invoicing:InvoiceObserver` | +| `ddd:policy` | Generate a policy | `php artisan ddd:policy Invoicing:InvoicePolicy` | +| `ddd:provider` | Generate a provider | `php artisan ddd:provider Invoicing:InvoiceServiceProvider` | +| `ddd:resource` | Generate a resource | `php artisan ddd:resource Invoicing:InvoiceResource` | +| `ddd:rule` | Generate a rule | `php artisan ddd:rule Invoicing:ValidPaymentMethod` | +| `ddd:request` | Generate a form request | `php artisan ddd:request Invoicing:StoreInvoiceRequest` | +| `ddd:scope` | Generate a scope | `php artisan ddd:scope Invoicing:ArchivedInvoicesScope` | +| `ddd:seeder` | Generate a seeder | `php artisan ddd:seeder Invoicing:InvoiceSeeder` | +| `ddd:class` | Generate a class (Laravel 11+) | `php artisan ddd:class Invoicing:Support/InvoiceBuilder` | +| `ddd:enum` | Generate an enum (Laravel 11+) | `php artisan ddd:enum Customer:CustomerType` | +| `ddd:interface` | Generate an interface (Laravel 11+) | `php artisan ddd:interface Customer:Contracts/Invoiceable` | +| `ddd:trait` | Generate a trait (Laravel 11+) | `php artisan ddd:trait Customer:Concerns/HasInvoices` | + Generated objects will be placed in the appropriate domain namespace as specified by `ddd.namespaces.*` in the [config file](#config-file). ### Other Commands @@ -110,13 +110,47 @@ Generated objects will be placed in the appropriate domain namespace as specifie php artisan ddd:list # Cache domain manifests (used for autoloading) -php artisan ddd:cache +php artisan ddd:optimize # Clear the domain cache php artisan ddd:clear ``` ## Advanced Usage +### Application Layer (since 1.2) +Some objects interact with the domain layer, but are not part of the domain layer themselves. By default, these include: `controller`, `request`, `middleware`. You may customize the path, namespace, and which `ddd:*` objects belong in the application layer. +```php +// In config/ddd.php +'application' => [ + 'path' => 'app/Modules', + 'namespace' => 'App\Modules', + 'objects' => [ + 'controller', + 'request', + 'middleware', + ], +], +``` +The configuration above will result in the following: +```bash +ddd:model Invoicing:Invoice --controller --resource --requests +``` +Output: +``` +├─ app +| └─ Modules +│ └─ Invoicing +│ ├─ Controllers +│ │ └─ InvoiceController.php +│ └─ Requests +│ ├─ StoreInvoiceRequest.php +│ └─ UpdateInvoiceRequest.php +├─ src/Domain + └─ Invoicing + └─ Models + └─ Invoice.php +``` + ### Nested Objects For any `ddd:*` generator command, nested objects can be specified with forward slashes. ```bash @@ -194,6 +228,7 @@ Autoloading behaviour can be configured with the `ddd.autoload` configuration op 'commands' => true, 'policies' => true, 'factories' => true, + 'migrations' => true, ], ``` ### Service Providers @@ -210,14 +245,18 @@ When `ddd.autoload.factories` is enabled, the package will register a custom fac If your application implements its own factory discovery using `Factory::guessFactoryNamesUsing()`, you should set `ddd.autoload.factories` to `false` to ensure it is not overridden. +### Migrations +When `ddd.autoload.migrations` is enabled, paths within the domain layer matching the configured `ddd.namespaces.migration` namespace will be auto-registered as a database migration path and recognized by `php artisan migrate`. + ### Ignoring Paths During Autoloading -To specify folders or paths that should be skipped during autoloading discovery, add them to the `ddd.autoload_ignore` configuration option. By default, the `Tests` and `Migrations` folders are ignored. +To specify folders or paths that should be skipped during autoloading class discovery, add them to the `ddd.autoload_ignore` configuration option. By default, the `Tests` and `Migrations` folders are ignored. ```php 'autoload_ignore' => [ 'Tests', 'Database/Migrations', ], ``` +Note that ignoring folders only applies to class-based autoloading: Service Providers, Console Commands, Policies, and Factories. Paths specified here are relative to the root of each domain. e.g., `src/Domain/Invoicing/{path-to-ignore}`. If more advanced filtering is needed, a callback can be registered using `DDD::filterAutoloadPathsUsing(callback $filter)` in your AppServiceProvider's boot method: ```php @@ -240,13 +279,16 @@ You may disable autoloading by setting the respective autoload options to `false // 'commands' => true, // 'policies' => true, // 'factories' => true, +// 'migrations' => true, // ], ``` ## Autoloading in Production -In production, you should cache the autoload manifests using the `ddd:cache` command as part of your application's deployment process. This will speed up the auto-discovery and registration of domain providers and commands. The `ddd:clear` command may be used to clear the cache if needed. +In production, you should cache the autoload manifests using the `ddd:optimize` command as part of your application's deployment process. This will speed up the auto-discovery and registration of domain providers and commands. The `ddd:clear` command may be used to clear the cache if needed. + +> **Note**: Since Laravel 11.27.1, the framework's `optimize` and `optimize:clear` commands will automatically invoke `ddd:optimize` and `ddd:clear` respectively. @@ -278,12 +320,34 @@ return [ /* |-------------------------------------------------------------------------- - | Domain Object Namespaces + | Application Layer |-------------------------------------------------------------------------- | - | This value contains the default namespaces of generated domain - | objects relative to the domain namespace of which the object - | belongs to. + | Configure objects that belong in the application layer. + | + | e.g., App\Modules\Invoicing\Controllers\* + | App\Modules\Invoicing\Requests\* + | + */ + 'application' => [ + 'path' => 'app/Modules', + 'namespace' => 'App\Modules', + + // Specify which ddd:* objects belong in the application layer + 'objects' => [ + 'controller', + 'request', + 'middleware', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Generator Object Namespaces + |-------------------------------------------------------------------------- + | + | This array maps the default relative namespaces of generated objects + | relative to their domain's root namespace. | | e.g., Domain\Invoicing\Models\* | Domain\Invoicing\Data\* @@ -302,6 +366,7 @@ return [ 'class' => '', 'channel' => 'Channels', 'command' => 'Commands', + 'controller' => 'Controllers', 'enum' => 'Enums', 'event' => 'Events', 'exception' => 'Exceptions', @@ -310,13 +375,17 @@ return [ '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' => '', ], @@ -325,12 +394,11 @@ return [ | 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. + | The base model class which generated domain models should extend. If + | set to null, the generated models will extend Laravel's default. | */ - 'base_model' => 'Domain\Shared\Models\BaseModel', + 'base_model' => null, /* |-------------------------------------------------------------------------- @@ -374,34 +442,16 @@ return [ | Autoloading |-------------------------------------------------------------------------- | - | Configure whether domain providers, commands, policies, and factories - | should be auto-discovered and registered. + | Configure whether domain providers, commands, policies, factories, + | and migrations should be auto-discovered and registered. | */ 'autoload' => [ - /** - * When enabled, any class within the domain layer extending `Illuminate\Support\ServiceProvider` - * will be auto-registered as a service provider - */ 'providers' => true, - - /** - * When enabled, any class within the domain layer extending `Illuminate\Console\Command` - * will be auto-registered as a command when running in console. - */ 'commands' => true, - - /** - * When enabled, the package will register a custom policy discovery callback to resolve policy names - * for domain models, and fallback to Laravel's default for all other cases. - */ 'policies' => true, - - /** - * When enabled, the package will register a custom factory discovery callback to resolve factory names - * for domain models, and fallback to Laravel's default for all other cases. - */ 'factories' => true, + 'migrations' => true, ], /* @@ -415,7 +465,7 @@ return [ | e.g., src/Domain/Invoicing/ | | If more advanced filtering is needed, a callback can be registered - | using the `DDD::filterAutoloadPathsUsing(callback $filter)` in + | using `DDD::filterAutoloadPathsUsing(callback $filter)` in | the AppServiceProvider's boot method. | */ diff --git a/composer.json b/composer.json index 243d503..c7fa76b 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "pestphp/pest-plugin-laravel": "^2.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0" + "phpstan/phpstan-phpunit": "^1.0", + "spatie/laravel-data": "^4.10" }, "autoload": { "psr-4": { diff --git a/config/ddd.php b/config/ddd.php index 66d8901..dfa4eda 100644 --- a/config/ddd.php +++ b/config/ddd.php @@ -24,12 +24,34 @@ /* |-------------------------------------------------------------------------- - | Domain Object Namespaces + | Application Layer |-------------------------------------------------------------------------- | - | This value contains the default namespaces of generated domain - | objects relative to the domain namespace of which the object - | belongs to. + | Configure objects that belong in the application layer. + | + | e.g., App\Modules\Invoicing\Controllers\* + | App\Modules\Invoicing\Requests\* + | + */ + 'application' => [ + 'path' => 'app/Modules', + 'namespace' => 'App\Modules', + + // Specify which ddd:* objects belong in the application layer + 'objects' => [ + 'controller', + 'request', + 'middleware', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Generator Object Namespaces + |-------------------------------------------------------------------------- + | + | This array maps the default relative namespaces of generated objects + | relative to their domain's root namespace. | | e.g., Domain\Invoicing\Models\* | Domain\Invoicing\Data\* @@ -48,6 +70,7 @@ 'class' => '', 'channel' => 'Channels', 'command' => 'Commands', + 'controller' => 'Controllers', 'enum' => 'Enums', 'event' => 'Events', 'exception' => 'Exceptions', @@ -56,13 +79,17 @@ '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' => '', ], @@ -71,12 +98,11 @@ | 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. + | The base model class which generated domain models should extend. If + | set to null, the generated models will extend Laravel's default. | */ - 'base_model' => 'Domain\Shared\Models\BaseModel', + 'base_model' => null, /* |-------------------------------------------------------------------------- @@ -120,34 +146,16 @@ | Autoloading |-------------------------------------------------------------------------- | - | Configure whether domain providers, commands, policies, and factories - | should be auto-discovered and registered. + | Configure whether domain providers, commands, policies, factories, + | and migrations should be auto-discovered and registered. | */ 'autoload' => [ - /** - * When enabled, any class within the domain layer extending `Illuminate\Support\ServiceProvider` - * will be auto-registered as a service provider - */ 'providers' => true, - - /** - * When enabled, any class within the domain layer extending `Illuminate\Console\Command` - * will be auto-registered as a command when running in console. - */ 'commands' => true, - - /** - * When enabled, the package will register a custom policy discovery callback to resolve policy names - * for domain models, and fallback to Laravel's default for all other cases. - */ 'policies' => true, - - /** - * When enabled, the package will register a custom factory discovery callback to resolve factory names - * for domain models, and fallback to Laravel's default for all other cases. - */ 'factories' => true, + 'migrations' => true, ], /* diff --git a/config/ddd.php.stub b/config/ddd.php.stub index 6c10645..612b1d8 100644 --- a/config/ddd.php.stub +++ b/config/ddd.php.stub @@ -22,6 +22,29 @@ return [ */ 'domain_namespace' => {{domain_namespace}}, + /* + |-------------------------------------------------------------------------- + | Application Layer + |-------------------------------------------------------------------------- + | + | Configure objects that belong in the application layer. + | + | e.g., App\Modules\Invoicing\Controllers\* + | App\Modules\Invoicing\Requests\* + | + */ + 'application' => [ + 'path' => 'app/Modules', + 'namespace' => 'App\Modules', + + // Specify which ddd:* objects belong in the application layer + 'objects' => [ + 'controller', + 'request', + 'middleware', + ], + ], + /* |-------------------------------------------------------------------------- | Domain Object Namespaces @@ -48,6 +71,7 @@ return [ 'class' => '', 'channel' => 'Channels', 'command' => 'Commands', + 'controller' => 'Controllers', 'enum' => 'Enums', 'event' => 'Events', 'exception' => 'Exceptions', @@ -56,13 +80,17 @@ return [ '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' => '', ], @@ -125,29 +153,11 @@ return [ | */ 'autoload' => [ - /** - * When enabled, any class within the domain layer extending `Illuminate\Support\ServiceProvider` - * will be auto-registered as a service provider - */ 'providers' => true, - - /** - * When enabled, any class within the domain layer extending `Illuminate\Console\Command` - * will be auto-registered as a command when running in console. - */ 'commands' => true, - - /** - * When enabled, the package will register a custom policy discovery callback to resolve policy names - * for domain models, and fallback to Laravel's default for all other cases. - */ 'policies' => true, - - /** - * When enabled, the package will register a custom factory discovery callback to resolve factory names - * for domain models, and fallback to Laravel's default for all other cases. - */ 'factories' => true, + 'migrations' => true, ], /* diff --git a/src/Commands/CacheCommand.php b/src/Commands/CacheCommand.php deleted file mode 100644 index 4d4fe68..0000000 --- a/src/Commands/CacheCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -components->info('Domain providers cached successfully.'); - - DomainAutoloader::cacheCommands(); - - $this->components->info('Domain commands cached successfully.'); - } -} diff --git a/src/Commands/Concerns/ForwardsToDomainCommands.php b/src/Commands/Concerns/ForwardsToDomainCommands.php new file mode 100644 index 0000000..6586bd7 --- /dev/null +++ b/src/Commands/Concerns/ForwardsToDomainCommands.php @@ -0,0 +1,62 @@ +getNameInput(), '/') + ? Str::beforeLast($this->getNameInput(), '/') + : null; + + $nameWithSubfolder = $subfolder ? "{$subfolder}/{$arguments['name']}" : $arguments['name']; + + return match ($command) { + 'make:request' => $this->runCommand('ddd:request', [ + ...$arguments, + 'name' => $nameWithSubfolder, + '--domain' => $this->domain->dotName, + ], $this->output), + + 'make:model' => $this->runCommand('ddd:model', [ + ...$arguments, + 'name' => $nameWithSubfolder, + '--domain' => $this->domain->dotName, + ], $this->output), + + 'make:factory' => $this->runCommand('ddd:factory', [ + ...$arguments, + 'name' => $nameWithSubfolder, + '--domain' => $this->domain->dotName, + ], $this->output), + + 'make:policy' => $this->runCommand('ddd:policy', [ + ...$arguments, + 'name' => $nameWithSubfolder, + '--domain' => $this->domain->dotName, + ], $this->output), + + 'make:migration' => $this->runCommand('ddd:migration', [ + ...$arguments, + '--domain' => $this->domain->dotName, + ], $this->output), + + 'make:seeder' => $this->runCommand('ddd:seeder', [ + ...$arguments, + 'name' => $nameWithSubfolder, + '--domain' => $this->domain->dotName, + ], $this->output), + + 'make:controller' => $this->runCommand('ddd:controller', [ + ...$arguments, + 'name' => $nameWithSubfolder, + '--domain' => $this->domain->dotName, + ], $this->output), + + default => $this->runCommand($command, $arguments, $this->output), + }; + } +} diff --git a/src/Commands/Concerns/HandleHooks.php b/src/Commands/Concerns/HandleHooks.php new file mode 100644 index 0000000..9a1ed27 --- /dev/null +++ b/src/Commands/Concerns/HandleHooks.php @@ -0,0 +1,25 @@ +beforeHandle(); + + parent::handle(); + + $this->afterHandle(); + } +} diff --git a/src/Commands/Concerns/QualifiesDomainModels.php b/src/Commands/Concerns/QualifiesDomainModels.php new file mode 100644 index 0000000..8e5af0a --- /dev/null +++ b/src/Commands/Concerns/QualifiesDomainModels.php @@ -0,0 +1,17 @@ +domain) { + $domainModel = $domain->model($model); + + return $domainModel->fullyQualifiedName; + } + + return parent::qualifyModel($model); + } +} diff --git a/src/Commands/Concerns/ResolvesDomainFromInput.php b/src/Commands/Concerns/ResolvesDomainFromInput.php index 9f323b6..9192aee 100644 --- a/src/Commands/Concerns/ResolvesDomainFromInput.php +++ b/src/Commands/Concerns/ResolvesDomainFromInput.php @@ -10,7 +10,9 @@ trait ResolvesDomainFromInput { - use CanPromptForDomain; + use CanPromptForDomain, + HandleHooks, + QualifiesDomainModels; protected $nameIsAbsolute = false; @@ -26,7 +28,9 @@ protected function getOptions() protected function rootNamespace() { - return Str::finish(DomainResolver::domainRootNamespace(), '\\'); + $type = $this->guessObjectType(); + + return Str::finish(DomainResolver::resolveRootNamespace($type), '\\'); } protected function guessObjectType(): string @@ -36,6 +40,7 @@ protected function guessObjectType(): string 'ddd:base-model' => 'model', 'ddd:value' => 'value_object', 'ddd:dto' => 'data_transfer_object', + 'ddd:migration' => 'migration', default => str($this->name)->after(':')->snake()->toString(), }; } @@ -66,7 +71,7 @@ protected function getPath($name) return parent::getPath($name); } - public function handle() + protected function beforeHandle() { $nameInput = $this->getNameInput(); @@ -106,6 +111,6 @@ public function handle() $this->input->setArgument('name', $nameInput); - parent::handle(); + app('ddd')->captureCommandContext($this, $this->domain, $this->guessObjectType()); } } diff --git a/src/Commands/DomainControllerMakeCommand.php b/src/Commands/DomainControllerMakeCommand.php new file mode 100644 index 0000000..aa2a373 --- /dev/null +++ b/src/Commands/DomainControllerMakeCommand.php @@ -0,0 +1,82 @@ +parseModel($this->option('model')); + + if ( + ! app()->runningUnitTests() + && ! class_exists($modelClass) + && confirm("A {$modelClass} model does not exist. Do you want to generate it?", default: true) + ) { + $this->call('make:model', ['name' => $modelClass]); + } + + $replace = $this->buildFormRequestReplacements($replace, $modelClass); + + return array_merge($replace, [ + 'DummyFullModelClass' => $modelClass, + '{{ namespacedModel }}' => $modelClass, + '{{namespacedModel}}' => $modelClass, + 'DummyModelClass' => class_basename($modelClass), + '{{ model }}' => class_basename($modelClass), + '{{model}}' => class_basename($modelClass), + 'DummyModelVariable' => lcfirst(class_basename($modelClass)), + '{{ modelVariable }}' => lcfirst(class_basename($modelClass)), + '{{modelVariable}}' => lcfirst(class_basename($modelClass)), + ]); + } + + protected function buildFormRequestReplacements(array $replace, $modelClass) + { + [$namespace, $storeRequestClass, $updateRequestClass] = [ + 'Illuminate\\Http', + 'Request', + 'Request', + ]; + + if ($this->option('requests')) { + $namespace = $this->domain->namespaceFor('request', $this->getNameInput()); + + [$storeRequestClass, $updateRequestClass] = $this->generateFormRequests( + $modelClass, + $storeRequestClass, + $updateRequestClass + ); + } + + $namespacedRequests = $namespace.'\\'.$storeRequestClass.';'; + + if ($storeRequestClass !== $updateRequestClass) { + $namespacedRequests .= PHP_EOL.'use '.$namespace.'\\'.$updateRequestClass.';'; + } + + return array_merge($replace, [ + '{{ storeRequest }}' => $storeRequestClass, + '{{storeRequest}}' => $storeRequestClass, + '{{ updateRequest }}' => $updateRequestClass, + '{{updateRequest}}' => $updateRequestClass, + '{{ namespacedStoreRequest }}' => $namespace.'\\'.$storeRequestClass, + '{{namespacedStoreRequest}}' => $namespace.'\\'.$storeRequestClass, + '{{ namespacedUpdateRequest }}' => $namespace.'\\'.$updateRequestClass, + '{{namespacedUpdateRequest}}' => $namespace.'\\'.$updateRequestClass, + '{{ namespacedRequests }}' => $namespacedRequests, + '{{namespacedRequests}}' => $namespacedRequests, + ]); + } +} diff --git a/src/Commands/DomainMiddlewareMakeCommand.php b/src/Commands/DomainMiddlewareMakeCommand.php new file mode 100644 index 0000000..2c6b2af --- /dev/null +++ b/src/Commands/DomainMiddlewareMakeCommand.php @@ -0,0 +1,13 @@ +argument('name')); } - protected function getStub() + public function handle() { - return $this->resolveStubPath('model.php.stub'); - } + $this->beforeHandle(); - protected function preparePlaceholders(): array - { - $baseClass = config('ddd.base_model'); - $baseClassName = class_basename($baseClass); + $this->createBaseModelIfNeeded(); - return [ - 'extends' => filled($baseClass) ? " extends {$baseClassName}" : '', - 'baseClassImport' => filled($baseClass) ? "use {$baseClass};" : '', - ]; + parent::handle(); + + $this->afterHandle(); } - public function handle() + protected function buildClass($name) { - $this->createBaseModelIfNeeded(); + $stub = parent::buildClass($name); - parent::handle(); + $replacements = [ + 'use Illuminate\Database\Eloquent\Factories\HasFactory;' => "use Lunarstorm\LaravelDDD\Factories\HasDomainFactory as HasFactory;", + ]; - if ($this->option('factory')) { - $this->createFactory(); + if ($baseModel = $this->getBaseModel()) { + $baseModelClass = class_basename($baseModel); + + $replacements = array_merge($replacements, [ + 'extends Model' => "extends {$baseModelClass}", + 'use Illuminate\Database\Eloquent\Model;' => "use {$baseModel};", + ]); } + + $stub = str_replace( + array_keys($replacements), + array_values($replacements), + $stub + ); + + $stub = $this->sortImports($stub); + + return $stub; } protected function createBaseModelIfNeeded() { - if (! $this->shouldCreateModel()) { + if (! $this->shouldCreateBaseModel()) { return; } @@ -74,10 +79,19 @@ protected function createBaseModelIfNeeded() ]); } - protected function shouldCreateModel(): bool + protected function getBaseModel(): ?string + { + return config('ddd.base_model', null); + } + + protected function shouldCreateBaseModel(): bool { $baseModel = config('ddd.base_model'); + if (is_null($baseModel)) { + return false; + } + // If the class exists, we don't need to create it. if (class_exists($baseModel)) { return false; @@ -96,13 +110,4 @@ protected function shouldCreateModel(): bool return true; } - - protected function createFactory() - { - $this->call(DomainFactoryMakeCommand::class, [ - 'name' => $this->getNameInput().'Factory', - '--domain' => $this->domain->dotName, - '--model' => $this->qualifyClass($this->getNameInput()), - ]); - } } diff --git a/src/Commands/DomainModelMakeLegacyCommand.php b/src/Commands/DomainModelMakeLegacyCommand.php new file mode 100644 index 0000000..37c8164 --- /dev/null +++ b/src/Commands/DomainModelMakeLegacyCommand.php @@ -0,0 +1,108 @@ +resolveStubPath('model.php.stub'); + } + + protected function preparePlaceholders(): array + { + $baseClass = config('ddd.base_model'); + $baseClassName = class_basename($baseClass); + + return [ + 'extends' => filled($baseClass) ? " extends {$baseClassName}" : '', + 'baseClassImport' => filled($baseClass) ? "use {$baseClass};" : '', + ]; + } + + public function handle() + { + $this->createBaseModelIfNeeded(); + + parent::handle(); + + if ($this->option('factory')) { + $this->createFactory(); + } + } + + protected function createBaseModelIfNeeded() + { + if (! $this->shouldCreateBaseModel()) { + return; + } + + $baseModel = config('ddd.base_model'); + + $this->warn("Base model {$baseModel} doesn't exist, generating..."); + + $domain = DomainResolver::guessDomainFromClass($baseModel); + + $name = Str::after($baseModel, $domain); + + $this->call(DomainBaseModelMakeCommand::class, [ + '--domain' => $domain, + 'name' => $name, + ]); + } + + protected function shouldCreateBaseModel(): bool + { + $baseModel = config('ddd.base_model'); + + // If the class exists, we don't need to create it. + if (class_exists($baseModel)) { + return false; + } + + // If the class is outside of the domain layer, we won't attempt to create it. + if (! DomainResolver::isDomainClass($baseModel)) { + return false; + } + + // At this point the class is probably a domain object, but we should + // check if the expected path exists. + if (file_exists(app()->basePath(DomainResolver::guessPathFromClass($baseModel)))) { + return false; + } + + return true; + } + + protected function createFactory() + { + $this->call(DomainFactoryMakeCommand::class, [ + 'name' => $this->getNameInput().'Factory', + '--domain' => $this->domain->dotName, + '--model' => $this->qualifyClass($this->getNameInput()), + ]); + } +} diff --git a/src/Commands/DomainRequestMakeCommand.php b/src/Commands/DomainRequestMakeCommand.php new file mode 100644 index 0000000..a6a1ddf --- /dev/null +++ b/src/Commands/DomainRequestMakeCommand.php @@ -0,0 +1,27 @@ +guessObjectType(); + + return Str::finish(DomainResolver::resolveRootNamespace($type), '\\'); + } +} diff --git a/src/Commands/DomainSeederMakeCommand.php b/src/Commands/DomainSeederMakeCommand.php new file mode 100644 index 0000000..6ee413d --- /dev/null +++ b/src/Commands/DomainSeederMakeCommand.php @@ -0,0 +1,13 @@ +argument('name')); + + return $name; + } + + protected function qualifyModel(string $model) {} + + protected function getDefaultNamespace($rootNamespace) {} + + protected function getPath($name) {} +} diff --git a/src/Commands/Migration/DomainMigrateMakeCommand.php b/src/Commands/Migration/DomainMigrateMakeCommand.php new file mode 100644 index 0000000..e0dcb73 --- /dev/null +++ b/src/Commands/Migration/DomainMigrateMakeCommand.php @@ -0,0 +1,27 @@ +domain) { + return $this->laravel->basePath($this->domain->migrationPath); + } + + return $this->laravel->databasePath().DIRECTORY_SEPARATOR.'migrations'; + } +} diff --git a/src/Commands/CacheClearCommand.php b/src/Commands/OptimizeClearCommand.php similarity index 66% rename from src/Commands/CacheClearCommand.php rename to src/Commands/OptimizeClearCommand.php index 7e4c465..3ad99c8 100644 --- a/src/Commands/CacheClearCommand.php +++ b/src/Commands/OptimizeClearCommand.php @@ -5,12 +5,21 @@ use Illuminate\Console\Command; use Lunarstorm\LaravelDDD\Support\DomainCache; -class CacheClearCommand extends Command +class OptimizeClearCommand extends Command { protected $name = 'ddd:clear'; protected $description = 'Clear cached domain autoloaded objects.'; + protected function configure() + { + $this->setAliases([ + 'ddd:optimize:clear', + ]); + + parent::configure(); + } + public function handle() { DomainCache::clear(); diff --git a/src/Commands/OptimizeCommand.php b/src/Commands/OptimizeCommand.php new file mode 100644 index 0000000..e73462e --- /dev/null +++ b/src/Commands/OptimizeCommand.php @@ -0,0 +1,34 @@ +setAliases([ + 'ddd:cache', + ]); + + parent::configure(); + } + + public function handle() + { + $this->components->info('Caching DDD providers, commands, migration paths.'); + + $this->components->task('domain providers', fn () => DomainAutoloader::cacheProviders()); + $this->components->task('domain commands', fn () => DomainAutoloader::cacheCommands()); + $this->components->task('domain migration paths', fn () => DomainMigration::cachePaths()); + + $this->newLine(); + } +} diff --git a/src/DomainManager.php b/src/DomainManager.php index 2de6b0f..13d465a 100755 --- a/src/DomainManager.php +++ b/src/DomainManager.php @@ -2,6 +2,10 @@ namespace Lunarstorm\LaravelDDD; +use Illuminate\Console\Command; +use Lunarstorm\LaravelDDD\Support\Domain; +use Lunarstorm\LaravelDDD\ValueObjects\DomainCommandContext; + class DomainManager { /** @@ -11,9 +15,28 @@ class DomainManager */ protected $autoloadFilter; + /** + * The application layer filter callback. + * + * @var callable|null + */ + protected $applicationLayerFilter; + + /** + * The application layer object resolver callback. + * + * @var callable|null + */ + protected $namespaceResolver; + + protected ?DomainCommandContext $commandContext; + public function __construct() { $this->autoloadFilter = null; + $this->applicationLayerFilter = null; + $this->namespaceResolver = null; + $this->commandContext = null; } public function filterAutoloadPathsUsing(callable $filter): void @@ -25,4 +48,34 @@ public function getAutoloadFilter(): ?callable { return $this->autoloadFilter; } + + public function filterApplicationLayerUsing(callable $filter): void + { + $this->applicationLayerFilter = $filter; + } + + public function getApplicationLayerFilter(): ?callable + { + return $this->applicationLayerFilter; + } + + public function resolveNamespaceUsing(callable $resolver): void + { + $this->namespaceResolver = $resolver; + } + + public function getNamespaceResolver(): ?callable + { + return $this->namespaceResolver; + } + + public function captureCommandContext(Command $command, ?Domain $domain, ?string $type): void + { + $this->commandContext = DomainCommandContext::fromCommand($command, $domain, $type); + } + + public function getCommandContext(): ?DomainCommandContext + { + return $this->commandContext; + } } diff --git a/src/Facades/DDD.php b/src/Facades/DDD.php index 5164cda..a5aba45 100644 --- a/src/Facades/DDD.php +++ b/src/Facades/DDD.php @@ -8,6 +8,7 @@ * @see \Lunarstorm\LaravelDDD\DomainManager * * @method static void filterAutoloadPathsUsing(callable $filter) + * @method static void resolveNamespaceUsing(callable $resolver) */ class DDD extends Facade { diff --git a/src/LaravelDDDServiceProvider.php b/src/LaravelDDDServiceProvider.php index 6555823..106ec30 100644 --- a/src/LaravelDDDServiceProvider.php +++ b/src/LaravelDDDServiceProvider.php @@ -3,6 +3,7 @@ namespace Lunarstorm\LaravelDDD; use Lunarstorm\LaravelDDD\Support\DomainAutoloader; +use Lunarstorm\LaravelDDD\Support\DomainMigration; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -27,8 +28,8 @@ public function configurePackage(Package $package): void ->hasCommands([ Commands\InstallCommand::class, Commands\UpgradeCommand::class, - Commands\CacheCommand::class, - Commands\CacheClearCommand::class, + Commands\OptimizeCommand::class, + Commands\OptimizeClearCommand::class, Commands\DomainListCommand::class, Commands\DomainModelMakeCommand::class, Commands\DomainFactoryMakeCommand::class, @@ -41,18 +42,23 @@ public function configurePackage(Package $package): void Commands\DomainCastMakeCommand::class, Commands\DomainChannelMakeCommand::class, Commands\DomainConsoleMakeCommand::class, + Commands\DomainControllerMakeCommand::class, Commands\DomainEventMakeCommand::class, Commands\DomainExceptionMakeCommand::class, Commands\DomainJobMakeCommand::class, Commands\DomainListenerMakeCommand::class, Commands\DomainMailMakeCommand::class, + Commands\DomainMiddlewareMakeCommand::class, Commands\DomainNotificationMakeCommand::class, Commands\DomainObserverMakeCommand::class, Commands\DomainPolicyMakeCommand::class, Commands\DomainProviderMakeCommand::class, Commands\DomainResourceMakeCommand::class, + Commands\DomainRequestMakeCommand::class, Commands\DomainRuleMakeCommand::class, Commands\DomainScopeMakeCommand::class, + Commands\DomainSeederMakeCommand::class, + Commands\Migration\DomainMigrateMakeCommand::class, ]); if (app()->version() >= 11) { @@ -63,15 +69,40 @@ public function configurePackage(Package $package): void } } + protected function registerMigrations() + { + $this->app->singleton(Commands\Migration\DomainMigrateMakeCommand::class, function ($app) { + // Once we have the migration creator registered, we will create the command + // and inject the creator. The creator is responsible for the actual file + // creation of the migrations, and may be extended by these developers. + $creator = $app['migration.creator']; + $composer = $app['composer']; + + return new Commands\Migration\DomainMigrateMakeCommand($creator, $composer); + }); + + $this->loadMigrationsFrom(DomainMigration::paths()); + } + public function packageBooted() { $this->publishes([ $this->package->basePath('/../stubs') => resource_path("stubs/{$this->package->shortName()}"), ], "{$this->package->shortName()}-stubs"); + + if ($this->app->runningInConsole() && method_exists($this, 'optimizes')) { + $this->optimizes( + optimize: 'ddd:optimize', + clear: 'ddd:clear', + key: 'ddd cache', + ); + } } public function packageRegistered() { (new DomainAutoloader)->autoload(); + + $this->registerMigrations(); } } diff --git a/src/Support/Domain.php b/src/Support/Domain.php index 062aedc..225fe61 100644 --- a/src/Support/Domain.php +++ b/src/Support/Domain.php @@ -11,6 +11,8 @@ class Domain public readonly string $path; + public readonly string $migrationPath; + public readonly string $domain; public readonly ?string $subdomain; @@ -57,6 +59,8 @@ public function __construct(string $domain, ?string $subdomain = null) $this->namespace = DomainNamespaces::from($this->domain, $this->subdomain); $this->path = Path::join(DomainResolver::domainPath(), $this->domainWithSubdomain); + + $this->migrationPath = Path::join($this->path, config('ddd.namespaces.migration', 'Database/Migrations')); } protected function getDomainBasePath() @@ -79,14 +83,29 @@ public function path(?string $path = null): string return Path::join($this->path, $path); } + public function pathInApplicationLayer(?string $path = null): string + { + if (is_null($path)) { + return $this->path; + } + + $path = str($path) + ->replace(DomainResolver::applicationLayerRootNamespace(), '') + ->replace(['\\', '/'], DIRECTORY_SEPARATOR) + ->append('.php') + ->toString(); + + return Path::join(DomainResolver::applicationLayerPath(), $path); + } + public function relativePath(string $path = ''): string { return collect([$this->domain, $path])->filter()->implode(DIRECTORY_SEPARATOR); } - public function namespaceFor(string $type): string + public function namespaceFor(string $type, ?string $name = null): string { - return DomainResolver::getDomainObjectNamespace($this->domainWithSubdomain, $type); + return DomainResolver::getDomainObjectNamespace($this->domainWithSubdomain, $type, $name); } public function guessNamespaceFromName(string $name): string @@ -102,20 +121,37 @@ public function guessNamespaceFromName(string $name): string public function object(string $type, string $name, bool $absolute = false): DomainObject { - $namespace = match (true) { + $resolvedNamespace = null; + + if (DomainResolver::isApplicationLayer($type)) { + $resolver = app('ddd')->getNamespaceResolver(); + + $resolvedNamespace = is_callable($resolver) + ? $resolver($this->domainWithSubdomain, $type, app('ddd')->getCommandContext()) + : null; + } + + $namespace = $resolvedNamespace ?? match (true) { $absolute => $this->namespace->root, str($name)->startsWith('\\') => $this->guessNamespaceFromName($name), default => $this->namespaceFor($type), }; - $baseName = str($name)->replace($namespace, '')->trim('\\')->toString(); + $baseName = str($name)->replace($namespace, '') + ->replace(['\\', '/'], '\\') + ->trim('\\') + ->toString(); + + $fullyQualifiedName = $namespace.'\\'.$baseName; return new DomainObject( name: $baseName, domain: $this->domain, namespace: $namespace, - fullyQualifiedName: $namespace.'\\'.$baseName, - path: $this->path($namespace.'\\'.$baseName), + fullyQualifiedName: $fullyQualifiedName, + path: DomainResolver::isApplicationLayer($type) + ? $this->pathInApplicationLayer($fullyQualifiedName) + : $this->path($fullyQualifiedName), type: $type ); } diff --git a/src/Support/DomainAutoloader.php b/src/Support/DomainAutoloader.php index beae39e..9e812d9 100644 --- a/src/Support/DomainAutoloader.php +++ b/src/Support/DomainAutoloader.php @@ -154,7 +154,9 @@ protected static function discoverProviders(): array } $paths = static::normalizePaths( - $configValue === true ? app()->basePath(DomainResolver::domainPath()) : $configValue + $configValue === true + ? app()->basePath(DomainResolver::domainPath()) + : $configValue ); if (empty($paths)) { diff --git a/src/Support/DomainMigration.php b/src/Support/DomainMigration.php new file mode 100644 index 0000000..9f3a939 --- /dev/null +++ b/src/Support/DomainMigration.php @@ -0,0 +1,81 @@ +filter(fn ($path) => is_dir($path)) + ->toArray(); + } + + public static function discoverPaths(): array + { + $configValue = config('ddd.autoload.migrations', true); + + if ($configValue === false) { + return []; + } + + $paths = static::normalizePaths([ + app()->basePath(DomainResolver::domainPath()), + ]); + + if (empty($paths)) { + return []; + } + + $finder = static::finder($paths); + + return Lody::filesFromFinder($finder) + ->map(fn ($file) => Path::normalize($file->getPath())) + ->unique() + ->values() + ->toArray(); + } + + protected static function finder(array $paths) + { + $filter = function (SplFileInfo $file) { + $configuredMigrationFolder = static::domainMigrationFolder(); + + $relativePath = Path::normalize($file->getRelativePath()); + + return Str::endsWith($relativePath, $configuredMigrationFolder); + }; + + return Finder::create() + ->files() + ->in($paths) + ->filter($filter); + } +} diff --git a/src/Support/DomainResolver.php b/src/Support/DomainResolver.php index 9d52db8..0d9110c 100644 --- a/src/Support/DomainResolver.php +++ b/src/Support/DomainResolver.php @@ -35,6 +35,22 @@ public static function domainRootNamespace(): ?string return config('ddd.domain_namespace'); } + /** + * Get the current configured application layer path. + */ + public static function applicationLayerPath(): ?string + { + return config('ddd.application.path'); + } + + /** + * Get the current configured root application layer namespace. + */ + public static function applicationLayerRootNamespace(): ?string + { + return config('ddd.application.namespace'); + } + /** * Resolve the relative domain object namespace. * @@ -45,19 +61,66 @@ 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 $object = null): string + /** + * Determine whether a given object type is part of the application layer. + */ + public static function isApplicationLayer(string $type): bool + { + $filter = app('ddd')->getApplicationLayerFilter() ?? function (string $type) { + $applicationObjects = config('ddd.application.objects', ['controller', 'request']); + + return in_array($type, $applicationObjects); + }; + + return $filter($type); + } + + /** + * Resolve the root namespace for a given domain object type. + * + * @param string $type The domain object type. + */ + public static function resolveRootNamespace(string $type): ?string { - $namespace = collect([ - static::domainRootNamespace(), - $domain, - static::getRelativeObjectNamespace($type), - ])->filter()->implode('\\'); - - if ($object) { - $namespace .= "\\{$object}"; + return static::isApplicationLayer($type) + ? static::applicationLayerRootNamespace() + : static::domainRootNamespace(); + } + + /** + * Get the fully qualified namespace for a domain object. + * + * @param string $domain The domain name. + * @param string $type The domain object type. + * @param string|null $name The domain object name. + */ + public static function getDomainObjectNamespace(string $domain, string $type, ?string $name = null): string + { + $customResolver = app('ddd')->getNamespaceResolver(); + + $resolved = is_callable($customResolver) + ? $customResolver($domain, $type, app('ddd')->getCommandContext()) + : null; + + if (! is_null($resolved)) { + return $resolved; } - return $namespace; + $resolver = function (string $domain, string $type, ?string $name) { + $namespace = collect([ + static::resolveRootNamespace($type), + $domain, + static::getRelativeObjectNamespace($type), + ])->filter()->implode('\\'); + + if ($name) { + $namespace .= "\\{$name}"; + } + + return $namespace; + }; + + return $resolver($domain, $type, $name); } /** @@ -95,6 +158,22 @@ public static function guessPathFromClass(string $class): ?string return Path::join(...[static::domainPath(), "{$classWithoutDomainRoot}.php"]); } + /** + * Attempt to resolve the folder of a given domain class. + */ + public static function guessFolderFromClass(string $class): ?string + { + $path = static::guessPathFromClass($class); + + if (! $path) { + return null; + } + + $filenamePortion = basename($path); + + return Str::beforeLast($path, $filenamePortion); + } + /** * Determine whether a class is an object within the domain layer. * diff --git a/src/Support/Path.php b/src/Support/Path.php index 931230c..7fbdb86 100644 --- a/src/Support/Path.php +++ b/src/Support/Path.php @@ -26,4 +26,9 @@ public static function filePathToNamespace(string $path, string $namespacePath, $path ); } + + public static function normalizeNamespace(string $namespace): string + { + return str_replace(['\\', '/'], '\\', $namespace); + } } diff --git a/src/ValueObjects/DomainCommandContext.php b/src/ValueObjects/DomainCommandContext.php new file mode 100644 index 0000000..692ceed --- /dev/null +++ b/src/ValueObjects/DomainCommandContext.php @@ -0,0 +1,51 @@ +getName(), + domain: $domain?->domainWithSubdomain, + type: $type, + resource: $command->argument('name'), + arguments: $command->arguments(), + options: $command->options(), + ); + } + + public function hasOption(string $key): bool + { + return array_key_exists($key, $this->options); + } + + public function option(string $key): mixed + { + return data_get($this->options, $key); + } + + public function hasArgument(string $key): bool + { + return array_key_exists($key, $this->arguments); + } + + public function argument(string $key): mixed + { + return data_get($this->arguments, $key); + } +} diff --git a/tests/.skeleton/src/Domain/Invoicing/Database/Migrations/2024_10_14_215911_do_nothing.php b/tests/.skeleton/src/Domain/Invoicing/Database/Migrations/2024_10_14_215911_do_nothing.php new file mode 100644 index 0000000..88fa2f3 --- /dev/null +++ b/tests/.skeleton/src/Domain/Invoicing/Database/Migrations/2024_10_14_215911_do_nothing.php @@ -0,0 +1,24 @@ +setupTestApplication(); +}); + +it('can register a custom namespace resolver', function () { + Config::set('ddd.application', [ + 'path' => 'src/App', + 'namespace' => 'App', + ]); + + DDD::resolveNamespaceUsing(function (string $domain, string $type, ?DomainCommandContext $context): ?string { + if ($type == 'controller' && $context->option('api')) { + return "App\\Api\\Controllers\\{$domain}"; + } + + return null; + }); + + Artisan::call('ddd:controller', [ + 'name' => 'PaymentApiController', + '--domain' => 'Invoicing', + '--api' => true, + ]); + + $output = Artisan::output(); + + expect($output) + ->toContainFilepath('src/App/Api/Controllers/Invoicing/PaymentApiController.php'); + + $expectedPath = base_path('src/App/Api/Controllers/Invoicing/PaymentApiController.php'); + + expect(file_get_contents($expectedPath)) + ->toContain("namespace App\Api\Controllers\Invoicing;"); +}); diff --git a/tests/Command/CacheTest.php b/tests/Command/CacheTest.php index 81a3587..f7aec3f 100644 --- a/tests/Command/CacheTest.php +++ b/tests/Command/CacheTest.php @@ -2,21 +2,31 @@ use Illuminate\Support\Facades\Artisan; use Lunarstorm\LaravelDDD\Support\DomainCache; +use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature; beforeEach(function () { $this->setupTestApplication(); + + config(['cache.default' => 'file']); + DomainCache::clear(); }); -it('can cache discovered domain providers and commands', function () { - expect(DomainCache::get('domain-providers'))->toBeNull(); +afterEach(function () { + $this->artisan('optimize:clear')->execute(); +}); +it('can cache discovered domain providers, commands, migrations', function () { + expect(DomainCache::get('domain-providers'))->toBeNull(); expect(DomainCache::get('domain-commands'))->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->toBeNull(); $this ->artisan('ddd:cache') - ->expectsOutputToContain('Domain providers cached successfully.') - ->expectsOutputToContain('Domain commands cached successfully.') + ->expectsOutputToContain('Caching DDD providers, commands, migration paths.') + ->expectsOutputToContain('domain providers') + ->expectsOutputToContain('domain commands') + ->expectsOutputToContain('domain migration paths') ->execute(); expect(DomainCache::get('domain-providers')) @@ -24,6 +34,10 @@ expect(DomainCache::get('domain-commands')) ->toContain('Domain\Invoicing\Commands\InvoiceDeliver'); + + $paths = collect(DomainCache::get('domain-migration-paths'))->join("\n"); + + expect($paths)->toContainFilepath('src/Domain/Invoicing/Database/Migrations'); }); it('can clear the cache', function () { @@ -31,6 +45,7 @@ expect(DomainCache::get('domain-providers'))->not->toBeNull(); expect(DomainCache::get('domain-commands'))->not->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->not->toBeNull(); $this ->artisan('ddd:clear') @@ -39,26 +54,63 @@ expect(DomainCache::get('domain-providers'))->toBeNull(); expect(DomainCache::get('domain-commands'))->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->toBeNull(); }); it('will not be cleared by laravel cache clearing', function () { - config(['cache.default' => 'file']); - expect(DomainCache::get('domain-providers'))->toBeNull(); expect(DomainCache::get('domain-commands'))->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->toBeNull(); $this->artisan('ddd:cache')->execute(); expect(DomainCache::get('domain-providers'))->not->toBeNull(); expect(DomainCache::get('domain-commands'))->not->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->not->toBeNull(); $this->artisan('cache:clear')->execute(); expect(DomainCache::get('domain-providers'))->not->toBeNull(); expect(DomainCache::get('domain-commands'))->not->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->not->toBeNull(); - $this->artisan('optimize:clear')->execute(); + if (Feature::LaravelPackageOptimizeCommands->missing()) { + $this->artisan('optimize:clear')->execute(); - expect(DomainCache::get('domain-providers'))->not->toBeNull(); - expect(DomainCache::get('domain-commands'))->not->toBeNull(); + expect(DomainCache::get('domain-providers'))->not->toBeNull(); + expect(DomainCache::get('domain-commands'))->not->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->not->toBeNull(); + } }); + +describe('laravel optimize', function () { + test('optimize will include ddd:cache', function () { + config(['cache.default' => 'file']); + + expect(DomainCache::get('domain-providers'))->toBeNull(); + expect(DomainCache::get('domain-commands'))->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->toBeNull(); + + $this->artisan('optimize')->execute(); + + expect(DomainCache::get('domain-providers'))->not->toBeNull(); + expect(DomainCache::get('domain-commands'))->not->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->not->toBeNull(); + }); + + test('optimize:clear will clear ddd cache', function () { + config(['cache.default' => 'file']); + + $this->artisan('ddd:cache')->execute(); + + expect(DomainCache::get('domain-providers'))->not->toBeNull(); + expect(DomainCache::get('domain-commands'))->not->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->not->toBeNull(); + + $this->artisan('optimize:clear')->execute(); + + expect(DomainCache::get('domain-providers'))->toBeNull(); + expect(DomainCache::get('domain-commands'))->toBeNull(); + expect(DomainCache::get('domain-migration-paths'))->toBeNull(); + }); +})->skipOnLaravelVersionsBelow(Feature::LaravelPackageOptimizeCommands->value); diff --git a/tests/Command/ListTest.php b/tests/Command/ListTest.php index 36afcee..6a1443b 100644 --- a/tests/Command/ListTest.php +++ b/tests/Command/ListTest.php @@ -13,6 +13,11 @@ '--domain' => 'Customer', ]); + $this->artisan('ddd:value', [ + 'name' => 'Subtotal', + '--domain' => 'Shared', + ]); + $this->expectedDomains = [ 'Customer', 'Invoicing', diff --git a/tests/Fixtures/Enums/Feature.php b/tests/Fixtures/Enums/Feature.php index 21c2aa1..9b82fbb 100644 --- a/tests/Fixtures/Enums/Feature.php +++ b/tests/Fixtures/Enums/Feature.php @@ -7,6 +7,7 @@ enum Feature: string case PromptForMissingInput = '9.49.0'; case IncludeFilepathInGeneratorCommandOutput = '9.32.0'; case LaravelPromptsPackage = '10.17'; + case LaravelPackageOptimizeCommands = '11.27.1'; public function exists(): bool { diff --git a/tests/Generator/MakeActionTest.php b/tests/Generator/ActionMakeTest.php similarity index 100% rename from tests/Generator/MakeActionTest.php rename to tests/Generator/ActionMakeTest.php diff --git a/tests/Generator/MakeBaseModelTest.php b/tests/Generator/BaseModelMakeTest.php similarity index 100% rename from tests/Generator/MakeBaseModelTest.php rename to tests/Generator/BaseModelMakeTest.php diff --git a/tests/Generator/MakeBaseViewModelTest.php b/tests/Generator/BaseViewModelMakeTest.php similarity index 100% rename from tests/Generator/MakeBaseViewModelTest.php rename to tests/Generator/BaseViewModelMakeTest.php diff --git a/tests/Generator/ControllerMakeTest.php b/tests/Generator/ControllerMakeTest.php new file mode 100644 index 0000000..5aae081 --- /dev/null +++ b/tests/Generator/ControllerMakeTest.php @@ -0,0 +1,177 @@ + 'app/Modules', + 'namespace' => 'App\Modules', + 'objects' => ['controller', 'request'], + ]); + + $this->setupTestApplication(); +}); + +it('can generate domain controller', function ($domainName, $controllerName, $relativePath, $expectedNamespace) { + $expectedPath = base_path($relativePath); + + if (file_exists($expectedPath)) { + unlink($expectedPath); + } + + expect(file_exists($expectedPath))->toBeFalse(); + + Artisan::call("ddd:controller {$domainName}:{$controllerName}"); + + expect($output = Artisan::output())->when( + Feature::IncludeFilepathInGeneratorCommandOutput->exists(), + fn ($output) => $output->toContainFilepath($relativePath), + ); + + expect(file_exists($expectedPath))->toBeTrue(); + + expect(file_get_contents($expectedPath)) + ->toContain("namespace {$expectedNamespace};"); +})->with([ + 'Invoicing:InvoiceController' => [ + 'Invoicing', + 'InvoiceController', + 'app/Modules/Invoicing/Controllers/InvoiceController.php', + 'App\Modules\Invoicing\Controllers', + ], + + 'Reporting.Internal:ReportSubmissionController' => [ + 'Reporting.Internal', + 'ReportSubmissionController', + 'app/Modules/Reporting/Internal/Controllers/ReportSubmissionController.php', + 'App\Modules\Reporting\Internal\Controllers', + ], +]); + +it('can generate domain resource controller from model', function ($domainName, $controllerName, $relativePath, $modelName, $modelClass) { + $expectedPath = base_path($relativePath); + + if (file_exists($expectedPath)) { + unlink($expectedPath); + } + + expect(file_exists($expectedPath))->toBeFalse(); + + Artisan::call('ddd:controller', [ + 'name' => $controllerName, + '--domain' => $domainName, + '--model' => $modelName, + ]); + + expect(file_exists($expectedPath))->toBeTrue(); + + $modelVariable = lcfirst(class_basename($modelClass)); + + expect(file_get_contents($expectedPath)) + ->toContain("use {$modelClass};") + ->toContain("{$modelName} \${$modelVariable})"); +})->with([ + 'Invoicing:InvoiceController --model=Invoice' => [ + 'Invoicing', + 'InvoiceController', + 'app/Modules/Invoicing/Controllers/InvoiceController.php', + 'Invoice', + 'Domain\Invoicing\Models\Invoice', + ], + + 'Invoicing:Payment/InvoicePaymentController --model=InvoicePayment' => [ + 'Invoicing', + 'Payment/InvoicePaymentController', + 'app/Modules/Invoicing/Controllers/Payment/InvoicePaymentController.php', + 'InvoicePayment', + 'Domain\Invoicing\Models\InvoicePayment', + ], + + 'Reporting.Internal:Archived/ReportArchiveController --model=ReportArchive' => [ + 'Reporting.Internal', + 'Archived/ReportArchiveController', + 'app/Modules/Reporting/Internal/Controllers/Archived/ReportArchiveController.php', + 'ReportArchive', + 'Domain\Reporting\Internal\Models\ReportArchive', + ], +]); + +it('can generate domain controller with requests', function ($domainName, $controllerName, $controllerPath, $modelName, $modelClass, $generatedPaths) { + $generatedPaths = [ + $controllerPath, + ...$generatedPaths, + ]; + + foreach ($generatedPaths as $path) { + $path = base_path($path); + + if (file_exists($path)) { + unlink($path); + } + } + + Artisan::call('ddd:controller', [ + 'name' => $controllerName, + '--domain' => $domainName, + '--model' => $modelName, + '--requests' => true, + ]); + + $output = Artisan::output(); + + foreach ($generatedPaths as $path) { + if (Feature::IncludeFilepathInGeneratorCommandOutput->exists()) { + expect($output)->toContainFilepath($path); + } + + expect(file_exists(base_path($path)))->toBeTrue("Expecting {$path} to exist"); + } + + $modelVariable = lcfirst(class_basename($modelClass)); + + expect(file_get_contents(base_path($controllerPath))) + ->toContain("use {$modelClass};") + ->toContain("store(Store{$modelName}Request \$request)") + ->toContain("update(Update{$modelName}Request \$request, {$modelName} \${$modelVariable})"); +})->with([ + 'Invoicing:InvoiceController --model=Invoice' => [ + 'Invoicing', + 'InvoiceController', + 'app/Modules/Invoicing/Controllers/InvoiceController.php', + 'Invoice', + 'Domain\Invoicing\Models\Invoice', + [ + 'app/Modules/Invoicing/Requests/StoreInvoiceRequest.php', + 'app/Modules/Invoicing/Requests/UpdateInvoiceRequest.php', + ], + ], + + 'Invoicing:Payment/InvoicePaymentController --model=InvoicePayment' => [ + 'Invoicing', + 'Payment/InvoicePaymentController', + 'app/Modules/Invoicing/Controllers/Payment/InvoicePaymentController.php', + 'InvoicePayment', + 'Domain\Invoicing\Models\InvoicePayment', + [ + 'app/Modules/Invoicing/Requests/Payment/StoreInvoicePaymentRequest.php', + 'app/Modules/Invoicing/Requests/Payment/UpdateInvoicePaymentRequest.php', + ], + ], + + 'Reporting.Internal:Archived/ReportArchiveController --model=ReportArchive' => [ + 'Reporting.Internal', + 'Archived/ReportArchiveController', + 'app/Modules/Reporting/Internal/Controllers/Archived/ReportArchiveController.php', + 'ReportArchive', + 'Domain\Reporting\Internal\Models\ReportArchive', + [ + 'app/Modules/Reporting/Internal/Requests/Archived/StoreReportArchiveRequest.php', + 'app/Modules/Reporting/Internal/Requests/Archived/UpdateReportArchiveRequest.php', + ], + ], +]); diff --git a/tests/Generator/MakeDataTransferObjectTest.php b/tests/Generator/DtoMakeTestTest.php similarity index 100% rename from tests/Generator/MakeDataTransferObjectTest.php rename to tests/Generator/DtoMakeTestTest.php diff --git a/tests/Generator/ExtendedCommandsTest.php b/tests/Generator/ExtendedCommandsTest.php index 4c0819f..52d8959 100644 --- a/tests/Generator/ExtendedCommandsTest.php +++ b/tests/Generator/ExtendedCommandsTest.php @@ -38,18 +38,22 @@ 'cast' => ['cast', 'SomeCast'], 'channel' => ['channel', 'SomeChannel'], 'command' => ['command', 'SomeCommand'], + 'controller' => ['controller', 'SomeController'], 'event' => ['event', 'SomeEvent'], 'exception' => ['exception', 'SomeException'], 'job' => ['job', 'SomeJob'], 'listener' => ['listener', 'SomeListener'], 'mail' => ['mail', 'SomeMail'], + 'middleware' => ['middleware', 'SomeMiddleware'], 'notification' => ['notification', 'SomeNotification'], 'observer' => ['observer', 'SomeObserver'], 'policy' => ['policy', 'SomePolicy'], 'provider' => ['provider', 'SomeProvider'], 'resource' => ['resource', 'SomeResource'], + 'request' => ['request', 'SomeRequest'], 'rule' => ['rule', 'SomeRule'], 'scope' => ['scope', 'SomeScope'], + 'seeder' => ['seeder', 'SomeSeeder'], 'class' => ['class', 'SomeClass'], 'enum' => ['enum', 'SomeEnum'], 'interface' => ['interface', 'SomeInterface'], diff --git a/tests/Generator/MakeFactoryTest.php b/tests/Generator/FactoryMakeTest.php similarity index 100% rename from tests/Generator/MakeFactoryTest.php rename to tests/Generator/FactoryMakeTest.php diff --git a/tests/Generator/MigrationMakeTest.php b/tests/Generator/MigrationMakeTest.php new file mode 100644 index 0000000..c32c614 --- /dev/null +++ b/tests/Generator/MigrationMakeTest.php @@ -0,0 +1,84 @@ + true, + ]); + + DomainCache::clear(); +}); + +it('can generate domain migrations', function ($domainPath, $domainRoot) { + Config::set('ddd.domain_path', $domainPath); + Config::set('ddd.domain_namespace', $domainRoot); + + $domain = 'Invoicing'; + + $relativePath = implode('/', [ + $domainPath, + $domain, + config('ddd.namespaces.migration'), + ]); + + $migrationFolder = base_path(Path::normalize($relativePath)); + + $filesBefore = glob("{$migrationFolder}/*"); + + expect(count($filesBefore))->toBe(0); + + Artisan::call("ddd:migration {$domain}:CreateInvoicesTable"); + + expect($output = Artisan::output())->when( + Feature::IncludeFilepathInGeneratorCommandOutput->exists(), + fn ($output) => $output + ->toContainFilepath($relativePath) + ->toContain('_create_invoices_table.php'), + ); + + $filesAfter = glob("{$migrationFolder}/*"); + + $createdMigrationFile = Arr::last($filesAfter); + + expect($createdMigrationFile)->toEndWith('_create_invoices_table.php'); + + expect(file_get_contents($createdMigrationFile)) + ->toContain('return new class extends Migration'); +})->with('domainPaths'); + +it('discovers domain migration folders', function ($domainPath, $domainRoot) { + Config::set('ddd.domain_path', $domainPath); + Config::set('ddd.domain_namespace', $domainRoot); + + $discoveredPaths = DomainMigration::discoverPaths(); + + expect($discoveredPaths)->toHaveCount(0); + + Artisan::call('ddd:migration Invoicing:'.uniqid('migration')); + Artisan::call('ddd:migration Shared:'.uniqid('migration')); + Artisan::call('ddd:migration Reporting:'.uniqid('migration')); + Artisan::call('ddd:migration Reporting:'.uniqid('migration')); + Artisan::call('ddd:migration Reporting:'.uniqid('migration')); + + $discoveredPaths = DomainMigration::discoverPaths(); + + expect($discoveredPaths)->toHaveCount(3); + + $expectedFolderPatterns = [ + Path::normalize('Invoicing/Database/Migrations'), + Path::normalize('Shared/Database/Migrations'), + Path::normalize('Reporting/Database/Migrations'), + ]; + + foreach ($discoveredPaths as $path) { + expect(str($path)->contains($expectedFolderPatterns)) + ->toBeTrue('Expecting path to contain one of the expected folder patterns'); + } +})->with('domainPaths'); diff --git a/tests/Generator/MakeModelTest.php b/tests/Generator/Model/MakeTest.php similarity index 83% rename from tests/Generator/MakeModelTest.php rename to tests/Generator/Model/MakeTest.php index 8772f20..4b841a5 100644 --- a/tests/Generator/MakeModelTest.php +++ b/tests/Generator/Model/MakeTest.php @@ -42,7 +42,8 @@ config('ddd.namespaces.model'), ]); - expect(file_get_contents($expectedModelPath))->toContain("namespace {$expectedNamespace};"); + expect(file_get_contents($expectedModelPath)) + ->toContain("namespace {$expectedNamespace};"); })->with('domainPaths'); it('can generate a domain model with factory', function ($domainPath, $domainRoot, $domainName, $subdomain) { @@ -179,3 +180,34 @@ ['Illuminate\Database\Eloquent\Model'], ['Lunarstorm\LaravelDDD\Models\DomainModel'], ]); + +it('extends custom base models when applicable', function ($baseModelClass, $baseModelName) { + Config::set('ddd.base_model', $baseModelClass); + + $domain = 'Fruits'; + $modelName = 'Lemon'; + + $expectedModelPath = base_path(implode('/', [ + config('ddd.domain_path'), + $domain, + config('ddd.namespaces.model'), + "{$modelName}.php", + ])); + + if (file_exists($expectedModelPath)) { + unlink($expectedModelPath); + } + + Artisan::call("ddd:model {$domain}:{$modelName}"); + + expect(file_exists($expectedModelPath))->toBeTrue(); + + expect(file_get_contents($expectedModelPath)) + ->toContain("use {$baseModelClass};") + ->toContain("extends {$baseModelName}"); +})->with([ + ['Domain\Shared\Models\BaseModel', 'BaseModel'], + ['Lunarstorm\LaravelDDD\Models\DomainModel', 'DomainModel'], + ['Illuminate\Database\Eloquent\NonExistentModel', 'NonExistentModel'], + ['OtherVendor\OtherPackage\Models\NonExistentModel', 'NonExistentModel'], +]); diff --git a/tests/Generator/Model/MakeWithControllerTest.php b/tests/Generator/Model/MakeWithControllerTest.php new file mode 100644 index 0000000..629933f --- /dev/null +++ b/tests/Generator/Model/MakeWithControllerTest.php @@ -0,0 +1,90 @@ +setupTestApplication(); +}); + +it('can generate domain model with controller', function ($domainName, $modelName, $controllerName, $generatedPaths) { + $domain = new Domain($domainName); + + foreach ($generatedPaths as $path) { + $path = base_path($path); + + if (file_exists($path)) { + unlink($path); + } + } + + $command = [ + 'ddd:model', + [ + 'name' => $modelName, + '--domain' => $domain->dotName, + '--controller' => true, + ], + ]; + + Artisan::call(...$command); + + $output = Artisan::output(); + + foreach ($generatedPaths as $path) { + if (Feature::IncludeFilepathInGeneratorCommandOutput->exists()) { + expect($output)->toContainFilepath($path); + } + + expect(file_exists(base_path($path)))->toBeTrue("Expecting {$path} to exist"); + } +})->with([ + 'Invoicing:Record' => [ + 'Invoicing', + 'Record', + 'RecordController', + [ + 'src/Domain/Invoicing/Models/Record.php', + 'app/Modules/Invoicing/Controllers/RecordController.php', + ], + ], + + 'Invoicing:RecordEntry' => [ + 'Invoicing', + 'RecordEntry', + 'RecordEntryController', + [ + 'src/Domain/Invoicing/Models/RecordEntry.php', + 'app/Modules/Invoicing/Controllers/RecordEntryController.php', + ], + ], + + 'Reporting.Internal:ReportSubmission' => [ + 'Reporting.Internal', + 'ReportSubmission', + 'ReportSubmissionController', + [ + 'src/Domain/Reporting/Internal/Models/ReportSubmission.php', + 'app/Modules/Reporting/Internal/Controllers/ReportSubmissionController.php', + ], + ], + + // '--controller --api' => [ + // ['--controller' => true, '--api' => true], + // 'RecordController', + // 'app/Http/Controllers/Invoicing/RecordController.php', + // ], + + // '--controller --requests' => [ + // ['--controller' => true, '--requests' => true], + // 'RecordController', + // 'app/Http/Controllers/Invoicing/RecordController.php', + // ], +]); diff --git a/tests/Generator/Model/MakeWithOptionsTest.php b/tests/Generator/Model/MakeWithOptionsTest.php new file mode 100644 index 0000000..09cb201 --- /dev/null +++ b/tests/Generator/Model/MakeWithOptionsTest.php @@ -0,0 +1,109 @@ +model($modelName); + +// $domainFactory = $domain->factory($factoryName); + +// $expectedModelPath = base_path($domainModel->path); + +// if (file_exists($expectedModelPath)) { +// unlink($expectedModelPath); +// } + +// $expectedFactoryPath = base_path($domainFactory->path); + +// if (file_exists($expectedFactoryPath)) { +// unlink($expectedFactoryPath); +// } + +// Artisan::call('ddd:model', [ +// 'name' => $modelName, +// '--domain' => $domain->dotName, +// '--factory' => true, +// ]); + +// $output = Artisan::output(); + +// 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->fullyQualifiedName};") +// ->toContain("protected \$model = {$modelName}::class;"); +// }); + +it('can generate domain model with options', function ($options, $objectType, $objectName, $expectedObjectPath) { + $domainName = 'Invoicing'; + $modelName = 'Record'; + + $domain = new Domain($domainName); + + $domainModel = $domain->model($modelName); + + $expectedModelPath = base_path($domainModel->path); + + if (file_exists($expectedModelPath)) { + unlink($expectedModelPath); + } + + if (file_exists($expectedObjectPath)) { + unlink($expectedObjectPath); + } + + $command = [ + 'ddd:model', [ + 'name' => $modelName, + '--domain' => $domain->dotName, + ...$options, + ], + ]; + + $this->artisan(...$command) + ->expectsOutputToContain(Path::normalize($domainModel->path)) + ->assertExitCode(0); + + $path = base_path($expectedObjectPath); + + expect(file_exists($path))->toBeTrue("Expecting {$objectType} to be generated at {$path}"); +})->with([ + '--factory' => [ + ['--factory' => true], + 'factory', + 'RecordFactory', + 'src/Domain/Invoicing/Database/Factories/RecordFactory.php', + ], + + '--seed' => [ + ['--seed' => true], + 'seeder', + 'RecordSeeder', + 'src/Domain/Invoicing/Database/Seeders/RecordSeeder.php', + ], + + '--policy' => [ + ['--policy' => true], + 'policy', + 'RecordPolicy', + 'src/Domain/Invoicing/Policies/RecordPolicy.php', + ], +]); diff --git a/tests/Generator/PromptTest.php b/tests/Generator/PromptTest.php index 3dea905..53bb086 100644 --- a/tests/Generator/PromptTest.php +++ b/tests/Generator/PromptTest.php @@ -23,6 +23,7 @@ it('[model] prompts for missing input', function () { $this->artisan('ddd:model') ->expectsQuestion('What should the model be named?', 'Belt') + ->expectsQuestion('Would you like any of the following?', []) ->expectsQuestion('What is the domain?', 'Utility') ->assertExitCode(0); }); diff --git a/tests/Generator/RequestMakeTest.php b/tests/Generator/RequestMakeTest.php new file mode 100644 index 0000000..cc24949 --- /dev/null +++ b/tests/Generator/RequestMakeTest.php @@ -0,0 +1,54 @@ + 'app/Modules', + 'namespace' => 'App\Modules', + 'objects' => ['controller', 'request'], + ]); + + $this->setupTestApplication(); +}); + +it('can generate domain request', function ($domainName, $requestName, $relativePath, $expectedNamespace) { + $expectedPath = base_path($relativePath); + + if (file_exists($expectedPath)) { + unlink($expectedPath); + } + + expect(file_exists($expectedPath))->toBeFalse(); + + Artisan::call("ddd:request {$domainName}:{$requestName}"); + + expect($output = Artisan::output())->when( + Feature::IncludeFilepathInGeneratorCommandOutput->exists(), + fn ($output) => $output->toContainFilepath($relativePath), + ); + + expect(file_exists($expectedPath))->toBeTrue(); + + expect(file_get_contents($expectedPath)) + ->toContain("namespace {$expectedNamespace};"); +})->with([ + 'Invoicing:StoreInvoiceRequest' => [ + 'Invoicing', + 'StoreInvoiceRequest', + 'app/Modules/Invoicing/Requests/StoreInvoiceRequest.php', + 'App\Modules\Invoicing\Requests', + ], + + 'Reporting.Internal:UpdateReportRequest' => [ + 'Reporting.Internal', + 'UpdateReportRequest', + 'app/Modules/Reporting/Internal/Requests/UpdateReportRequest.php', + 'App\Modules\Reporting\Internal\Requests', + ], +]); diff --git a/tests/Generator/MakeValueObjectTest.php b/tests/Generator/ValueObjectMakeTest.php similarity index 100% rename from tests/Generator/MakeValueObjectTest.php rename to tests/Generator/ValueObjectMakeTest.php diff --git a/tests/Generator/MakeViewModelTest.php b/tests/Generator/ViewModelMakeTest.php similarity index 100% rename from tests/Generator/MakeViewModelTest.php rename to tests/Generator/ViewModelMakeTest.php diff --git a/tests/Support/DomainResolverTest.php b/tests/Support/DomainResolverTest.php index 37b63b9..846c815 100644 --- a/tests/Support/DomainResolverTest.php +++ b/tests/Support/DomainResolverTest.php @@ -13,6 +13,11 @@ '--domain' => 'Customer', ]); + $this->artisan('ddd:value', [ + 'name' => 'Subtotal', + '--domain' => 'Shared', + ]); + $this->expectedDomains = [ 'Customer', 'Invoicing', diff --git a/tests/Support/DomainTest.php b/tests/Support/DomainTest.php index e263500..8a701ae 100644 --- a/tests/Support/DomainTest.php +++ b/tests/Support/DomainTest.php @@ -1,5 +1,6 @@ 'app/Modules', + 'namespace' => 'App\Modules', + 'objects' => ['controller', 'request'], + ]); + }); + + it('can describe objects in the application layer', function ($domainName, $objectType, $objectName, $expectedFQN, $expectedPath) { + expect((new Domain($domainName))->object($objectType, $objectName)) + ->name->toBe($objectName) + ->fullyQualifiedName->toBe($expectedFQN) + ->path->toBe(Path::normalize($expectedPath)); + })->with([ + ['Invoicing', 'controller', 'InvoiceController', 'App\\Modules\\Invoicing\\Controllers\\InvoiceController', 'app/Modules/Invoicing/Controllers/InvoiceController.php'], + ['Invoicing', 'controller', 'Nested\\InvoiceController', 'App\\Modules\\Invoicing\\Controllers\\Nested\\InvoiceController', 'app/Modules/Invoicing/Controllers/Nested/InvoiceController.php'], + ['Invoicing', 'request', 'StoreInvoiceRequest', 'App\\Modules\\Invoicing\\Requests\\StoreInvoiceRequest', 'app/Modules/Invoicing/Requests/StoreInvoiceRequest.php'], + ['Invoicing', 'request', 'Nested\\StoreInvoiceRequest', 'App\\Modules\\Invoicing\\Requests\\Nested\\StoreInvoiceRequest', 'app/Modules/Invoicing/Requests/Nested/StoreInvoiceRequest.php'], + ]); +}); + +it('normalizes slashes in nested objects', function ($nameInput, $normalized) { + expect((new Domain('Invoicing'))->object('class', $nameInput)) + ->name->toBe($normalized); +})->with([ + ['Nested\\Thing', 'Nested\\Thing'], + ['Nested/Thing', 'Nested\\Thing'], + ['Nested/Thing/Deeply', 'Nested\\Thing\\Deeply'], + ['Nested\\Thing/Deeply', 'Nested\\Thing\\Deeply'], +]); diff --git a/tests/TestCase.php b/tests/TestCase.php index fb69cab..b04ef5a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -151,6 +151,7 @@ protected function cleanSlate() File::deleteDirectory(base_path('Custom')); File::deleteDirectory(base_path('src/Domain')); File::deleteDirectory(base_path('src/Domains')); + File::deleteDirectory(base_path('src/App')); File::deleteDirectory(app_path('Models')); File::deleteDirectory(base_path('bootstrap/cache/ddd')); From e83594239d6ea9bd750fd421d605f11f77c2820d Mon Sep 17 00:00:00 2001 From: Jasper Tey Date: Tue, 5 Nov 2024 11:36:26 -0500 Subject: [PATCH 2/5] Bump laravel prompts dependency. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 243d503..e7baac8 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "require": { "php": "^8.1|^8.2|^8.3", "illuminate/contracts": "^10.25|^11.0", - "laravel/prompts": "^0.1.16", + "laravel/prompts": "^0.3.1", "lorisleiva/lody": "^0.5.0", "spatie/laravel-package-tools": "^1.13.0" }, From 2dbd371b64c9af035c3272b4c55665ae24cf2080 Mon Sep 17 00:00:00 2001 From: Jasper Tey Date: Tue, 5 Nov 2024 11:39:55 -0500 Subject: [PATCH 3/5] Ensure laravel/prompts Laravel 10 compatibility. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e7baac8..2bfcb1b 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "require": { "php": "^8.1|^8.2|^8.3", "illuminate/contracts": "^10.25|^11.0", - "laravel/prompts": "^0.3.1", + "laravel/prompts": "^0.1.16|^0.3.1", "lorisleiva/lody": "^0.5.0", "spatie/laravel-package-tools": "^1.13.0" }, From f8349538eac79d8b9cd206115b1a2a225654906e Mon Sep 17 00:00:00 2001 From: Jasper Tey Date: Tue, 5 Nov 2024 11:48:45 -0500 Subject: [PATCH 4/5] Release 1.1.3 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2926890..e296e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-ddd` will be documented in this file. +## [1.1.3] - 2024-11-05 +### Chore +- Allow `laravel/prompts` dependency to use latest version when possible. + ## [1.1.2] - 2024-09-02 ### Fixed - During domain factory autoloading, ensure that `guessFactoryNamesUsing` returns a string when a domain factory is resolved. From 3c73e2db17e0955da75d975b2f382d53a1163449 Mon Sep 17 00:00:00 2001 From: Jasper Tey Date: Sun, 10 Nov 2024 00:24:29 -0500 Subject: [PATCH 5/5] [1.2] Refactoring stubs (#71) * Updates: - normalize domain stub trait across all domain generators - ensure ddd:controller is able to check for base controller existence - refine ddd:model stub replacements * Introduce ddd:publish and ddd:stub commands. * Normalize the resolution of published stubs. * Ensure post-build replacements are skipped in ddd:model when using custom stubs. * Implement ddd:publish and ddd:stub commands. * Refinements to readme and stub documentation. --------- Co-authored-by: JasperTey --- CHANGELOG.md | 6 +- README.md | 48 ++++- src/Commands/Concerns/HasDomainStubs.php | 70 +++++++ src/Commands/Concerns/InteractsWithStubs.php | 32 +++ src/Commands/DomainActionMakeCommand.php | 6 +- src/Commands/DomainBaseModelMakeCommand.php | 5 +- .../DomainBaseViewModelMakeCommand.php | 5 +- src/Commands/DomainCastMakeCommand.php | 4 +- src/Commands/DomainChannelMakeCommand.php | 4 +- src/Commands/DomainClassMakeCommand.php | 4 +- src/Commands/DomainConsoleMakeCommand.php | 4 +- src/Commands/DomainControllerMakeCommand.php | 36 ++++ src/Commands/DomainDtoMakeCommand.php | 6 +- src/Commands/DomainEnumMakeCommand.php | 4 +- src/Commands/DomainEventMakeCommand.php | 4 +- src/Commands/DomainExceptionMakeCommand.php | 4 +- src/Commands/DomainFactoryMakeCommand.php | 57 ++---- src/Commands/DomainGeneratorCommand.php | 60 +++--- src/Commands/DomainInterfaceMakeCommand.php | 4 +- src/Commands/DomainJobMakeCommand.php | 4 +- src/Commands/DomainListenerMakeCommand.php | 4 +- src/Commands/DomainMailMakeCommand.php | 4 +- src/Commands/DomainMiddlewareMakeCommand.php | 4 +- src/Commands/DomainModelMakeCommand.php | 39 +++- src/Commands/DomainModelMakeLegacyCommand.php | 108 ----------- .../DomainNotificationMakeCommand.php | 4 +- src/Commands/DomainObserverMakeCommand.php | 4 +- src/Commands/DomainPolicyMakeCommand.php | 4 +- src/Commands/DomainProviderMakeCommand.php | 4 +- src/Commands/DomainRequestMakeCommand.php | 9 +- src/Commands/DomainResourceMakeCommand.php | 4 +- src/Commands/DomainRuleMakeCommand.php | 4 +- src/Commands/DomainScopeMakeCommand.php | 4 +- src/Commands/DomainSeederMakeCommand.php | 4 +- src/Commands/DomainTraitMakeCommand.php | 4 +- src/Commands/DomainValueObjectMakeCommand.php | 6 +- src/Commands/DomainViewModelMakeCommand.php | 5 +- src/Commands/InstallCommand.php | 6 +- src/Commands/PublishCommand.php | 63 ++++++ src/Commands/StubCommand.php | 163 ++++++++++++++++ src/DomainManager.php | 19 ++ src/Facades/DDD.php | 3 + src/Factories/DomainFactory.php | 3 +- src/LaravelDDDServiceProvider.php | 18 +- src/StubManager.php | 102 ++++++++++ src/Support/Domain.php | 1 + stubs/{action.php.stub => action.stub} | 0 .../{base-model.php.stub => base-model.stub} | 0 ...ew-model.php.stub => base-view-model.stub} | 0 stubs/{dto.php.stub => dto.stub} | 0 stubs/{factory.php.stub => factory.stub} | 0 stubs/model.php.stub | 13 -- ...alue-object.php.stub => value-object.stub} | 0 .../{view-model.php.stub => view-model.stub} | 0 .../app/Http/Controllers/Controller.php | 8 + tests/.skeleton/stubs/ddd/dummy.stub | 0 tests/Command/PublishTest.php | 116 +++++++++++ tests/Command/StubTest.php | 182 ++++++++++++++++++ tests/Factory/DomainFactoryTest.php | 9 + tests/Fixtures/Enums/Feature.php | 2 + tests/Generator/ControllerMakeTest.php | 107 +++++++++- tests/Generator/Model/MakeTest.php | 56 ++++++ .../Model/MakeWithControllerTest.php | 12 -- tests/Pest.php | 2 +- tests/Setup/PublishTest.php | 2 +- tests/TestCase.php | 15 ++ 66 files changed, 1216 insertions(+), 268 deletions(-) create mode 100644 src/Commands/Concerns/HasDomainStubs.php create mode 100644 src/Commands/Concerns/InteractsWithStubs.php delete mode 100644 src/Commands/DomainModelMakeLegacyCommand.php create mode 100644 src/Commands/PublishCommand.php create mode 100644 src/Commands/StubCommand.php create mode 100755 src/StubManager.php rename stubs/{action.php.stub => action.stub} (100%) rename stubs/{base-model.php.stub => base-model.stub} (100%) rename stubs/{base-view-model.php.stub => base-view-model.stub} (100%) rename stubs/{dto.php.stub => dto.stub} (100%) rename stubs/{factory.php.stub => factory.stub} (100%) delete mode 100644 stubs/model.php.stub rename stubs/{value-object.php.stub => value-object.stub} (100%) rename stubs/{view-model.php.stub => view-model.stub} (100%) create mode 100644 tests/.skeleton/app/Http/Controllers/Controller.php create mode 100644 tests/.skeleton/stubs/ddd/dummy.stub create mode 100644 tests/Command/PublishTest.php create mode 100644 tests/Command/StubTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6c9af..0a7dfed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,11 @@ All notable changes to `laravel-ddd` will be documented in this file. ## [Unreleased] +### Breaking +- Stubs are now published to `/stubs/ddd/*` instead of `resources/stubs/ddd/*`. If you have ddd stubs published from a prior version, they should be relocated. + ### Added -- Experimental: Ability to configure the Application Layer, to generate domain objects that don't typically belong inside the domain layer. +- Ability to configure the Application Layer, to generate domain objects that don't typically belong inside the domain layer. ```php // In config/ddd.php 'application' => [ @@ -22,6 +25,7 @@ All notable changes to `laravel-ddd` will be documented in this file. - Added `ddd:middleware` to generate domain-specific middleware in the application layer. - Added `ddd:migration` to generate domain migrations. - Added `ddd:seeder` to generate domain seeders. +- Added `ddd:stub` to list, search, and publish one or more stubs as needed. - Migration folders across domains will be registered and scanned when running `php artisan migrate`, in addition to the standard application `database/migrations` path. ### Changed diff --git a/README.md b/README.md index beb7412..0906314 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Actions: [lorisleiva/laravel-actions](https://github.com/lorisleiva/laravel-acti ```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 accordingly. +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 In production, run `ddd:optimize` during the deployment process to [optimize autoloading](#autoloading-in-production). @@ -147,8 +147,8 @@ Output: │ └─ UpdateInvoiceRequest.php ├─ src/Domain └─ Invoicing - └─ Models - └─ Invoice.php + └─ Models + └─ Invoice.php ``` ### Nested Objects @@ -211,13 +211,49 @@ php artisan ddd:view-model Reporting.Customer:MonthlyInvoicesReportViewModel ``` ## Customization +### Config File This package ships with opinionated (but sensible) configuration defaults. You may customize by publishing the [config file](#config-file) and generator stubs as needed: ```bash -php artisan vendor:publish --tag="ddd-config" -php artisan vendor:publish --tag="ddd-stubs" +php artisan ddd:publish --config +php artisan ddd:publish --stubs +``` + +### Publishing Stubs (Advanced) +For more granular management of stubs, you may use the `ddd:stub` command: +```bash +# Publish one or more stubs interactively via prompts +php artisan ddd:stub + +# Publish all stubs +php artisan ddd:stub --all + +# Publish and overwrite only the files that have already been published +php artisan ddd:stub --all --existing + +# Overwrite any existing files +php artisan ddd:stub --all --force + +# Publish one or more stubs specified as arguments +php artisan ddd:stub model +php artisan ddd:stub model dto action +php artisan ddd:stub controller controller.plain controller.api +``` +To publish multiple related stubs at once, use `*` or `.` as a wildcard ending. +```bash +php artisan ddd:stub listener. +``` +Output: +```bash +Publishing /stubs/ddd/listener.typed.queued.stub +Publishing /stubs/ddd/listener.queued.stub +Publishing /stubs/ddd/listener.typed.stub +Publishing /stubs/ddd/listener.stub +``` +For a quick reference of available stubs, use the `--list` option: +```bash +php artisan ddd:stub --list ``` -Note that the extended commands do not publish ddd-specific stubs, and inherit the respective application-level stubs published by Laravel. ## Domain Autoloading and Discovery Autoloading behaviour can be configured with the `ddd.autoload` configuration option. By default, domain providers, commands, policies, and factories are auto-discovered and registered. diff --git a/src/Commands/Concerns/HasDomainStubs.php b/src/Commands/Concerns/HasDomainStubs.php new file mode 100644 index 0000000..591511d --- /dev/null +++ b/src/Commands/Concerns/HasDomainStubs.php @@ -0,0 +1,70 @@ +resolvePublishedDddStub($stub)) { + $stub = $publishedStub; + } + + $this->usingPublishedStub(str($stub)->startsWith(app()->basePath('stubs'))); + + return $stub; + } + + protected function resolvePublishedDddStub($path) + { + $stubFilename = str($path) + ->basename() + ->ltrim('/\\') + ->toString(); + + // Check if there is a user-published stub + if (file_exists($publishedPath = app()->basePath('stubs/ddd/'.$stubFilename))) { + return $publishedPath; + } + + // Also check for legacy stub extensions + if (file_exists($legacyPublishedPath = Str::replaceLast('.stub', '.php.stub', $publishedPath))) { + return $legacyPublishedPath; + } + + return null; + } + + protected function resolveDddStubPath($path) + { + $path = str($path) + ->basename() + ->ltrim('/\\') + ->toString(); + + if ($publishedPath = $this->resolvePublishedDddStub($path)) { + return $publishedPath; + } + + return DDD::packagePath('stubs/'.$path); + } +} diff --git a/src/Commands/Concerns/InteractsWithStubs.php b/src/Commands/Concerns/InteractsWithStubs.php new file mode 100644 index 0000000..02e9472 --- /dev/null +++ b/src/Commands/Concerns/InteractsWithStubs.php @@ -0,0 +1,32 @@ +preparePlaceholders(); + + foreach ($placeholders as $placeholder => $value) { + $stub = $this->fillPlaceholder($stub, $placeholder, $value ?? ''); + } + + return $stub; + } + + protected function buildClass($name) + { + return $this->applyPlaceholders(parent::buildClass($name)); + } +} diff --git a/src/Commands/DomainActionMakeCommand.php b/src/Commands/DomainActionMakeCommand.php index c1a14f2..3218457 100644 --- a/src/Commands/DomainActionMakeCommand.php +++ b/src/Commands/DomainActionMakeCommand.php @@ -2,8 +2,12 @@ namespace Lunarstorm\LaravelDDD\Commands; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; + class DomainActionMakeCommand extends DomainGeneratorCommand { + use HasDomainStubs; + protected $name = 'ddd:action'; /** @@ -17,7 +21,7 @@ class DomainActionMakeCommand extends DomainGeneratorCommand protected function getStub() { - return $this->resolveStubPath('action.php.stub'); + return $this->resolveDddStubPath('action.stub'); } protected function preparePlaceholders(): array diff --git a/src/Commands/DomainBaseModelMakeCommand.php b/src/Commands/DomainBaseModelMakeCommand.php index 559cd59..1dac48f 100644 --- a/src/Commands/DomainBaseModelMakeCommand.php +++ b/src/Commands/DomainBaseModelMakeCommand.php @@ -2,10 +2,13 @@ namespace Lunarstorm\LaravelDDD\Commands; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Symfony\Component\Console\Input\InputArgument; class DomainBaseModelMakeCommand extends DomainGeneratorCommand { + use HasDomainStubs; + protected $name = 'ddd:base-model'; /** @@ -31,7 +34,7 @@ protected function getArguments() protected function getStub() { - return $this->resolveStubPath('base-model.php.stub'); + return $this->resolveDddStubPath('base-model.stub'); } protected function getRelativeDomainNamespace(): string diff --git a/src/Commands/DomainBaseViewModelMakeCommand.php b/src/Commands/DomainBaseViewModelMakeCommand.php index afac4f7..3436b12 100644 --- a/src/Commands/DomainBaseViewModelMakeCommand.php +++ b/src/Commands/DomainBaseViewModelMakeCommand.php @@ -2,10 +2,13 @@ namespace Lunarstorm\LaravelDDD\Commands; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Symfony\Component\Console\Input\InputArgument; class DomainBaseViewModelMakeCommand extends DomainGeneratorCommand { + use HasDomainStubs; + protected $name = 'ddd:base-view-model'; /** @@ -31,7 +34,7 @@ protected function getArguments() protected function getStub() { - return $this->resolveStubPath('base-view-model.php.stub'); + return $this->resolveDddStubPath('base-view-model.stub'); } protected function getRelativeDomainNamespace(): string diff --git a/src/Commands/DomainCastMakeCommand.php b/src/Commands/DomainCastMakeCommand.php index 30531e4..da230a0 100644 --- a/src/Commands/DomainCastMakeCommand.php +++ b/src/Commands/DomainCastMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\CastMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainCastMakeCommand extends CastMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:cast'; } diff --git a/src/Commands/DomainChannelMakeCommand.php b/src/Commands/DomainChannelMakeCommand.php index f3e5ba5..3bee9b9 100644 --- a/src/Commands/DomainChannelMakeCommand.php +++ b/src/Commands/DomainChannelMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ChannelMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainChannelMakeCommand extends ChannelMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:channel'; } diff --git a/src/Commands/DomainClassMakeCommand.php b/src/Commands/DomainClassMakeCommand.php index 242788b..a657407 100644 --- a/src/Commands/DomainClassMakeCommand.php +++ b/src/Commands/DomainClassMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ClassMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainClassMakeCommand extends ClassMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:class'; } diff --git a/src/Commands/DomainConsoleMakeCommand.php b/src/Commands/DomainConsoleMakeCommand.php index 6496db9..f78cd39 100644 --- a/src/Commands/DomainConsoleMakeCommand.php +++ b/src/Commands/DomainConsoleMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ConsoleMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainConsoleMakeCommand extends ConsoleMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:command'; } diff --git a/src/Commands/DomainControllerMakeCommand.php b/src/Commands/DomainControllerMakeCommand.php index aa2a373..47f03dc 100644 --- a/src/Commands/DomainControllerMakeCommand.php +++ b/src/Commands/DomainControllerMakeCommand.php @@ -4,6 +4,7 @@ use Illuminate\Routing\Console\ControllerMakeCommand; use Lunarstorm\LaravelDDD\Commands\Concerns\ForwardsToDomainCommands; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; use function Laravel\Prompts\confirm; @@ -11,6 +12,7 @@ class DomainControllerMakeCommand extends ControllerMakeCommand { use ForwardsToDomainCommands, + HasDomainStubs, ResolvesDomainFromInput; protected $name = 'ddd:controller'; @@ -79,4 +81,38 @@ protected function buildFormRequestReplacements(array $replace, $modelClass) '{{namespacedRequests}}' => $namespacedRequests, ]); } + + protected function buildClass($name) + { + $stub = parent::buildClass($name); + + if ($this->isUsingPublishedStub()) { + return $stub; + } + + $replace = []; + + // Todo: these were attempted tweaks to counteract failing CI tests + // on Laravel 10, and should be revisited at some point. + // $replace["use {$this->rootNamespace()}Http\Controllers\Controller;\n"] = ''; + // $replace[' extends Controller'] = ''; + + $appRootNamespace = $this->laravel->getNamespace(); + $pathToAppBaseController = parent::getPath("Http\Controllers\Controller"); + + $baseControllerExists = $this->files->exists($pathToAppBaseController); + + if ($baseControllerExists) { + $controllerClass = class_basename($name); + $replace["\nclass {$controllerClass}\n"] = "\nuse {$appRootNamespace}Http\Controllers\Controller;\n\nclass {$controllerClass} extends Controller\n"; + } + + $stub = str_replace( + array_keys($replace), + array_values($replace), + $stub + ); + + return $this->sortImports($stub); + } } diff --git a/src/Commands/DomainDtoMakeCommand.php b/src/Commands/DomainDtoMakeCommand.php index ac4ef3f..b8612cf 100644 --- a/src/Commands/DomainDtoMakeCommand.php +++ b/src/Commands/DomainDtoMakeCommand.php @@ -2,8 +2,12 @@ namespace Lunarstorm\LaravelDDD\Commands; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; + class DomainDtoMakeCommand extends DomainGeneratorCommand { + use HasDomainStubs; + protected $name = 'ddd:dto'; /** @@ -28,7 +32,7 @@ protected function configure() protected function getStub() { - return $this->resolveStubPath('dto.php.stub'); + return $this->resolveDddStubPath('dto.stub'); } protected function getRelativeDomainNamespace(): string diff --git a/src/Commands/DomainEnumMakeCommand.php b/src/Commands/DomainEnumMakeCommand.php index d3110b7..3348186 100644 --- a/src/Commands/DomainEnumMakeCommand.php +++ b/src/Commands/DomainEnumMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\EnumMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainEnumMakeCommand extends EnumMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:enum'; } diff --git a/src/Commands/DomainEventMakeCommand.php b/src/Commands/DomainEventMakeCommand.php index de4a11b..fcba974 100644 --- a/src/Commands/DomainEventMakeCommand.php +++ b/src/Commands/DomainEventMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\EventMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainEventMakeCommand extends EventMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:event'; } diff --git a/src/Commands/DomainExceptionMakeCommand.php b/src/Commands/DomainExceptionMakeCommand.php index c9871e7..f794dc1 100644 --- a/src/Commands/DomainExceptionMakeCommand.php +++ b/src/Commands/DomainExceptionMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ExceptionMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainExceptionMakeCommand extends ExceptionMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:exception'; } diff --git a/src/Commands/DomainFactoryMakeCommand.php b/src/Commands/DomainFactoryMakeCommand.php index 6a1a104..1c53382 100644 --- a/src/Commands/DomainFactoryMakeCommand.php +++ b/src/Commands/DomainFactoryMakeCommand.php @@ -2,50 +2,27 @@ namespace Lunarstorm\LaravelDDD\Commands; -use Symfony\Component\Console\Input\InputOption; +use Illuminate\Database\Console\Factories\FactoryMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; +use Lunarstorm\LaravelDDD\Commands\Concerns\InteractsWithStubs; +use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; -class DomainFactoryMakeCommand extends DomainGeneratorCommand +class DomainFactoryMakeCommand extends FactoryMakeCommand { - protected $name = 'ddd:factory'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Generate a domain model factory'; + use HasDomainStubs, + InteractsWithStubs, + ResolvesDomainFromInput; - protected $type = 'Factory'; - - protected function getOptions() - { - return [ - ...parent::getOptions(), - ['model', 'm', InputOption::VALUE_OPTIONAL, 'The name of the model'], - ]; - } + protected $name = 'ddd:factory'; protected function getStub() { - return $this->resolveStubPath('factory.php.stub'); + return $this->resolveDddStubPath('factory.stub'); } - protected function getPath($name) + protected function getNamespace($name) { - if (! str_ends_with($name, 'Factory')) { - $name .= 'Factory'; - } - - return parent::getPath($name); - } - - protected function getFactoryName() - { - $name = $this->getNameInput(); - - return str_ends_with($name, 'Factory') - ? substr($name, 0, -7) - : $name; + return $this->domain->namespaceFor('factory'); } protected function preparePlaceholders(): array @@ -60,16 +37,10 @@ protected function preparePlaceholders(): array $domainFactory = $domain->factory($name); - // dump('preparing placeholders', [ - // 'name' => $name, - // 'modelName' => $modelName, - // 'domainFactory' => $domainFactory, - // ]); - return [ 'namespacedModel' => $domainModel->fullyQualifiedName, 'model' => class_basename($domainModel->fullyQualifiedName), - 'factory' => $this->getFactoryName(), + 'factory' => $domainFactory->name, 'namespace' => $domainFactory->namespace, ]; } @@ -80,6 +51,6 @@ protected function guessModelName($name) $name = substr($name, 0, -7); } - return $this->domain->model($name)->name; + return $this->domain->model(class_basename($name))->name; } } diff --git a/src/Commands/DomainGeneratorCommand.php b/src/Commands/DomainGeneratorCommand.php index 6421409..9dc8dd2 100644 --- a/src/Commands/DomainGeneratorCommand.php +++ b/src/Commands/DomainGeneratorCommand.php @@ -4,12 +4,14 @@ use Illuminate\Console\GeneratorCommand; use Illuminate\Support\Str; +use Lunarstorm\LaravelDDD\Commands\Concerns\InteractsWithStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; use Lunarstorm\LaravelDDD\Support\DomainResolver; abstract class DomainGeneratorCommand extends GeneratorCommand { - use ResolvesDomainFromInput; + use InteractsWithStubs, + ResolvesDomainFromInput; protected function getRelativeDomainNamespace(): string { @@ -21,40 +23,40 @@ protected function getNameInput() return Str::studly($this->argument('name')); } - protected function resolveStubPath($path) - { - $path = ltrim($path, '/\\'); + // protected function resolveStubPath($path) + // { + // $path = ltrim($path, '/\\'); - $publishedPath = resource_path('stubs/ddd/'.$path); + // $publishedPath = resource_path('stubs/ddd/'.$path); - return file_exists($publishedPath) - ? $publishedPath - : __DIR__.DIRECTORY_SEPARATOR.'../../stubs'.DIRECTORY_SEPARATOR.$path; - } + // return file_exists($publishedPath) + // ? $publishedPath + // : __DIR__.DIRECTORY_SEPARATOR.'../../stubs'.DIRECTORY_SEPARATOR.$path; + // } - protected function fillPlaceholder($stub, $placeholder, $value) - { - return str_replace(["{{$placeholder}}", "{{ $placeholder }}"], $value, $stub); - } + // protected function fillPlaceholder($stub, $placeholder, $value) + // { + // return str_replace(["{{$placeholder}}", "{{ $placeholder }}"], $value, $stub); + // } - protected function preparePlaceholders(): array - { - return []; - } + // protected function preparePlaceholders(): array + // { + // return []; + // } - protected function applyPlaceholders($stub) - { - $placeholders = $this->preparePlaceholders(); + // protected function applyPlaceholders($stub) + // { + // $placeholders = $this->preparePlaceholders(); - foreach ($placeholders as $placeholder => $value) { - $stub = $this->fillPlaceholder($stub, $placeholder, $value ?? ''); - } + // foreach ($placeholders as $placeholder => $value) { + // $stub = $this->fillPlaceholder($stub, $placeholder, $value ?? ''); + // } - return $stub; - } + // return $stub; + // } - protected function buildClass($name) - { - return $this->applyPlaceholders(parent::buildClass($name)); - } + // protected function buildClass($name) + // { + // return $this->applyPlaceholders(parent::buildClass($name)); + // } } diff --git a/src/Commands/DomainInterfaceMakeCommand.php b/src/Commands/DomainInterfaceMakeCommand.php index 3fbccef..82735d6 100644 --- a/src/Commands/DomainInterfaceMakeCommand.php +++ b/src/Commands/DomainInterfaceMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\InterfaceMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainInterfaceMakeCommand extends InterfaceMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:interface'; } diff --git a/src/Commands/DomainJobMakeCommand.php b/src/Commands/DomainJobMakeCommand.php index 7fc5e37..fd9afbd 100644 --- a/src/Commands/DomainJobMakeCommand.php +++ b/src/Commands/DomainJobMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\JobMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainJobMakeCommand extends JobMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:job'; } diff --git a/src/Commands/DomainListenerMakeCommand.php b/src/Commands/DomainListenerMakeCommand.php index 9726d20..0072172 100644 --- a/src/Commands/DomainListenerMakeCommand.php +++ b/src/Commands/DomainListenerMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ListenerMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainListenerMakeCommand extends ListenerMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:listener'; } diff --git a/src/Commands/DomainMailMakeCommand.php b/src/Commands/DomainMailMakeCommand.php index 15f9508..ef6d0e8 100644 --- a/src/Commands/DomainMailMakeCommand.php +++ b/src/Commands/DomainMailMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\MailMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainMailMakeCommand extends MailMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:mail'; } diff --git a/src/Commands/DomainMiddlewareMakeCommand.php b/src/Commands/DomainMiddlewareMakeCommand.php index 2c6b2af..9a2a041 100644 --- a/src/Commands/DomainMiddlewareMakeCommand.php +++ b/src/Commands/DomainMiddlewareMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Routing\Console\MiddlewareMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainMiddlewareMakeCommand extends MiddlewareMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:middleware'; } diff --git a/src/Commands/DomainModelMakeCommand.php b/src/Commands/DomainModelMakeCommand.php index c92ccdd..1bc72b3 100644 --- a/src/Commands/DomainModelMakeCommand.php +++ b/src/Commands/DomainModelMakeCommand.php @@ -5,12 +5,14 @@ use Illuminate\Foundation\Console\ModelMakeCommand; use Illuminate\Support\Str; use Lunarstorm\LaravelDDD\Commands\Concerns\ForwardsToDomainCommands; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; use Lunarstorm\LaravelDDD\Support\DomainResolver; class DomainModelMakeCommand extends ModelMakeCommand { use ForwardsToDomainCommands, + HasDomainStubs, ResolvesDomainFromInput; protected $name = 'ddd:model'; @@ -31,32 +33,51 @@ public function handle() $this->afterHandle(); } + protected function buildFactoryReplacements() + { + $replacements = parent::buildFactoryReplacements(); + + if ($this->option('factory')) { + $factoryNamespace = Str::start($this->domain->factory($this->getNameInput())->fullyQualifiedName, '\\'); + + $factoryCode = << */ + use HasFactory; + EOT; + + $replacements['{{ factory }}'] = $factoryCode; + $replacements['{{ factoryImport }}'] = 'use Lunarstorm\LaravelDDD\Factories\HasDomainFactory as HasFactory;'; + } + + return $replacements; + } + protected function buildClass($name) { $stub = parent::buildClass($name); - $replacements = [ - 'use Illuminate\Database\Eloquent\Factories\HasFactory;' => "use Lunarstorm\LaravelDDD\Factories\HasDomainFactory as HasFactory;", - ]; + if ($this->isUsingPublishedStub()) { + return $stub; + } + + $replace = []; if ($baseModel = $this->getBaseModel()) { $baseModelClass = class_basename($baseModel); - $replacements = array_merge($replacements, [ + $replace = array_merge($replace, [ 'extends Model' => "extends {$baseModelClass}", 'use Illuminate\Database\Eloquent\Model;' => "use {$baseModel};", ]); } $stub = str_replace( - array_keys($replacements), - array_values($replacements), + array_keys($replace), + array_values($replace), $stub ); - $stub = $this->sortImports($stub); - - return $stub; + return $this->sortImports($stub); } protected function createBaseModelIfNeeded() diff --git a/src/Commands/DomainModelMakeLegacyCommand.php b/src/Commands/DomainModelMakeLegacyCommand.php deleted file mode 100644 index 37c8164..0000000 --- a/src/Commands/DomainModelMakeLegacyCommand.php +++ /dev/null @@ -1,108 +0,0 @@ -resolveStubPath('model.php.stub'); - } - - protected function preparePlaceholders(): array - { - $baseClass = config('ddd.base_model'); - $baseClassName = class_basename($baseClass); - - return [ - 'extends' => filled($baseClass) ? " extends {$baseClassName}" : '', - 'baseClassImport' => filled($baseClass) ? "use {$baseClass};" : '', - ]; - } - - public function handle() - { - $this->createBaseModelIfNeeded(); - - parent::handle(); - - if ($this->option('factory')) { - $this->createFactory(); - } - } - - protected function createBaseModelIfNeeded() - { - if (! $this->shouldCreateBaseModel()) { - return; - } - - $baseModel = config('ddd.base_model'); - - $this->warn("Base model {$baseModel} doesn't exist, generating..."); - - $domain = DomainResolver::guessDomainFromClass($baseModel); - - $name = Str::after($baseModel, $domain); - - $this->call(DomainBaseModelMakeCommand::class, [ - '--domain' => $domain, - 'name' => $name, - ]); - } - - protected function shouldCreateBaseModel(): bool - { - $baseModel = config('ddd.base_model'); - - // If the class exists, we don't need to create it. - if (class_exists($baseModel)) { - return false; - } - - // If the class is outside of the domain layer, we won't attempt to create it. - if (! DomainResolver::isDomainClass($baseModel)) { - return false; - } - - // At this point the class is probably a domain object, but we should - // check if the expected path exists. - if (file_exists(app()->basePath(DomainResolver::guessPathFromClass($baseModel)))) { - return false; - } - - return true; - } - - protected function createFactory() - { - $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 index 04de8ab..099cd9a 100644 --- a/src/Commands/DomainNotificationMakeCommand.php +++ b/src/Commands/DomainNotificationMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\NotificationMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainNotificationMakeCommand extends NotificationMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:notification'; } diff --git a/src/Commands/DomainObserverMakeCommand.php b/src/Commands/DomainObserverMakeCommand.php index 9668230..9351b53 100644 --- a/src/Commands/DomainObserverMakeCommand.php +++ b/src/Commands/DomainObserverMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ObserverMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainObserverMakeCommand extends ObserverMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:observer'; } diff --git a/src/Commands/DomainPolicyMakeCommand.php b/src/Commands/DomainPolicyMakeCommand.php index bbf57f3..180223f 100644 --- a/src/Commands/DomainPolicyMakeCommand.php +++ b/src/Commands/DomainPolicyMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\PolicyMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainPolicyMakeCommand extends PolicyMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:policy'; } diff --git a/src/Commands/DomainProviderMakeCommand.php b/src/Commands/DomainProviderMakeCommand.php index dcc2c8a..8ee4dcc 100644 --- a/src/Commands/DomainProviderMakeCommand.php +++ b/src/Commands/DomainProviderMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ProviderMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainProviderMakeCommand extends ProviderMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:provider'; } diff --git a/src/Commands/DomainRequestMakeCommand.php b/src/Commands/DomainRequestMakeCommand.php index a6a1ddf..88cad30 100644 --- a/src/Commands/DomainRequestMakeCommand.php +++ b/src/Commands/DomainRequestMakeCommand.php @@ -4,20 +4,17 @@ use Illuminate\Foundation\Console\RequestMakeCommand; use Illuminate\Support\Str; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; use Lunarstorm\LaravelDDD\Support\DomainResolver; class DomainRequestMakeCommand extends RequestMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:request'; - // protected function getDefaultNamespace($rootNamespace) - // { - // return $rootNamespace.'\Http\Requests'; - // } - protected function rootNamespace() { $type = $this->guessObjectType(); diff --git a/src/Commands/DomainResourceMakeCommand.php b/src/Commands/DomainResourceMakeCommand.php index 36b3715..4e79208 100644 --- a/src/Commands/DomainResourceMakeCommand.php +++ b/src/Commands/DomainResourceMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ResourceMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainResourceMakeCommand extends ResourceMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:resource'; } diff --git a/src/Commands/DomainRuleMakeCommand.php b/src/Commands/DomainRuleMakeCommand.php index 50c6083..f82aec1 100644 --- a/src/Commands/DomainRuleMakeCommand.php +++ b/src/Commands/DomainRuleMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\RuleMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainRuleMakeCommand extends RuleMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:rule'; } diff --git a/src/Commands/DomainScopeMakeCommand.php b/src/Commands/DomainScopeMakeCommand.php index 9dfe7d7..43e2fd0 100644 --- a/src/Commands/DomainScopeMakeCommand.php +++ b/src/Commands/DomainScopeMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\ScopeMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainScopeMakeCommand extends ScopeMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:scope'; } diff --git a/src/Commands/DomainSeederMakeCommand.php b/src/Commands/DomainSeederMakeCommand.php index 6ee413d..302a073 100644 --- a/src/Commands/DomainSeederMakeCommand.php +++ b/src/Commands/DomainSeederMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Database\Console\Seeds\SeederMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainSeederMakeCommand extends SeederMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:seeder'; } diff --git a/src/Commands/DomainTraitMakeCommand.php b/src/Commands/DomainTraitMakeCommand.php index d377b52..1cb86a5 100644 --- a/src/Commands/DomainTraitMakeCommand.php +++ b/src/Commands/DomainTraitMakeCommand.php @@ -3,11 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Foundation\Console\TraitMakeCommand; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Commands\Concerns\ResolvesDomainFromInput; class DomainTraitMakeCommand extends TraitMakeCommand { - use ResolvesDomainFromInput; + use HasDomainStubs, + ResolvesDomainFromInput; protected $name = 'ddd:trait'; } diff --git a/src/Commands/DomainValueObjectMakeCommand.php b/src/Commands/DomainValueObjectMakeCommand.php index 27de72b..16e7102 100644 --- a/src/Commands/DomainValueObjectMakeCommand.php +++ b/src/Commands/DomainValueObjectMakeCommand.php @@ -2,8 +2,12 @@ namespace Lunarstorm\LaravelDDD\Commands; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; + class DomainValueObjectMakeCommand extends DomainGeneratorCommand { + use HasDomainStubs; + protected $name = 'ddd:value'; /** @@ -27,6 +31,6 @@ protected function configure() protected function getStub() { - return $this->resolveStubPath('value-object.php.stub'); + return $this->resolveDddStubPath('value-object.stub'); } } diff --git a/src/Commands/DomainViewModelMakeCommand.php b/src/Commands/DomainViewModelMakeCommand.php index e097cae..c457405 100644 --- a/src/Commands/DomainViewModelMakeCommand.php +++ b/src/Commands/DomainViewModelMakeCommand.php @@ -3,10 +3,13 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Support\Str; +use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs; use Lunarstorm\LaravelDDD\Support\DomainResolver; class DomainViewModelMakeCommand extends DomainGeneratorCommand { + use HasDomainStubs; + protected $name = 'ddd:view-model'; /** @@ -29,7 +32,7 @@ protected function configure() protected function getStub() { - return $this->resolveStubPath('view-model.php.stub'); + return $this->resolveDddStubPath('view-model.stub'); } protected function preparePlaceholders(): array diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index 1ad031e..e37d186 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -23,11 +23,7 @@ public function handle(): int $this->registerDomainAutoload(); if ($this->confirm('Would you like to publish stubs?')) { - $this->comment('Publishing stubs...'); - - $this->callSilently('vendor:publish', [ - '--tag' => 'ddd-stubs', - ]); + $this->call('ddd:stub'); } return self::SUCCESS; diff --git a/src/Commands/PublishCommand.php b/src/Commands/PublishCommand.php new file mode 100644 index 0000000..9180b84 --- /dev/null +++ b/src/Commands/PublishCommand.php @@ -0,0 +1,63 @@ + 'Stubs', + 'config' => 'Config File', + ]; + + return multiselect( + label: 'What should be published?', + options: $options, + required: true + ); + } + + public function handle(): int + { + $thingsToPublish = [ + ...$this->option('config') ? ['config'] : [], + ...$this->option('stubs') ? ['stubs'] : [], + ...$this->option('all') ? ['config', 'stubs'] : [], + ] ?: $this->askForThingsToPublish(); + + if (in_array('config', $thingsToPublish)) { + $this->comment('Publishing config...'); + $this->call('vendor:publish', [ + '--tag' => 'ddd-config', + ]); + } + + if (in_array('stubs', $thingsToPublish)) { + $this->comment('Publishing stubs...'); + $this->call('ddd:stub', [ + '--all' => true, + ]); + } + + return self::SUCCESS; + } +} diff --git a/src/Commands/StubCommand.php b/src/Commands/StubCommand.php new file mode 100644 index 0000000..984ad52 --- /dev/null +++ b/src/Commands/StubCommand.php @@ -0,0 +1,163 @@ +stubs()->dddStubs(), + ...app('ddd')->stubs()->frameworkStubs(), + ]; + } + + protected function resolveSelectedStubs(array $names = []) + { + $stubs = $this->getStubChoices(); + + if ($names) { + [$startsWith, $exactNames] = collect($names) + ->partition(fn ($name) => str($name)->endsWith(['*', '.'])); + + $startsWith = $startsWith->map( + fn ($name) => str($name) + ->replaceEnd('*', '.') + ->replaceEnd('.', '') + ); + + return collect($stubs) + ->filter(function ($stub, $path) use ($startsWith, $exactNames) { + $stubWithoutExtension = str($stub)->replaceEnd('.stub', ''); + + return $exactNames->contains($stub) + || $exactNames->contains($stubWithoutExtension) + || str($stub)->startsWith($startsWith); + }) + ->all(); + } + + $selected = multisearch( + label: 'Which stub should be published?', + placeholder: 'Search for a stub...', + options: fn (string $value) => strlen($value) > 0 + ? collect($stubs)->filter(fn ($stub, $path) => str($stub)->contains($value))->all() + : $stubs, + required: true + ); + + return collect($stubs) + ->filter(fn ($stub, $path) => in_array($stub, $selected)) + ->all(); + } + + public function handle(): int + { + $option = match (true) { + $this->option('list') => 'list', + $this->option('all') => 'all', + count($this->argument('name')) > 0 => 'named', + default => select( + label: 'What do you want to do?', + options: [ + 'some' => 'Choose stubs to publish', + 'all' => 'Publish all stubs', + ], + required: true, + default: 'some' + ) + }; + + if ($option === 'list') { + // $this->table( + // ['Stub', 'Path'], + // collect($this->getStubChoices())->map( + // fn($stub, $path) => [ + // $stub, + // Str::after($path, $this->laravel->basePath()) + // ] + // ) + // ); + + table( + headers: ['Stub', 'Source'], + rows: collect($this->getStubChoices())->map( + fn ($stub, $path) => [ + Str::replaceLast('.stub', '', $stub), + str($path)->startsWith(DDD::packagePath()) + ? 'ddd' + : 'laravel', + ] + ) + ); + + return self::SUCCESS; + } + + $stubs = $option === 'all' + ? $this->getStubChoices() + : $this->resolveSelectedStubs($this->argument('name')); + + if (empty($stubs)) { + $this->warn('No matching stubs found.'); + + return self::INVALID; + } + + File::ensureDirectoryExists($stubsPath = $this->laravel->basePath('stubs/ddd')); + + $this->laravel['events']->dispatch($event = new PublishingStubs($stubs)); + + foreach ($event->stubs as $from => $to) { + $to = $stubsPath.DIRECTORY_SEPARATOR.ltrim($to, DIRECTORY_SEPARATOR); + + $relativePath = Str::after($to, $this->laravel->basePath()); + + $this->info("Publishing {$relativePath}"); + + if ((! $this->option('existing') && (! file_exists($to) || $this->option('force'))) + || ($this->option('existing') && file_exists($to)) + ) { + file_put_contents($to, file_get_contents($from)); + } + } + + $this->components->info('Stubs published successfully.'); + + return self::SUCCESS; + } +} diff --git a/src/DomainManager.php b/src/DomainManager.php index 13d465a..317c453 100755 --- a/src/DomainManager.php +++ b/src/DomainManager.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Lunarstorm\LaravelDDD\Support\Domain; +use Lunarstorm\LaravelDDD\Support\Path; use Lunarstorm\LaravelDDD\ValueObjects\DomainCommandContext; class DomainManager @@ -31,12 +32,15 @@ class DomainManager protected ?DomainCommandContext $commandContext; + protected StubManager $stubs; + public function __construct() { $this->autoloadFilter = null; $this->applicationLayerFilter = null; $this->namespaceResolver = null; $this->commandContext = null; + $this->stubs = new StubManager; } public function filterAutoloadPathsUsing(callable $filter): void @@ -78,4 +82,19 @@ public function getCommandContext(): ?DomainCommandContext { return $this->commandContext; } + + public function packagePath($path = ''): string + { + return Path::normalize(realpath(__DIR__.'/../'.$path)); + } + + public function laravelVersion($value) + { + return version_compare(app()->version(), $value, '>='); + } + + public function stubs(): StubManager + { + return $this->stubs; + } } diff --git a/src/Facades/DDD.php b/src/Facades/DDD.php index a5aba45..93ad644 100644 --- a/src/Facades/DDD.php +++ b/src/Facades/DDD.php @@ -3,12 +3,15 @@ namespace Lunarstorm\LaravelDDD\Facades; use Illuminate\Support\Facades\Facade; +use Lunarstorm\LaravelDDD\StubManager; /** * @see \Lunarstorm\LaravelDDD\DomainManager * * @method static void filterAutoloadPathsUsing(callable $filter) * @method static void resolveNamespaceUsing(callable $resolver) + * @method static string packagePath(string $path = '') + * @method static StubManager stubs() */ class DDD extends Facade { diff --git a/src/Factories/DomainFactory.php b/src/Factories/DomainFactory.php index 47f7fe1..5c0b7b8 100644 --- a/src/Factories/DomainFactory.php +++ b/src/Factories/DomainFactory.php @@ -36,7 +36,8 @@ public static function resolveFactoryName(string $modelName) } // First try resolving as a factory class in the domain layer - if (class_exists($factoryClass = DomainResolver::getDomainObjectNamespace($model->domain, 'factory', "{$model->name}Factory"))) { + $factoryClass = DomainResolver::getDomainObjectNamespace($model->domain, 'factory', "{$model->name}Factory"); + if (class_exists($factoryClass)) { return $factoryClass; } diff --git a/src/LaravelDDDServiceProvider.php b/src/LaravelDDDServiceProvider.php index 106ec30..c35ff7b 100644 --- a/src/LaravelDDDServiceProvider.php +++ b/src/LaravelDDDServiceProvider.php @@ -27,6 +27,8 @@ public function configurePackage(Package $package): void ->hasConfigFile() ->hasCommands([ Commands\InstallCommand::class, + Commands\PublishCommand::class, + Commands\StubCommand::class, Commands\UpgradeCommand::class, Commands\OptimizeCommand::class, Commands\OptimizeClearCommand::class, @@ -61,12 +63,22 @@ public function configurePackage(Package $package): void Commands\Migration\DomainMigrateMakeCommand::class, ]); - if (app()->version() >= 11) { + if ($this->laravelVersion(11)) { $package->hasCommand(Commands\DomainClassMakeCommand::class); $package->hasCommand(Commands\DomainEnumMakeCommand::class); $package->hasCommand(Commands\DomainInterfaceMakeCommand::class); $package->hasCommand(Commands\DomainTraitMakeCommand::class); } + + // if ($this->laravelVersion('11.30.0')) { + // $package->hasCommand(Commands\PublishCommand::class); + // $package->hasCommand(Commands\StubCommand::class); + // } + } + + protected function laravelVersion($value) + { + return version_compare(app()->version(), $value, '>='); } protected function registerMigrations() @@ -87,14 +99,14 @@ protected function registerMigrations() public function packageBooted() { $this->publishes([ - $this->package->basePath('/../stubs') => resource_path("stubs/{$this->package->shortName()}"), + $this->package->basePath('/../stubs') => $this->app->basePath("stubs/{$this->package->shortName()}"), ], "{$this->package->shortName()}-stubs"); if ($this->app->runningInConsole() && method_exists($this, 'optimizes')) { $this->optimizes( optimize: 'ddd:optimize', clear: 'ddd:clear', - key: 'ddd cache', + key: 'laravel-ddd', ); } } diff --git a/src/StubManager.php b/src/StubManager.php new file mode 100755 index 0000000..5844f40 --- /dev/null +++ b/src/StubManager.php @@ -0,0 +1,102 @@ +dddStubs(), + ...$this->frameworkStubs(), + ]; + } + + public function dddStubs() + { + return [ + realpath(__DIR__.'/../stubs/action.stub') => 'action.stub', + realpath(__DIR__.'/../stubs/dto.stub') => 'dto.stub', + realpath(__DIR__.'/../stubs/value-object.stub') => 'value-object.stub', + realpath(__DIR__.'/../stubs/view-model.stub') => 'view-model.stub', + realpath(__DIR__.'/../stubs/base-view-model.stub') => 'base-view-model.stub', + realpath(__DIR__.'/../stubs/factory.stub') => 'factory.stub', + ]; + } + + public function frameworkStubs() + { + $laravelStubCommand = new ReflectionClass(new StubPublishCommand); + + $dir = dirname($laravelStubCommand->getFileName()); + + $stubs = [ + $dir.'/stubs/cast.inbound.stub' => 'cast.inbound.stub', + $dir.'/stubs/cast.stub' => 'cast.stub', + $dir.'/stubs/class.stub' => 'class.stub', + $dir.'/stubs/class.invokable.stub' => 'class.invokable.stub', + $dir.'/stubs/console.stub' => 'console.stub', + $dir.'/stubs/enum.stub' => 'enum.stub', + $dir.'/stubs/enum.backed.stub' => 'enum.backed.stub', + $dir.'/stubs/event.stub' => 'event.stub', + $dir.'/stubs/job.queued.stub' => 'job.queued.stub', + $dir.'/stubs/job.stub' => 'job.stub', + $dir.'/stubs/listener.typed.queued.stub' => 'listener.typed.queued.stub', + $dir.'/stubs/listener.queued.stub' => 'listener.queued.stub', + $dir.'/stubs/listener.typed.stub' => 'listener.typed.stub', + $dir.'/stubs/listener.stub' => 'listener.stub', + $dir.'/stubs/mail.stub' => 'mail.stub', + $dir.'/stubs/markdown-mail.stub' => 'markdown-mail.stub', + $dir.'/stubs/markdown-notification.stub' => 'markdown-notification.stub', + $dir.'/stubs/model.pivot.stub' => 'model.pivot.stub', + $dir.'/stubs/model.stub' => 'model.stub', + $dir.'/stubs/notification.stub' => 'notification.stub', + $dir.'/stubs/observer.plain.stub' => 'observer.plain.stub', + $dir.'/stubs/observer.stub' => 'observer.stub', + // $dir . '/stubs/pest.stub' => 'pest.stub', + // $dir . '/stubs/pest.unit.stub' => 'pest.unit.stub', + $dir.'/stubs/policy.plain.stub' => 'policy.plain.stub', + $dir.'/stubs/policy.stub' => 'policy.stub', + $dir.'/stubs/provider.stub' => 'provider.stub', + $dir.'/stubs/request.stub' => 'request.stub', + $dir.'/stubs/resource.stub' => 'resource.stub', + $dir.'/stubs/resource-collection.stub' => 'resource-collection.stub', + $dir.'/stubs/rule.stub' => 'rule.stub', + $dir.'/stubs/scope.stub' => 'scope.stub', + // $dir.'/stubs/test.stub' => 'test.stub', + // $dir.'/stubs/test.unit.stub' => 'test.unit.stub', + $dir.'/stubs/trait.stub' => 'trait.stub', + $dir.'/stubs/view-component.stub' => 'view-component.stub', + // Factories will use a ddd-specific stub + // realpath($dir . '/../../Database/Console/Factories/stubs/factory.stub') => 'factory.stub', + realpath($dir.'/../../Database/Console/Seeds/stubs/seeder.stub') => 'seeder.stub', + realpath($dir.'/../../Database/Migrations/stubs/migration.create.stub') => 'migration.create.stub', + realpath($dir.'/../../Database/Migrations/stubs/migration.stub') => 'migration.stub', + realpath($dir.'/../../Database/Migrations/stubs/migration.update.stub') => 'migration.update.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.api.stub') => 'controller.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.invokable.stub') => 'controller.invokable.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.model.api.stub') => 'controller.model.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.model.stub') => 'controller.model.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.api.stub') => 'controller.nested.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.api.stub') => 'controller.nested.singleton.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.stub') => 'controller.nested.singleton.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.stub') => 'controller.nested.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.plain.stub') => 'controller.plain.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.singleton.api.stub') => 'controller.singleton.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.singleton.stub') => 'controller.singleton.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.stub') => 'controller.stub', + realpath($dir.'/../../Routing/Console/stubs/middleware.stub') => 'middleware.stub', + ]; + + // Some stubs are not available across all Laravel versions, + // so we'll just skip the files that don't exist. + return collect($stubs)->filter(function ($stub, $path) { + return file_exists($path); + })->all(); + } +} diff --git a/src/Support/Domain.php b/src/Support/Domain.php index 225fe61..e36fab8 100644 --- a/src/Support/Domain.php +++ b/src/Support/Domain.php @@ -140,6 +140,7 @@ public function object(string $type, string $name, bool $absolute = false): Doma $baseName = str($name)->replace($namespace, '') ->replace(['\\', '/'], '\\') ->trim('\\') + ->when($type === 'factory', fn ($name) => $name->finish('Factory')) ->toString(); $fullyQualifiedName = $namespace.'\\'.$baseName; diff --git a/stubs/action.php.stub b/stubs/action.stub similarity index 100% rename from stubs/action.php.stub rename to stubs/action.stub diff --git a/stubs/base-model.php.stub b/stubs/base-model.stub similarity index 100% rename from stubs/base-model.php.stub rename to stubs/base-model.stub diff --git a/stubs/base-view-model.php.stub b/stubs/base-view-model.stub similarity index 100% rename from stubs/base-view-model.php.stub rename to stubs/base-view-model.stub diff --git a/stubs/dto.php.stub b/stubs/dto.stub similarity index 100% rename from stubs/dto.php.stub rename to stubs/dto.stub diff --git a/stubs/factory.php.stub b/stubs/factory.stub similarity index 100% rename from stubs/factory.php.stub rename to stubs/factory.stub diff --git a/stubs/model.php.stub b/stubs/model.php.stub deleted file mode 100644 index e775c33..0000000 --- a/stubs/model.php.stub +++ /dev/null @@ -1,13 +0,0 @@ -cleanSlate(); + + $path = app()->configPath('ddd.php'); + + if (file_exists($path)) { + unlink($path); + } + + expect(file_exists($path))->toBeFalse(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + File::deleteDirectory($publishedStubFolder); + + assertDirectoryDoesNotExist($publishedStubFolder); +}); + +afterEach(function () { + $this->cleanSlate(); +}); + +it('can publish config using --config option', function () { + $path = app()->configPath('ddd.php'); + + $this + ->artisan('ddd:publish --config') + ->expectsOutputToContain('Publishing config...') + ->doesntExpectOutput('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); +}); + +it('can publish everything', function ($options) { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:publish', $options) + ->expectsOutputToContain('Publishing config...') + ->expectsOutputToContain('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); +})->with([ + '--all' => [['--all' => true]], + '--config --stubs' => [['--config' => true, '--stubs' => true]], +]); + +it('can publish config interactively', function () { + $path = app()->configPath('ddd.php'); + + $this + ->artisan('ddd:publish') + ->expectsQuestion('What should be published?', ['config']) + ->expectsOutputToContain('Publishing config...') + ->doesntExpectOutput('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); +}); + +it('can publish stubs interactively', function () { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:publish') + ->expectsQuestion('What should be published?', ['stubs']) + ->expectsOutputToContain('Publishing stubs...') + ->doesntExpectOutput('Publishing config...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeFalse(); + + assertDirectoryExists($publishedStubFolder); +}); + +it('can publish everything interactively', function () { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:publish') + ->expectsQuestion('What should be published?', ['config', 'stubs']) + ->expectsOutputToContain('Publishing config...') + ->expectsOutputToContain('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); +}); diff --git a/tests/Command/StubTest.php b/tests/Command/StubTest.php new file mode 100644 index 0000000..abdfbd3 --- /dev/null +++ b/tests/Command/StubTest.php @@ -0,0 +1,182 @@ +cleanSlate(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + File::deleteDirectory($publishedStubFolder); + + assertDirectoryDoesNotExist($publishedStubFolder); +}); + +afterEach(function () { + $this->cleanSlate(); +}); + +it('can publish all stubs using --all option', function () { + $this + ->artisan('ddd:stub --all') + ->doesntExpectOutput('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); + + expect(count($stubFiles))->toEqual(count([ + ...app('ddd')->stubs()->dddStubs(), + ...app('ddd')->stubs()->frameworkStubs(), + ])); +}); + +it('can publish all stubs interactively', function () { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:stub') + ->expectsQuestion('What do you want to do?', 'all') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeFalse(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); + + expect(count($stubFiles))->toEqual(count([ + ...app('ddd')->stubs()->dddStubs(), + ...app('ddd')->stubs()->frameworkStubs(), + ])); +}); + +it('can publish specific stubs using arguments', function ($stubsToPublish) { + $expectedStubFilenames = collect($stubsToPublish) + ->map(fn ($stub) => $stub.'.stub') + ->all(); + + $arguments = collect($stubsToPublish)->join(' '); + + $this + ->artisan("ddd:stub {$arguments}") + ->assertSuccessful() + ->execute(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toEqual(count($stubsToPublish)); + + foreach ($stubFiles as $file) { + expect($file->getFilename())->toBeIn($expectedStubFilenames); + } +})->with([ + 'model' => [['model']], + 'model/action/dto' => [['model', 'action', 'dto']], + 'model/model.pivot' => [['model', 'model.pivot']], + 'controller' => [['controller']], +]); + +it('can publish stubs using wildcard', function ($argument, $stubsToPublish) { + $expectedStubFilenames = collect($stubsToPublish) + ->map(fn ($stub) => $stub.'.stub') + ->all(); + + $this + ->artisan("ddd:stub {$argument}") + ->assertSuccessful() + ->execute(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toEqual(count($stubsToPublish)); + + foreach ($stubFiles as $file) { + expect($file->getFilename())->toBeIn($expectedStubFilenames); + } +})->with([ + 'model*' => ['model*', ['model', 'model.pivot']], + 'model.' => ['model.', ['model', 'model.pivot']], + 'policy.' => ['policy.', ['policy', 'policy.plain']], +]); + +it('can publish stubs using wildcard (laravel 11 stubs)', function ($argument, $stubsToPublish) { + $expectedStubFilenames = collect($stubsToPublish) + ->map(fn ($stub) => $stub.'.stub') + ->all(); + + $this + ->artisan("ddd:stub {$argument}") + ->assertSuccessful() + ->execute(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toEqual(count($stubsToPublish)); + + foreach ($stubFiles as $file) { + expect($file->getFilename())->toBeIn($expectedStubFilenames); + } +})->with([ + 'listener*' => ['listener*', ['listener', 'listener.typed', 'listener.queued', 'listener.typed.queued']], + 'listener.' => ['listener.', ['listener', 'listener.typed', 'listener.queued', 'listener.typed.queued']], + 'enum.' => ['enum.', ['enum', 'enum.backed']], +])->skipOnLaravelVersionsBelow(11); + +it('can publish specific stubs interactively', function () { + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryDoesNotExist($publishedStubFolder); + + $options = app('ddd')->stubs()->allStubs(); + + $matches = collect($options) + ->filter(fn ($stub, $path) => str($stub)->contains('model')) + ->all(); + + $this + ->artisan('ddd:stub') + ->expectsQuestion('What do you want to do?', 'some') + ->expectsSearch( + 'Which stub should be published?', + search: 'model', + answers: $matches, + answer: ['model.stub'] + ) + ->assertSuccessful() + ->execute(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toEqual(1); + + expect($stubFiles[0]->getFilename())->toEqual('model.stub'); +})->skip(fn () => Feature::PromptMultiSearchAssertion->missing(), 'Multi-search assertion not available'); diff --git a/tests/Factory/DomainFactoryTest.php b/tests/Factory/DomainFactoryTest.php index 7c44d46..82e6e54 100644 --- a/tests/Factory/DomainFactoryTest.php +++ b/tests/Factory/DomainFactoryTest.php @@ -1,9 +1,11 @@ setupTestApplication(); @@ -26,8 +28,15 @@ ]); it('can instantiate a domain model factory', function ($domainParameter, $modelName, $modelClass) { + $this->afterApplicationCreated(function () { + (new DomainAutoloader)->autoload(); + }); + + $this->setupTestApplication(); + Config::set('ddd.base_model', 'Lunarstorm\LaravelDDD\Models\DomainModel'); Artisan::call("ddd:model -f {$domainParameter}:{$modelName}"); + expect(class_exists($modelClass))->toBeTrue(); expect($modelClass::factory())->toBeInstanceOf(Factory::class); })->with([ diff --git a/tests/Fixtures/Enums/Feature.php b/tests/Fixtures/Enums/Feature.php index 9b82fbb..d43587c 100644 --- a/tests/Fixtures/Enums/Feature.php +++ b/tests/Fixtures/Enums/Feature.php @@ -6,8 +6,10 @@ enum Feature: string { case PromptForMissingInput = '9.49.0'; case IncludeFilepathInGeneratorCommandOutput = '9.32.0'; + case Laravel11 = '11.0.0'; case LaravelPromptsPackage = '10.17'; case LaravelPackageOptimizeCommands = '11.27.1'; + case PromptMultiSearchAssertion = '11.30.0'; public function exists(): bool { diff --git a/tests/Generator/ControllerMakeTest.php b/tests/Generator/ControllerMakeTest.php index 5aae081..e814913 100644 --- a/tests/Generator/ControllerMakeTest.php +++ b/tests/Generator/ControllerMakeTest.php @@ -2,9 +2,13 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\File; use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature; beforeEach(function () { + $this->cleanSlate(); + $this->setupTestApplication(); + Config::set('ddd.domain_path', 'src/Domain'); Config::set('ddd.domain_namespace', 'Domain'); @@ -13,8 +17,6 @@ 'namespace' => 'App\Modules', 'objects' => ['controller', 'request'], ]); - - $this->setupTestApplication(); }); it('can generate domain controller', function ($domainName, $controllerName, $relativePath, $expectedNamespace) { @@ -35,8 +37,15 @@ expect(file_exists($expectedPath))->toBeTrue(); - expect(file_get_contents($expectedPath)) + expect($contents = file_get_contents($expectedPath)) ->toContain("namespace {$expectedNamespace};"); + + if (Feature::Laravel11->exists()) { + // These assertions don't seem to pass on Laravel 10 + expect($contents) + ->toContain("use App\Http\Controllers\Controller;") + ->toContain('extends Controller'); + } })->with([ 'Invoicing:InvoiceController' => [ 'Invoicing', @@ -175,3 +184,95 @@ ], ], ]); + +it('does not extend base controller if base controller not found', function ($domainName, $controllerName, $relativePath, $expectedNamespace) { + $expectedPath = base_path($relativePath); + + if (file_exists($expectedPath)) { + unlink($expectedPath); + } + + expect(file_exists($expectedPath))->toBeFalse(); + + // Remove the base controller + $baseControllerPath = base_path('app/Http/Controllers/Controller.php'); + + if (file_exists($baseControllerPath)) { + unlink($baseControllerPath); + } + + expect(file_exists($baseControllerPath))->toBeFalse(); + + Artisan::call("ddd:controller {$domainName}:{$controllerName}"); + + expect(file_exists($expectedPath))->toBeTrue(); + + expect($contents = file_get_contents($expectedPath)) + ->toContain("namespace {$expectedNamespace};"); + + if (Feature::Laravel11->exists()) { + // These assertions don't seem to pass on Laravel 10 + expect($contents) + ->not->toContain("use App\Http\Controllers\Controller;") + ->not->toContain('extends Controller'); + } +})->with([ + 'Invoicing:InvoiceController' => [ + 'Invoicing', + 'InvoiceController', + 'app/Modules/Invoicing/Controllers/InvoiceController.php', + 'App\Modules\Invoicing\Controllers', + ], +]); + +it('does not attempt to extend base controller when using custom stubs', function ($domainName, $controllerName, $relativePath, $expectedNamespace, $stubFolder) { + $expectedPath = base_path($relativePath); + + if (file_exists($expectedPath)) { + unlink($expectedPath); + } + + expect(file_exists($expectedPath))->toBeFalse(); + + $baseControllerPath = app()->basePath('app/Http/Controllers/Controller.php'); + + expect(file_exists($baseControllerPath))->toBeTrue(); + + // Publish a custom controller.stub + $customStub = <<<'STUB' +basePath($stubFolder)); + file_put_contents(app()->basePath($stubFolder.'/controller.plain.stub'), $customStub); + expect(file_exists(app()->basePath($stubFolder.'/controller.plain.stub')))->toBeTrue(); + + Artisan::call("ddd:controller {$domainName}:{$controllerName}"); + + expect(file_exists($expectedPath))->toBeTrue(); + + expect(file_get_contents($expectedPath)) + ->toContain("namespace {$expectedNamespace};") + ->toContain('use CustomControllerTrait;') + ->not->toContain("use App\Http\Controllers\Controller;") + ->not->toContain('extends Controller'); + + $this->cleanStubs(); +})->with([ + 'Invoicing:InvoiceController' => [ + 'Invoicing', + 'InvoiceController', + 'app/Modules/Invoicing/Controllers/InvoiceController.php', + 'App\Modules\Invoicing\Controllers', + ], +])->with([ + 'stubs', + 'stubs/ddd', +]); diff --git a/tests/Generator/Model/MakeTest.php b/tests/Generator/Model/MakeTest.php index 4b841a5..5367043 100644 --- a/tests/Generator/Model/MakeTest.php +++ b/tests/Generator/Model/MakeTest.php @@ -2,9 +2,15 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\File; use Lunarstorm\LaravelDDD\Support\Domain; use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature; +beforeEach(function () { + $this->cleanSlate(); + $this->setupTestApplication(); +}); + it('can generate domain models', function ($domainPath, $domainRoot) { Config::set('ddd.domain_path', $domainPath); Config::set('ddd.domain_namespace', $domainRoot); @@ -211,3 +217,53 @@ ['Illuminate\Database\Eloquent\NonExistentModel', 'NonExistentModel'], ['OtherVendor\OtherPackage\Models\NonExistentModel', 'NonExistentModel'], ]); + +it('does not attempt to extend custom base models when using custom stubs', function ($baseModelClass, $baseModelName, $stubFolder) { + Config::set('ddd.base_model', $baseModelClass); + + $domain = 'Fruits'; + $modelName = 'Lemon'; + + $expectedModelPath = base_path(implode('/', [ + config('ddd.domain_path'), + $domain, + config('ddd.namespaces.model'), + "{$modelName}.php", + ])); + + if (file_exists($expectedModelPath)) { + unlink($expectedModelPath); + } + + // Publish a custom stub + $customStub = <<<'STUB' +basePath($stubFolder)); + file_put_contents(app()->basePath($stubFolder.'/model.stub'), $customStub); + expect(file_exists(app()->basePath($stubFolder.'/model.stub')))->toBeTrue(); + + Artisan::call("ddd:model {$domain}:{$modelName}"); + + expect(file_exists($expectedModelPath))->toBeTrue(); + + expect(file_get_contents($expectedModelPath)) + ->toContain('use CustomModelTrait;') + ->not->toContain("use {$baseModelClass};") + ->not->toContain("extends {$baseModelName}"); + + $this->cleanStubs(); +})->with([ + ['Domain\Shared\Models\BaseModel', 'BaseModel'], +])->with([ + 'stubs', + 'stubs/ddd', +]); diff --git a/tests/Generator/Model/MakeWithControllerTest.php b/tests/Generator/Model/MakeWithControllerTest.php index 629933f..c5f1077 100644 --- a/tests/Generator/Model/MakeWithControllerTest.php +++ b/tests/Generator/Model/MakeWithControllerTest.php @@ -75,16 +75,4 @@ 'app/Modules/Reporting/Internal/Controllers/ReportSubmissionController.php', ], ], - - // '--controller --api' => [ - // ['--controller' => true, '--api' => true], - // 'RecordController', - // 'app/Http/Controllers/Invoicing/RecordController.php', - // ], - - // '--controller --requests' => [ - // ['--controller' => true, '--requests' => true], - // 'RecordController', - // 'app/Http/Controllers/Invoicing/RecordController.php', - // ], ]); diff --git a/tests/Pest.php b/tests/Pest.php index 1691c65..d31d21e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -9,7 +9,7 @@ function skipOnLaravelVersionsBelow($minimumVersion) $version = app()->version(); if (version_compare($version, $minimumVersion, '<')) { - test()->markTestSkipped("Only relevant from Laravel {$minimumVersion} onwards (Current version: {$version})."); + test()->markTestSkipped("Only available on Laravel {$minimumVersion}+ (Current version: {$version})."); } } diff --git a/tests/Setup/PublishTest.php b/tests/Setup/PublishTest.php index ac0410e..69858bc 100644 --- a/tests/Setup/PublishTest.php +++ b/tests/Setup/PublishTest.php @@ -20,7 +20,7 @@ }); it('can publish stubs', function () { - $dir = resource_path('stubs/ddd'); + $dir = base_path('stubs/ddd'); if (File::exists($dir)) { File::deleteDirectory($dir); diff --git a/tests/TestCase.php b/tests/TestCase.php index b04ef5a..f2baed7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -136,6 +136,8 @@ protected function composerReload() (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) ->setTimeout(null) ->run(function ($type, $output) {}); + + return $this; } protected function cleanSlate() @@ -148,13 +150,24 @@ protected function cleanSlate() File::cleanDirectory(base_path('database/factories')); File::deleteDirectory(resource_path('stubs/ddd')); + File::deleteDirectory(base_path('stubs')); File::deleteDirectory(base_path('Custom')); File::deleteDirectory(base_path('src/Domain')); File::deleteDirectory(base_path('src/Domains')); File::deleteDirectory(base_path('src/App')); + File::deleteDirectory(app_path('Modules')); File::deleteDirectory(app_path('Models')); File::deleteDirectory(base_path('bootstrap/cache/ddd')); + + return $this; + } + + protected function cleanStubs() + { + File::cleanDirectory(base_path('stubs')); + + return $this; } protected function setupTestApplication() @@ -166,6 +179,8 @@ protected function setupTestApplication() File::ensureDirectoryExists(app_path('Models')); $this->setDomainPathInComposer('Domain', 'src/Domain'); + + return $this; } protected function setDomainPathInComposer($domainNamespace, $domainPath, bool $reload = true)