├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config ├── ddd.php └── ddd.php.stub ├── database └── factories │ └── ModelFactory.php ├── resources └── views │ └── .gitkeep ├── routes └── testing.php ├── src ├── Commands │ ├── Concerns │ │ ├── ForwardsToDomainCommands.php │ │ ├── HandleHooks.php │ │ ├── HasDomainStubs.php │ │ ├── HasGeneratorBlueprint.php │ │ ├── InteractsWithStubs.php │ │ ├── QualifiesDomainModels.php │ │ └── ResolvesDomainFromInput.php │ ├── ConfigCommand.php │ ├── DomainActionMakeCommand.php │ ├── DomainBaseModelMakeCommand.php │ ├── DomainBaseViewModelMakeCommand.php │ ├── DomainCastMakeCommand.php │ ├── DomainChannelMakeCommand.php │ ├── DomainClassMakeCommand.php │ ├── DomainConsoleMakeCommand.php │ ├── DomainControllerMakeCommand.php │ ├── DomainDtoMakeCommand.php │ ├── DomainEnumMakeCommand.php │ ├── DomainEventMakeCommand.php │ ├── DomainExceptionMakeCommand.php │ ├── DomainFactoryMakeCommand.php │ ├── DomainGeneratorCommand.php │ ├── DomainInterfaceMakeCommand.php │ ├── DomainJobMakeCommand.php │ ├── DomainListCommand.php │ ├── DomainListenerMakeCommand.php │ ├── DomainMailMakeCommand.php │ ├── DomainMiddlewareMakeCommand.php │ ├── DomainModelMakeCommand.php │ ├── DomainNotificationMakeCommand.php │ ├── DomainObserverMakeCommand.php │ ├── DomainPolicyMakeCommand.php │ ├── DomainProviderMakeCommand.php │ ├── DomainRequestMakeCommand.php │ ├── DomainResourceMakeCommand.php │ ├── DomainRuleMakeCommand.php │ ├── DomainScopeMakeCommand.php │ ├── DomainSeederMakeCommand.php │ ├── DomainTraitMakeCommand.php │ ├── DomainValueObjectMakeCommand.php │ ├── DomainViewModelMakeCommand.php │ ├── InstallCommand.php │ ├── Migration │ │ ├── BaseMigrateMakeCommand.php │ │ └── DomainMigrateMakeCommand.php │ ├── OptimizeClearCommand.php │ ├── OptimizeCommand.php │ ├── PublishCommand.php │ ├── StubCommand.php │ └── UpgradeCommand.php ├── ComposerManager.php ├── ConfigManager.php ├── DomainManager.php ├── Enums │ └── LayerType.php ├── Facades │ ├── Autoload.php │ └── DDD.php ├── Factories │ ├── DomainFactory.php │ └── HasDomainFactory.php ├── LaravelDDDServiceProvider.php ├── Listeners │ └── CacheClearSubscriber.php ├── Models │ └── DomainModel.php ├── StubManager.php ├── Support │ ├── AutoloadManager.php │ ├── Domain.php │ ├── DomainCache.php │ ├── DomainMigration.php │ ├── DomainResolver.php │ ├── GeneratorBlueprint.php │ ├── Layer.php │ └── Path.php └── ValueObjects │ ├── CommandContext.php │ ├── DomainNamespaces.php │ ├── DomainObject.php │ ├── DomainObjectNamespace.php │ └── ObjectSchema.php └── stubs ├── action.stub ├── base-model.stub ├── base-view-model.stub ├── dto.stub ├── factory.stub ├── value-object.stub └── view-model.stub /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-ddd` will be documented in this file. 4 | 5 | ## [2.0.0] - 2025-03-02 6 | ### Chore 7 | - Add support for Laravel 12. 8 | - Minimum PHP version is now 8.2. 9 | - Minimum Laravel 11 version is now 11.44. 10 | - Drop support for Laravel 10. 11 | 12 | ## [1.2.1] - 2025-02-17 13 | ### Fixed 14 | - Ensure deeply-nested subdomains specified in dot notation are normalized to slashes when generating the objects into their destination path. 15 | 16 | ### Chore 17 | - Bump dependencies. 18 | 19 | ## [1.2.0] - 2024-11-23 20 | ### Breaking 21 | - Stubs are now published to `base_path('stubs/ddd')` instead of `resource_path('stubs/ddd')`. In other words, they are now co-located alongside the framework's published stubs, within a ddd subfolder. 22 | - Published stubs now use `.stub` extension instead of `.php.stub` (following Laravel's convention). 23 | - If you are using published stubs from pre 1.2, you will need to refactor your stubs accordingly. 24 | 25 | ### Added 26 | - Support for the Application Layer, to generate domain-specific objects that don't belong directly in the domain layer: 27 | ```php 28 | // In config/ddd.php 29 | 'application_path' => 'app/Modules', 30 | 'application_namespace' => 'App\Modules', 31 | 'application_objects' => [ 32 | 'controller', 33 | 'request', 34 | 'middleware', 35 | ], 36 | ``` 37 | - Support for Custom Layers, additional top-level namespaces of your choosing, such as `Infrastructure`, `Integrations`, etc.: 38 | ```php 39 | // In config/ddd.php 40 | 'layers' => [ 41 | 'Infrastructure' => 'src/Infrastructure', 42 | ], 43 | ``` 44 | - Added config utility command `ddd:config` to help manage the package's configuration over time. 45 | - Added stub utility command `ddd:stub` to publish one or more stubs selectively. 46 | - Added `ddd:controller` to generate domain-specific controllers. 47 | - Added `ddd:request` to generate domain-spefic requests. 48 | - Added `ddd:middleware` to generate domain-specific middleware. 49 | - Added `ddd:migration` to generate domain migrations. 50 | - Added `ddd:seeder` to generate domain seeders. 51 | - Added `ddd:stub` to manage stubs. 52 | - Migration folders across domains will be registered and scanned when running `php artisan migrate`, in addition to the standard application `database/migrations` path. 53 | - Ability to customize generator object naming conventions with your own logic using `DDD::resolveObjectSchemaUsing()`. 54 | 55 | ### Changed 56 | - `ddd:model` now extends Laravel's native `make:model` and inherits all standard options: 57 | - `--migration|-m` 58 | - `--factory|-f` 59 | - `--seed|-s` 60 | - `--controller --resource --requests|-crR` 61 | - `--policy` 62 | - `-mfsc` 63 | - `--all|-a` 64 | - `--pivot|-p` 65 | - `ddd:cache` is now `ddd:optimize` (`ddd:cache` is still available as an alias). 66 | - Since Laravel 11.27.1, the framework's `optimize` and `optimize:clear` commands will automatically invoke `ddd:optimize` (`ddd:cache`) and `ddd:clear` respectively. 67 | 68 | ### Deprecated 69 | - Domain base models are no longer required by default, and `config('ddd.base_model')` is now `null` by default. 70 | - Stubs are no longer published via `php artisan vendor:publish --tag="ddd-stubs"`. Instead, use `php artisan ddd:stub` to manage them. 71 | 72 | ## [1.1.3] - 2024-11-05 73 | ### Chore 74 | - Allow `laravel/prompts` dependency to use latest version when possible. 75 | 76 | ## [1.1.2] - 2024-09-02 77 | ### Fixed 78 | - During domain factory autoloading, ensure that `guessFactoryNamesUsing` returns a string when a domain factory is resolved. 79 | - Resolve issues with failing tests caused by mutations to `composer.json` that weren't rolled back. 80 | 81 | ## [1.1.1] - 2024-04-17 82 | ### Added 83 | - Ability to ignore folders during autoloading via `config('ddd.autoload_ignore')`, or register a custom filter callback via `DDD::filterAutoloadPathsUsing(callable $filter)`. 84 | ```php 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Autoload Ignore Folders 88 | |-------------------------------------------------------------------------- 89 | | 90 | | Folders that should be skipped during autoloading discovery, 91 | | relative to the root of each domain. 92 | | 93 | | e.g., src/Domain/Invoicing/ 94 | | 95 | | If more advanced filtering is needed, a callback can be registered 96 | | using `DDD::filterAutoloadPathsUsing(callback $filter)` in 97 | | the AppServiceProvider's boot method. 98 | | 99 | */ 100 | 'autoload_ignore' => [ 101 | 'Tests', 102 | 'Database/Migrations', 103 | ], 104 | ``` 105 | 106 | ### Changed 107 | - Internals: Domain cache is no longer quietly cleared on laravel's `cache:clearing` event, so that `ddd:cache` yields consistent results no matter which order it runs in production (before or after `cache:clear` or `optimize:clear` commands). 108 | 109 | ## [1.1.0] - 2024-04-07 110 | ### Added 111 | - Add `ddd:class` generator extending Laravel's `make:class` (Laravel 11 only). 112 | - Add `ddd:interface` generator extending Laravel's `make:interface` (Laravel 11 only). 113 | - Add `ddd:trait` generator extending Laravel's `make:trait` (Laravel 11 only). 114 | - Allow overriding configured namespaces at runtime by specifying an absolute name starting with /: 115 | ```bash 116 | # The usual: generate a provider in the configured provider namespace 117 | php artisan ddd:provider Invoicing:InvoiceServiceProvider 118 | # -> Domain\Invoicing\Providers\InvoiceServiceProvider 119 | 120 | # Override the configured namespace at runtime 121 | php artisan ddd:provider Invoicing:/InvoiceServiceProvider 122 | # -> Domain\Invoicing\InvoiceServiceProvider 123 | 124 | # Generate an event inside the Models namespace (hypothetical) 125 | php artisan ddd:event Invoicing:/Models/EventDoesNotBelongHere 126 | # -> Domain\Invoicing\Models\EventDoesNotBelongHere 127 | 128 | # Deep nesting is supported 129 | php artisan ddd:exception Invoicing:/Models/Exceptions/InvoiceNotFoundException 130 | # -> Domain\Invoicing\Models\Exceptions\InvoiceNotFoundException 131 | ``` 132 | 133 | ### Fixed 134 | - Internals: Handle a variety of additional edge cases when generating base models and base view models. 135 | 136 | ## [1.0.0] - 2024-03-31 137 | ### Added 138 | - `ddd:list` to show a summary of current domains in the domain folder. 139 | - For all generator commands, if a domain isn't specified, prompt for it with auto-completion suggestions based on the contents of the root domain folder. 140 | - Command aliases for some generators: 141 | - Data Transfer Object: `ddd:dto`, `ddd:data`, `ddd:data-transfer-object`, `ddd:datatransferobject` 142 | - Value Object: `ddd:value`, `ddd:valueobject`, `ddd:value-object` 143 | - View Model: `ddd:view-model`, `ddd:viewmodel` 144 | - Additional generators that extend Laravel's generators and funnel the generated objects into the domain layer: 145 | - `ddd:cast {domain}:{name}` 146 | - `ddd:channel {domain}:{name}` 147 | - `ddd:command {domain}:{name}` 148 | - `ddd:enum {domain}:{name}` (Laravel 11 only) 149 | - `ddd:event {domain}:{name}` 150 | - `ddd:exception {domain}:{name}` 151 | - `ddd:job {domain}:{name}` 152 | - `ddd:listener {domain}:{name}` 153 | - `ddd:mail {domain}:{name}` 154 | - `ddd:notification {domain}:{name}` 155 | - `ddd:observer {domain}:{name}` 156 | - `ddd:policy {domain}:{name}` 157 | - `ddd:provider {domain}:{name}` 158 | - `ddd:resource {domain}:{name}` 159 | - `ddd:rule {domain}:{name}` 160 | - `ddd:scope {domain}:{name}` 161 | - Support for autoloading and discovery of domain service providers, commands, policies, and factories. 162 | 163 | ### Changed 164 | - (BREAKING) For applications that published the config prior to this release, config should be removed, re-published, and re-configured. 165 | - (BREAKING) Generator commands no longer receive a domain argument. Instead of `ddd:action Invoicing CreateInvoice`, one of the following would be used: 166 | - Using the --domain option: `ddd:action CreateInvoice --domain=Invoicing` (this takes precedence). 167 | - Shorthand syntax: `ddd:action Invoicing:CreateInvoice`. 168 | - Or simply `ddd:action CreateInvoice` to be prompted for the domain afterwards. 169 | - Improved the reliability of generating base view models when `ddd.base_view_model` is something other than the default `Domain\Shared\ViewModels\ViewModel`. 170 | - Domain factories are now generated inside the domain layer under the configured factory namespace `ddd.namespaces.factory` (default `Database\Factories`). Factories located in `/database/factories//*` (v0.x) will continue to work as a fallback when attempting to resolve a domain model's factory. 171 | - Minimum supported Laravel version is now 10.25. 172 | 173 | ### Chore 174 | - Dropped Laravel 9 support. 175 | 176 | ## [0.10.0] - 2024-03-23 177 | ### Added 178 | - Add `ddd.domain_path` and `ddd.domain_namespace` to config, to specify the path to the domain layer and root domain namespace more explicitly (replaces the previous `ddd.paths.domains` config). 179 | - Implement `Lunarstorm\LaravelDDD\Factories\HasDomainFactory` trait which can be used on domain models that are unable to extend the base domain model. 180 | 181 | ### Changed 182 | - Default `base-model.php.stub` now utilizes the `HasDomainFactory` trait. 183 | 184 | ### Deprecated 185 | - Config `ddd.paths.domains` deprecated in favour of `ddd.domain_path` and `ddd.domain_namespace`. Existing config files published prior to this release should remove `ddd.paths.domains` and add `ddd.domain_path` and `ddd.domain_namespace` accordingly. 186 | 187 | ## [0.9.0] - 2024-03-11 188 | ### Changed 189 | - Internals: normalize generator file paths using `DIRECTORY_SEPARATOR` for consistency across different operating systems when it comes to console output and test expectations. 190 | 191 | ### Chore 192 | - Add Laravel 11 support. 193 | - Add PHP 8.3 support. 194 | 195 | ## [0.8.1] - 2023-12-05 196 | ### Chore 197 | - Update dependencies. 198 | 199 | ## [0.8.0] - 2023-11-12 200 | ### Changed 201 | - Implement proper support for custom base models when using `ddd:model`: 202 | - If the configured `ddd.base_model` exists (evaluated using `class_exists`), base model generation is skipped. 203 | - If `ddd.base_model` does not exist and falls under a domain namespace, base model will be generated. 204 | - Falling under a domain namespace means `Domain\**\Models\**`. 205 | - If `ddd.base_model` were set to `App\Models\NonExistentModel` or `Illuminate\Database\Eloquent\NonExistentModel`, they fall outside of the domain namespace and will not be generated for you. 206 | 207 | ### Fixed 208 | - Resolve long-standing issue where `ddd:model` would not properly detect whether the configured `ddd.base_model` already exists, leading to unintended results. 209 | 210 | ### Chore 211 | - Update composer dependencies. 212 | 213 | ### BREAKING 214 | - The default domain model stub `model.php.stub` has changed. If stubs were published prior to this release, you may have to delete and re-publish; unless the published `model.php.stub` has been entirely customized with independent logic for your respective application. 215 | 216 | ## [0.7.0] - 2023-10-22 217 | ### Added 218 | - Formal support for subdomains (nested domains). For example, to generate model `Domain\Reporting\Internal\Models\InvoiceReport`, the domain argument can be specified with dot notation: `ddd:model Reporting.Internal InvoiceReport`. Specifying `Reporting/Internal` or `Reporting\\Internal` will also be accepted and normalized to dot notation internally. 219 | - Implement abstract `Lunarstorm\LaravelDDD\Factories\DomainFactory` extension of `Illuminate\Database\Eloquent\Factories\Factory`: 220 | - Implements `DomainFactory::resolveFactoryName()` to resolve the corresponding factory for a domain model. 221 | - Will resolve the correct factory if the model belongs to a subdomain; `Domain\Reporting\Internal\Models\InvoiceReport` will correctly resolve to `Database\Factories\Reporting\Internal\InvoiceReportFactory`. 222 | 223 | ### Changed 224 | - Default base model implementation in `base-model.php.stub` now uses `DomainFactory::factoryForModel()` inside the `newFactory` method to resolve the model factory. 225 | 226 | ### BREAKING 227 | - For existing installations of the package to support sub-domain model factories, the base model's `newFactory()` should be updated where applicable; see `base-model.php.stub`. 228 | ```php 229 | use Lunarstorm\LaravelDDD\Factories\DomainFactory; 230 | 231 | // ... 232 | 233 | protected static function newFactory() 234 | { 235 | return DomainFactory::factoryForModel(get_called_class()); 236 | } 237 | ``` 238 | 239 | ## [0.6.1] - 2023-08-14 240 | ### Fixed 241 | - Ensure generated domain factories set the `protected $model` property. 242 | - Ensure generated factory classes are always suffixed by `Factory`. 243 | 244 | ## [0.6.0] - 2023-08-14 245 | ### Added 246 | - Ability to generate domain model factories, in a few ways: 247 | - `ddd:factory Invoicing InvoiceFactory` 248 | - `ddd:model Invoicing Invoice --factory` 249 | - `ddd:model Invoicing Invoice -f` 250 | - `ddd:model -f` (if relying on prompts) 251 | 252 | ## [0.5.1] - 2023-06-27 253 | ### Changed 254 | - Clean up default stubs; get rid of extraneous ellipses in comment blocks and ensure code style is consistent. 255 | 256 | ### Fixed 257 | - Ensure generator commands show a nicely sanitized path to generated file in the console output (previously, double slashes were present). Only applies to Laravel 9.32.0 onwards, when file paths were added to the console output. 258 | 259 | ### Chore 260 | - Upgrade test suite to use Pest 2.x. 261 | 262 | ## [0.5.0] - 2023-06-14 263 | ### Added 264 | - Ability to generate actions (`ddd:action`), which by default generates an action class based on the `lorisleiva/laravel-actions` package. 265 | 266 | ### Changed 267 | - Minor cleanups and updates to the default `ddd.php` config file. 268 | - Update stubs to be less opinionated where possible. 269 | 270 | ## [0.4.0] - 2023-05-08 271 | ### Changed 272 | - Update argument definitions across generator commands to play nicely with `PromptsForMissingInput` behaviour introduced in Laravel v9.49.0. 273 | 274 | ### Fixed 275 | - Ensure the configured domain path and namespace is respected by `ddd:base-model` and `ddd:base-view-model`. 276 | 277 | ## [0.3.0] - 2023-03-23 278 | ### Added 279 | - Increase test coverage to ensure expected namespaces are present in generated objects. 280 | 281 | ### Changed 282 | - Domain generator commands will infer the root domain namespace based on the configured `ddd.paths.domains`. 283 | - Change the default domain path in config to `src/Domain` (was previously `src/Domains`), thereby generating objects with the singular `Domain` root namespace. 284 | 285 | ## [0.2.0] - 2023-03-20 286 | ### Added 287 | - Support for Laravel 10. 288 | 289 | ### Changed 290 | - Install command now publishes config, registers the default domain path in composer.json, and prompts to publish stubs. 291 | - Generator command signatures simplified to `ddd:*` (previously `ddd:make:*`). 292 | 293 | ### Fixed 294 | - When ViewModels are generated, base view model import statement is now properly substituted. 295 | 296 | ## [0.1.0] - 2023-01-19 297 | ### Added 298 | - Early version of generator commands for domain models, dto, value objects, view models. 299 | - `ddd:install` command to automatically register the domain folder inside the application's composer.json (experimental) 300 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) lunarstorm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Domain Driven Design Toolkit for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/lunarstorm/laravel-ddd.svg?style=flat-square)](https://packagist.org/packages/lunarstorm/laravel-ddd) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/lunarstorm/laravel-ddd/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/lunarstorm/laravel-ddd/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/lunarstorm/laravel-ddd/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/lunarstorm/laravel-ddd/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/lunarstorm/laravel-ddd.svg?style=flat-square)](https://packagist.org/packages/lunarstorm/laravel-ddd) 7 | 8 | Laravel-DDD is a toolkit to support domain driven design (DDD) in Laravel applications. One of the pain points when adopting DDD is the inability to use Laravel's native `make` commands to generate objects outside the `App\*` namespace. This package aims to fill the gaps by providing equivalent commands such as `ddd:model`, `ddd:dto`, `ddd:view-model` and many more. 9 | 10 | ## Installation 11 | You can install the package via composer: 12 | 13 | ```bash 14 | composer require lunarstorm/laravel-ddd 15 | ``` 16 | 17 | You may initialize the package using the `ddd:install` artisan command. This will publish the [config file](#config-file), register the domain path in your project's composer.json psr-4 autoload configuration on your behalf, and allow you to publish generator stubs for customization if needed. 18 | ```bash 19 | php artisan ddd:install 20 | ``` 21 | 22 | ### Configuration 23 | For first-time installations, a config wizard is available to populate the `ddd.php` config file interactively: 24 | ```bash 25 | php artisan ddd:config wizard 26 | ``` 27 | For existing installations with a config file published from a previous version, you may use the `ddd:config update` command to rebuild and merge it with the latest package copy: 28 | ```bash 29 | php artisan ddd:config update 30 | ``` 31 | See [Configuration Utility](#config-utility) for details about other available options. 32 | 33 | ### Peer Dependencies 34 | The following additional packages are suggested (but not required) while working with this package. 35 | - Data Transfer Objects: [spatie/laravel-data](https://github.com/spatie/laravel-data) 36 | - Actions: [lorisleiva/laravel-actions](https://github.com/lorisleiva/laravel-actions) 37 | 38 | The default DTO and Action stubs of this package reference classes from these packages. If this doesn't apply to your application, you may [publish and customize the stubs](#customizing-stubs) accordingly. 39 | 40 | ### Deployment 41 | In production, ensure you run `php artisan ddd:optimize` during the deployment process to [optimize autoloading](#autoloading-in-production). If you already run `php artisan optimize` in production, this will be handled automatically. 42 | 43 | ### Version Compatibility 44 | Laravel | LaravelDDD | | 45 | :---------------|:-----------|:-------------------------------------------------------------------------------------| 46 | 9.x - 10.24.x | 0.x | **[0.x README](https://github.com/lunarstorm/laravel-ddd/blob/v0.10.0/README.md)** | 47 | 10.25.x | 1.x | 48 | 11.x | 1.x | 49 | 11.44.x | 2.x | 50 | 12.x | 2.x | 51 | 52 | See **[UPGRADING](UPGRADING.md)** for more details about upgrading across different versions. 53 | 54 | 55 | 56 | ## Usage 57 | ### Syntax 58 | All domain generator commands use the following syntax: 59 | ```bash 60 | # Specifying the domain as an option 61 | php artisan ddd:{object} {name} --domain={domain} 62 | 63 | # Specifying the domain as part of the name (short-hand syntax) 64 | php artisan ddd:{object} {domain}:{name} 65 | 66 | # Not specifying the domain at all, which will then 67 | # prompt for it (with auto-completion) 68 | php artisan ddd:{object} {name} 69 | ``` 70 | 71 | ## Available Commands 72 | ### Generators 73 | The following generators are currently available: 74 | | Command | Description | Usage | 75 | |---|---|---| 76 | | `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`
| 77 | | `ddd:factory` | Generate a domain factory | `php artisan ddd:factory Invoicing:InvoiceFactory` | 78 | | `ddd:dto` | Generate a data transfer object | `php artisan ddd:dto Invoicing:LineItemPayload` | 79 | | `ddd:value` | Generate a value object | `php artisan ddd:value Shared:DollarAmount` | 80 | | `ddd:view-model` | Generate a view model | `php artisan ddd:view-model Invoicing:ShowInvoiceViewModel` | 81 | | `ddd:action` | Generate an action | `php artisan ddd:action Invoicing:SendInvoiceToCustomer` | 82 | | `ddd:cast` | Generate a cast | `php artisan ddd:cast Invoicing:MoneyCast` | 83 | | `ddd:channel` | Generate a channel | `php artisan ddd:channel Invoicing:InvoiceChannel` | 84 | | `ddd:command` | Generate a command | `php artisan ddd:command Invoicing:InvoiceDeliver` | 85 | | `ddd:controller` | Generate a controller | `php artisan ddd:controller Invoicing:InvoiceController`

Options: inherits options from *make:controller* | 86 | | `ddd:event` | Generate an event | `php artisan ddd:event Invoicing:PaymentWasReceived` | 87 | | `ddd:exception` | Generate an exception | `php artisan ddd:exception Invoicing:InvoiceNotFoundException` | 88 | | `ddd:job` | Generate a job | `php artisan ddd:job Invoicing:GenerateInvoicePdf` | 89 | | `ddd:listener` | Generate a listener | `php artisan ddd:listener Invoicing:HandlePaymentReceived` | 90 | | `ddd:mail` | Generate a mail | `php artisan ddd:mail Invoicing:OverduePaymentReminderEmail` | 91 | | `ddd:middleware` | Generate a middleware | `php artisan ddd:middleware Invoicing:VerifiedCustomerMiddleware` | 92 | | `ddd:migration` | Generate a migration | `php artisan ddd:migration Invoicing:CreateInvoicesTable` | 93 | | `ddd:notification` | Generate a notification | `php artisan ddd:notification Invoicing:YourPaymentWasReceived` | 94 | | `ddd:observer` | Generate an observer | `php artisan ddd:observer Invoicing:InvoiceObserver` | 95 | | `ddd:policy` | Generate a policy | `php artisan ddd:policy Invoicing:InvoicePolicy` | 96 | | `ddd:provider` | Generate a provider | `php artisan ddd:provider Invoicing:InvoiceServiceProvider` | 97 | | `ddd:resource` | Generate a resource | `php artisan ddd:resource Invoicing:InvoiceResource` | 98 | | `ddd:rule` | Generate a rule | `php artisan ddd:rule Invoicing:ValidPaymentMethod` | 99 | | `ddd:request` | Generate a form request | `php artisan ddd:request Invoicing:StoreInvoiceRequest` | 100 | | `ddd:scope` | Generate a scope | `php artisan ddd:scope Invoicing:ArchivedInvoicesScope` | 101 | | `ddd:seeder` | Generate a seeder | `php artisan ddd:seeder Invoicing:InvoiceSeeder` | 102 | | `ddd:class` | Generate a class | `php artisan ddd:class Invoicing:Support/InvoiceBuilder` | 103 | | `ddd:enum` | Generate an enum | `php artisan ddd:enum Customer:CustomerType` | 104 | | `ddd:interface` | Generate an interface | `php artisan ddd:interface Customer:Contracts/Invoiceable` | 105 | | `ddd:trait` | Generate a trait | `php artisan ddd:trait Customer:Concerns/HasInvoices` | 106 | 107 | Generated objects will be placed in the appropriate domain namespace as specified by `ddd.namespaces.*` in the [config file](#config-file). 108 | 109 | 110 | 111 | ### Config Utility (Since 1.2) 112 | A configuration utility was introduced in 1.2 to help manage the package's configuration over time. 113 | ```bash 114 | php artisan ddd:config 115 | ``` 116 | Output: 117 | ``` 118 | ┌ Laravel-DDD Config Utility ──────────────────────────────────┐ 119 | │ › ● Run the configuration wizard │ 120 | │ ○ Rebuild and merge ddd.php with latest package copy │ 121 | │ ○ Detect domain namespace from composer.json │ 122 | │ ○ Sync composer.json from ddd.php │ 123 | │ ○ Exit │ 124 | └──────────────────────────────────────────────────────────────┘ 125 | ``` 126 | These config tasks are also invokeable directly using arguments: 127 | ```bash 128 | # Run the configuration wizard 129 | php artisan ddd:config wizard 130 | 131 | # Rebuild and merge ddd.php with latest package copy 132 | php artisan ddd:config update 133 | 134 | # Detect domain namespace from composer.json 135 | php artisan ddd:config detect 136 | 137 | # Sync composer.json from ddd.php 138 | php artisan ddd:config composer 139 | ``` 140 | 141 | ### Other Commands 142 | ```bash 143 | # Show a summary of current domains in the domain folder 144 | php artisan ddd:list 145 | 146 | # Cache domain manifests (used for autoloading) 147 | php artisan ddd:optimize 148 | 149 | # Clear the domain cache 150 | php artisan ddd:clear 151 | ``` 152 | 153 | ## Advanced Usage 154 | ### Application Layer (since 1.2) 155 | 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. 156 | ```php 157 | // In config/ddd.php 158 | 'application_path' => 'app/Modules', 159 | 'application_namespace' => 'App\Modules', 160 | 'application_objects' => [ 161 | 'controller', 162 | 'request', 163 | 'middleware', 164 | ], 165 | ``` 166 | The configuration above will result in the following: 167 | ```bash 168 | ddd:model Invoicing:Invoice --controller --resource --requests 169 | ``` 170 | Output: 171 | ``` 172 | ├─ app 173 | | └─ Modules 174 | │ └─ Invoicing 175 | │ ├─ Controllers 176 | │ │ └─ InvoiceController.php 177 | │ └─ Requests 178 | │ ├─ StoreInvoiceRequest.php 179 | │ └─ UpdateInvoiceRequest.php 180 | ├─ src/Domain 181 | └─ Invoicing 182 | └─ Models 183 | └─ Invoice.php 184 | ``` 185 | 186 | ### Custom Layers (since 1.2) 187 | Often times, additional top-level namespaces are needed to hold shared components, helpers, and things that are not domain-specific. A common example is the `Infrastructure` layer. You may configure these additional layers in the `ddd.layers` array. 188 | ```php 189 | // In config/ddd.php 190 | 'layers' => [ 191 | 'Infrastructure' => 'src/Infrastructure', 192 | ], 193 | ``` 194 | The configuration above will result in the following: 195 | ```bash 196 | ddd:model Invoicing:Invoice 197 | ddd:trait Infrastructure:Concerns/HasExpiryDate 198 | ``` 199 | Output: 200 | ``` 201 | ├─ src/Domain 202 | | └─ Invoicing 203 | | └─ Models 204 | | └─ Invoice.php 205 | ├─ src/Infrastructure 206 | └─ Concerns 207 | └─ HasExpiryDate.php 208 | ``` 209 | After defining new layers in `ddd.php`, make sure the corresponding namespaces are also registered in your `composer.json` file. You may use the `ddd:config` helper command to handle this for you. 210 | ```bash 211 | # Sync composer.json with ddd.php 212 | php artisan ddd:config composer 213 | ``` 214 | 215 | ### Nested Objects 216 | For any `ddd:*` generator command, nested objects can be specified with forward slashes. 217 | ```bash 218 | php artisan ddd:model Invoicing:Payment/Transaction 219 | # -> Domain\Invoicing\Models\Payment\Transaction 220 | 221 | php artisan ddd:action Invoicing:Payment/ProcessTransaction 222 | # -> Domain\Invoicing\Actions\Payment\ProcessTransaction 223 | 224 | php artisan ddd:exception Invoicing:Payment/PaymentFailedException 225 | # -> Domain\Invoicing\Exceptions\Payment\PaymentFailedException 226 | ``` 227 | This is essential for objects without a fixed namespace such as `class`, `interface`, `trait`, 228 | each of which have a blank namespace by default. In other words, these objects originate 229 | from the root of the domain. 230 | ```bash 231 | php artisan ddd:class Invoicing:Support/InvoiceBuilder 232 | # -> Domain\Invoicing\Support\InvoiceBuilder 233 | 234 | php artisan ddd:interface Invoicing:Contracts/PayableByCreditCard 235 | # -> Domain\Invoicing\Contracts\PayableByCreditCard 236 | 237 | php artisan ddd:interface Invoicing:Models/Concerns/HasLineItems 238 | # -> Domain\Invoicing\Models\Concerns\HasLineItems 239 | ``` 240 | 241 | ### Subdomains (nested domains) 242 | Subdomains can be specified with dot notation wherever a domain option is accepted. 243 | ```bash 244 | # Domain/Reporting/Internal/ViewModels/MonthlyInvoicesReportViewModel 245 | php artisan ddd:view-model Reporting.Internal:MonthlyInvoicesReportViewModel 246 | 247 | # Domain/Reporting/Customer/ViewModels/MonthlyInvoicesReportViewModel 248 | php artisan ddd:view-model Reporting.Customer:MonthlyInvoicesReportViewModel 249 | 250 | # (supported by all commands where a domain option is accepted) 251 | ``` 252 | 253 | ### Overriding Configured Namespaces at Runtime 254 | If for some reason you need to generate a domain object under a namespace different to what is configured in `ddd.namespaces.*`, 255 | you may do so using an absolute name starting with `/`. This will generate the object from the root of the domain. 256 | ```bash 257 | # The usual: generate a provider in the configured provider namespace 258 | php artisan ddd:provider Invoicing:InvoiceServiceProvider 259 | # -> Domain\Invoicing\Providers\InvoiceServiceProvider 260 | 261 | # Override the configured namespace at runtime 262 | php artisan ddd:provider Invoicing:/InvoiceServiceProvider 263 | # -> Domain\Invoicing\InvoiceServiceProvider 264 | 265 | # Generate an event inside the Models namespace (hypothetical) 266 | php artisan ddd:event Invoicing:/Models/EventDoesNotBelongHere 267 | # -> Domain\Invoicing\Models\EventDoesNotBelongHere 268 | 269 | # Deep nesting is supported 270 | php artisan ddd:exception Invoicing:/Models/Exceptions/InvoiceNotFoundException 271 | # -> Domain\Invoicing\Models\Exceptions\InvoiceNotFoundException 272 | ``` 273 | 274 | ### Custom Object Resolution 275 | If you require advanced customization of generated object naming conventions, you may register a custom resolver using `DDD::resolveObjectSchemaUsing()` in your AppServiceProvider's boot method: 276 | ```php 277 | use Lunarstorm\LaravelDDD\Facades\DDD; 278 | use Lunarstorm\LaravelDDD\ValueObjects\CommandContext; 279 | use Lunarstorm\LaravelDDD\ValueObjects\ObjectSchema; 280 | 281 | DDD::resolveObjectSchemaUsing(function (string $domainName, string $nameInput, string $type, CommandContext $command): ?ObjectSchema { 282 | if ($type === 'controller' && $command->option('api')) { 283 | return new ObjectSchema( 284 | name: $name = str($nameInput)->replaceEnd('Controller', '')->finish('ApiController')->toString(), 285 | namespace: "App\\Api\\Controllers\\{$domainName}", 286 | fullyQualifiedName: "App\\Api\\Controllers\\{$domainName}\\{$name}", 287 | path: "src/App/Api/Controllers/{$domainName}/{$name}.php", 288 | ); 289 | } 290 | 291 | // Return null to fall back to the default 292 | return null; 293 | }); 294 | ``` 295 | The example above will result in the following: 296 | ```bash 297 | php artisan ddd:controller Invoicing:PaymentController --api 298 | # Controller [src/App/Api/Controllers/Invoicing/PaymentApiController.php] created successfully. 299 | ``` 300 | 301 | 302 | 303 | ## Customizing Stubs 304 | This package ships with a few ddd-specific stubs, while the rest are pulled from the framework. For a quick reference of available stubs and their source, you may use the `ddd:stub --list` command: 305 | ```bash 306 | php artisan ddd:stub --list 307 | ``` 308 | 309 | ### Stub Priority 310 | When generating objects using `ddd:*`, stubs are prioritized as follows: 311 | - Try `stubs/ddd/*.stub` (customized for `ddd:*` only) 312 | - Try `stubs/*.stub` (shared by both `make:*` and `ddd:*`) 313 | - Fallback to the package or framework default 314 | 315 | ### Publishing Stubs 316 | To publish stubs interactively, you may use the `ddd:stub` command: 317 | ```bash 318 | php artisan ddd:stub 319 | ``` 320 | ``` 321 | ┌ What do you want to do? ─────────────────────────────────────┐ 322 | │ › ● Choose stubs to publish │ 323 | │ ○ Publish all stubs │ 324 | └──────────────────────────────────────────────────────────────┘ 325 | 326 | ┌ Which stub should be published? ─────────────────────────────┐ 327 | │ policy │ 328 | ├──────────────────────────────────────────────────────────────┤ 329 | │ › ◼ policy.plain.stub │ 330 | │ ◻ policy.stub │ 331 | └────────────────────────────────────────────────── 1 selected ┘ 332 | Use the space bar to select options. 333 | ``` 334 | You may also use shortcuts to skip the interactive steps: 335 | ```bash 336 | # Publish all stubs 337 | php artisan ddd:stub --all 338 | 339 | # Publish one or more stubs specified as arguments (see ddd:stub --list) 340 | php artisan ddd:stub model 341 | php artisan ddd:stub model dto action 342 | php artisan ddd:stub controller controller.plain controller.api 343 | 344 | # Options: 345 | 346 | # Publish and overwrite only the files that have already been published 347 | php artisan ddd:stub ... --existing 348 | 349 | # Overwrite any existing files 350 | php artisan ddd:stub ... --force 351 | ``` 352 | To publish multiple stubs with common prefixes at once, use `*` or `.` as a wildcard ending to indicate "stubs that starts with": 353 | ```bash 354 | php artisan ddd:stub listener. 355 | ``` 356 | Output: 357 | ```bash 358 | Publishing /stubs/ddd/listener.typed.queued.stub 359 | Publishing /stubs/ddd/listener.queued.stub 360 | Publishing /stubs/ddd/listener.typed.stub 361 | Publishing /stubs/ddd/listener.stub 362 | ``` 363 | 364 | ## Domain Autoloading and Discovery 365 | Autoloading behaviour can be configured with the `ddd.autoload` configuration option. By default, domain providers, commands, policies, and factories are auto-discovered and registered. 366 | 367 | ```php 368 | 'autoload' => [ 369 | 'providers' => true, 370 | 'commands' => true, 371 | 'policies' => true, 372 | 'factories' => true, 373 | 'migrations' => true, 374 | ], 375 | ``` 376 | ### Service Providers 377 | When `ddd.autoload.providers` is enabled, any class within the domain layer extending `Illuminate\Support\ServiceProvider` will be auto-registered as a service provider. 378 | 379 | ### Console Commands 380 | When `ddd.autoload.commands` is enabled, any class within the domain layer extending `Illuminate\Console\Command` will be auto-registered as a command when running in console. 381 | 382 | ### Policies 383 | When `ddd.autoload.policies` is 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. If your application implements its own policy discovery using `Gate::guessPolicyNamesUsing()`, you should set `ddd.autoload.policies` to `false` to ensure it is not overridden. 384 | 385 | ### Factories 386 | When `ddd.autoload.factories` is 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. Note that this does not affect domain models using the `Lunarstorm\LaravelDDD\Factories\HasDomainFactory` trait. Where this is useful is with regular models in the domain layer that use the standard `Illuminate\Database\Eloquent\Factories\HasFactory` trait. 387 | 388 | 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. 389 | 390 | ### Migrations 391 | 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`. 392 | 393 | ### Ignoring Paths During Autoloading 394 | 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. 395 | ```php 396 | 'autoload_ignore' => [ 397 | 'Tests', 398 | 'Database/Migrations', 399 | ], 400 | ``` 401 | Note that ignoring folders only applies to class-based autoloading: Service Providers, Console Commands, Policies, and Factories. 402 | 403 | 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: 404 | ```php 405 | use Lunarstorm\LaravelDDD\Facades\DDD; 406 | use Symfony\Component\Finder\SplFileInfo; 407 | 408 | DDD::filterAutoloadPathsUsing(function (SplFileInfo $file) { 409 | if (basename($file->getRelativePathname()) === 'functions.php') { 410 | return false; 411 | } 412 | }); 413 | ``` 414 | The filter callback is based on Symfony's [Finder Component](https://symfony.com/doc/current/components/finder.html#custom-filtering). 415 | 416 | ### Disabling Autoloading 417 | You may disable autoloading by setting the respective autoload options to `false` in the configuration file as needed, or by commenting out the autoload configuration entirely. 418 | ```php 419 | // 'autoload' => [ 420 | // 'providers' => true, 421 | // 'commands' => true, 422 | // 'policies' => true, 423 | // 'factories' => true, 424 | // 'migrations' => true, 425 | // ], 426 | ``` 427 | 428 | 429 | 430 | ## Autoloading in Production 431 | 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. If you are already running `php artisan optimize`, `ddd:optimize` will be included within that pipeline. The framework's `optimize` and `optimize:clear` commands will automatically invoke `ddd:optimize` and `ddd:clear` respectively. 432 | 433 | 434 | 435 | ## Configuration File 436 | This is the content of the published config file (`ddd.php`): 437 | 438 | ```php 439 | return [ 440 | 441 | /* 442 | |-------------------------------------------------------------------------- 443 | | Domain Layer 444 | |-------------------------------------------------------------------------- 445 | | 446 | | The path and namespace of the domain layer. 447 | | 448 | */ 449 | 'domain_path' => 'src/Domain', 450 | 'domain_namespace' => 'Domain', 451 | 452 | /* 453 | |-------------------------------------------------------------------------- 454 | | Application Layer 455 | |-------------------------------------------------------------------------- 456 | | 457 | | The path and namespace of the application layer, and the objects 458 | | that should be recognized as part of the application layer. 459 | | 460 | */ 461 | 'application_path' => 'app/Modules', 462 | 'application_namespace' => 'App\Modules', 463 | 'application_objects' => [ 464 | 'controller', 465 | 'request', 466 | 'middleware', 467 | ], 468 | 469 | /* 470 | |-------------------------------------------------------------------------- 471 | | Custom Layers 472 | |-------------------------------------------------------------------------- 473 | | 474 | | Additional top-level namespaces and paths that should be recognized as 475 | | layers when generating ddd:* objects. 476 | | 477 | | e.g., 'Infrastructure' => 'src/Infrastructure', 478 | | 479 | */ 480 | 'layers' => [ 481 | 'Infrastructure' => 'src/Infrastructure', 482 | // 'Integrations' => 'src/Integrations', 483 | // 'Support' => 'src/Support', 484 | ], 485 | 486 | /* 487 | |-------------------------------------------------------------------------- 488 | | Object Namespaces 489 | |-------------------------------------------------------------------------- 490 | | 491 | | This value contains the default namespaces of ddd:* generated 492 | | objects relative to the layer of which the object belongs to. 493 | | 494 | */ 495 | 'namespaces' => [ 496 | 'model' => 'Models', 497 | 'data_transfer_object' => 'Data', 498 | 'view_model' => 'ViewModels', 499 | 'value_object' => 'ValueObjects', 500 | 'action' => 'Actions', 501 | 'cast' => 'Casts', 502 | 'class' => '', 503 | 'channel' => 'Channels', 504 | 'command' => 'Commands', 505 | 'controller' => 'Controllers', 506 | 'enum' => 'Enums', 507 | 'event' => 'Events', 508 | 'exception' => 'Exceptions', 509 | 'factory' => 'Database\Factories', 510 | 'interface' => '', 511 | 'job' => 'Jobs', 512 | 'listener' => 'Listeners', 513 | 'mail' => 'Mail', 514 | 'middleware' => 'Middleware', 515 | 'migration' => 'Database\Migrations', 516 | 'notification' => 'Notifications', 517 | 'observer' => 'Observers', 518 | 'policy' => 'Policies', 519 | 'provider' => 'Providers', 520 | 'resource' => 'Resources', 521 | 'request' => 'Requests', 522 | 'rule' => 'Rules', 523 | 'scope' => 'Scopes', 524 | 'seeder' => 'Database\Seeders', 525 | 'trait' => '', 526 | ], 527 | 528 | /* 529 | |-------------------------------------------------------------------------- 530 | | Base Model 531 | |-------------------------------------------------------------------------- 532 | | 533 | | The base model class which generated domain models should extend. If 534 | | set to null, the generated models will extend Laravel's default. 535 | | 536 | */ 537 | 'base_model' => null, 538 | 539 | /* 540 | |-------------------------------------------------------------------------- 541 | | Base DTO 542 | |-------------------------------------------------------------------------- 543 | | 544 | | The base class which generated data transfer objects should extend. By 545 | | default, generated DTOs will extend `Spatie\LaravelData\Data` from 546 | | Spatie's Laravel-data package, a highly recommended data object 547 | | package to work with. 548 | | 549 | */ 550 | 'base_dto' => 'Spatie\LaravelData\Data', 551 | 552 | /* 553 | |-------------------------------------------------------------------------- 554 | | Base ViewModel 555 | |-------------------------------------------------------------------------- 556 | | 557 | | The base class which generated view models should extend. By default, 558 | | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`, 559 | | which will be created if it doesn't already exist. 560 | | 561 | */ 562 | 'base_view_model' => 'Domain\Shared\ViewModels\ViewModel', 563 | 564 | /* 565 | |-------------------------------------------------------------------------- 566 | | Base Action 567 | |-------------------------------------------------------------------------- 568 | | 569 | | The base class which generated action objects should extend. By default, 570 | | generated actions are based on the `lorisleiva/laravel-actions` package 571 | | and do not extend anything. 572 | | 573 | */ 574 | 'base_action' => null, 575 | 576 | /* 577 | |-------------------------------------------------------------------------- 578 | | Autoloading 579 | |-------------------------------------------------------------------------- 580 | | 581 | | Configure whether domain providers, commands, policies, factories, 582 | | and migrations should be auto-discovered and registered. 583 | | 584 | */ 585 | 'autoload' => [ 586 | 'providers' => true, 587 | 'commands' => true, 588 | 'policies' => true, 589 | 'factories' => true, 590 | 'migrations' => true, 591 | ], 592 | 593 | /* 594 | |-------------------------------------------------------------------------- 595 | | Autoload Ignore Folders 596 | |-------------------------------------------------------------------------- 597 | | 598 | | Folders that should be skipped during autoloading discovery, 599 | | relative to the root of each domain. 600 | | 601 | | e.g., src/Domain/Invoicing/ 602 | | 603 | | If more advanced filtering is needed, a callback can be registered 604 | | using `DDD::filterAutoloadPathsUsing(callback $filter)` in 605 | | the AppServiceProvider's boot method. 606 | | 607 | */ 608 | 'autoload_ignore' => [ 609 | 'Tests', 610 | 'Database/Migrations', 611 | ], 612 | 613 | /* 614 | |-------------------------------------------------------------------------- 615 | | Caching 616 | |-------------------------------------------------------------------------- 617 | | 618 | | The folder where the domain cache files will be stored. Used for domain 619 | | autoloading. 620 | | 621 | */ 622 | 'cache_directory' => 'bootstrap/cache/ddd', 623 | ]; 624 | ``` 625 | 626 | ## Testing 627 | 628 | ```bash 629 | composer test 630 | ``` 631 | 632 | ## Changelog 633 | 634 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 635 | 636 | ## Security Vulnerabilities 637 | 638 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 639 | 640 | ## Credits 641 | 642 | - [Jasper Tey](https://github.com/JasperTey) 643 | - [All Contributors](../../contributors) 644 | 645 | ## License 646 | 647 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 648 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lunarstorm/laravel-ddd", 3 | "description": "A Laravel toolkit for Domain Driven Design patterns", 4 | "keywords": [ 5 | "lunarstorm", 6 | "laravel", 7 | "laravel-ddd", 8 | "ddd", 9 | "domain driven design" 10 | ], 11 | "homepage": "https://github.com/lunarstorm/laravel-ddd", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Jasper Tey", 16 | "email": "jasper@lunarstorm.ca", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.2|^8.3|^8.4", 22 | "illuminate/contracts": "^11.44|^12.0", 23 | "laravel/pint": "^1.21", 24 | "laravel/prompts": "^0.3.1", 25 | "lorisleiva/lody": "^0.6", 26 | "spatie/laravel-package-tools": "^1.19.0", 27 | "symfony/var-exporter": "^7.1" 28 | }, 29 | "require-dev": { 30 | "larastan/larastan": "^2.0.1|^3.0", 31 | "nunomaduro/collision": "^8.6", 32 | "orchestra/testbench": "^9.11|^10.0", 33 | "pestphp/pest": "^3.0", 34 | "pestphp/pest-plugin-laravel": "^3.0", 35 | "phpstan/extension-installer": "^1.1", 36 | "phpstan/phpstan-deprecation-rules": "^2.0", 37 | "phpstan/phpstan-phpunit": "^2.0", 38 | "spatie/laravel-data": "^4.11.1", 39 | "lorisleiva/laravel-actions": "^2.9.0" 40 | }, 41 | "suggest": { 42 | "spatie/laravel-data": "Recommended for Data Transfer Objects.", 43 | "lorisleiva/laravel-actions": "Recommended for Actions." 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Lunarstorm\\LaravelDDD\\": "src", 48 | "Lunarstorm\\LaravelDDD\\Database\\Factories\\": "database/factories" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "Lunarstorm\\LaravelDDD\\Tests\\": "tests", 54 | "App\\": "vendor/orchestra/testbench-core/laravel/app", 55 | "Database\\Factories\\": "vendor/orchestra/testbench-core/laravel/database/factories", 56 | "Database\\Seeders\\": "vendor/orchestra/testbench-core/laravel/database/seeders", 57 | "Domain\\": "vendor/orchestra/testbench-core/laravel/src/Domain", 58 | "Application\\": "vendor/orchestra/testbench-core/laravel/src/Application", 59 | "Infrastructure\\": "vendor/orchestra/testbench-core/laravel/src/Infrastructure" 60 | } 61 | }, 62 | "scripts": { 63 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 64 | "analyse": "vendor/bin/phpstan analyse", 65 | "test": "@composer dump-autoload && vendor/bin/pest", 66 | "test-coverage": "@composer dump-autoload && vendor/bin/pest --coverage", 67 | "purge-skeleton": "vendor/bin/testbench package:purge-skeleton", 68 | "format": "vendor/bin/pint", 69 | "lint": "vendor/bin/pint" 70 | }, 71 | "config": { 72 | "sort-packages": true, 73 | "allow-plugins": { 74 | "pestphp/pest-plugin": true, 75 | "phpstan/extension-installer": true 76 | } 77 | }, 78 | "extra": { 79 | "laravel": { 80 | "providers": [ 81 | "Lunarstorm\\LaravelDDD\\LaravelDDDServiceProvider" 82 | ], 83 | "aliases": { 84 | "DDD": "Lunarstorm\\LaravelDDD\\Facades\\DDD" 85 | } 86 | } 87 | }, 88 | "minimum-stability": "dev", 89 | "prefer-stable": true 90 | } 91 | -------------------------------------------------------------------------------- /config/ddd.php: -------------------------------------------------------------------------------- 1 | 'src/Domain', 14 | 'domain_namespace' => 'Domain', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Application Layer 19 | |-------------------------------------------------------------------------- 20 | | 21 | | The path and namespace of the application layer, and the objects 22 | | that should be recognized as part of the application layer. 23 | | 24 | */ 25 | 'application_path' => 'app/Modules', 26 | 'application_namespace' => 'App\Modules', 27 | 'application_objects' => [ 28 | 'controller', 29 | 'request', 30 | 'middleware', 31 | ], 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Custom Layers 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Additional top-level namespaces and paths that should be recognized as 39 | | layers when generating ddd:* objects. 40 | | 41 | | e.g., 'Infrastructure' => 'src/Infrastructure', 42 | | 43 | */ 44 | 'layers' => [ 45 | 'Infrastructure' => 'src/Infrastructure', 46 | // 'Integrations' => 'src/Integrations', 47 | // 'Support' => 'src/Support', 48 | ], 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Object Namespaces 53 | |-------------------------------------------------------------------------- 54 | | 55 | | This value contains the default namespaces of ddd:* generated 56 | | objects relative to the layer of which the object belongs to. 57 | | 58 | */ 59 | 'namespaces' => [ 60 | 'model' => 'Models', 61 | 'data_transfer_object' => 'Data', 62 | 'view_model' => 'ViewModels', 63 | 'value_object' => 'ValueObjects', 64 | 'action' => 'Actions', 65 | 'cast' => 'Casts', 66 | 'class' => '', 67 | 'channel' => 'Channels', 68 | 'command' => 'Commands', 69 | 'controller' => 'Controllers', 70 | 'enum' => 'Enums', 71 | 'event' => 'Events', 72 | 'exception' => 'Exceptions', 73 | 'factory' => 'Database\Factories', 74 | 'interface' => '', 75 | 'job' => 'Jobs', 76 | 'listener' => 'Listeners', 77 | 'mail' => 'Mail', 78 | 'middleware' => 'Middleware', 79 | 'migration' => 'Database\Migrations', 80 | 'notification' => 'Notifications', 81 | 'observer' => 'Observers', 82 | 'policy' => 'Policies', 83 | 'provider' => 'Providers', 84 | 'resource' => 'Resources', 85 | 'request' => 'Requests', 86 | 'rule' => 'Rules', 87 | 'scope' => 'Scopes', 88 | 'seeder' => 'Database\Seeders', 89 | 'trait' => '', 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Base Model 95 | |-------------------------------------------------------------------------- 96 | | 97 | | The base model class which generated domain models should extend. If 98 | | set to null, the generated models will extend Laravel's default. 99 | | 100 | */ 101 | 'base_model' => null, 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Base DTO 106 | |-------------------------------------------------------------------------- 107 | | 108 | | The base class which generated data transfer objects should extend. By 109 | | default, generated DTOs will extend `Spatie\LaravelData\Data` from 110 | | Spatie's Laravel-data package, a highly recommended data object 111 | | package to work with. 112 | | 113 | */ 114 | 'base_dto' => 'Spatie\LaravelData\Data', 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Base ViewModel 119 | |-------------------------------------------------------------------------- 120 | | 121 | | The base class which generated view models should extend. By default, 122 | | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`, 123 | | which will be created if it doesn't already exist. 124 | | 125 | */ 126 | 'base_view_model' => 'Domain\Shared\ViewModels\ViewModel', 127 | 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | Base Action 131 | |-------------------------------------------------------------------------- 132 | | 133 | | The base class which generated action objects should extend. By default, 134 | | generated actions are based on the `lorisleiva/laravel-actions` package 135 | | and do not extend anything. 136 | | 137 | */ 138 | 'base_action' => null, 139 | 140 | /* 141 | |-------------------------------------------------------------------------- 142 | | Autoloading 143 | |-------------------------------------------------------------------------- 144 | | 145 | | Configure whether domain providers, commands, policies, factories, 146 | | and migrations should be auto-discovered and registered. 147 | | 148 | */ 149 | 'autoload' => [ 150 | 'providers' => true, 151 | 'commands' => true, 152 | 'policies' => true, 153 | 'factories' => true, 154 | 'migrations' => true, 155 | ], 156 | 157 | /* 158 | |-------------------------------------------------------------------------- 159 | | Autoload Ignore Folders 160 | |-------------------------------------------------------------------------- 161 | | 162 | | Folders that should be skipped during autoloading discovery, 163 | | relative to the root of each domain. 164 | | 165 | | e.g., src/Domain/Invoicing/ 166 | | 167 | | If more advanced filtering is needed, a callback can be registered 168 | | using `DDD::filterAutoloadPathsUsing(callback $filter)` in 169 | | the AppServiceProvider's boot method. 170 | | 171 | */ 172 | 'autoload_ignore' => [ 173 | 'Tests', 174 | 'Database/Migrations', 175 | ], 176 | 177 | /* 178 | |-------------------------------------------------------------------------- 179 | | Caching 180 | |-------------------------------------------------------------------------- 181 | | 182 | | The folder where the domain cache files will be stored. Used for domain 183 | | autoloading. 184 | | 185 | */ 186 | 'cache_directory' => 'bootstrap/cache/ddd', 187 | ]; 188 | -------------------------------------------------------------------------------- /config/ddd.php.stub: -------------------------------------------------------------------------------- 1 | {{domain_path}}, 14 | 'domain_namespace' => {{domain_namespace}}, 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Application Layer 19 | |-------------------------------------------------------------------------- 20 | | 21 | | The path and namespace of the application layer, and the objects 22 | | that should be recognized as part of the application layer. 23 | | 24 | */ 25 | 'application_path' => {{application_path}}, 26 | 'application_namespace' => {{application_namespace}}, 27 | 'application_objects' => {{application_objects}}, 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Custom Layers 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Additional top-level namespaces and paths that should be recognized as 35 | | layers when generating ddd:* objects. 36 | | 37 | | e.g., 'Infrastructure' => 'src/Infrastructure', 38 | | 39 | */ 40 | 'layers' => {{layers}}, 41 | 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Object Namespaces 46 | |-------------------------------------------------------------------------- 47 | | 48 | | This value contains the default namespaces of ddd:* generated 49 | | objects relative to the layer of which the object belongs to. 50 | | 51 | */ 52 | 'namespaces' => {{namespaces}}, 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Base Model 57 | |-------------------------------------------------------------------------- 58 | | 59 | | The base class which generated domain models should extend. By default, 60 | | generated domain models will extend `Domain\Shared\Models\BaseModel`, 61 | | which will be created if it doesn't already exist. 62 | | 63 | */ 64 | 'base_model' => {{base_model}}, 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Base DTO 69 | |-------------------------------------------------------------------------- 70 | | 71 | | The base class which generated data transfer objects should extend. By 72 | | default, generated DTOs will extend `Spatie\LaravelData\Data` from 73 | | Spatie's Laravel-data package, a highly recommended data object 74 | | package to work with. 75 | | 76 | */ 77 | 'base_dto' => {{base_dto}}, 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Base ViewModel 82 | |-------------------------------------------------------------------------- 83 | | 84 | | The base class which generated view models should extend. By default, 85 | | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`, 86 | | which will be created if it doesn't already exist. 87 | | 88 | */ 89 | 'base_view_model' => {{base_view_model}}, 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Base Action 94 | |-------------------------------------------------------------------------- 95 | | 96 | | The base class which generated action objects should extend. By default, 97 | | generated actions are based on the `lorisleiva/laravel-actions` package 98 | | and do not extend anything. 99 | | 100 | */ 101 | 'base_action' => {{base_action}}, 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Autoloading 106 | |-------------------------------------------------------------------------- 107 | | 108 | | Configure whether domain providers, commands, policies, and factories 109 | | should be auto-discovered and registered. 110 | | 111 | */ 112 | 'autoload' => {{autoload}}, 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Autoload Ignore Folders 117 | |-------------------------------------------------------------------------- 118 | | 119 | | Folders that should be skipped during autoloading discovery, 120 | | relative to the root of each domain. 121 | | 122 | | e.g., src/Domain/Invoicing/ 123 | | 124 | | If more advanced filtering is needed, a callback can be registered 125 | | using `DDD::filterAutoloadPathsUsing(callback $filter)` in 126 | | the AppServiceProvider's boot method. 127 | | 128 | */ 129 | 'autoload_ignore' => {{autoload_ignore}}, 130 | 131 | /* 132 | |-------------------------------------------------------------------------- 133 | | Caching 134 | |-------------------------------------------------------------------------- 135 | | 136 | | The folder where the domain cache files will be stored. Used for domain 137 | | autoloading. 138 | | 139 | */ 140 | 'cache_directory' => {{cache_directory}}, 141 | ]; 142 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | middleware(['web']) 8 | ->as('ddd.') 9 | ->group(function () { 10 | Route::get('/', function () { 11 | return response('home'); 12 | })->name('home'); 13 | 14 | Route::get('/config', function () { 15 | return response(config('ddd')); 16 | })->name('config'); 17 | 18 | Route::get('/autoload', function () { 19 | return response([ 20 | 'providers' => Autoload::getRegisteredProviders(), 21 | 'commands' => Autoload::getRegisteredCommands(), 22 | ]); 23 | })->name('autoload'); 24 | }); 25 | -------------------------------------------------------------------------------- /src/Commands/Concerns/ForwardsToDomainCommands.php: -------------------------------------------------------------------------------- 1 | getNameInput(), '/') 12 | ? Str::beforeLast($this->getNameInput(), '/') 13 | : null; 14 | 15 | $nameWithSubfolder = $subfolder ? "{$subfolder}/{$arguments['name']}" : $arguments['name']; 16 | 17 | return match ($command) { 18 | 'make:request' => $this->runCommand('ddd:request', [ 19 | ...$arguments, 20 | 'name' => $nameWithSubfolder, 21 | '--domain' => $this->blueprint->domain->dotName, 22 | ], $this->output), 23 | 24 | 'make:model' => $this->runCommand('ddd:model', [ 25 | ...$arguments, 26 | 'name' => $nameWithSubfolder, 27 | '--domain' => $this->blueprint->domain->dotName, 28 | ], $this->output), 29 | 30 | 'make:factory' => $this->runCommand('ddd:factory', [ 31 | ...$arguments, 32 | 'name' => $nameWithSubfolder, 33 | '--domain' => $this->blueprint->domain->dotName, 34 | ], $this->output), 35 | 36 | 'make:policy' => $this->runCommand('ddd:policy', [ 37 | ...$arguments, 38 | 'name' => $nameWithSubfolder, 39 | '--domain' => $this->blueprint->domain->dotName, 40 | ], $this->output), 41 | 42 | 'make:migration' => $this->runCommand('ddd:migration', [ 43 | ...$arguments, 44 | '--domain' => $this->blueprint->domain->dotName, 45 | ], $this->output), 46 | 47 | 'make:seeder' => $this->runCommand('ddd:seeder', [ 48 | ...$arguments, 49 | 'name' => $nameWithSubfolder, 50 | '--domain' => $this->blueprint->domain->dotName, 51 | ], $this->output), 52 | 53 | 'make:controller' => $this->runCommand('ddd:controller', [ 54 | ...$arguments, 55 | 'name' => $nameWithSubfolder, 56 | '--domain' => $this->blueprint->domain->dotName, 57 | ], $this->output), 58 | 59 | default => $this->runCommand($command, $arguments, $this->output), 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Commands/Concerns/HandleHooks.php: -------------------------------------------------------------------------------- 1 | beforeHandle(); 25 | 26 | parent::handle(); 27 | 28 | $this->afterHandle(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Commands/Concerns/HasDomainStubs.php: -------------------------------------------------------------------------------- 1 | resolvePublishedDddStub($stub)) { 29 | $stub = $publishedStub; 30 | } 31 | 32 | $this->usingPublishedStub(str($stub)->startsWith(app()->basePath('stubs'))); 33 | 34 | return $stub; 35 | } 36 | 37 | protected function resolvePublishedDddStub($path) 38 | { 39 | $stubFilename = str($path) 40 | ->basename() 41 | ->ltrim('/\\') 42 | ->toString(); 43 | 44 | // Check if there is a user-published stub 45 | if (file_exists($publishedPath = app()->basePath('stubs/ddd/'.$stubFilename))) { 46 | return $publishedPath; 47 | } 48 | 49 | // Also check for legacy stub extensions 50 | if (file_exists($legacyPublishedPath = Str::replaceLast('.stub', '.php.stub', $publishedPath))) { 51 | return $legacyPublishedPath; 52 | } 53 | 54 | return null; 55 | } 56 | 57 | protected function resolveDddStubPath($path) 58 | { 59 | $path = str($path) 60 | ->basename() 61 | ->ltrim('/\\') 62 | ->toString(); 63 | 64 | if ($publishedPath = $this->resolvePublishedDddStub($path)) { 65 | return $publishedPath; 66 | } 67 | 68 | return DDD::packagePath('stubs/'.$path); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Commands/Concerns/HasGeneratorBlueprint.php: -------------------------------------------------------------------------------- 1 | preparePlaceholders(); 20 | 21 | foreach ($placeholders as $placeholder => $value) { 22 | $stub = $this->fillPlaceholder($stub, $placeholder, $value ?? ''); 23 | } 24 | 25 | return $stub; 26 | } 27 | 28 | protected function buildClass($name) 29 | { 30 | return $this->applyPlaceholders(parent::buildClass($name)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Commands/Concerns/QualifiesDomainModels.php: -------------------------------------------------------------------------------- 1 | blueprint->domain) { 10 | $domainModel = $domain->model($model); 11 | 12 | return $domainModel->fullyQualifiedName; 13 | } 14 | 15 | return parent::qualifyModel($model); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Commands/Concerns/ResolvesDomainFromInput.php: -------------------------------------------------------------------------------- 1 | blueprint->rootNamespace(); 32 | } 33 | 34 | protected function getDefaultNamespace($rootNamespace) 35 | { 36 | return $this->blueprint 37 | ? $this->blueprint->getDefaultNamespace($rootNamespace) 38 | : parent::getDefaultNamespace($rootNamespace); 39 | } 40 | 41 | protected function getPath($name) 42 | { 43 | return $this->blueprint 44 | ? $this->blueprint->getPath($name) 45 | : parent::getPath($name); 46 | } 47 | 48 | protected function qualifyClass($name) 49 | { 50 | return $this->blueprint->qualifyClass($name); 51 | } 52 | 53 | protected function promptForDomainName(): string 54 | { 55 | $choices = collect(DomainResolver::domainChoices()) 56 | ->mapWithKeys(fn ($name) => [Str::lower($name) => $name]); 57 | 58 | // Prompt for the domain 59 | $domainName = suggest( 60 | label: 'What is the domain?', 61 | options: fn ($value) => collect($choices) 62 | ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true)) 63 | ->toArray(), 64 | placeholder: 'Start typing to search...', 65 | required: true 66 | ); 67 | 68 | // Normalize the case of the domain name 69 | // if it is an existing domain. 70 | if ($match = $choices->get(Str::lower($domainName))) { 71 | $domainName = $match; 72 | } 73 | 74 | return $domainName; 75 | } 76 | 77 | protected function beforeHandle() 78 | { 79 | $nameInput = $this->getNameInput(); 80 | 81 | // If the name contains a domain prefix, extract it 82 | // and strip it from the name argument. 83 | $domainExtractedFromName = null; 84 | 85 | if (Str::contains($nameInput, ':')) { 86 | $domainExtractedFromName = Str::before($nameInput, ':'); 87 | $nameInput = Str::after($nameInput, ':'); 88 | } 89 | 90 | $domainName = match (true) { 91 | // Domain was specified explicitly via option (priority) 92 | filled($this->option('domain')) => $this->option('domain'), 93 | 94 | // Domain was specified as a prefix in the name 95 | filled($domainExtractedFromName) => $domainExtractedFromName, 96 | 97 | default => $this->promptForDomainName(), 98 | }; 99 | 100 | $this->blueprint = new GeneratorBlueprint( 101 | commandName: $this->getName(), 102 | nameInput: $nameInput, 103 | domainName: $domainName, 104 | arguments: $this->arguments(), 105 | options: $this->options(), 106 | ); 107 | 108 | $this->input->setArgument('name', $this->blueprint->nameInput); 109 | 110 | $this->input->setOption('domain', $this->blueprint->domainName); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Commands/ConfigCommand.php: -------------------------------------------------------------------------------- 1 | composer = DDD::composer()->usingOutput($this->output); 47 | 48 | $action = str($this->argument('action'))->trim()->lower()->toString(); 49 | 50 | if (! $action && $this->option('layer')) { 51 | $action = 'layers'; 52 | } 53 | 54 | return match ($action) { 55 | 'wizard' => $this->wizard(), 56 | 'update' => $this->update(), 57 | 'detect' => $this->detect(), 58 | 'composer' => $this->syncComposer(), 59 | 'layers' => $this->layers(), 60 | default => $this->home(), 61 | }; 62 | } 63 | 64 | protected function home(): int 65 | { 66 | $action = select('Laravel-DDD Config Utility', [ 67 | 'wizard' => 'Run the configuration wizard', 68 | 'update' => 'Rebuild and merge ddd.php with latest package version', 69 | 'detect' => 'Detect domain namespace from composer.json', 70 | 'composer' => 'Sync composer.json from ddd.php', 71 | 'exit' => 'Exit', 72 | ], scroll: 10); 73 | 74 | return match ($action) { 75 | 'wizard' => $this->wizard(), 76 | 'update' => $this->update(), 77 | 'detect' => $this->detect(), 78 | 'composer' => $this->syncComposer(), 79 | 'exit' => $this->exit(), 80 | default => $this->exit(), 81 | }; 82 | } 83 | 84 | protected function layers() 85 | { 86 | $layers = $this->option('layer'); 87 | 88 | if ($layers = $this->option('layer')) { 89 | foreach ($layers as $layer) { 90 | $parts = explode(':', $layer); 91 | 92 | $this->composer->registerPsr4Autoload( 93 | namespace: data_get($parts, 0), 94 | path: data_get($parts, 1) 95 | ); 96 | } 97 | 98 | $this->composer->saveAndReload(); 99 | } 100 | 101 | $this->info('Configuration updated.'); 102 | 103 | return self::SUCCESS; 104 | } 105 | 106 | public static function hasRequiredVersionOfLaravelPrompts(): bool 107 | { 108 | return function_exists('\Laravel\Prompts\form'); 109 | } 110 | 111 | protected function wizard(): int 112 | { 113 | if (! static::hasRequiredVersionOfLaravelPrompts()) { 114 | $this->error('This command is not supported with your currently installed version of Laravel Prompts.'); 115 | 116 | return self::FAILURE; 117 | } 118 | 119 | $namespaces = collect($this->composer->getPsr4Namespaces()); 120 | 121 | $layers = $namespaces->map(fn ($path, $namespace) => new Layer($namespace, $path)); 122 | $laravelAppLayer = $layers->first(fn (Layer $layer) => str($layer->namespace)->exactly('App')); 123 | $possibleDomainLayers = $layers->filter(fn (Layer $layer) => str($layer->namespace)->startsWith('Domain')); 124 | $possibleApplicationLayers = $layers->filter(fn (Layer $layer) => str($layer->namespace)->startsWith('App')); 125 | 126 | $domainLayer = $possibleDomainLayers->first(); 127 | $applicationLayer = $possibleApplicationLayers->first(); 128 | 129 | $detected = collect([ 130 | 'domain_path' => $domainLayer?->path, 131 | 'domain_namespace' => $domainLayer?->namespace, 132 | 'application_path' => $applicationLayer?->path, 133 | 'application_namespace' => $applicationLayer?->namespace, 134 | ]); 135 | 136 | $config = $detected->merge(Config::get('ddd')); 137 | 138 | info('Detected DDD configuration:'); 139 | 140 | table( 141 | headers: ['Key', 'Value'], 142 | rows: $detected->dot()->map(fn ($value, $key) => [$key, $value])->all() 143 | ); 144 | 145 | $choices = [ 146 | 'domain_path' => [ 147 | 'src/Domain' => 'src/Domain', 148 | 'src/Domains' => 'src/Domains', 149 | ...[ 150 | $config->get('domain_path') => $config->get('domain_path'), 151 | ], 152 | ...$possibleDomainLayers->mapWithKeys( 153 | fn (Layer $layer) => [$layer->path => $layer->path] 154 | ), 155 | ], 156 | 'domain_namespace' => [ 157 | 'Domain' => 'Domain', 158 | 'Domains' => 'Domains', 159 | ...[ 160 | $config->get('domain_namespace') => $config->get('domain_namespace'), 161 | ], 162 | ...$possibleDomainLayers->mapWithKeys( 163 | fn (Layer $layer) => [$layer->namespace => $layer->namespace] 164 | ), 165 | ], 166 | 'application_path' => [ 167 | 'app/Modules' => 'app/Modules', 168 | 'src/Modules' => 'src/Modules', 169 | 'Modules' => 'Modules', 170 | 'src/Application' => 'src/Application', 171 | 'Application' => 'Application', 172 | ...[ 173 | data_get($config, 'application_path') => data_get($config, 'application_path'), 174 | ], 175 | ...$possibleApplicationLayers->mapWithKeys( 176 | fn (Layer $layer) => [$layer->path => $layer->path] 177 | ), 178 | ], 179 | 'application_namespace' => [ 180 | 'App\Modules' => 'App\Modules', 181 | 'Application' => 'Application', 182 | 'Modules' => 'Modules', 183 | ...[ 184 | data_get($config, 'application_namespace') => data_get($config, 'application_namespace'), 185 | ], 186 | ...$possibleApplicationLayers->mapWithKeys( 187 | fn (Layer $layer) => [$layer->namespace => $layer->namespace] 188 | ), 189 | ], 190 | 'layers' => [ 191 | 'src/Infrastructure' => 'src/Infrastructure', 192 | 'src/Integrations' => 'src/Integrations', 193 | 'src/Support' => 'src/Support', 194 | ], 195 | ]; 196 | 197 | $form = form() 198 | ->add( 199 | function ($responses) use ($choices, $detected, $config) { 200 | return suggest( 201 | label: 'Domain Path', 202 | options: $choices['domain_path'], 203 | default: $detected->get('domain_path') ?: $config->get('domain_path'), 204 | hint: 'The path to the domain layer relative to the base path.', 205 | required: true, 206 | ); 207 | }, 208 | name: 'domain_path' 209 | ) 210 | ->add( 211 | function ($responses) use ($choices, $config) { 212 | return suggest( 213 | label: 'Domain Namespace', 214 | options: $choices['domain_namespace'], 215 | default: class_basename($responses['domain_path']) ?: $config->get('domain_namespace'), 216 | required: true, 217 | hint: 'The root domain namespace.', 218 | ); 219 | }, 220 | name: 'domain_namespace' 221 | ) 222 | ->add( 223 | function ($responses) use ($choices) { 224 | return suggest( 225 | label: 'Path to Application Layer', 226 | options: $choices['application_path'], 227 | hint: "For objects that don't belong in the domain layer (controllers, form requests, etc.)", 228 | placeholder: 'Leave blank to skip and use defaults', 229 | scroll: 10, 230 | ); 231 | }, 232 | name: 'application_path' 233 | ) 234 | ->addIf( 235 | fn ($responses) => filled($responses['application_path']), 236 | function ($responses) use ($choices, $laravelAppLayer) { 237 | $applicationPath = $responses['application_path']; 238 | $laravelAppPath = $laravelAppLayer->path; 239 | 240 | $namespace = match (true) { 241 | str($applicationPath)->exactly($laravelAppPath) => $laravelAppLayer->namespace, 242 | str($applicationPath)->startsWith("{$laravelAppPath}/") => str($applicationPath)->studly()->toString(), 243 | default => str($applicationPath)->classBasename()->studly()->toString(), 244 | }; 245 | 246 | return suggest( 247 | label: 'Application Layer Namespace', 248 | options: $choices['application_namespace'], 249 | default: $namespace, 250 | hint: 'The root application namespace.', 251 | placeholder: 'Leave blank to use defaults', 252 | ); 253 | }, 254 | name: 'application_namespace' 255 | ) 256 | ->add( 257 | function ($responses) use ($choices) { 258 | return multiselect( 259 | label: 'Custom Layers (Optional)', 260 | options: $choices['layers'], 261 | hint: 'Layers can be customized in the ddd.php config file at any time.', 262 | ); 263 | }, 264 | name: 'layers' 265 | ); 266 | 267 | $responses = $form->submit(); 268 | 269 | $this->info('Building configuration...'); 270 | 271 | foreach ($responses as $key => $value) { 272 | $responses[$key] = $value ?: $config->get($key); 273 | } 274 | 275 | DDD::config()->fill($responses)->save(); 276 | 277 | $this->info('Configuration updated: '.config_path('ddd.php')); 278 | 279 | return self::SUCCESS; 280 | } 281 | 282 | protected function detect(): int 283 | { 284 | $search = ['Domain', 'Domains']; 285 | 286 | $detected = []; 287 | 288 | foreach ($search as $namespace) { 289 | if ($path = $this->composer->getAutoloadPath($namespace)) { 290 | $detected['domain_path'] = $path; 291 | $detected['domain_namespace'] = $namespace; 292 | break; 293 | } 294 | } 295 | 296 | $this->info('Detected configuration:'); 297 | 298 | table( 299 | headers: ['Config', 'Value'], 300 | rows: collect($detected) 301 | ->map(fn ($value, $key) => [$key, $value]) 302 | ->all() 303 | ); 304 | 305 | if (confirm('Update configuration with these values?', true)) { 306 | DDD::config()->fill($detected)->save(); 307 | 308 | $this->info('Configuration updated: '.config_path('ddd.php')); 309 | } 310 | 311 | return self::SUCCESS; 312 | } 313 | 314 | protected function update(): int 315 | { 316 | $config = DDD::config(); 317 | 318 | $confirmed = confirm('Are you sure you want to update ddd.php and merge with latest copy from the package?'); 319 | 320 | if (! $confirmed) { 321 | $this->info('Configuration update aborted.'); 322 | 323 | return self::SUCCESS; 324 | } 325 | 326 | $this->info('Merging ddd.php...'); 327 | 328 | $config->syncWithLatest()->save(); 329 | 330 | $this->info('Configuration updated: '.config_path('ddd.php')); 331 | $this->warn('Note: Some values may require manual adjustment.'); 332 | 333 | return self::SUCCESS; 334 | } 335 | 336 | protected function syncComposer(): int 337 | { 338 | $namespaces = [ 339 | config('ddd.domain_namespace', 'Domain') => config('ddd.domain_path', 'src/Domain'), 340 | config('ddd.application_namespace', 'App\\Modules') => config('ddd.application_path', 'app/Modules'), 341 | ...collect(config('ddd.layers', [])) 342 | ->all(), 343 | ]; 344 | 345 | $this->info('Syncing composer.json from ddd.php...'); 346 | 347 | $results = []; 348 | 349 | $added = 0; 350 | 351 | foreach ($namespaces as $namespace => $path) { 352 | if ($this->composer->hasPsr4Autoload($namespace)) { 353 | $results[] = [$namespace, $path, 'Already Registered']; 354 | 355 | continue; 356 | } 357 | 358 | $rootNamespace = Str::before($namespace, '\\'); 359 | 360 | if ($this->composer->hasPsr4Autoload($rootNamespace)) { 361 | $results[] = [$namespace, $path, 'Skipped']; 362 | 363 | continue; 364 | } 365 | 366 | $this->composer->registerPsr4Autoload($rootNamespace, $path); 367 | 368 | $results[] = [$namespace, $path, 'Added']; 369 | 370 | $added++; 371 | } 372 | 373 | if ($added > 0) { 374 | $this->composer->saveAndReload(); 375 | } 376 | 377 | table( 378 | headers: ['Namespace', 'Path', 'Status'], 379 | rows: $results 380 | ); 381 | 382 | return self::SUCCESS; 383 | } 384 | 385 | protected function exit(): int 386 | { 387 | $this->info('Goodbye!'); 388 | 389 | return self::SUCCESS; 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/Commands/DomainActionMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveDddStubPath('action.stub'); 25 | } 26 | 27 | protected function preparePlaceholders(): array 28 | { 29 | $baseClass = config('ddd.base_action'); 30 | 31 | return [ 32 | 'extends' => filled($baseClass) ? " extends {$baseClass}" : '', 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Commands/DomainBaseModelMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveDddStubPath('base-model.stub'); 38 | } 39 | 40 | protected function getRelativeDomainNamespace(): string 41 | { 42 | return config('ddd.namespaces.model', 'Models'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Commands/DomainBaseViewModelMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveDddStubPath('base-view-model.stub'); 38 | } 39 | 40 | protected function getRelativeDomainNamespace(): string 41 | { 42 | return config('ddd.namespaces.view_model', 'ViewModels'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Commands/DomainCastMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('requests')) { 28 | $namespace = $this->blueprint->getNamespaceFor('request', $this->getNameInput()); 29 | 30 | [$storeRequestClass, $updateRequestClass] = $this->generateFormRequests( 31 | $modelClass, 32 | $storeRequestClass, 33 | $updateRequestClass 34 | ); 35 | } 36 | 37 | $namespacedRequests = $namespace.'\\'.$storeRequestClass.';'; 38 | 39 | if ($storeRequestClass !== $updateRequestClass) { 40 | $namespacedRequests .= PHP_EOL.'use '.$namespace.'\\'.$updateRequestClass.';'; 41 | } 42 | 43 | return array_merge($replace, [ 44 | '{{ storeRequest }}' => $storeRequestClass, 45 | '{{storeRequest}}' => $storeRequestClass, 46 | '{{ updateRequest }}' => $updateRequestClass, 47 | '{{updateRequest}}' => $updateRequestClass, 48 | '{{ namespacedStoreRequest }}' => $namespace.'\\'.$storeRequestClass, 49 | '{{namespacedStoreRequest}}' => $namespace.'\\'.$storeRequestClass, 50 | '{{ namespacedUpdateRequest }}' => $namespace.'\\'.$updateRequestClass, 51 | '{{namespacedUpdateRequest}}' => $namespace.'\\'.$updateRequestClass, 52 | '{{ namespacedRequests }}' => $namespacedRequests, 53 | '{{namespacedRequests}}' => $namespacedRequests, 54 | ]); 55 | } 56 | 57 | protected function buildClass($name) 58 | { 59 | $stub = parent::buildClass($name); 60 | 61 | if ($this->isUsingPublishedStub()) { 62 | return $stub; 63 | } 64 | 65 | // Handle Laravel 10 side effect 66 | // todo: deprecated since L10 is no longer supported. 67 | if (str($stub)->contains($invalidUse = "use {$this->getNamespace($name)}\Http\Controllers\Controller;\n")) { 68 | $laravel10Replacements = [ 69 | ' extends Controller' => '', 70 | $invalidUse => '', 71 | ]; 72 | 73 | $stub = str_replace( 74 | array_keys($laravel10Replacements), 75 | array_values($laravel10Replacements), 76 | $stub 77 | ); 78 | } 79 | 80 | $replace = []; 81 | 82 | $appRootNamespace = $this->laravel->getNamespace(); 83 | $pathToAppBaseController = Path::normalize(app()->path('Http/Controllers/Controller.php')); 84 | 85 | $baseControllerExists = $this->files->exists($pathToAppBaseController); 86 | 87 | if ($baseControllerExists) { 88 | $controllerClass = class_basename($name); 89 | $fullyQualifiedBaseController = "{$appRootNamespace}Http\Controllers\Controller"; 90 | $namespaceLine = "namespace {$this->getNamespace($name)};"; 91 | $replace["{$namespaceLine}\n"] = "{$namespaceLine}\n\nuse {$fullyQualifiedBaseController};"; 92 | $replace["class {$controllerClass}\n"] = "class {$controllerClass} extends Controller\n"; 93 | } 94 | 95 | $stub = str_replace( 96 | array_keys($replace), 97 | array_values($replace), 98 | $stub 99 | ); 100 | 101 | return $this->sortImports($stub); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Commands/DomainDtoMakeCommand.php: -------------------------------------------------------------------------------- 1 | setAliases([ 25 | 'ddd:data-transfer-object', 26 | 'ddd:datatransferobject', 27 | 'ddd:data', 28 | ]); 29 | 30 | parent::configure(); 31 | } 32 | 33 | protected function getStub() 34 | { 35 | return $this->resolveDddStubPath('dto.stub'); 36 | } 37 | 38 | protected function getRelativeDomainNamespace(): string 39 | { 40 | return config('ddd.namespaces.data_transfer_object', 'Data'); 41 | } 42 | 43 | protected function preparePlaceholders(): array 44 | { 45 | $baseClass = config('ddd.base_dto'); 46 | 47 | return [ 48 | 'extends' => filled($baseClass) ? " extends {$baseClass}" : '', 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Commands/DomainEnumMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveDddStubPath('factory.stub'); 21 | } 22 | 23 | protected function getNamespace($name) 24 | { 25 | return $this->blueprint->getNamespaceFor('factory'); 26 | } 27 | 28 | protected function preparePlaceholders(): array 29 | { 30 | $domain = $this->blueprint->domain; 31 | 32 | $name = $this->getNameInput(); 33 | 34 | $modelName = $this->option('model') ?: $this->guessModelName($name); 35 | 36 | $domainModel = $domain->model($modelName); 37 | 38 | $domainFactory = $domain->factory($name); 39 | 40 | return [ 41 | 'namespacedModel' => $domainModel->fullyQualifiedName, 42 | 'model' => class_basename($domainModel->fullyQualifiedName), 43 | 'factory' => $domainFactory->name, 44 | 'namespace' => $domainFactory->namespace, 45 | ]; 46 | } 47 | 48 | protected function guessModelName($name) 49 | { 50 | if (str_ends_with($name, 'Factory')) { 51 | $name = substr($name, 0, -7); 52 | } 53 | 54 | return $this->blueprint->domain->model(class_basename($name))->name; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Commands/DomainGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | blueprint->type); 19 | } 20 | 21 | protected function getNameInput() 22 | { 23 | return Str::studly($this->argument('name')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Commands/DomainInterfaceMakeCommand.php: -------------------------------------------------------------------------------- 1 | map(function (string $name) { 24 | $domain = new Domain($name); 25 | 26 | return [ 27 | $domain->domain, 28 | $domain->layer->namespace, 29 | Path::normalize($domain->layer->path), 30 | ]; 31 | }) 32 | ->toArray(); 33 | 34 | table($headings, $table); 35 | 36 | $countDomains = count($table); 37 | 38 | $this->info(trans_choice("{$countDomains} domain|{$countDomains} domains", $countDomains)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Commands/DomainListenerMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('name')); 23 | } 24 | 25 | public function handle() 26 | { 27 | $this->beforeHandle(); 28 | 29 | $this->createBaseModelIfNeeded(); 30 | 31 | parent::handle(); 32 | 33 | $this->afterHandle(); 34 | } 35 | 36 | protected function buildFactoryReplacements() 37 | { 38 | $replacements = parent::buildFactoryReplacements(); 39 | 40 | if ($this->option('factory')) { 41 | $factoryNamespace = Str::start($this->blueprint->getFactoryFor($this->getNameInput())->fullyQualifiedName, '\\'); 42 | 43 | $factoryCode = << */ 45 | use HasFactory; 46 | EOT; 47 | 48 | $replacements['{{ factory }}'] = $factoryCode; 49 | $replacements['{{ factoryImport }}'] = 'use Lunarstorm\LaravelDDD\Factories\HasDomainFactory as HasFactory;'; 50 | } 51 | 52 | return $replacements; 53 | } 54 | 55 | protected function buildClass($name) 56 | { 57 | $stub = parent::buildClass($name); 58 | 59 | if ($this->isUsingPublishedStub()) { 60 | return $stub; 61 | } 62 | 63 | $replace = []; 64 | 65 | if ($baseModel = $this->getBaseModel()) { 66 | $baseModelClass = class_basename($baseModel); 67 | 68 | $replace = array_merge($replace, [ 69 | 'extends Model' => "extends {$baseModelClass}", 70 | 'use Illuminate\Database\Eloquent\Model;' => "use {$baseModel};", 71 | ]); 72 | } 73 | 74 | $stub = str_replace( 75 | array_keys($replace), 76 | array_values($replace), 77 | $stub 78 | ); 79 | 80 | return $this->sortImports($stub); 81 | } 82 | 83 | protected function createBaseModelIfNeeded() 84 | { 85 | if (! $this->shouldCreateBaseModel()) { 86 | return; 87 | } 88 | 89 | $baseModel = config('ddd.base_model'); 90 | 91 | $this->warn("Base model {$baseModel} doesn't exist, generating..."); 92 | 93 | $domain = DomainResolver::guessDomainFromClass($baseModel); 94 | 95 | $name = str($baseModel) 96 | ->after($domain) 97 | ->replace(['\\', '/'], '/') 98 | ->toString(); 99 | 100 | $this->call(DomainBaseModelMakeCommand::class, [ 101 | '--domain' => $domain, 102 | 'name' => $name, 103 | ]); 104 | } 105 | 106 | protected function getBaseModel(): ?string 107 | { 108 | return config('ddd.base_model', null); 109 | } 110 | 111 | protected function shouldCreateBaseModel(): bool 112 | { 113 | $baseModel = config('ddd.base_model'); 114 | 115 | if (is_null($baseModel)) { 116 | return false; 117 | } 118 | 119 | // If the class exists, we don't need to create it. 120 | if (class_exists($baseModel)) { 121 | return false; 122 | } 123 | 124 | // If the class is outside of the domain layer, we won't attempt to create it. 125 | if (! DomainResolver::isDomainClass($baseModel)) { 126 | return false; 127 | } 128 | 129 | // At this point the class is probably a domain object, but we should 130 | // check if the expected path exists. 131 | if (file_exists(app()->basePath(DomainResolver::guessPathFromClass($baseModel)))) { 132 | return false; 133 | } 134 | 135 | return true; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Commands/DomainNotificationMakeCommand.php: -------------------------------------------------------------------------------- 1 | blueprint->type), '\\'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Commands/DomainResourceMakeCommand.php: -------------------------------------------------------------------------------- 1 | setAliases([ 25 | 'ddd:value-object', 26 | 'ddd:valueobject', 27 | ]); 28 | 29 | parent::configure(); 30 | } 31 | 32 | protected function getStub() 33 | { 34 | return $this->resolveDddStubPath('value-object.stub'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Commands/DomainViewModelMakeCommand.php: -------------------------------------------------------------------------------- 1 | setAliases([ 26 | 'ddd:viewmodel', 27 | ]); 28 | 29 | parent::configure(); 30 | } 31 | 32 | protected function getStub() 33 | { 34 | return $this->resolveDddStubPath('view-model.stub'); 35 | } 36 | 37 | protected function preparePlaceholders(): array 38 | { 39 | $baseClass = config('ddd.base_view_model'); 40 | $baseClassName = class_basename($baseClass); 41 | 42 | return [ 43 | 'extends' => filled($baseClass) ? " extends {$baseClassName}" : '', 44 | 'baseClassImport' => filled($baseClass) ? "use {$baseClass};" : '', 45 | ]; 46 | } 47 | 48 | public function handle() 49 | { 50 | if ($this->shouldCreateBaseViewModel()) { 51 | $baseViewModel = config('ddd.base_view_model'); 52 | 53 | $this->warn("Base view model {$baseViewModel} doesn't exist, generating..."); 54 | 55 | $domain = DomainResolver::guessDomainFromClass($baseViewModel); 56 | 57 | $name = str($baseViewModel) 58 | ->after($domain) 59 | ->replace(['\\', '/'], '/') 60 | ->toString(); 61 | 62 | $this->call(DomainBaseViewModelMakeCommand::class, [ 63 | '--domain' => $domain, 64 | 'name' => $name, 65 | ]); 66 | } 67 | 68 | return parent::handle(); 69 | } 70 | 71 | protected function shouldCreateBaseViewModel(): bool 72 | { 73 | $baseViewModel = config('ddd.base_view_model'); 74 | 75 | // If the class exists, we don't need to create it. 76 | if (class_exists($baseViewModel)) { 77 | return false; 78 | } 79 | 80 | // If the class is outside of the domain layer, we won't attempt to create it. 81 | if (! DomainResolver::isDomainClass($baseViewModel)) { 82 | return false; 83 | } 84 | 85 | // At this point the class is probably a domain object, but we should 86 | // check if the expected path exists. 87 | if (file_exists(app()->basePath(DomainResolver::guessPathFromClass($baseViewModel)))) { 88 | return false; 89 | } 90 | 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | call('ddd:publish', ['--config' => true]); 18 | 19 | $this->comment('Updating composer.json...'); 20 | $this->callSilently('ddd:config', ['action' => 'composer']); 21 | 22 | if (confirm('Would you like to publish stubs now?', default: false, hint: 'You may do this at any time via ddd:stub')) { 23 | $this->call('ddd:stub'); 24 | } 25 | 26 | return self::SUCCESS; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/Migration/BaseMigrateMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('name')); 33 | 34 | return $name; 35 | } 36 | 37 | protected function qualifyModel(string $model) {} 38 | 39 | protected function getDefaultNamespace($rootNamespace) {} 40 | 41 | protected function getPath($name) {} 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/Migration/DomainMigrateMakeCommand.php: -------------------------------------------------------------------------------- 1 | blueprint) { 22 | return $this->laravel->basePath($this->blueprint->getMigrationPath()); 23 | } 24 | 25 | return $this->laravel->databasePath().DIRECTORY_SEPARATOR.'migrations'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Commands/OptimizeClearCommand.php: -------------------------------------------------------------------------------- 1 | setAliases([ 17 | 'ddd:optimize:clear', 18 | ]); 19 | 20 | parent::configure(); 21 | } 22 | 23 | public function handle() 24 | { 25 | DomainCache::clear(); 26 | 27 | $this->components->info('Domain cache cleared successfully.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Commands/OptimizeCommand.php: -------------------------------------------------------------------------------- 1 | setAliases([ 18 | 'ddd:cache', 19 | ]); 20 | 21 | parent::configure(); 22 | } 23 | 24 | public function handle() 25 | { 26 | $this->components->info('Caching DDD providers, commands, migration paths.'); 27 | $this->components->task('domain providers', fn () => Autoload::cacheProviders()); 28 | $this->components->task('domain commands', fn () => Autoload::cacheCommands()); 29 | $this->components->task('domain migration paths', fn () => DomainMigration::cachePaths()); 30 | $this->newLine(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Commands/PublishCommand.php: -------------------------------------------------------------------------------- 1 | 'Stubs', 29 | 'config' => 'Config File', 30 | ]; 31 | 32 | return multiselect( 33 | label: 'What should be published?', 34 | options: $options, 35 | required: true 36 | ); 37 | } 38 | 39 | public function handle(): int 40 | { 41 | $thingsToPublish = [ 42 | ...$this->option('config') ? ['config'] : [], 43 | ...$this->option('stubs') ? ['stubs'] : [], 44 | ...$this->option('all') ? ['config', 'stubs'] : [], 45 | ] ?: $this->askForThingsToPublish(); 46 | 47 | if (in_array('config', $thingsToPublish)) { 48 | $this->comment('Publishing config...'); 49 | $this->call('vendor:publish', [ 50 | '--tag' => 'ddd-config', 51 | ]); 52 | } 53 | 54 | if (in_array('stubs', $thingsToPublish)) { 55 | $this->comment('Publishing stubs...'); 56 | $this->call('ddd:stub', [ 57 | '--all' => true, 58 | ]); 59 | } 60 | 61 | return self::SUCCESS; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Commands/StubCommand.php: -------------------------------------------------------------------------------- 1 | stubs()->dddStubs(), 45 | ...app('ddd')->stubs()->frameworkStubs(), 46 | ]; 47 | } 48 | 49 | protected function resolveSelectedStubs(array $names = []) 50 | { 51 | $stubs = $this->getStubChoices(); 52 | 53 | if ($names) { 54 | [$startsWith, $exactNames] = collect($names) 55 | ->partition(fn ($name) => str($name)->endsWith(['*', '.'])); 56 | 57 | $startsWith = $startsWith->map( 58 | fn ($name) => str($name) 59 | ->replaceEnd('*', '.') 60 | ->replaceEnd('.', '') 61 | ); 62 | 63 | return collect($stubs) 64 | ->filter(function ($stub, $path) use ($startsWith, $exactNames) { 65 | $stubWithoutExtension = str($stub)->replaceEnd('.stub', ''); 66 | 67 | return $exactNames->contains($stub) 68 | || $exactNames->contains($stubWithoutExtension) 69 | || str($stub)->startsWith($startsWith); 70 | }) 71 | ->all(); 72 | } 73 | 74 | $selected = multisearch( 75 | label: 'Which stub should be published?', 76 | placeholder: 'Search for a stub...', 77 | options: fn (string $value) => strlen($value) > 0 78 | ? collect($stubs)->filter(fn ($stub, $path) => str($stub)->contains($value))->all() 79 | : $stubs, 80 | required: true 81 | ); 82 | 83 | return collect($stubs) 84 | ->filter(fn ($stub, $path) => in_array($stub, $selected)) 85 | ->all(); 86 | } 87 | 88 | public function handle(): int 89 | { 90 | $option = match (true) { 91 | $this->option('list') => 'list', 92 | $this->option('all') => 'all', 93 | count($this->argument('name')) > 0 => 'named', 94 | default => select( 95 | label: 'What do you want to do?', 96 | options: [ 97 | 'some' => 'Choose stubs to publish', 98 | 'all' => 'Publish all stubs', 99 | ], 100 | required: true, 101 | default: 'some' 102 | ) 103 | }; 104 | 105 | if ($option === 'list') { 106 | // $this->table( 107 | // ['Stub', 'Path'], 108 | // collect($this->getStubChoices())->map( 109 | // fn($stub, $path) => [ 110 | // $stub, 111 | // Str::after($path, $this->laravel->basePath()) 112 | // ] 113 | // ) 114 | // ); 115 | 116 | table( 117 | headers: ['Stub', 'Source'], 118 | rows: collect($this->getStubChoices())->map( 119 | fn ($stub, $path) => [ 120 | Str::replaceLast('.stub', '', $stub), 121 | str($path)->startsWith(DDD::packagePath()) 122 | ? 'ddd' 123 | : 'laravel', 124 | ] 125 | ) 126 | ); 127 | 128 | return self::SUCCESS; 129 | } 130 | 131 | $stubs = $option === 'all' 132 | ? $this->getStubChoices() 133 | : $this->resolveSelectedStubs($this->argument('name')); 134 | 135 | if (empty($stubs)) { 136 | $this->warn('No matching stubs found.'); 137 | 138 | return self::INVALID; 139 | } 140 | 141 | File::ensureDirectoryExists($stubsPath = $this->laravel->basePath('stubs/ddd')); 142 | 143 | $this->laravel['events']->dispatch($event = new PublishingStubs($stubs)); 144 | 145 | foreach ($event->stubs as $from => $to) { 146 | $to = $stubsPath.DIRECTORY_SEPARATOR.ltrim($to, DIRECTORY_SEPARATOR); 147 | 148 | $relativePath = Str::after($to, $this->laravel->basePath()); 149 | 150 | $this->info("Publishing {$relativePath}"); 151 | 152 | if ((! $this->option('existing') && (! file_exists($to) || $this->option('force'))) 153 | || ($this->option('existing') && file_exists($to)) 154 | ) { 155 | file_put_contents($to, file_get_contents($from)); 156 | } 157 | } 158 | 159 | $this->components->info('Stubs published successfully.'); 160 | 161 | return self::SUCCESS; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Commands/UpgradeCommand.php: -------------------------------------------------------------------------------- 1 | components->warn('Config file was not published. Nothing to upgrade!'); 19 | 20 | return; 21 | } 22 | 23 | $legacyMapping = [ 24 | 'domain_path' => 'paths.domain', 25 | 'domain_namespace' => 'domain_namespace', 26 | 'application' => null, 27 | 'layers' => null, 28 | 'namespaces' => [ 29 | 'model' => 'namespaces.models', 30 | 'data_transfer_object' => 'namespaces.data_transfer_objects', 31 | 'view_model' => 'namespaces.view_models', 32 | 'value_object' => 'namespaces.value_objects', 33 | 'action' => 'namespaces.actions', 34 | ], 35 | 'base_model' => 'base_model', 36 | 'base_dto' => 'base_dto', 37 | 'base_view_model' => 'base_view_model', 38 | 'base_action' => 'base_action', 39 | 'autoload' => null, 40 | 'autoload_ignore' => null, 41 | 'cache_directory' => null, 42 | ]; 43 | 44 | $factoryConfig = require __DIR__.'/../../config/ddd.php'; 45 | $oldConfig = require config_path('ddd.php'); 46 | $oldConfig = Arr::dot($oldConfig); 47 | 48 | $replacements = []; 49 | 50 | $map = Arr::dot($legacyMapping); 51 | 52 | foreach ($map as $dotPath => $legacyKey) { 53 | $value = match (true) { 54 | array_key_exists($dotPath, $oldConfig) => $oldConfig[$dotPath], 55 | array_key_exists($legacyKey, $oldConfig) => $oldConfig[$legacyKey], 56 | default => config("ddd.{$dotPath}"), 57 | }; 58 | 59 | $replacements[$dotPath] = $value ?? data_get($factoryConfig, $dotPath); 60 | } 61 | 62 | $replacements = Arr::undot($replacements); 63 | 64 | $freshConfig = $factoryConfig; 65 | 66 | // Grab a fresh copy of the new config 67 | $newConfigContent = file_get_contents(__DIR__.'/../../config/ddd.php.stub'); 68 | 69 | foreach ($freshConfig as $key => $value) { 70 | $resolved = null; 71 | 72 | if (is_array($value)) { 73 | $resolved = [ 74 | ...$value, 75 | ...data_get($replacements, $key, []), 76 | ]; 77 | 78 | if (array_is_list($resolved)) { 79 | $resolved = array_unique($resolved); 80 | } 81 | } else { 82 | $resolved = data_get($replacements, $key, $value); 83 | } 84 | 85 | $freshConfig[$key] = $resolved; 86 | 87 | $newConfigContent = str_replace( 88 | '{{'.$key.'}}', 89 | var_export($resolved, true), 90 | $newConfigContent 91 | ); 92 | } 93 | 94 | // Write the new config to the config file 95 | file_put_contents(config_path('ddd.php'), $newConfigContent); 96 | 97 | $this->components->info('Configuration upgraded successfully.'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ComposerManager.php: -------------------------------------------------------------------------------- 1 | composer = app(Composer::class)->setWorkingPath(app()->basePath()); 24 | 25 | $this->composerFile = $composerFile ?? app()->basePath('composer.json'); 26 | 27 | $this->data = json_decode(file_get_contents($this->composerFile), true); 28 | } 29 | 30 | public static function make(?string $composerFile = null): self 31 | { 32 | return new self($composerFile); 33 | } 34 | 35 | public function usingOutput(OutputStyle $output) 36 | { 37 | $this->output = $output; 38 | 39 | return $this; 40 | } 41 | 42 | protected function guessAutoloadPathFromNamespace(string $namespace): string 43 | { 44 | $rootFolders = [ 45 | 'src', 46 | '', 47 | ]; 48 | 49 | $relativePath = Str::rtrim(Path::fromNamespace($namespace), '/\\'); 50 | 51 | foreach ($rootFolders as $folder) { 52 | $path = Path::join($folder, $relativePath); 53 | 54 | if (is_dir($path)) { 55 | return $this->normalizePathForComposer($path); 56 | } 57 | } 58 | 59 | return $this->normalizePathForComposer("src/{$relativePath}"); 60 | } 61 | 62 | protected function normalizePathForComposer($path): string 63 | { 64 | $path = Path::normalize($path); 65 | 66 | return str_replace(['\\', '/'], '/', $path); 67 | } 68 | 69 | public function hasPsr4Autoload(string $namespace): bool 70 | { 71 | return collect($this->getPsr4Namespaces()) 72 | ->hasAny([ 73 | $namespace, 74 | Str::finish($namespace, '\\'), 75 | ]); 76 | } 77 | 78 | public function registerPsr4Autoload(string $namespace, $path) 79 | { 80 | $namespace = str($namespace) 81 | ->rtrim('/\\') 82 | ->finish('\\') 83 | ->toString(); 84 | 85 | $path = $path ?? $this->guessAutoloadPathFromNamespace($namespace); 86 | 87 | return $this->fill( 88 | ['autoload', 'psr-4', $namespace], 89 | $this->normalizePathForComposer($path) 90 | ); 91 | } 92 | 93 | public function fill($path, $value) 94 | { 95 | data_fill($this->data, $path, $value); 96 | 97 | return $this; 98 | } 99 | 100 | protected function update($set = [], $forget = []) 101 | { 102 | foreach ($forget as $key) { 103 | $this->forget($key); 104 | } 105 | 106 | foreach ($set as $pair) { 107 | [$path, $value] = $pair; 108 | $this->fill($path, $value); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | public function forget($key) 115 | { 116 | $keys = Arr::wrap($key); 117 | 118 | foreach ($keys as $key) { 119 | Arr::forget($this->data, $key); 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | public function get($path, $default = null) 126 | { 127 | return data_get($this->data, $path, $default); 128 | } 129 | 130 | public function getPsr4Namespaces() 131 | { 132 | return $this->get(['autoload', 'psr-4'], []); 133 | } 134 | 135 | public function getAutoloadPath($namespace) 136 | { 137 | $namespace = Str::finish($namespace, '\\'); 138 | 139 | return $this->get(['autoload', 'psr-4', $namespace]); 140 | } 141 | 142 | public function unsetPsr4Autoload($namespace) 143 | { 144 | $namespace = Str::finish($namespace, '\\'); 145 | 146 | return $this->forget(['autoload', 'psr-4', $namespace]); 147 | } 148 | 149 | public function reload() 150 | { 151 | $this->output?->writeLn('Reloading composer (dump-autoload)...'); 152 | 153 | $this->composer->dumpAutoloads(); 154 | 155 | return $this; 156 | } 157 | 158 | public function save() 159 | { 160 | $this->composer->modify(fn ($composerData) => $this->data); 161 | 162 | return $this; 163 | } 164 | 165 | public function saveAndReload() 166 | { 167 | return $this->save()->reload(); 168 | } 169 | 170 | public function toJson() 171 | { 172 | return json_encode($this->data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 173 | } 174 | 175 | public function toArray() 176 | { 177 | return $this->data; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/ConfigManager.php: -------------------------------------------------------------------------------- 1 | configPath = $configPath ?? app()->configPath('ddd.php'); 21 | 22 | $this->packageConfig = require DDD::packagePath('config/ddd.php'); 23 | 24 | $this->config = file_exists($configPath) ? require ($configPath) : $this->packageConfig; 25 | 26 | $this->stub = file_get_contents(DDD::packagePath('config/ddd.php.stub')); 27 | } 28 | 29 | protected function mergeArray($path, $array) 30 | { 31 | $path = Arr::wrap($path); 32 | 33 | $merged = []; 34 | 35 | foreach ($array as $key => $value) { 36 | $merged[$key] = is_array($value) 37 | ? $this->mergeArray([...$path, $key], $value) 38 | : $this->resolve([...$path, $key], $value); 39 | } 40 | 41 | if (array_is_list($merged)) { 42 | $merged = array_unique($merged); 43 | } 44 | 45 | return $merged; 46 | } 47 | 48 | public function resolve($path, $value) 49 | { 50 | $path = Arr::wrap($path); 51 | 52 | return data_get($this->config, $path, $value); 53 | } 54 | 55 | public function syncWithLatest() 56 | { 57 | $fresh = []; 58 | 59 | foreach ($this->packageConfig as $key => $value) { 60 | $resolved = is_array($value) 61 | ? $this->mergeArray($key, $value) 62 | : $this->resolve($key, $value); 63 | 64 | $fresh[$key] = $resolved; 65 | } 66 | 67 | $this->config = $fresh; 68 | 69 | return $this; 70 | } 71 | 72 | public function get($key = null) 73 | { 74 | if (is_null($key)) { 75 | return $this->config; 76 | } 77 | 78 | return data_get($this->config, $key); 79 | } 80 | 81 | public function set($key, $value) 82 | { 83 | data_set($this->config, $key, $value); 84 | 85 | return $this; 86 | } 87 | 88 | public function fill($values) 89 | { 90 | foreach ($values as $key => $value) { 91 | $this->set($key, $value); 92 | } 93 | 94 | return $this; 95 | } 96 | 97 | public function save() 98 | { 99 | $content = $this->stub; 100 | 101 | // We will temporary substitute namespace slashes 102 | // with a placeholder to avoid double exporter 103 | // escaping them as double backslashes. 104 | $keysWithNamespaces = [ 105 | 'domain_namespace', 106 | 'application_namespace', 107 | 'layers', 108 | 'namespaces', 109 | 'base_model', 110 | 'base_dto', 111 | 'base_view_model', 112 | 'base_action', 113 | ]; 114 | 115 | foreach ($keysWithNamespaces as $key) { 116 | $value = $this->get($key); 117 | 118 | if (is_string($value)) { 119 | $value = str_replace('\\', '[[BACKSLASH]]', $value); 120 | } 121 | 122 | if (is_array($value)) { 123 | $array = $value; 124 | foreach ($array as $k => $v) { 125 | $array[$k] = str_replace('\\', '[[BACKSLASH]]', $v); 126 | } 127 | $value = $array; 128 | } 129 | 130 | $this->set($key, $value); 131 | } 132 | 133 | foreach ($this->config as $key => $value) { 134 | $content = str_replace( 135 | '{{'.$key.'}}', 136 | VarExporter::export($value), 137 | $content 138 | ); 139 | } 140 | 141 | // Restore namespace slashes 142 | $content = str_replace('[[BACKSLASH]]', '\\', $content); 143 | 144 | // Write it to a temporary file first 145 | $tempPath = sys_get_temp_dir().'/ddd.php'; 146 | file_put_contents($tempPath, $content); 147 | 148 | // Format it using pint 149 | Process::run("./vendor/bin/pint {$tempPath}"); 150 | 151 | // Copy the temporary file to the config path 152 | copy($tempPath, $this->configPath); 153 | 154 | return $this; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/DomainManager.php: -------------------------------------------------------------------------------- 1 | autoloadFilter = null; 42 | $this->applicationLayerFilter = null; 43 | $this->commandContext = null; 44 | } 45 | 46 | public function autoloader(): AutoloadManager 47 | { 48 | return app(AutoloadManager::class); 49 | } 50 | 51 | public function composer(): ComposerManager 52 | { 53 | return app(ComposerManager::class); 54 | } 55 | 56 | public function config(): ConfigManager 57 | { 58 | return app(ConfigManager::class); 59 | } 60 | 61 | public function stubs(): StubManager 62 | { 63 | return app(StubManager::class); 64 | } 65 | 66 | public function filterAutoloadPathsUsing(callable $filter): void 67 | { 68 | $this->autoloadFilter = $filter; 69 | } 70 | 71 | public function getAutoloadFilter(): ?callable 72 | { 73 | return $this->autoloadFilter; 74 | } 75 | 76 | public function filterApplicationLayerUsing(callable $filter): void 77 | { 78 | $this->applicationLayerFilter = $filter; 79 | } 80 | 81 | public function getApplicationLayerFilter(): ?callable 82 | { 83 | return $this->applicationLayerFilter; 84 | } 85 | 86 | public function resolveObjectSchemaUsing(callable $resolver): void 87 | { 88 | $this->objectSchemaResolver = $resolver; 89 | } 90 | 91 | public function getObjectSchemaResolver(): ?callable 92 | { 93 | return $this->objectSchemaResolver; 94 | } 95 | 96 | public function packagePath($path = ''): string 97 | { 98 | return Path::normalize(realpath(__DIR__.'/../'.$path)); 99 | } 100 | 101 | public function laravelVersion($value) 102 | { 103 | return version_compare(app()->version(), $value, '>='); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Enums/LayerType.php: -------------------------------------------------------------------------------- 1 | $modelName 26 | * @return null|class-string<\Illuminate\Database\Eloquent\Factories\Factory> 27 | */ 28 | public static function resolveFactoryName(string $modelName) 29 | { 30 | $resolver = function (string $modelName) { 31 | $model = DomainObject::fromClass($modelName, 'model'); 32 | 33 | if (! $model) { 34 | // Not a domain model 35 | return null; 36 | } 37 | 38 | // First try resolving as a factory class in the domain layer 39 | $factoryClass = DomainResolver::getDomainObjectNamespace($model->domain, 'factory', "{$model->name}Factory"); 40 | if (class_exists($factoryClass)) { 41 | return $factoryClass; 42 | } 43 | 44 | // Otherwise, fallback to the the standard location under /database/factories 45 | return static::$namespace."{$model->domain}\\{$model->name}Factory"; 46 | }; 47 | 48 | return $resolver($modelName); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Factories/HasDomainFactory.php: -------------------------------------------------------------------------------- 1 | name('laravel-ddd') 23 | ->hasConfigFile() 24 | ->hasCommands([ 25 | Commands\InstallCommand::class, 26 | Commands\ConfigCommand::class, 27 | Commands\PublishCommand::class, 28 | Commands\StubCommand::class, 29 | Commands\UpgradeCommand::class, 30 | Commands\OptimizeCommand::class, 31 | Commands\OptimizeClearCommand::class, 32 | Commands\DomainListCommand::class, 33 | Commands\DomainModelMakeCommand::class, 34 | Commands\DomainFactoryMakeCommand::class, 35 | Commands\DomainBaseModelMakeCommand::class, 36 | Commands\DomainDtoMakeCommand::class, 37 | Commands\DomainValueObjectMakeCommand::class, 38 | Commands\DomainViewModelMakeCommand::class, 39 | Commands\DomainBaseViewModelMakeCommand::class, 40 | Commands\DomainActionMakeCommand::class, 41 | Commands\DomainCastMakeCommand::class, 42 | Commands\DomainChannelMakeCommand::class, 43 | Commands\DomainConsoleMakeCommand::class, 44 | Commands\DomainControllerMakeCommand::class, 45 | Commands\DomainClassMakeCommand::class, 46 | Commands\DomainEnumMakeCommand::class, 47 | Commands\DomainEventMakeCommand::class, 48 | Commands\DomainExceptionMakeCommand::class, 49 | Commands\DomainInterfaceMakeCommand::class, 50 | Commands\DomainJobMakeCommand::class, 51 | Commands\DomainListenerMakeCommand::class, 52 | Commands\DomainMailMakeCommand::class, 53 | Commands\DomainMiddlewareMakeCommand::class, 54 | Commands\DomainNotificationMakeCommand::class, 55 | Commands\DomainObserverMakeCommand::class, 56 | Commands\DomainPolicyMakeCommand::class, 57 | Commands\DomainProviderMakeCommand::class, 58 | Commands\DomainResourceMakeCommand::class, 59 | Commands\DomainRequestMakeCommand::class, 60 | Commands\DomainRuleMakeCommand::class, 61 | Commands\DomainScopeMakeCommand::class, 62 | Commands\DomainSeederMakeCommand::class, 63 | Commands\DomainTraitMakeCommand::class, 64 | Commands\Migration\DomainMigrateMakeCommand::class, 65 | ]); 66 | 67 | if ($this->app->runningUnitTests()) { 68 | $package->hasRoutes(['testing']); 69 | } 70 | 71 | $this->registerBindings(); 72 | } 73 | 74 | protected function laravelVersion($value) 75 | { 76 | return version_compare(app()->version(), $value, '>='); 77 | } 78 | 79 | protected function registerMigrations() 80 | { 81 | $this->app->when(MigrationCreator::class) 82 | ->needs('$customStubPath') 83 | ->give(fn () => $this->app->basePath('stubs')); 84 | 85 | $this->app->singleton(Commands\Migration\DomainMigrateMakeCommand::class, function ($app) { 86 | // Once we have the migration creator registered, we will create the command 87 | // and inject the creator. The creator is responsible for the actual file 88 | // creation of the migrations, and may be extended by these developers. 89 | $creator = $app['migration.creator']; 90 | $composer = $app['composer']; 91 | 92 | return new Commands\Migration\DomainMigrateMakeCommand($creator, $composer); 93 | }); 94 | 95 | $this->loadMigrationsFrom(DomainMigration::paths()); 96 | 97 | return $this; 98 | } 99 | 100 | protected function registerBindings() 101 | { 102 | $this->app->scoped(DomainManager::class, function () { 103 | return new DomainManager; 104 | }); 105 | 106 | $this->app->scoped(ComposerManager::class, function () { 107 | return ComposerManager::make($this->app->basePath('composer.json')); 108 | }); 109 | 110 | $this->app->scoped(ConfigManager::class, function () { 111 | return new ConfigManager($this->app->configPath('ddd.php')); 112 | }); 113 | 114 | $this->app->scoped(StubManager::class, function () { 115 | return new StubManager; 116 | }); 117 | 118 | $this->app->scoped(AutoloadManager::class, function () { 119 | return new AutoloadManager; 120 | }); 121 | 122 | $this->app->bind('ddd', DomainManager::class); 123 | $this->app->bind('ddd.autoloader', AutoloadManager::class); 124 | $this->app->bind('ddd.config', ConfigManager::class); 125 | $this->app->bind('ddd.composer', ComposerManager::class); 126 | $this->app->bind('ddd.stubs', StubManager::class); 127 | 128 | return $this; 129 | } 130 | 131 | public function packageBooted() 132 | { 133 | Autoload::run(); 134 | 135 | if ($this->app->runningInConsole()) { 136 | $this->optimizes( 137 | optimize: 'ddd:optimize', 138 | clear: 'ddd:clear', 139 | key: 'laravel-ddd', 140 | ); 141 | } 142 | } 143 | 144 | public function packageRegistered() 145 | { 146 | $this->registerMigrations(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Listeners/CacheClearSubscriber.php: -------------------------------------------------------------------------------- 1 | listen('cache:clearing', [$this, 'handle']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Models/DomainModel.php: -------------------------------------------------------------------------------- 1 | dddStubs(), 16 | ...$this->frameworkStubs(), 17 | ]; 18 | } 19 | 20 | public function dddStubs() 21 | { 22 | return [ 23 | realpath(__DIR__.'/../stubs/action.stub') => 'action.stub', 24 | realpath(__DIR__.'/../stubs/dto.stub') => 'dto.stub', 25 | realpath(__DIR__.'/../stubs/value-object.stub') => 'value-object.stub', 26 | realpath(__DIR__.'/../stubs/view-model.stub') => 'view-model.stub', 27 | realpath(__DIR__.'/../stubs/base-view-model.stub') => 'base-view-model.stub', 28 | realpath(__DIR__.'/../stubs/factory.stub') => 'factory.stub', 29 | ]; 30 | } 31 | 32 | public function frameworkStubs() 33 | { 34 | $laravelStubCommand = new ReflectionClass(new StubPublishCommand); 35 | 36 | $dir = dirname($laravelStubCommand->getFileName()); 37 | 38 | $stubs = [ 39 | $dir.'/stubs/cast.inbound.stub' => 'cast.inbound.stub', 40 | $dir.'/stubs/cast.stub' => 'cast.stub', 41 | $dir.'/stubs/class.stub' => 'class.stub', 42 | $dir.'/stubs/class.invokable.stub' => 'class.invokable.stub', 43 | $dir.'/stubs/console.stub' => 'console.stub', 44 | $dir.'/stubs/enum.stub' => 'enum.stub', 45 | $dir.'/stubs/enum.backed.stub' => 'enum.backed.stub', 46 | $dir.'/stubs/event.stub' => 'event.stub', 47 | $dir.'/stubs/job.queued.stub' => 'job.queued.stub', 48 | $dir.'/stubs/job.stub' => 'job.stub', 49 | $dir.'/stubs/listener.typed.queued.stub' => 'listener.typed.queued.stub', 50 | $dir.'/stubs/listener.queued.stub' => 'listener.queued.stub', 51 | $dir.'/stubs/listener.typed.stub' => 'listener.typed.stub', 52 | $dir.'/stubs/listener.stub' => 'listener.stub', 53 | $dir.'/stubs/mail.stub' => 'mail.stub', 54 | $dir.'/stubs/markdown-mail.stub' => 'markdown-mail.stub', 55 | $dir.'/stubs/markdown-notification.stub' => 'markdown-notification.stub', 56 | $dir.'/stubs/model.pivot.stub' => 'model.pivot.stub', 57 | $dir.'/stubs/model.stub' => 'model.stub', 58 | $dir.'/stubs/notification.stub' => 'notification.stub', 59 | $dir.'/stubs/observer.plain.stub' => 'observer.plain.stub', 60 | $dir.'/stubs/observer.stub' => 'observer.stub', 61 | // $dir . '/stubs/pest.stub' => 'pest.stub', 62 | // $dir . '/stubs/pest.unit.stub' => 'pest.unit.stub', 63 | $dir.'/stubs/policy.plain.stub' => 'policy.plain.stub', 64 | $dir.'/stubs/policy.stub' => 'policy.stub', 65 | $dir.'/stubs/provider.stub' => 'provider.stub', 66 | $dir.'/stubs/request.stub' => 'request.stub', 67 | $dir.'/stubs/resource.stub' => 'resource.stub', 68 | $dir.'/stubs/resource-collection.stub' => 'resource-collection.stub', 69 | $dir.'/stubs/rule.stub' => 'rule.stub', 70 | $dir.'/stubs/scope.stub' => 'scope.stub', 71 | // $dir.'/stubs/test.stub' => 'test.stub', 72 | // $dir.'/stubs/test.unit.stub' => 'test.unit.stub', 73 | $dir.'/stubs/trait.stub' => 'trait.stub', 74 | $dir.'/stubs/view-component.stub' => 'view-component.stub', 75 | // Factories will use a ddd-specific stub 76 | // realpath($dir . '/../../Database/Console/Factories/stubs/factory.stub') => 'factory.stub', 77 | realpath($dir.'/../../Database/Console/Seeds/stubs/seeder.stub') => 'seeder.stub', 78 | realpath($dir.'/../../Database/Migrations/stubs/migration.create.stub') => 'migration.create.stub', 79 | realpath($dir.'/../../Database/Migrations/stubs/migration.stub') => 'migration.stub', 80 | realpath($dir.'/../../Database/Migrations/stubs/migration.update.stub') => 'migration.update.stub', 81 | realpath($dir.'/../../Routing/Console/stubs/controller.api.stub') => 'controller.api.stub', 82 | realpath($dir.'/../../Routing/Console/stubs/controller.invokable.stub') => 'controller.invokable.stub', 83 | realpath($dir.'/../../Routing/Console/stubs/controller.model.api.stub') => 'controller.model.api.stub', 84 | realpath($dir.'/../../Routing/Console/stubs/controller.model.stub') => 'controller.model.stub', 85 | realpath($dir.'/../../Routing/Console/stubs/controller.nested.api.stub') => 'controller.nested.api.stub', 86 | realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.api.stub') => 'controller.nested.singleton.api.stub', 87 | realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.stub') => 'controller.nested.singleton.stub', 88 | realpath($dir.'/../../Routing/Console/stubs/controller.nested.stub') => 'controller.nested.stub', 89 | realpath($dir.'/../../Routing/Console/stubs/controller.plain.stub') => 'controller.plain.stub', 90 | realpath($dir.'/../../Routing/Console/stubs/controller.singleton.api.stub') => 'controller.singleton.api.stub', 91 | realpath($dir.'/../../Routing/Console/stubs/controller.singleton.stub') => 'controller.singleton.stub', 92 | realpath($dir.'/../../Routing/Console/stubs/controller.stub') => 'controller.stub', 93 | realpath($dir.'/../../Routing/Console/stubs/middleware.stub') => 'middleware.stub', 94 | ]; 95 | 96 | // Some stubs are not available across all Laravel versions, 97 | // so we'll just skip the files that don't exist. 98 | return collect($stubs)->filter(function ($stub, $path) { 99 | return file_exists($path); 100 | })->all(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Support/AutoloadManager.php: -------------------------------------------------------------------------------- 1 | container = $container ?? Container::getInstance(); 55 | 56 | $this->app = $this->container->make(Application::class); 57 | 58 | $this->appNamespace = $this->app->getNamespace(); 59 | } 60 | 61 | public function boot() 62 | { 63 | $this->booted = true; 64 | 65 | if (! config()->has('ddd.autoload')) { 66 | return $this->flush(); 67 | } 68 | 69 | $this 70 | ->flush() 71 | ->when(config('ddd.autoload.providers') === true, fn () => $this->handleProviders()) 72 | ->when($this->app->runningInConsole() && config('ddd.autoload.commands') === true, fn () => $this->handleCommands()) 73 | ->when(config('ddd.autoload.policies') === true, fn () => $this->handlePolicies()) 74 | ->when(config('ddd.autoload.factories') === true, fn () => $this->handleFactories()); 75 | 76 | return $this; 77 | } 78 | 79 | public function isBooted(): bool 80 | { 81 | return $this->booted; 82 | } 83 | 84 | public function isConsoleBooted(): bool 85 | { 86 | return $this->consoleBooted; 87 | } 88 | 89 | public function hasRun(): bool 90 | { 91 | return $this->ran; 92 | } 93 | 94 | protected function flush() 95 | { 96 | foreach (static::$registeredProviders as $provider) { 97 | $this->app?->forgetInstance($provider); 98 | } 99 | 100 | static::$registeredProviders = []; 101 | 102 | static::$registeredCommands = []; 103 | 104 | static::$resolvedPolicies = []; 105 | 106 | static::$resolvedFactories = []; 107 | 108 | return $this; 109 | } 110 | 111 | protected function normalizePaths($path): array 112 | { 113 | return collect($path) 114 | ->filter(fn ($path) => is_dir($path)) 115 | ->toArray(); 116 | } 117 | 118 | public function getAllLayerPaths(): array 119 | { 120 | return collect([ 121 | DomainResolver::domainPath(), 122 | DomainResolver::applicationLayerPath(), 123 | ...array_values(config('ddd.layers', [])), 124 | ])->map(fn ($path) => Path::normalize($this->app->basePath($path)))->toArray(); 125 | } 126 | 127 | protected function getCustomLayerPaths(): array 128 | { 129 | return collect([ 130 | ...array_values(config('ddd.layers', [])), 131 | ])->map(fn ($path) => Path::normalize($this->app->basePath($path)))->toArray(); 132 | } 133 | 134 | protected function handleProviders() 135 | { 136 | $providers = DomainCache::has('domain-providers') 137 | ? DomainCache::get('domain-providers') 138 | : $this->discoverProviders(); 139 | 140 | foreach ($providers as $provider) { 141 | static::$registeredProviders[$provider] = $provider; 142 | } 143 | 144 | return $this; 145 | } 146 | 147 | protected function handleCommands() 148 | { 149 | $commands = DomainCache::has('domain-commands') 150 | ? DomainCache::get('domain-commands') 151 | : $this->discoverCommands(); 152 | 153 | foreach ($commands as $command) { 154 | static::$registeredCommands[$command] = $command; 155 | } 156 | 157 | return $this; 158 | } 159 | 160 | public function run() 161 | { 162 | if (! $this->isBooted()) { 163 | $this->boot(); 164 | } 165 | 166 | foreach (static::$registeredProviders as $provider) { 167 | $this->app->register($provider); 168 | } 169 | 170 | if ($this->app->runningInConsole() && ! $this->isConsoleBooted()) { 171 | ConsoleApplication::starting(function (ConsoleApplication $artisan) { 172 | foreach (static::$registeredCommands as $command) { 173 | $artisan->resolve($command); 174 | } 175 | }); 176 | 177 | $this->consoleBooted = true; 178 | } 179 | 180 | $this->ran = true; 181 | 182 | return $this; 183 | } 184 | 185 | public function getRegisteredCommands(): array 186 | { 187 | return static::$registeredCommands; 188 | } 189 | 190 | public function getRegisteredProviders(): array 191 | { 192 | return static::$registeredProviders; 193 | } 194 | 195 | public function getResolvedPolicies(): array 196 | { 197 | return static::$resolvedPolicies; 198 | } 199 | 200 | public function getResolvedFactories(): array 201 | { 202 | return static::$resolvedFactories; 203 | } 204 | 205 | protected function handlePolicies() 206 | { 207 | Gate::guessPolicyNamesUsing(static::$policyResolver = function (string $class): array|string { 208 | if ($model = DomainObject::fromClass($class, 'model')) { 209 | $resolved = (new Domain($model->domain)) 210 | ->object('policy', "{$model->name}Policy") 211 | ->fullyQualifiedName; 212 | 213 | static::$resolvedPolicies[$class] = $resolved; 214 | 215 | return $resolved; 216 | } 217 | 218 | $classDirname = str_replace('/', '\\', dirname(str_replace('\\', '/', $class))); 219 | 220 | $classDirnameSegments = explode('\\', $classDirname); 221 | 222 | return Arr::wrap(Collection::times(count($classDirnameSegments), function ($index) use ($class, $classDirnameSegments) { 223 | $classDirname = implode('\\', array_slice($classDirnameSegments, 0, $index)); 224 | 225 | return $classDirname.'\\Policies\\'.class_basename($class).'Policy'; 226 | })->reverse()->values()->first(function ($class) { 227 | return class_exists($class); 228 | }) ?: [$classDirname.'\\Policies\\'.class_basename($class).'Policy']); 229 | }); 230 | 231 | return $this; 232 | } 233 | 234 | protected function handleFactories() 235 | { 236 | Factory::guessFactoryNamesUsing(static::$factoryResolver = function (string $modelName) { 237 | if ($factoryName = DomainFactory::resolveFactoryName($modelName)) { 238 | static::$resolvedFactories[$modelName] = $factoryName; 239 | 240 | return $factoryName; 241 | } 242 | 243 | $modelName = Str::startsWith($modelName, $this->appNamespace.'Models\\') 244 | ? Str::after($modelName, $this->appNamespace.'Models\\') 245 | : Str::after($modelName, $this->appNamespace); 246 | 247 | return 'Database\\Factories\\'.$modelName.'Factory'; 248 | }); 249 | 250 | return $this; 251 | } 252 | 253 | protected function finder($paths) 254 | { 255 | $filter = DDD::getAutoloadFilter() ?? function (SplFileInfo $file) { 256 | $pathAfterDomain = str($file->getRelativePath()) 257 | ->replace('\\', '/') 258 | ->after('/') 259 | ->finish('/'); 260 | 261 | $ignoredFolders = collect(config('ddd.autoload_ignore', [])) 262 | ->map(fn ($path) => Str::finish($path, '/')); 263 | 264 | if ($pathAfterDomain->startsWith($ignoredFolders)) { 265 | return false; 266 | } 267 | }; 268 | 269 | return Finder::create() 270 | ->files() 271 | ->in($paths) 272 | ->filter($filter); 273 | } 274 | 275 | public function discoverProviders(): array 276 | { 277 | $configValue = config('ddd.autoload.providers'); 278 | 279 | if ($configValue === false) { 280 | return []; 281 | } 282 | 283 | $paths = $this->normalizePaths( 284 | $configValue === true 285 | ? $this->getAllLayerPaths() 286 | : $configValue 287 | ); 288 | 289 | if (empty($paths)) { 290 | return []; 291 | } 292 | 293 | return Lody::classesFromFinder($this->finder($paths)) 294 | ->isNotAbstract() 295 | ->isInstanceOf(ServiceProvider::class) 296 | ->values() 297 | ->toArray(); 298 | } 299 | 300 | public function discoverCommands(): array 301 | { 302 | $configValue = config('ddd.autoload.commands'); 303 | 304 | if ($configValue === false) { 305 | return []; 306 | } 307 | 308 | $paths = $this->normalizePaths( 309 | $configValue === true 310 | ? $this->getAllLayerPaths() 311 | : $configValue 312 | ); 313 | 314 | if (empty($paths)) { 315 | return []; 316 | } 317 | 318 | return Lody::classesFromFinder($this->finder($paths)) 319 | ->isNotAbstract() 320 | ->isInstanceOf(Command::class) 321 | ->values() 322 | ->toArray(); 323 | } 324 | 325 | public function cacheCommands() 326 | { 327 | DomainCache::set('domain-commands', $this->discoverCommands()); 328 | 329 | return $this; 330 | } 331 | 332 | public function cacheProviders() 333 | { 334 | DomainCache::set('domain-providers', $this->discoverProviders()); 335 | 336 | return $this; 337 | } 338 | 339 | protected function resolveAppNamespace() 340 | { 341 | try { 342 | return Container::getInstance() 343 | ->make(Application::class) 344 | ->getNamespace(); 345 | } catch (Throwable) { 346 | return 'App\\'; 347 | } 348 | } 349 | 350 | public static function partialMock() 351 | { 352 | $mock = Mockery::mock(AutoloadManager::class, [null]) 353 | ->makePartial() 354 | ->shouldAllowMockingProtectedMethods(); 355 | 356 | $mock->shouldReceive('isBooted')->andReturn(false); 357 | 358 | return $mock; 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/Support/Domain.php: -------------------------------------------------------------------------------- 1 | replace(['\\', '/'], '.') 32 | ->explode('.') 33 | ->filter(); 34 | 35 | $domain = $parts->shift(); 36 | 37 | if ($parts->count() > 0) { 38 | $subdomain = $parts->implode('.'); 39 | } 40 | } 41 | 42 | $domain = str($domain)->trim('\\/')->toString(); 43 | 44 | $subdomain = str($subdomain)->trim('\\/')->replace('.', '/')->toString(); 45 | 46 | $this->domainWithSubdomain = str($domain) 47 | ->when($subdomain, fn ($domain) => $domain->append("\\{$subdomain}")) 48 | ->toString(); 49 | 50 | $this->domain = $domain; 51 | 52 | $this->subdomain = $subdomain ?: null; 53 | 54 | $this->dotName = $this->subdomain 55 | ? "{$this->domain}.{$this->subdomain}" 56 | : $this->domain; 57 | 58 | $this->layer = DomainResolver::resolveLayer($this->domainWithSubdomain); 59 | 60 | $this->path = $this->layer->path; 61 | 62 | $this->migrationPath = Path::join($this->path, config('ddd.namespaces.migration', 'Database/Migrations')); 63 | } 64 | 65 | protected function getDomainBasePath() 66 | { 67 | return app()->basePath(DomainResolver::domainPath()); 68 | } 69 | 70 | public function path(?string $path = null): string 71 | { 72 | if (is_null($path)) { 73 | return $this->path; 74 | } 75 | 76 | $resolvedPath = str($path) 77 | ->replace($this->layer->namespace, '') 78 | ->replace(['\\', '/'], DIRECTORY_SEPARATOR) 79 | ->append('.php') 80 | ->toString(); 81 | 82 | return Path::join($this->path, $resolvedPath); 83 | } 84 | 85 | public function pathInApplicationLayer(?string $path = null): string 86 | { 87 | if (is_null($path)) { 88 | return $this->path; 89 | } 90 | 91 | $path = str($path) 92 | ->replace(DomainResolver::applicationLayerRootNamespace(), '') 93 | ->replace(['\\', '/'], DIRECTORY_SEPARATOR) 94 | ->append('.php') 95 | ->toString(); 96 | 97 | return Path::join(DomainResolver::applicationLayerPath(), $path); 98 | } 99 | 100 | public function relativePath(string $path = ''): string 101 | { 102 | return collect([$this->domain, $path])->filter()->implode(DIRECTORY_SEPARATOR); 103 | } 104 | 105 | public function rootNamespace(): string 106 | { 107 | return $this->layer->namespace; 108 | } 109 | 110 | public function intendedLayerFor(string $type) 111 | { 112 | return DomainResolver::resolveLayer($this->domainWithSubdomain, $type); 113 | } 114 | 115 | public function namespaceFor(string $type, ?string $name = null): string 116 | { 117 | return DomainResolver::getDomainObjectNamespace($this->domainWithSubdomain, $type, $name); 118 | } 119 | 120 | public function guessNamespaceFromName(string $name): string 121 | { 122 | $baseName = class_basename($name); 123 | 124 | return str($name) 125 | ->before($baseName) 126 | ->trim('\\') 127 | ->prepend(DomainResolver::domainRootNamespace().'\\'.$this->domainWithSubdomain.'\\') 128 | ->toString(); 129 | } 130 | 131 | public function object(string $type, string $name, bool $absolute = false): DomainObject 132 | { 133 | $layer = $this->intendedLayerFor($type); 134 | 135 | $namespace = match (true) { 136 | $absolute => $layer->namespace, 137 | str($name)->startsWith('\\') => $layer->guessNamespaceFromName($name), 138 | default => $layer->namespaceFor($type), 139 | }; 140 | 141 | $baseName = str($name)->replace($namespace, '') 142 | ->replace(['\\', '/'], '\\') 143 | ->trim('\\') 144 | ->when($type === 'factory', fn ($name) => $name->finish('Factory')) 145 | ->toString(); 146 | 147 | $fullyQualifiedName = $namespace.'\\'.$baseName; 148 | 149 | return new DomainObject( 150 | name: $baseName, 151 | domain: $this->domain, 152 | namespace: $namespace, 153 | fullyQualifiedName: $fullyQualifiedName, 154 | path: $layer->path($fullyQualifiedName), 155 | type: $type 156 | ); 157 | } 158 | 159 | public function model(string $name): DomainObject 160 | { 161 | return $this->object('model', $name); 162 | } 163 | 164 | public function factory(string $name): DomainObject 165 | { 166 | return $this->object('factory', $name); 167 | } 168 | 169 | public function dataTransferObject(string $name): DomainObject 170 | { 171 | return $this->object('data_transfer_object', $name); 172 | } 173 | 174 | public function dto(string $name): DomainObject 175 | { 176 | return $this->dataTransferObject($name); 177 | } 178 | 179 | public function viewModel(string $name): DomainObject 180 | { 181 | return $this->object('view_model', $name); 182 | } 183 | 184 | public function valueObject(string $name): DomainObject 185 | { 186 | return $this->object('value_object', $name); 187 | } 188 | 189 | public function action(string $name): DomainObject 190 | { 191 | return $this->object('action', $name); 192 | } 193 | 194 | public function cast(string $name): DomainObject 195 | { 196 | return $this->object('cast', $name); 197 | } 198 | 199 | public function command(string $name): DomainObject 200 | { 201 | return $this->object('command', $name); 202 | } 203 | 204 | public function enum(string $name): DomainObject 205 | { 206 | return $this->object('enum', $name); 207 | } 208 | 209 | public function job(string $name): DomainObject 210 | { 211 | return $this->object('job', $name); 212 | } 213 | 214 | public function mail(string $name): DomainObject 215 | { 216 | return $this->object('mail', $name); 217 | } 218 | 219 | public function notification(string $name): DomainObject 220 | { 221 | return $this->object('notification', $name); 222 | } 223 | 224 | public function resource(string $name): DomainObject 225 | { 226 | return $this->object('resource', $name); 227 | } 228 | 229 | public function rule(string $name): DomainObject 230 | { 231 | return $this->object('rule', $name); 232 | } 233 | 234 | public function event(string $name): DomainObject 235 | { 236 | return $this->object('event', $name); 237 | } 238 | 239 | public function exception(string $name): DomainObject 240 | { 241 | return $this->object('exception', $name); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Support/DomainCache.php: -------------------------------------------------------------------------------- 1 | filter(fn ($path) => is_dir($path)) 38 | ->toArray(); 39 | } 40 | 41 | public static function discoverPaths(): array 42 | { 43 | $configValue = config('ddd.autoload.migrations', true); 44 | 45 | if ($configValue === false) { 46 | return []; 47 | } 48 | 49 | $paths = static::filterDirectories([ 50 | app()->basePath(DomainResolver::domainPath()), 51 | ]); 52 | 53 | if (empty($paths)) { 54 | return []; 55 | } 56 | 57 | $finder = static::finder($paths); 58 | 59 | return Lody::filesFromFinder($finder) 60 | ->map(fn ($file) => Path::normalize($file->getPath())) 61 | ->unique() 62 | ->values() 63 | ->toArray(); 64 | } 65 | 66 | protected static function finder(array $paths) 67 | { 68 | $filter = function (SplFileInfo $file) { 69 | $configuredMigrationFolder = static::domainMigrationFolder(); 70 | 71 | $relativePath = Path::normalize($file->getRelativePath()); 72 | 73 | return Str::endsWith($relativePath, $configuredMigrationFolder); 74 | }; 75 | 76 | return Finder::create() 77 | ->files() 78 | ->in($paths) 79 | ->filter($filter); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Support/DomainResolver.php: -------------------------------------------------------------------------------- 1 | basePath(static::domainPath().'/*'), GLOB_ONLYDIR); 16 | 17 | return collect($folders) 18 | ->map(fn ($path) => basename($path)) 19 | ->sort() 20 | ->toArray(); 21 | } 22 | 23 | /** 24 | * Get the current configured domain path. 25 | */ 26 | public static function domainPath(): ?string 27 | { 28 | return config('ddd.domain_path'); 29 | } 30 | 31 | /** 32 | * Get the current configured root domain namespace. 33 | */ 34 | public static function domainRootNamespace(): ?string 35 | { 36 | return config('ddd.domain_namespace'); 37 | } 38 | 39 | /** 40 | * Get the current configured application layer path. 41 | */ 42 | public static function applicationLayerPath(): ?string 43 | { 44 | return config('ddd.application_path'); 45 | } 46 | 47 | /** 48 | * Get the current configured root application layer namespace. 49 | */ 50 | public static function applicationLayerRootNamespace(): ?string 51 | { 52 | return config('ddd.application_namespace'); 53 | } 54 | 55 | /** 56 | * Resolve the relative domain object namespace. 57 | * 58 | * @param string $type The domain object type. 59 | */ 60 | public static function getRelativeObjectNamespace(string $type): string 61 | { 62 | return config("ddd.namespaces.{$type}", str($type)->plural()->studly()->toString()); 63 | } 64 | 65 | /** 66 | * Determine whether a given object type is part of the application layer. 67 | */ 68 | public static function isApplicationLayer(string $type): bool 69 | { 70 | $filter = app('ddd')->getApplicationLayerFilter() ?? function (string $type) { 71 | $applicationObjects = config('ddd.application_objects', ['controller', 'request']); 72 | 73 | return in_array($type, $applicationObjects); 74 | }; 75 | 76 | return $filter($type); 77 | } 78 | 79 | /** 80 | * Resolve the root namespace for a given domain object type. 81 | * 82 | * @param string $type The domain object type. 83 | */ 84 | public static function resolveRootNamespace(string $type): ?string 85 | { 86 | return static::isApplicationLayer($type) 87 | ? static::applicationLayerRootNamespace() 88 | : static::domainRootNamespace(); 89 | } 90 | 91 | /** 92 | * Resolve the intended layer of a specified domain name keyword. 93 | */ 94 | public static function resolveLayer(string $domain, ?string $type = null): ?Layer 95 | { 96 | $layers = config('ddd.layers', []); 97 | 98 | // Objects in the application layer take precedence 99 | if ($type && static::isApplicationLayer($type)) { 100 | return new Layer( 101 | static::applicationLayerRootNamespace().'\\'.$domain, 102 | Path::join(static::applicationLayerPath(), $domain), 103 | LayerType::Application, 104 | ); 105 | } 106 | 107 | return match (true) { 108 | array_key_exists($domain, $layers) 109 | && is_string($layers[$domain]) => new Layer($domain, $layers[$domain], LayerType::Custom), 110 | 111 | default => new Layer( 112 | static::domainRootNamespace().'\\'.$domain, 113 | Path::join(static::domainPath(), $domain), 114 | LayerType::Domain, 115 | ) 116 | }; 117 | } 118 | 119 | /** 120 | * Get the fully qualified namespace for a domain object. 121 | * 122 | * @param string $domain The domain name. 123 | * @param string $type The domain object type. 124 | * @param string|null $name The domain object name. 125 | */ 126 | public static function getDomainObjectNamespace(string $domain, string $type, ?string $name = null): string 127 | { 128 | $resolver = function (string $domain, string $type, ?string $name) { 129 | $layer = static::resolveLayer($domain, $type); 130 | 131 | $namespace = collect([ 132 | $layer->namespace, 133 | static::getRelativeObjectNamespace($type), 134 | ])->filter()->implode('\\'); 135 | 136 | if ($name) { 137 | $namespace .= "\\{$name}"; 138 | } 139 | 140 | return $namespace; 141 | }; 142 | 143 | return $resolver($domain, $type, $name); 144 | } 145 | 146 | /** 147 | * Attempt to resolve the domain of a given domain class. 148 | */ 149 | public static function guessDomainFromClass(string $class): ?string 150 | { 151 | if (! static::isDomainClass($class)) { 152 | // Not a domain object 153 | return null; 154 | } 155 | 156 | $domain = str($class) 157 | ->after(Str::finish(static::domainRootNamespace(), '\\')) 158 | ->before('\\') 159 | ->toString(); 160 | 161 | return $domain; 162 | } 163 | 164 | /** 165 | * Attempt to resolve the file path of a given domain class. 166 | */ 167 | public static function guessPathFromClass(string $class): ?string 168 | { 169 | if (! static::isDomainClass($class)) { 170 | // Not a domain object 171 | return null; 172 | } 173 | 174 | $classWithoutDomainRoot = str($class) 175 | ->after(Str::finish(static::domainRootNamespace(), '\\')) 176 | ->toString(); 177 | 178 | return Path::join(...[static::domainPath(), "{$classWithoutDomainRoot}.php"]); 179 | } 180 | 181 | /** 182 | * Attempt to resolve the folder of a given domain class. 183 | */ 184 | public static function guessFolderFromClass(string $class): ?string 185 | { 186 | $path = static::guessPathFromClass($class); 187 | 188 | if (! $path) { 189 | return null; 190 | } 191 | 192 | $filenamePortion = basename($path); 193 | 194 | return Str::beforeLast($path, $filenamePortion); 195 | } 196 | 197 | /** 198 | * Determine whether a class is an object within the domain layer. 199 | * 200 | * @param string $class The fully qualified class name. 201 | */ 202 | public static function isDomainClass(string $class): bool 203 | { 204 | return str($class)->startsWith(Str::finish(static::domainRootNamespace(), '\\')); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Support/GeneratorBlueprint.php: -------------------------------------------------------------------------------- 1 | command = new CommandContext($commandName, $arguments, $options); 40 | 41 | $this->nameInput = str($nameInput)->toString(); 42 | 43 | $this->isAbsoluteName = str($this->nameInput)->startsWith('/'); 44 | 45 | $this->type = $this->guessObjectType(); 46 | 47 | $this->normalizedName = Path::normalizeNamespace( 48 | str($nameInput) 49 | ->studly() 50 | ->replace(['.', '\\', '/'], '\\') 51 | ->trim('\\') 52 | ->when($this->type === 'factory', fn ($name) => $name->finish('Factory')) 53 | ->toString() 54 | ); 55 | 56 | $this->baseName = class_basename($this->normalizedName); 57 | 58 | $this->domain = new Domain($domainName); 59 | 60 | $this->domainName = $this->domain->domainWithSubdomain; 61 | 62 | $this->layer = DomainResolver::resolveLayer($this->domainName, $this->type); 63 | 64 | $this->schema = $this->resolveSchema(); 65 | } 66 | 67 | public static function capture(Command $command) {} 68 | 69 | protected function guessObjectType(): string 70 | { 71 | return match ($this->command->name) { 72 | 'ddd:base-view-model' => 'view_model', 73 | 'ddd:base-model' => 'model', 74 | 'ddd:value' => 'value_object', 75 | 'ddd:dto' => 'data_transfer_object', 76 | 'ddd:migration' => 'migration', 77 | default => str($this->command->name)->after(':')->snake()->toString(), 78 | }; 79 | } 80 | 81 | protected function resolveSchema(): ObjectSchema 82 | { 83 | $customResolver = app('ddd')->getObjectSchemaResolver(); 84 | 85 | $blueprint = is_callable($customResolver) 86 | ? App::call($customResolver, [ 87 | 'domainName' => $this->domainName, 88 | 'nameInput' => $this->nameInput, 89 | 'type' => $this->type, 90 | 'command' => $this->command, 91 | ]) 92 | : null; 93 | 94 | if ($blueprint instanceof ObjectSchema) { 95 | return $blueprint; 96 | } 97 | 98 | $namespace = match (true) { 99 | $this->isAbsoluteName => $this->layer->namespace, 100 | str($this->nameInput)->startsWith('\\') => $this->layer->guessNamespaceFromName($this->nameInput), 101 | default => $this->layer->namespaceFor($this->type), 102 | }; 103 | 104 | $fullyQualifiedName = str($this->normalizedName) 105 | ->start($namespace.'\\') 106 | ->toString(); 107 | 108 | return new ObjectSchema( 109 | name: $this->normalizedName, 110 | namespace: $namespace, 111 | fullyQualifiedName: $fullyQualifiedName, 112 | path: $this->layer->path($fullyQualifiedName), 113 | ); 114 | } 115 | 116 | public function rootNamespace() 117 | { 118 | return str($this->schema->namespace)->finish('\\')->toString(); 119 | } 120 | 121 | public function getDefaultNamespace($rootNamespace) 122 | { 123 | return $this->schema->namespace; 124 | } 125 | 126 | public function getPath($name) 127 | { 128 | return Path::normalize(app()->basePath($this->schema->path)); 129 | } 130 | 131 | public function qualifyClass($name) 132 | { 133 | return $this->schema->fullyQualifiedName; 134 | } 135 | 136 | public function getFactoryFor(string $name) 137 | { 138 | return $this->domain->factory($name); 139 | } 140 | 141 | public function getMigrationPath() 142 | { 143 | return $this->domain->migrationPath; 144 | } 145 | 146 | public function getNamespaceFor($type, $name = null) 147 | { 148 | return $this->domain->namespaceFor($type, $name); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Support/Layer.php: -------------------------------------------------------------------------------- 1 | namespace = Path::normalizeNamespace(Str::replaceEnd('\\', '', $namespace)); 20 | 21 | $this->path = is_null($path) 22 | ? Path::fromNamespace($this->namespace) 23 | : Path::normalize(Str::replaceEnd('/', '', $path)); 24 | } 25 | 26 | public static function fromNamespace(string $namespace): self 27 | { 28 | return new self($namespace); 29 | } 30 | 31 | public function path(?string $path = null): string 32 | { 33 | if (is_null($path)) { 34 | return $this->path; 35 | } 36 | 37 | $baseName = class_basename($path); 38 | 39 | $relativePath = str($path) 40 | ->beforeLast($baseName) 41 | ->replaceStart($this->namespace, '') 42 | ->replace(['\\', '/'], DIRECTORY_SEPARATOR) 43 | ->append($baseName) 44 | ->finish('.php') 45 | ->toString(); 46 | 47 | return Path::join($this->path, $relativePath); 48 | } 49 | 50 | public function namespaceFor(string $type, ?string $name = null): string 51 | { 52 | $namespace = collect([ 53 | $this->namespace, 54 | DomainResolver::getRelativeObjectNamespace($type), 55 | ])->filter()->implode('\\'); 56 | 57 | if ($name) { 58 | $namespace .= "\\{$name}"; 59 | } 60 | 61 | return Path::normalizeNamespace($namespace); 62 | } 63 | 64 | public function guessNamespaceFromName(string $name): string 65 | { 66 | $baseName = class_basename($name); 67 | 68 | return Path::normalizeNamespace( 69 | str($name) 70 | ->before($baseName) 71 | ->trim('\\') 72 | ->prepend($this->namespace.'\\') 73 | ->toString() 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Support/Path.php: -------------------------------------------------------------------------------- 1 | replace(['\\', '/'], DIRECTORY_SEPARATOR) 25 | ->when($classname, fn ($s) => $s->append("{$classname}.php")) 26 | ->toString(); 27 | } 28 | 29 | public static function filePathToNamespace(string $path, string $namespacePath, string $namespace): string 30 | { 31 | return str_replace( 32 | [base_path().'/'.$namespacePath, '/', '.php'], 33 | [$namespace, '\\', ''], 34 | $path 35 | ); 36 | } 37 | 38 | public static function normalizeNamespace(string $namespace): string 39 | { 40 | return str_replace(['\\', '/'], '\\', $namespace); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ValueObjects/CommandContext.php: -------------------------------------------------------------------------------- 1 | options); 16 | } 17 | 18 | public function option(string $key): mixed 19 | { 20 | return data_get($this->options, $key); 21 | } 22 | 23 | public function hasArgument(string $key): bool 24 | { 25 | return array_key_exists($key, $this->arguments); 26 | } 27 | 28 | public function argument(string $key): mixed 29 | { 30 | return data_get($this->arguments, $key); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ValueObjects/DomainNamespaces.php: -------------------------------------------------------------------------------- 1 | when($subdomain, fn ($domain) => $domain->append("\\{$subdomain}")) 33 | ->toString(); 34 | 35 | $root = DomainResolver::domainRootNamespace(); 36 | 37 | $domainNamespace = implode('\\', [$root, $domainWithSubdomain]); 38 | 39 | return new self( 40 | root: $domainNamespace, 41 | models: "{$domainNamespace}\\".config('ddd.namespaces.model', 'Models'), 42 | factories: "Database\\Factories\\{$domainWithSubdomain}", 43 | dataTransferObjects: "{$domainNamespace}\\".config('ddd.namespaces.data_transfer_object', 'Data'), 44 | viewModels: "{$domainNamespace}\\".config('ddd.namespaces.view_model', 'ViewModels'), 45 | valueObjects: "{$domainNamespace}\\".config('ddd.namespaces.value_object', 'ValueObjects'), 46 | actions: "{$domainNamespace}\\".config('ddd.namespaces.action', 'Actions'), 47 | enums: "{$domainNamespace}\\".config('ddd.namespaces.enums', 'Enums'), 48 | events: "{$domainNamespace}\\".config('ddd.namespaces.event', 'Events'), 49 | casts: "{$domainNamespace}\\".config('ddd.namespaces.cast', 'Casts'), 50 | commands: "{$domainNamespace}\\".config('ddd.namespaces.command', 'Commands'), 51 | exceptions: "{$domainNamespace}\\".config('ddd.namespaces.exception', 'Exceptions'), 52 | jobs: "{$domainNamespace}\\".config('ddd.namespaces.job', 'Jobs'), 53 | mail: "{$domainNamespace}\\".config('ddd.namespaces.mail', 'Mail'), 54 | notifications: "{$domainNamespace}\\".config('ddd.namespaces.notification', 'Notifications'), 55 | resources: "{$domainNamespace}\\".config('ddd.namespaces.resource', 'Resources'), 56 | rules: "{$domainNamespace}\\".config('ddd.namespaces.rule', 'Rules'), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ValueObjects/DomainObject.php: -------------------------------------------------------------------------------- 1 | config("ddd.namespaces.{$objectType}")] 33 | : config('ddd.namespaces', []); 34 | 35 | foreach ($possibleObjectNamespaces as $type => $namespace) { 36 | if (blank($namespace)) { 37 | continue; 38 | } 39 | 40 | $rootObjectNamespace = preg_quote($namespace); 41 | 42 | $pattern = "/({$rootObjectNamespace})(.*)$/"; 43 | 44 | $result = preg_match($pattern, $fullyQualifiedClass, $matches); 45 | 46 | if (! $result) { 47 | continue; 48 | } 49 | 50 | $objectNamespace = str(data_get($matches, 1))->toString(); 51 | 52 | $objectName = str(data_get($matches, 2)) 53 | ->trim('\\') 54 | ->toString(); 55 | 56 | $objectType = $type; 57 | 58 | break; 59 | } 60 | 61 | // If there wasn't a resolvable namespace, we'll treat it 62 | // as a root-level domain object. 63 | if (! $objectNamespace) { 64 | // Examples: 65 | // - Domain\Invoicing\[Nested\Thing] 66 | // - Domain\Invoicing\[Deeply\Nested\Thing] 67 | // - Domain\Invoicing\[Thing] 68 | $objectName = str($fullyQualifiedClass) 69 | ->after(Str::finish(DomainResolver::domainRootNamespace(), '\\')) 70 | ->after('\\') 71 | ->toString(); 72 | } 73 | 74 | // Extract the domain portion 75 | $domainName = str($fullyQualifiedClass) 76 | ->after(Str::finish(DomainResolver::domainRootNamespace(), '\\')) 77 | ->before("\\{$objectNamespace}") 78 | ->toString(); 79 | 80 | // Edge case to handle root-level domain objects 81 | if ( 82 | $objectName === $objectNamespace 83 | && ! str($fullyQualifiedClass)->endsWith("{$objectNamespace}\\{$objectName}") 84 | ) { 85 | $objectNamespace = ''; 86 | } 87 | 88 | // Reconstruct the path 89 | $path = Path::join( 90 | DomainResolver::domainPath(), 91 | $domainName, 92 | $objectNamespace, 93 | "{$objectName}.php", 94 | ); 95 | 96 | // dump([ 97 | // 'fullyQualifiedClass' => $fullyQualifiedClass, 98 | // 'fullNamespace' => $fullNamespace, 99 | // 'domainName' => $domainName, 100 | // 'objectNamespace' => $objectNamespace, 101 | // 'objectName' => $objectName, 102 | // 'objectType' => $objectType, 103 | // ]); 104 | 105 | return new self( 106 | name: $objectName, 107 | domain: $domainName, 108 | namespace: $objectNamespace, 109 | fullyQualifiedName: $fullyQualifiedClass, 110 | path: $path, 111 | type: $objectType, 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/ValueObjects/DomainObjectNamespace.php: -------------------------------------------------------------------------------- 1 | when($subdomain, fn ($domain) => $domain->append("\\{$subdomain}")) 19 | ->toString(); 20 | 21 | $root = DomainResolver::domainRootNamespace(); 22 | 23 | $domainNamespace = implode('\\', [$root, $domainWithSubdomain]); 24 | 25 | $namespace = "{$domainNamespace}\\".config("ddd.namespaces.{$key}", Str::studly($key)); 26 | 27 | return new self(type: $key, namespace: $namespace); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ValueObjects/ObjectSchema.php: -------------------------------------------------------------------------------- 1 | getMethods()) 22 | ->reject( 23 | fn (ReflectionMethod $method) => in_array($method->getName(), [ 24 | '__construct', 'make', 'toArray', 25 | ...$this->hidden, 26 | ]) 27 | ) 28 | ->filter(fn (ReflectionMethod $method) => in_array('public', Reflection::getModifierNames($method->getModifiers()))) 29 | ->mapWithKeys(fn (ReflectionMethod $method) => [ 30 | $method->getName() => $this->{$method->getName()}(), 31 | ]) 32 | ->toArray(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /stubs/dto.stub: -------------------------------------------------------------------------------- 1 |