diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 5698f95..e904482 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -54,4 +54,4 @@ jobs:
run: composer show -D
- name: Execute tests
- run: vendor/bin/pest
+ run: vendor/bin/pest --ci
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a7dfed..80cc295 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,32 +4,42 @@ All notable changes to `laravel-ddd` will be documented in this file.
## [Unreleased]
### Breaking
-- Stubs are now published to `/stubs/ddd/*` instead of `resources/stubs/ddd/*`. If you have ddd stubs published from a prior version, they should be relocated.
+- 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.
+- Published stubs now use `.stub` extension instead of `.php.stub` (following Laravel's convention).
+- If you are using published stubs from pre 1.2, you will need to refactor your stubs accordingly.
### Added
-- Ability to configure the Application Layer, to generate domain objects that don't typically belong inside the domain layer.
+- Support for the Application Layer, to generate domain-specific objects that don't belong directly in the domain layer:
```php
// In config/ddd.php
- 'application' => [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
- 'objects' => [
- 'controller',
- 'request',
- 'middleware',
- ],
+ 'application_path' => 'app/Modules',
+ 'application_namespace' => 'App\Modules',
+ 'application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
],
```
-- Added `ddd:controller` to generate domain-specific controllers in the application layer.
-- Added `ddd:request` to generate domain-spefic requests in the application layer.
-- Added `ddd:middleware` to generate domain-specific middleware in the application layer.
+- Support for Custom Layers, additional top-level namespaces of your choosing, such as `Infrastructure`, `Integrations`, etc.:
+ ```php
+ // In config/ddd.php
+ 'layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ ],
+ ```
+- Added config utility command `ddd:config` to help manage the package's configuration over time.
+- Added stub utility command `ddd:stub` to publish one or more stubs selectively.
+- Added `ddd:controller` to generate domain-specific controllers.
+- Added `ddd:request` to generate domain-spefic requests.
+- Added `ddd:middleware` to generate domain-specific middleware.
- Added `ddd:migration` to generate domain migrations.
- Added `ddd:seeder` to generate domain seeders.
-- Added `ddd:stub` to list, search, and publish one or more stubs as needed.
+- Added `ddd:stub` to manage stubs.
- Migration folders across domains will be registered and scanned when running `php artisan migrate`, in addition to the standard application `database/migrations` path.
+- Ability to customize generator object naming conventions with your own logic using `DDD::resolveObjectSchemaUsing()`.
### Changed
-- `ddd:model` now internally extends Laravel's native `make:model` and inherits all standard options:
+- `ddd:model` now extends Laravel's native `make:model` and inherits all standard options:
- `--migration|-m`
- `--factory|-f`
- `--seed|-s`
@@ -39,10 +49,11 @@ All notable changes to `laravel-ddd` will be documented in this file.
- `--all|-a`
- `--pivot|-p`
- `ddd:cache` is now `ddd:optimize` (`ddd:cache` is still available as an alias).
-- For Laravel 11.27.1+, the framework's `optimize` and `optimize:clear` commands will automatically invoke `ddd:optimize` and `ddd:clear` respectively.
+- Since Laravel 11.27.1, the framework's `optimize` and `optimize:clear` commands will automatically invoke `ddd:optimize` (`ddd:cache`) and `ddd:clear` respectively.
### Deprecated
- Domain base models are no longer required by default, and `config('ddd.base_model')` is now `null` by default.
+- Stubs are no longer published via `php artisan vendor:publish --tag="ddd-stubs"`. Instead, use `php artisan ddd:stub` to manage them.
## [1.1.3] - 2024-11-05
### Chore
diff --git a/README.md b/README.md
index 0906314..11905a1 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
-# Domain Driven Design toolkit for Laravel
+# Domain Driven Design Toolkit for Laravel
[![Latest Version on Packagist](https://img.shields.io/packagist/v/lunarstorm/laravel-ddd.svg?style=flat-square)](https://packagist.org/packages/lunarstorm/laravel-ddd)
[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/lunarstorm/laravel-ddd/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/lunarstorm/laravel-ddd/actions?query=workflow%3Arun-tests+branch%3Amain)
[![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/lunarstorm/laravel-ddd/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/lunarstorm/laravel-ddd/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain)
[![Total Downloads](https://img.shields.io/packagist/dt/lunarstorm/laravel-ddd.svg?style=flat-square)](https://packagist.org/packages/lunarstorm/laravel-ddd)
-Laravel-DDD is a toolkit to support domain driven design (DDD) in Laravel applications. One of the pain points when adopting DDD is the inability to use Laravel's native `make` commands to generate domain objects since they are typically stored 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.
+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.
## Installation
You can install the package via composer:
@@ -21,24 +21,17 @@ php artisan ddd:install
### Peer Dependencies
The following additional packages are suggested (but not required) while working with this package.
+- Data Transfer Objects: [spatie/laravel-data](https://github.com/spatie/laravel-data)
+- Actions: [lorisleiva/laravel-actions](https://github.com/lorisleiva/laravel-actions)
-Data Transfer Objects: [spatie/laravel-data](https://github.com/spatie/laravel-data)
-```bash
-composer require spatie/laravel-data
-```
-
-Actions: [lorisleiva/laravel-actions](https://github.com/lorisleiva/laravel-actions)
-```bash
-composer require lorisleiva/laravel-actions
-```
-The default DTO and Action stubs of this package reference classes from these packages. If this doesn't apply to your application, you may [customize the stubs](#publishing-stubs-advanced) accordingly.
+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.
### Deployment
In production, run `ddd:optimize` during the deployment process to [optimize autoloading](#autoloading-in-production).
```bash
php artisan ddd:optimize
```
-Since Laravel 11.27.1, `php artisan optimize` automatically invokes `ddd:optimize`. If you already run `optimize` in production, a separate `ddd:optimize` is no longer necessary.
+Since Laravel 11.27.1, `php artisan optimize` automatically invokes `ddd:optimize`. If you already run `optimize` in production, a separate `ddd:optimize` is no longer necessary. In previous versions of this package, this command was named `ddd:cache`, which will continue to work as an alias.
### Version Compatibility
Laravel | LaravelDDD | |
@@ -47,7 +40,7 @@ Since Laravel 11.27.1, `php artisan optimize` automatically invokes `ddd:optimiz
10.25.x | 1.x |
11.x | 1.x |
-See **[UPGRADING](UPGRADING.md)** for more details about upgrading from 0.x.
+See **[UPGRADING](UPGRADING.md)** for more details about upgrading across different versions.
@@ -104,6 +97,36 @@ The following generators are currently available:
Generated objects will be placed in the appropriate domain namespace as specified by `ddd.namespaces.*` in the [config file](#config-file).
+### Config Utility (Since 1.2)
+A configuration utility was introduced in 1.2 to help manage the package's configuration over time.
+```bash
+php artisan ddd:config
+```
+Output:
+```
+ ┌ Laravel-DDD Config Utility ──────────────────────────────────┐
+ │ › ● Run the configuration wizard │
+ │ ○ Update and merge ddd.php with latest package version │
+ │ ○ Detect domain namespace from composer.json │
+ │ ○ Sync composer.json from ddd.php │
+ │ ○ Exit │
+ └──────────────────────────────────────────────────────────────┘
+```
+These config tasks are also invokeable directly using arguments:
+```bash
+# Run the configuration wizard
+php artisan ddd:config wizard
+
+# Update and merge ddd.php with latest package version
+php artisan ddd:config update
+
+# Detect domain namespace from composer.json
+php artisan ddd:config detect
+
+# Sync composer.json from ddd.php
+php artisan ddd:config composer
+```
+
### Other Commands
```bash
# Show a summary of current domains in the domain folder
@@ -121,14 +144,12 @@ php artisan ddd:clear
Some objects interact with the domain layer, but are not part of the domain layer themselves. By default, these include: `controller`, `request`, `middleware`. You may customize the path, namespace, and which `ddd:*` objects belong in the application layer.
```php
// In config/ddd.php
-'application' => [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
- 'objects' => [
- 'controller',
- 'request',
- 'middleware',
- ],
+'application_path' => 'app/Modules',
+'application_namespace' => 'App\Modules',
+'application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
],
```
The configuration above will result in the following:
@@ -151,6 +172,35 @@ Output:
└─ Invoice.php
```
+### Custom Layers (since 1.2)
+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.
+```php
+// In config/ddd.php
+'layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+],
+```
+The configuration above will result in the following:
+```bash
+ddd:model Invoicing:Invoice
+ddd:trait Infrastructure:Concerns/HasExpiryDate
+```
+Output:
+```
+├─ src/Domain
+| └─ Invoicing
+| └─ Models
+| └─ Invoice.php
+├─ src/Infrastructure
+ └─ Concerns
+ └─ HasExpiryDate.php
+```
+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.
+```bash
+# Sync composer.json with ddd.php
+php artisan ddd:config composer
+```
+
### Nested Objects
For any `ddd:*` generator command, nested objects can be specified with forward slashes.
```bash
@@ -177,6 +227,18 @@ php artisan ddd:interface Invoicing:Models/Concerns/HasLineItems
# -> Domain\Invoicing\Models\Concerns\HasLineItems
```
+### Subdomains (nested domains)
+Subdomains can be specified with dot notation wherever a domain option is accepted.
+```bash
+# Domain/Reporting/Internal/ViewModels/MonthlyInvoicesReportViewModel
+php artisan ddd:view-model Reporting.Internal:MonthlyInvoicesReportViewModel
+
+# Domain/Reporting/Customer/ViewModels/MonthlyInvoicesReportViewModel
+php artisan ddd:view-model Reporting.Customer:MonthlyInvoicesReportViewModel
+
+# (supported by all commands where a domain option is accepted)
+```
+
### Overriding Configured Namespaces at Runtime
If for some reason you need to generate a domain object under a namespace different to what is configured in `ddd.namespaces.*`,
you may do so using an absolute name starting with `/`. This will generate the object from the root of the domain.
@@ -198,48 +260,85 @@ php artisan ddd:exception Invoicing:/Models/Exceptions/InvoiceNotFoundException
# -> Domain\Invoicing\Models\Exceptions\InvoiceNotFoundException
```
-### Subdomains (nested domains)
-Subdomains can be specified with dot notation wherever a domain option is accepted.
-```bash
-# Domain/Reporting/Internal/ViewModels/MonthlyInvoicesReportViewModel
-php artisan ddd:view-model Reporting.Internal:MonthlyInvoicesReportViewModel
-
-# Domain/Reporting/Customer/ViewModels/MonthlyInvoicesReportViewModel
-php artisan ddd:view-model Reporting.Customer:MonthlyInvoicesReportViewModel
+### Custom Object Resolution
+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:
+```php
+use Lunarstorm\LaravelDDD\Facades\DDD;
+use Lunarstorm\LaravelDDD\ValueObjects\CommandContext;
+use Lunarstorm\LaravelDDD\ValueObjects\ObjectSchema;
+
+DDD::resolveObjectSchemaUsing(function (string $domainName, string $nameInput, string $type, CommandContext $command): ?ObjectSchema {
+ if ($type === 'controller' && $command->option('api')) {
+ return new ObjectSchema(
+ name: $name = str($nameInput)->replaceEnd('Controller', '')->finish('ApiController')->toString(),
+ namespace: "App\\Api\\Controllers\\{$domainName}",
+ fullyQualifiedName: "App\\Api\\Controllers\\{$domainName}\\{$name}",
+ path: "src/App/Api/Controllers/{$domainName}/{$name}.php",
+ );
+ }
-# (supported by all commands where a domain option is accepted)
+ // Return null to fall back to the default
+ return null;
+});
+```
+The example above will result in the following:
+```bash
+php artisan ddd:controller Invoicing:PaymentController --api
+# Controller [src/App/Api/Controllers/Invoicing/PaymentApiController.php] created successfully.
```
-## Customization
-### Config File
-This package ships with opinionated (but sensible) configuration defaults. You may customize by publishing the [config file](#config-file) and generator stubs as needed:
+
+## Customizing Stubs
+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:
```bash
-php artisan ddd:publish --config
-php artisan ddd:publish --stubs
+php artisan ddd:stub --list
```
-### Publishing Stubs (Advanced)
-For more granular management of stubs, you may use the `ddd:stub` command:
+### Stub Priority
+When generating objects using `ddd:*`, stubs are prioritized as follows:
+- Try `stubs/ddd/*.stub` (customized for `ddd:*` only)
+- Try `stubs/*.stub` (shared by both `make:*` and `ddd:*`)
+- Fallback to the package or framework default
+
+### Publishing Stubs
+To publish stubs interactively, you may use the `ddd:stub` command:
```bash
-# Publish one or more stubs interactively via prompts
php artisan ddd:stub
-
+```
+```
+ ┌ What do you want to do? ─────────────────────────────────────┐
+ │ › ● Choose stubs to publish │
+ │ ○ Publish all stubs │
+ └──────────────────────────────────────────────────────────────┘
+
+ ┌ Which stub should be published? ─────────────────────────────┐
+ │ policy │
+ ├──────────────────────────────────────────────────────────────┤
+ │ › ◼ policy.plain.stub │
+ │ ◻ policy.stub │
+ └────────────────────────────────────────────────── 1 selected ┘
+ Use the space bar to select options.
+```
+You may also use shortcuts to skip the interactive steps:
+```bash
# Publish all stubs
php artisan ddd:stub --all
-# Publish and overwrite only the files that have already been published
-php artisan ddd:stub --all --existing
-
-# Overwrite any existing files
-php artisan ddd:stub --all --force
-
-# Publish one or more stubs specified as arguments
+# Publish one or more stubs specified as arguments (see ddd:stub --list)
php artisan ddd:stub model
php artisan ddd:stub model dto action
php artisan ddd:stub controller controller.plain controller.api
+
+# Options:
+
+# Publish and overwrite only the files that have already been published
+php artisan ddd:stub ... --existing
+
+# Overwrite any existing files
+php artisan ddd:stub ... --force
```
-To publish multiple related stubs at once, use `*` or `.` as a wildcard ending.
+To publish multiple stubs with common prefixes at once, use `*` or `.` as a wildcard ending to indicate "stubs that starts with":
```bash
php artisan ddd:stub listener.
```
@@ -250,10 +349,6 @@ Publishing /stubs/ddd/listener.queued.stub
Publishing /stubs/ddd/listener.typed.stub
Publishing /stubs/ddd/listener.stub
```
-For a quick reference of available stubs, use the `--list` option:
-```bash
-php artisan ddd:stub --list
-```
## Domain Autoloading and Discovery
Autoloading behaviour can be configured with the `ddd.autoload` configuration option. By default, domain providers, commands, policies, and factories are auto-discovered and registered.
@@ -336,60 +431,56 @@ return [
/*
|--------------------------------------------------------------------------
- | Domain Path
+ | Domain Layer
|--------------------------------------------------------------------------
|
- | The path to the domain folder relative to the application root.
+ | The path and namespace of the domain layer.
|
*/
'domain_path' => 'src/Domain',
+ 'domain_namespace' => 'Domain',
/*
|--------------------------------------------------------------------------
- | Domain Namespace
+ | Application Layer
|--------------------------------------------------------------------------
|
- | The root domain namespace.
+ | The path and namespace of the application layer, and the objects
+ | that should be recognized as part of the application layer.
|
*/
- 'domain_namespace' => 'Domain',
+ 'application_path' => 'app/Modules',
+ 'application_namespace' => 'App\Modules',
+ 'application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
+ ],
/*
|--------------------------------------------------------------------------
- | Application Layer
+ | Custom Layers
|--------------------------------------------------------------------------
|
- | Configure objects that belong in the application layer.
+ | Additional top-level namespaces and paths that should be recognized as
+ | layers when generating ddd:* objects.
|
- | e.g., App\Modules\Invoicing\Controllers\*
- | App\Modules\Invoicing\Requests\*
+ | e.g., 'Infrastructure' => 'src/Infrastructure',
|
*/
- 'application' => [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
-
- // Specify which ddd:* objects belong in the application layer
- 'objects' => [
- 'controller',
- 'request',
- 'middleware',
- ],
+ 'layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ // 'Integrations' => 'src/Integrations',
+ // 'Support' => 'src/Support',
],
/*
|--------------------------------------------------------------------------
- | Generator Object Namespaces
+ | Object Namespaces
|--------------------------------------------------------------------------
|
- | This array maps the default relative namespaces of generated objects
- | relative to their domain's root namespace.
- |
- | e.g., Domain\Invoicing\Models\*
- | Domain\Invoicing\Data\*
- | Domain\Invoicing\ViewModels\*
- | Domain\Invoicing\ValueObjects\*
- | Domain\Invoicing\Actions\*
+ | This value contains the default namespaces of ddd:* generated
+ | objects relative to the layer of which the object belongs to.
|
*/
'namespaces' => [
diff --git a/UPGRADING.md b/UPGRADING.md
index b9177b4..f7f6b04 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -1,6 +1,21 @@
# Upgrading
-
- ## From 0.x to 1.x
+
+## From 1.1.x to 1.2.x
+### Breaking
+- 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.
+- Published stubs now use `.stub` extension instead of `.php.stub` (following Laravel's convention).
+- If you are using published stubs from pre 1.2, you will need to refactor your stubs accordingly.
+
+### Update Config
+- Support for Application Layer and Custom Layers was added, introducing changes to the config file.
+- Run `php artisan ddd:config update` to rebuild your application's published `ddd.php` config to align with the package's latest copy.
+- The update utility will attempt to respect your existing customizations, but you should still review and verify manually.
+
+### Publishing Stubs
+- Old way (removed): `php artisan vendor:publish --tag="ddd-stubs"`
+- New way: `php artisan ddd:stub` (see [Customizing Stubs](README.md#customizing-stubs) in README for more details).
+
+## From 0.x to 1.x
- Minimum required Laravel version is 10.25.
- The ddd generator [command syntax](README.md#usage) in 1.x. Generator commands no longer receive a domain argument. For example, instead of `ddd:action Invoicing CreateInvoice`, one of the following would be used:
- Using the --domain option: ddd:action CreateInvoice --domain=Invoicing (this takes precedence).
diff --git a/composer.json b/composer.json
index fd4f4e5..882a0f7 100644
--- a/composer.json
+++ b/composer.json
@@ -20,13 +20,14 @@
"require": {
"php": "^8.1|^8.2|^8.3",
"illuminate/contracts": "^10.25|^11.0",
- "laravel/prompts": "^0.1.16|^0.3.1",
+ "laravel/pint": "^1.18",
+ "laravel/prompts": "^0.1.16|^0.2|^0.3.1",
"lorisleiva/lody": "^0.5.0",
- "spatie/laravel-package-tools": "^1.13.0"
+ "spatie/laravel-package-tools": "^1.13.0",
+ "symfony/var-exporter": "^6|^7.1"
},
"require-dev": {
"larastan/larastan": "^2.0.1",
- "laravel/pint": "^1.0",
"nunomaduro/collision": "^7.0|^8.1",
"orchestra/testbench": "^8|^9.0",
"pestphp/pest": "^2.34",
@@ -34,7 +35,11 @@
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
- "spatie/laravel-data": "^4.10"
+ "spatie/laravel-data": "^4.11.1"
+ },
+ "suggest": {
+ "spatie/laravel-data": "Recommended for Data Transfer Objects.",
+ "lorisleiva/laravel-actions": "Recommended for Actions."
},
"autoload": {
"psr-4": {
@@ -48,7 +53,9 @@
"App\\": "vendor/orchestra/testbench-core/laravel/app",
"Database\\Factories\\": "vendor/orchestra/testbench-core/laravel/database/factories",
"Database\\Seeders\\": "vendor/orchestra/testbench-core/laravel/database/seeders",
- "Domain\\": "vendor/orchestra/testbench-core/laravel/src/Domain"
+ "Domain\\": "vendor/orchestra/testbench-core/laravel/src/Domain",
+ "Application\\": "vendor/orchestra/testbench-core/laravel/src/Application",
+ "Infrastructure\\": "vendor/orchestra/testbench-core/laravel/src/Infrastructure"
}
},
"scripts": {
@@ -56,6 +63,7 @@
"analyse": "vendor/bin/phpstan analyse",
"test": "@composer dump-autoload && vendor/bin/pest",
"test-coverage": "@composer dump-autoload && vendor/bin/pest --coverage",
+ "purge-skeleton": "vendor/bin/testbench package:purge-skeleton",
"format": "vendor/bin/pint",
"lint": "vendor/bin/pint"
},
@@ -72,7 +80,7 @@
"Lunarstorm\\LaravelDDD\\LaravelDDDServiceProvider"
],
"aliases": {
- "LaravelDDD": "Lunarstorm\\LaravelDDD\\Facades\\LaravelDDD"
+ "DDD": "Lunarstorm\\LaravelDDD\\Facades\\DDD"
}
}
},
diff --git a/config/ddd.php b/config/ddd.php
index dfa4eda..20321d8 100644
--- a/config/ddd.php
+++ b/config/ddd.php
@@ -4,60 +4,56 @@
/*
|--------------------------------------------------------------------------
- | Domain Path
+ | Domain Layer
|--------------------------------------------------------------------------
|
- | The path to the domain folder relative to the application root.
+ | The path and namespace of the domain layer.
|
*/
'domain_path' => 'src/Domain',
+ 'domain_namespace' => 'Domain',
/*
|--------------------------------------------------------------------------
- | Domain Namespace
+ | Application Layer
|--------------------------------------------------------------------------
|
- | The root domain namespace.
+ | The path and namespace of the application layer, and the objects
+ | that should be recognized as part of the application layer.
|
*/
- 'domain_namespace' => 'Domain',
+ 'application_path' => 'app/Modules',
+ 'application_namespace' => 'App\Modules',
+ 'application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
+ ],
/*
|--------------------------------------------------------------------------
- | Application Layer
+ | Custom Layers
|--------------------------------------------------------------------------
|
- | Configure objects that belong in the application layer.
+ | Additional top-level namespaces and paths that should be recognized as
+ | layers when generating ddd:* objects.
|
- | e.g., App\Modules\Invoicing\Controllers\*
- | App\Modules\Invoicing\Requests\*
+ | e.g., 'Infrastructure' => 'src/Infrastructure',
|
*/
- 'application' => [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
-
- // Specify which ddd:* objects belong in the application layer
- 'objects' => [
- 'controller',
- 'request',
- 'middleware',
- ],
+ 'layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ // 'Integrations' => 'src/Integrations',
+ // 'Support' => 'src/Support',
],
/*
|--------------------------------------------------------------------------
- | Generator Object Namespaces
+ | Object Namespaces
|--------------------------------------------------------------------------
|
- | This array maps the default relative namespaces of generated objects
- | relative to their domain's root namespace.
- |
- | e.g., Domain\Invoicing\Models\*
- | Domain\Invoicing\Data\*
- | Domain\Invoicing\ViewModels\*
- | Domain\Invoicing\ValueObjects\*
- | Domain\Invoicing\Actions\*
+ | This value contains the default namespaces of ddd:* generated
+ | objects relative to the layer of which the object belongs to.
|
*/
'namespaces' => [
diff --git a/config/ddd.php.stub b/config/ddd.php.stub
index 612b1d8..911a351 100644
--- a/config/ddd.php.stub
+++ b/config/ddd.php.stub
@@ -4,95 +4,52 @@ return [
/*
|--------------------------------------------------------------------------
- | Domain Path
+ | Domain Layer
|--------------------------------------------------------------------------
|
- | The path to the domain folder relative to the application root.
+ | The path and namespace of the domain layer.
|
*/
'domain_path' => {{domain_path}},
+ 'domain_namespace' => {{domain_namespace}},
/*
|--------------------------------------------------------------------------
- | Domain Namespace
+ | Application Layer
|--------------------------------------------------------------------------
|
- | The root domain namespace.
+ | The path and namespace of the application layer, and the objects
+ | that should be recognized as part of the application layer.
|
*/
- 'domain_namespace' => {{domain_namespace}},
+ 'application_path' => {{application_path}},
+ 'application_namespace' => {{application_namespace}},
+ 'application_objects' => {{application_objects}},
/*
|--------------------------------------------------------------------------
- | Application Layer
+ | Custom Layers
|--------------------------------------------------------------------------
|
- | Configure objects that belong in the application layer.
+ | Additional top-level namespaces and paths that should be recognized as
+ | layers when generating ddd:* objects.
|
- | e.g., App\Modules\Invoicing\Controllers\*
- | App\Modules\Invoicing\Requests\*
+ | e.g., 'Infrastructure' => 'src/Infrastructure',
|
*/
- 'application' => [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
+ 'layers' => {{layers}},
- // Specify which ddd:* objects belong in the application layer
- 'objects' => [
- 'controller',
- 'request',
- 'middleware',
- ],
- ],
/*
|--------------------------------------------------------------------------
- | Domain Object Namespaces
+ | Object Namespaces
|--------------------------------------------------------------------------
|
- | This value contains the default namespaces of generated domain
- | objects relative to the domain namespace of which the object
- | belongs to.
- |
- | e.g., Domain\Invoicing\Models\*
- | Domain\Invoicing\Data\*
- | Domain\Invoicing\ViewModels\*
- | Domain\Invoicing\ValueObjects\*
- | Domain\Invoicing\Actions\*
+ | This value contains the default namespaces of ddd:* generated
+ | objects relative to the layer of which the object belongs to.
|
*/
- 'namespaces' => [
- 'model' => {{namespaces.model}},
- 'data_transfer_object' => {{namespaces.data_transfer_object}},
- 'view_model' => {{namespaces.view_model}},
- 'value_object' => {{namespaces.value_object}},
- 'action' => {{namespaces.action}},
- 'cast' => 'Casts',
- 'class' => '',
- 'channel' => 'Channels',
- 'command' => 'Commands',
- 'controller' => 'Controllers',
- 'enum' => 'Enums',
- 'event' => 'Events',
- 'exception' => 'Exceptions',
- 'factory' => 'Database\Factories',
- 'interface' => '',
- 'job' => 'Jobs',
- 'listener' => 'Listeners',
- 'mail' => 'Mail',
- 'middleware' => 'Middleware',
- 'migration' => 'Database\Migrations',
- 'notification' => 'Notifications',
- 'observer' => 'Observers',
- 'policy' => 'Policies',
- 'provider' => 'Providers',
- 'resource' => 'Resources',
- 'request' => 'Requests',
- 'rule' => 'Rules',
- 'scope' => 'Scopes',
- 'seeder' => 'Database\Seeders',
- 'trait' => '',
- ],
+ 'namespaces' => {{namespaces}},
/*
|--------------------------------------------------------------------------
@@ -152,13 +109,7 @@ return [
| should be auto-discovered and registered.
|
*/
- 'autoload' => [
- 'providers' => true,
- 'commands' => true,
- 'policies' => true,
- 'factories' => true,
- 'migrations' => true,
- ],
+ 'autoload' => {{autoload}},
/*
|--------------------------------------------------------------------------
@@ -175,10 +126,7 @@ return [
| the AppServiceProvider's boot method.
|
*/
- 'autoload_ignore' => [
- 'Tests',
- 'Database/Migrations',
- ],
+ 'autoload_ignore' => {{autoload_ignore}},
/*
|--------------------------------------------------------------------------
@@ -189,5 +137,5 @@ return [
| autoloading.
|
*/
- 'cache_directory' => 'bootstrap/cache/ddd',
+ 'cache_directory' => {{cache_directory}},
];
diff --git a/routes/testing.php b/routes/testing.php
new file mode 100644
index 0000000..667d8d9
--- /dev/null
+++ b/routes/testing.php
@@ -0,0 +1,24 @@
+middleware(['web'])
+ ->as('ddd.')
+ ->group(function () {
+ Route::get('/', function () {
+ return response('home');
+ })->name('home');
+
+ Route::get('/config', function () {
+ return response(config('ddd'));
+ })->name('config');
+
+ Route::get('/autoload', function () {
+ return response([
+ 'providers' => Autoload::getRegisteredProviders(),
+ 'commands' => Autoload::getRegisteredCommands(),
+ ]);
+ })->name('autoload');
+ });
diff --git a/src/Commands/Concerns/CanPromptForDomain.php b/src/Commands/Concerns/CanPromptForDomain.php
deleted file mode 100644
index f4d95e9..0000000
--- a/src/Commands/Concerns/CanPromptForDomain.php
+++ /dev/null
@@ -1,35 +0,0 @@
-mapWithKeys(fn ($name) => [Str::lower($name) => $name]);
-
- // Prompt for the domain
- $domainName = suggest(
- label: 'What is the domain?',
- options: fn ($value) => collect($choices)
- ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true))
- ->toArray(),
- placeholder: 'Start typing to search...',
- required: true
- );
-
- // Normalize the case of the domain name
- // if it is an existing domain.
- if ($match = $choices->get(Str::lower($domainName))) {
- $domainName = $match;
- }
-
- return $domainName;
- }
-}
diff --git a/src/Commands/Concerns/ForwardsToDomainCommands.php b/src/Commands/Concerns/ForwardsToDomainCommands.php
index 6586bd7..2b753f9 100644
--- a/src/Commands/Concerns/ForwardsToDomainCommands.php
+++ b/src/Commands/Concerns/ForwardsToDomainCommands.php
@@ -18,42 +18,42 @@ public function call($command, array $arguments = [])
'make:request' => $this->runCommand('ddd:request', [
...$arguments,
'name' => $nameWithSubfolder,
- '--domain' => $this->domain->dotName,
+ '--domain' => $this->blueprint->domain->dotName,
], $this->output),
'make:model' => $this->runCommand('ddd:model', [
...$arguments,
'name' => $nameWithSubfolder,
- '--domain' => $this->domain->dotName,
+ '--domain' => $this->blueprint->domain->dotName,
], $this->output),
'make:factory' => $this->runCommand('ddd:factory', [
...$arguments,
'name' => $nameWithSubfolder,
- '--domain' => $this->domain->dotName,
+ '--domain' => $this->blueprint->domain->dotName,
], $this->output),
'make:policy' => $this->runCommand('ddd:policy', [
...$arguments,
'name' => $nameWithSubfolder,
- '--domain' => $this->domain->dotName,
+ '--domain' => $this->blueprint->domain->dotName,
], $this->output),
'make:migration' => $this->runCommand('ddd:migration', [
...$arguments,
- '--domain' => $this->domain->dotName,
+ '--domain' => $this->blueprint->domain->dotName,
], $this->output),
'make:seeder' => $this->runCommand('ddd:seeder', [
...$arguments,
'name' => $nameWithSubfolder,
- '--domain' => $this->domain->dotName,
+ '--domain' => $this->blueprint->domain->dotName,
], $this->output),
'make:controller' => $this->runCommand('ddd:controller', [
...$arguments,
'name' => $nameWithSubfolder,
- '--domain' => $this->domain->dotName,
+ '--domain' => $this->blueprint->domain->dotName,
], $this->output),
default => $this->runCommand($command, $arguments, $this->output),
diff --git a/src/Commands/Concerns/HasGeneratorBlueprint.php b/src/Commands/Concerns/HasGeneratorBlueprint.php
new file mode 100644
index 0000000..1cf2a36
--- /dev/null
+++ b/src/Commands/Concerns/HasGeneratorBlueprint.php
@@ -0,0 +1,10 @@
+domain) {
+ if ($domain = $this->blueprint->domain) {
$domainModel = $domain->model($model);
return $domainModel->fullyQualifiedName;
diff --git a/src/Commands/Concerns/ResolvesDomainFromInput.php b/src/Commands/Concerns/ResolvesDomainFromInput.php
index 9192aee..efe35a0 100644
--- a/src/Commands/Concerns/ResolvesDomainFromInput.php
+++ b/src/Commands/Concerns/ResolvesDomainFromInput.php
@@ -5,19 +5,19 @@
use Illuminate\Support\Str;
use Lunarstorm\LaravelDDD\Support\Domain;
use Lunarstorm\LaravelDDD\Support\DomainResolver;
-use Lunarstorm\LaravelDDD\Support\Path;
+use Lunarstorm\LaravelDDD\Support\GeneratorBlueprint;
use Symfony\Component\Console\Input\InputOption;
+use function Laravel\Prompts\suggest;
+
trait ResolvesDomainFromInput
{
- use CanPromptForDomain,
- HandleHooks,
+ use HandleHooks,
+ HasGeneratorBlueprint,
QualifiesDomainModels;
protected $nameIsAbsolute = false;
- protected ?Domain $domain = null;
-
protected function getOptions()
{
return [
@@ -28,47 +28,50 @@ protected function getOptions()
protected function rootNamespace()
{
- $type = $this->guessObjectType();
-
- return Str::finish(DomainResolver::resolveRootNamespace($type), '\\');
+ return $this->blueprint->rootNamespace();
}
- protected function guessObjectType(): string
+ protected function getDefaultNamespace($rootNamespace)
{
- return match ($this->name) {
- 'ddd:base-view-model' => 'view_model',
- 'ddd:base-model' => 'model',
- 'ddd:value' => 'value_object',
- 'ddd:dto' => 'data_transfer_object',
- 'ddd:migration' => 'migration',
- default => str($this->name)->after(':')->snake()->toString(),
- };
+ return $this->blueprint
+ ? $this->blueprint->getDefaultNamespace($rootNamespace)
+ : parent::getDefaultNamespace($rootNamespace);
}
- protected function getDefaultNamespace($rootNamespace)
+ protected function getPath($name)
{
- if ($this->domain) {
- return $this->nameIsAbsolute
- ? $this->domain->namespace->root
- : $this->domain->namespaceFor($this->guessObjectType());
- }
+ return $this->blueprint
+ ? $this->blueprint->getPath($name)
+ : parent::getPath($name);
+ }
- return parent::getDefaultNamespace($rootNamespace);
+ protected function qualifyClass($name)
+ {
+ return $this->blueprint->qualifyClass($name);
}
- protected function getPath($name)
+ protected function promptForDomainName(): string
{
- if ($this->domain) {
- return Path::normalize($this->laravel->basePath(
- $this->domain->object(
- type: $this->guessObjectType(),
- name: $name,
- absolute: $this->nameIsAbsolute
- )->path
- ));
+ $choices = collect(DomainResolver::domainChoices())
+ ->mapWithKeys(fn ($name) => [Str::lower($name) => $name]);
+
+ // Prompt for the domain
+ $domainName = suggest(
+ label: 'What is the domain?',
+ options: fn ($value) => collect($choices)
+ ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true))
+ ->toArray(),
+ placeholder: 'Start typing to search...',
+ required: true
+ );
+
+ // Normalize the case of the domain name
+ // if it is an existing domain.
+ if ($match = $choices->get(Str::lower($domainName))) {
+ $domainName = $match;
}
- return parent::getPath($name);
+ return $domainName;
}
protected function beforeHandle()
@@ -84,33 +87,26 @@ protected function beforeHandle()
$nameInput = Str::after($nameInput, ':');
}
- $this->domain = match (true) {
+ $domainName = match (true) {
// Domain was specified explicitly via option (priority)
- filled($this->option('domain')) => new Domain($this->option('domain')),
+ filled($this->option('domain')) => $this->option('domain'),
// Domain was specified as a prefix in the name
- filled($domainExtractedFromName) => new Domain($domainExtractedFromName),
+ filled($domainExtractedFromName) => $domainExtractedFromName,
- default => null,
+ default => $this->promptForDomainName(),
};
- // If the domain is not set, prompt for it
- if (! $this->domain) {
- $this->domain = new Domain($this->promptForDomainName());
- }
-
- // Now that the domain part is handled,
- // we will deal with the name portion.
-
- // Normalize slash and dot separators
- $nameInput = Str::replace(['.', '\\', '/'], '/', $nameInput);
-
- if ($this->nameIsAbsolute = Str::startsWith($nameInput, ['/'])) {
- // $nameInput = Str::after($nameInput, '/');
- }
+ $this->blueprint = new GeneratorBlueprint(
+ commandName: $this->getName(),
+ nameInput: $nameInput,
+ domainName: $domainName,
+ arguments: $this->arguments(),
+ options: $this->options(),
+ );
- $this->input->setArgument('name', $nameInput);
+ $this->input->setArgument('name', $this->blueprint->nameInput);
- app('ddd')->captureCommandContext($this, $this->domain, $this->guessObjectType());
+ $this->input->setOption('domain', $this->blueprint->domainName);
}
}
diff --git a/src/Commands/Concerns/UpdatesComposer.php b/src/Commands/Concerns/UpdatesComposer.php
new file mode 100644
index 0000000..6f094de
--- /dev/null
+++ b/src/Commands/Concerns/UpdatesComposer.php
@@ -0,0 +1,54 @@
+rtrim('/\\')
+ ->finish('\\')
+ ->toString();
+
+ $this->comment("Registering `{$namespace}`:`{$path}` in composer.json...");
+
+ $this->fillComposerValue(['autoload', 'psr-4', $namespace], $path);
+
+ $this->composerReload();
+
+ return $this;
+ }
+
+ protected function composerReload()
+ {
+ $composer = $this->hasOption('composer') ? $this->option('composer') : 'global';
+
+ if ($composer !== 'global') {
+ $command = ['php', $composer, 'dump-autoload'];
+ } else {
+ $command = ['composer', 'dump-autoload'];
+ }
+
+ (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1']))
+ ->setTimeout(null)
+ ->run(function ($type, $output) {
+ $this->output->write($output);
+ });
+
+ return $this;
+ }
+}
diff --git a/src/Commands/ConfigCommand.php b/src/Commands/ConfigCommand.php
new file mode 100644
index 0000000..e4acee0
--- /dev/null
+++ b/src/Commands/ConfigCommand.php
@@ -0,0 +1,392 @@
+composer = DDD::composer()->usingOutput($this->output);
+
+ $action = str($this->argument('action'))->trim()->lower()->toString();
+
+ if (! $action && $this->option('layer')) {
+ $action = 'layers';
+ }
+
+ return match ($action) {
+ 'wizard' => $this->wizard(),
+ 'update' => $this->update(),
+ 'detect' => $this->detect(),
+ 'composer' => $this->syncComposer(),
+ 'layers' => $this->layers(),
+ default => $this->home(),
+ };
+ }
+
+ protected function home(): int
+ {
+ $action = select('Laravel-DDD Config Utility', [
+ 'wizard' => 'Run the configuration wizard',
+ 'update' => 'Update and merge ddd.php with latest package version',
+ 'detect' => 'Detect domain namespace from composer.json',
+ 'composer' => 'Sync composer.json from ddd.php',
+ 'exit' => 'Exit',
+ ], scroll: 10);
+
+ return match ($action) {
+ 'wizard' => $this->wizard(),
+ 'update' => $this->update(),
+ 'detect' => $this->detect(),
+ 'composer' => $this->syncComposer(),
+ 'exit' => $this->exit(),
+ default => $this->exit(),
+ };
+ }
+
+ protected function layers()
+ {
+ $layers = $this->option('layer');
+
+ if ($layers = $this->option('layer')) {
+ foreach ($layers as $layer) {
+ $parts = explode(':', $layer);
+
+ $this->composer->registerPsr4Autoload(
+ namespace: data_get($parts, 0),
+ path: data_get($parts, 1)
+ );
+ }
+
+ $this->composer->saveAndReload();
+ }
+
+ $this->info('Configuration updated.');
+
+ return self::SUCCESS;
+ }
+
+ public static function hasRequiredVersionOfLaravelPrompts(): bool
+ {
+ return function_exists('\Laravel\Prompts\form')
+ && method_exists(\Laravel\Prompts\FormBuilder::class, 'addIf');
+ }
+
+ protected function wizard(): int
+ {
+ if (! static::hasRequiredVersionOfLaravelPrompts()) {
+ $this->error('This command is not supported with your currently installed version of Laravel Prompts.');
+
+ return self::FAILURE;
+ }
+
+ $namespaces = collect($this->composer->getPsr4Namespaces());
+
+ $layers = $namespaces->map(fn ($path, $namespace) => new Layer($namespace, $path));
+ $laravelAppLayer = $layers->first(fn (Layer $layer) => str($layer->namespace)->exactly('App'));
+ $possibleDomainLayers = $layers->filter(fn (Layer $layer) => str($layer->namespace)->startsWith('Domain'));
+ $possibleApplicationLayers = $layers->filter(fn (Layer $layer) => str($layer->namespace)->startsWith('App'));
+
+ $domainLayer = $possibleDomainLayers->first();
+ $applicationLayer = $possibleApplicationLayers->first();
+
+ $detected = collect([
+ 'domain_path' => $domainLayer?->path,
+ 'domain_namespace' => $domainLayer?->namespace,
+ 'application_path' => $applicationLayer?->path,
+ 'application_namespace' => $applicationLayer?->namespace,
+ ]);
+
+ $config = $detected->merge(Config::get('ddd'));
+
+ info('Detected DDD configuration:');
+
+ table(
+ headers: ['Key', 'Value'],
+ rows: $detected->dot()->map(fn ($value, $key) => [$key, $value])->all()
+ );
+
+ $choices = [
+ 'domain_path' => [
+ 'src/Domain' => 'src/Domain',
+ 'src/Domains' => 'src/Domains',
+ ...[
+ $config->get('domain_path') => $config->get('domain_path'),
+ ],
+ ...$possibleDomainLayers->mapWithKeys(
+ fn (Layer $layer) => [$layer->path => $layer->path]
+ ),
+ ],
+ 'domain_namespace' => [
+ 'Domain' => 'Domain',
+ 'Domains' => 'Domains',
+ ...[
+ $config->get('domain_namespace') => $config->get('domain_namespace'),
+ ],
+ ...$possibleDomainLayers->mapWithKeys(
+ fn (Layer $layer) => [$layer->namespace => $layer->namespace]
+ ),
+ ],
+ 'application_path' => [
+ 'app/Modules' => 'app/Modules',
+ 'src/Modules' => 'src/Modules',
+ 'Modules' => 'Modules',
+ 'src/Application' => 'src/Application',
+ 'Application' => 'Application',
+ ...[
+ data_get($config, 'application_path') => data_get($config, 'application_path'),
+ ],
+ ...$possibleApplicationLayers->mapWithKeys(
+ fn (Layer $layer) => [$layer->path => $layer->path]
+ ),
+ ],
+ 'application_namespace' => [
+ 'App\Modules' => 'App\Modules',
+ 'Application' => 'Application',
+ 'Modules' => 'Modules',
+ ...[
+ data_get($config, 'application_namespace') => data_get($config, 'application_namespace'),
+ ],
+ ...$possibleApplicationLayers->mapWithKeys(
+ fn (Layer $layer) => [$layer->namespace => $layer->namespace]
+ ),
+ ],
+ 'layers' => [
+ 'src/Infrastructure' => 'src/Infrastructure',
+ 'src/Integrations' => 'src/Integrations',
+ 'src/Support' => 'src/Support',
+ ],
+ ];
+
+ $form = form()
+ ->add(
+ function ($responses) use ($choices, $detected, $config) {
+ return suggest(
+ label: 'Domain Path',
+ options: $choices['domain_path'],
+ default: $detected->get('domain_path') ?: $config->get('domain_path'),
+ hint: 'The path to the domain layer relative to the base path.',
+ required: true,
+ );
+ },
+ name: 'domain_path'
+ )
+ ->add(
+ function ($responses) use ($choices, $config) {
+ return suggest(
+ label: 'Domain Namespace',
+ options: $choices['domain_namespace'],
+ default: class_basename($responses['domain_path']) ?: $config->get('domain_namespace'),
+ required: true,
+ hint: 'The root domain namespace.',
+ );
+ },
+ name: 'domain_namespace'
+ )
+ ->add(
+ function ($responses) use ($choices) {
+ return suggest(
+ label: 'Path to Application Layer',
+ options: $choices['application_path'],
+ hint: "For objects that don't belong in the domain layer (controllers, form requests, etc.)",
+ placeholder: 'Leave blank to skip and use defaults',
+ scroll: 10,
+ );
+ },
+ name: 'application_path'
+ )
+ ->addIf(
+ fn ($responses) => filled($responses['application_path']),
+ function ($responses) use ($choices, $laravelAppLayer) {
+ $applicationPath = $responses['application_path'];
+ $laravelAppPath = $laravelAppLayer->path;
+
+ $namespace = match (true) {
+ str($applicationPath)->exactly($laravelAppPath) => $laravelAppLayer->namespace,
+ str($applicationPath)->startsWith("{$laravelAppPath}/") => str($applicationPath)->studly()->toString(),
+ default => str($applicationPath)->classBasename()->studly()->toString(),
+ };
+
+ return suggest(
+ label: 'Application Layer Namespace',
+ options: $choices['application_namespace'],
+ default: $namespace,
+ hint: 'The root application namespace.',
+ placeholder: 'Leave blank to use defaults',
+ );
+ },
+ name: 'application_namespace'
+ )
+ ->add(
+ function ($responses) use ($choices) {
+ return multiselect(
+ label: 'Custom Layers (Optional)',
+ options: $choices['layers'],
+ hint: 'Layers can be customized in the ddd.php config file at any time.',
+ );
+ },
+ name: 'layers'
+ );
+
+ $responses = $form->submit();
+
+ $this->info('Building configuration...');
+
+ foreach ($responses as $key => $value) {
+ $responses[$key] = $value ?: $config->get($key);
+ }
+
+ DDD::config()->fill($responses)->save();
+
+ $this->info('Configuration updated: '.config_path('ddd.php'));
+
+ return self::SUCCESS;
+ }
+
+ protected function detect(): int
+ {
+ $search = ['Domain', 'Domains'];
+
+ $detected = [];
+
+ foreach ($search as $namespace) {
+ if ($path = $this->composer->getAutoloadPath($namespace)) {
+ $detected['domain_path'] = $path;
+ $detected['domain_namespace'] = $namespace;
+ break;
+ }
+ }
+
+ $this->info('Detected configuration:');
+
+ table(
+ headers: ['Config', 'Value'],
+ rows: collect($detected)
+ ->map(fn ($value, $key) => [$key, $value])
+ ->all()
+ );
+
+ if (confirm('Update configuration with these values?', true)) {
+ DDD::config()->fill($detected)->save();
+
+ $this->info('Configuration updated: '.config_path('ddd.php'));
+ }
+
+ return self::SUCCESS;
+ }
+
+ protected function update(): int
+ {
+ $config = DDD::config();
+
+ $confirmed = confirm('Are you sure you want to update ddd.php and merge with latest copy from the package?');
+
+ if (! $confirmed) {
+ $this->info('Configuration update aborted.');
+
+ return self::SUCCESS;
+ }
+
+ $this->info('Merging ddd.php...');
+
+ $config->syncWithLatest()->save();
+
+ $this->info('Configuration updated: '.config_path('ddd.php'));
+ $this->warn('Note: Some values may require manual adjustment.');
+
+ return self::SUCCESS;
+ }
+
+ protected function syncComposer(): int
+ {
+ $namespaces = [
+ config('ddd.domain_namespace', 'Domain') => config('ddd.domain_path', 'src/Domain'),
+ config('ddd.application_namespace', 'App\\Modules') => config('ddd.application_path', 'app/Modules'),
+ ...collect(config('ddd.layers', []))
+ ->all(),
+ ];
+
+ $this->info('Syncing composer.json from ddd.php...');
+
+ $results = [];
+
+ $added = 0;
+
+ foreach ($namespaces as $namespace => $path) {
+ if ($this->composer->hasPsr4Autoload($namespace)) {
+ $results[] = [$namespace, $path, 'Already Registered'];
+
+ continue;
+ }
+
+ $rootNamespace = Str::before($namespace, '\\');
+
+ if ($this->composer->hasPsr4Autoload($rootNamespace)) {
+ $results[] = [$namespace, $path, 'Skipped'];
+
+ continue;
+ }
+
+ $this->composer->registerPsr4Autoload($rootNamespace, $path);
+
+ $results[] = [$namespace, $path, 'Added'];
+
+ $added++;
+ }
+
+ if ($added > 0) {
+ $this->composer->saveAndReload();
+ }
+
+ table(
+ headers: ['Namespace', 'Path', 'Status'],
+ rows: $results
+ );
+
+ return self::SUCCESS;
+ }
+
+ protected function exit(): int
+ {
+ $this->info('Goodbye!');
+
+ return self::SUCCESS;
+ }
+}
diff --git a/src/Commands/DomainControllerMakeCommand.php b/src/Commands/DomainControllerMakeCommand.php
index 47f03dc..1cba0c9 100644
--- a/src/Commands/DomainControllerMakeCommand.php
+++ b/src/Commands/DomainControllerMakeCommand.php
@@ -53,7 +53,7 @@ protected function buildFormRequestReplacements(array $replace, $modelClass)
];
if ($this->option('requests')) {
- $namespace = $this->domain->namespaceFor('request', $this->getNameInput());
+ $namespace = $this->blueprint->getNamespaceFor('request', $this->getNameInput());
[$storeRequestClass, $updateRequestClass] = $this->generateFormRequests(
$modelClass,
@@ -92,11 +92,6 @@ protected function buildClass($name)
$replace = [];
- // Todo: these were attempted tweaks to counteract failing CI tests
- // on Laravel 10, and should be revisited at some point.
- // $replace["use {$this->rootNamespace()}Http\Controllers\Controller;\n"] = '';
- // $replace[' extends Controller'] = '';
-
$appRootNamespace = $this->laravel->getNamespace();
$pathToAppBaseController = parent::getPath("Http\Controllers\Controller");
diff --git a/src/Commands/DomainFactoryMakeCommand.php b/src/Commands/DomainFactoryMakeCommand.php
index 1c53382..646d036 100644
--- a/src/Commands/DomainFactoryMakeCommand.php
+++ b/src/Commands/DomainFactoryMakeCommand.php
@@ -22,12 +22,12 @@ protected function getStub()
protected function getNamespace($name)
{
- return $this->domain->namespaceFor('factory');
+ return $this->blueprint->getNamespaceFor('factory');
}
protected function preparePlaceholders(): array
{
- $domain = $this->domain;
+ $domain = $this->blueprint->domain;
$name = $this->getNameInput();
@@ -51,6 +51,6 @@ protected function guessModelName($name)
$name = substr($name, 0, -7);
}
- return $this->domain->model(class_basename($name))->name;
+ return $this->blueprint->domain->model(class_basename($name))->name;
}
}
diff --git a/src/Commands/DomainGeneratorCommand.php b/src/Commands/DomainGeneratorCommand.php
index 9dc8dd2..9d5dcd2 100644
--- a/src/Commands/DomainGeneratorCommand.php
+++ b/src/Commands/DomainGeneratorCommand.php
@@ -15,48 +15,11 @@ abstract class DomainGeneratorCommand extends GeneratorCommand
protected function getRelativeDomainNamespace(): string
{
- return DomainResolver::getRelativeObjectNamespace($this->guessObjectType());
+ return DomainResolver::getRelativeObjectNamespace($this->blueprint->type);
}
protected function getNameInput()
{
return Str::studly($this->argument('name'));
}
-
- // protected function resolveStubPath($path)
- // {
- // $path = ltrim($path, '/\\');
-
- // $publishedPath = resource_path('stubs/ddd/'.$path);
-
- // return file_exists($publishedPath)
- // ? $publishedPath
- // : __DIR__.DIRECTORY_SEPARATOR.'../../stubs'.DIRECTORY_SEPARATOR.$path;
- // }
-
- // protected function fillPlaceholder($stub, $placeholder, $value)
- // {
- // return str_replace(["{{$placeholder}}", "{{ $placeholder }}"], $value, $stub);
- // }
-
- // protected function preparePlaceholders(): array
- // {
- // return [];
- // }
-
- // protected function applyPlaceholders($stub)
- // {
- // $placeholders = $this->preparePlaceholders();
-
- // foreach ($placeholders as $placeholder => $value) {
- // $stub = $this->fillPlaceholder($stub, $placeholder, $value ?? '');
- // }
-
- // return $stub;
- // }
-
- // protected function buildClass($name)
- // {
- // return $this->applyPlaceholders(parent::buildClass($name));
- // }
}
diff --git a/src/Commands/DomainListCommand.php b/src/Commands/DomainListCommand.php
index 2cd3a2f..1014b8a 100644
--- a/src/Commands/DomainListCommand.php
+++ b/src/Commands/DomainListCommand.php
@@ -7,6 +7,8 @@
use Lunarstorm\LaravelDDD\Support\DomainResolver;
use Lunarstorm\LaravelDDD\Support\Path;
+use function Laravel\Prompts\table;
+
class DomainListCommand extends Command
{
protected $name = 'ddd:list';
@@ -23,13 +25,13 @@ public function handle()
return [
$domain->domain,
- $domain->namespace->root,
- Path::normalize($domain->path),
+ $domain->layer->namespace,
+ Path::normalize($domain->layer->path),
];
})
->toArray();
- $this->table($headings, $table);
+ table($headings, $table);
$countDomains = count($table);
diff --git a/src/Commands/DomainModelMakeCommand.php b/src/Commands/DomainModelMakeCommand.php
index 1bc72b3..9739576 100644
--- a/src/Commands/DomainModelMakeCommand.php
+++ b/src/Commands/DomainModelMakeCommand.php
@@ -38,7 +38,7 @@ protected function buildFactoryReplacements()
$replacements = parent::buildFactoryReplacements();
if ($this->option('factory')) {
- $factoryNamespace = Str::start($this->domain->factory($this->getNameInput())->fullyQualifiedName, '\\');
+ $factoryNamespace = Str::start($this->blueprint->getFactoryFor($this->getNameInput())->fullyQualifiedName, '\\');
$factoryCode = << */
@@ -92,7 +92,10 @@ protected function createBaseModelIfNeeded()
$domain = DomainResolver::guessDomainFromClass($baseModel);
- $name = Str::after($baseModel, $domain);
+ $name = str($baseModel)
+ ->after($domain)
+ ->replace(['\\', '/'], '/')
+ ->toString();
$this->call(DomainBaseModelMakeCommand::class, [
'--domain' => $domain,
diff --git a/src/Commands/DomainRequestMakeCommand.php b/src/Commands/DomainRequestMakeCommand.php
index 88cad30..30f7b88 100644
--- a/src/Commands/DomainRequestMakeCommand.php
+++ b/src/Commands/DomainRequestMakeCommand.php
@@ -17,8 +17,6 @@ class DomainRequestMakeCommand extends RequestMakeCommand
protected function rootNamespace()
{
- $type = $this->guessObjectType();
-
- return Str::finish(DomainResolver::resolveRootNamespace($type), '\\');
+ return Str::finish(DomainResolver::resolveRootNamespace($this->blueprint->type), '\\');
}
}
diff --git a/src/Commands/DomainViewModelMakeCommand.php b/src/Commands/DomainViewModelMakeCommand.php
index c457405..e75700b 100644
--- a/src/Commands/DomainViewModelMakeCommand.php
+++ b/src/Commands/DomainViewModelMakeCommand.php
@@ -2,7 +2,6 @@
namespace Lunarstorm\LaravelDDD\Commands;
-use Illuminate\Support\Str;
use Lunarstorm\LaravelDDD\Commands\Concerns\HasDomainStubs;
use Lunarstorm\LaravelDDD\Support\DomainResolver;
@@ -55,7 +54,10 @@ public function handle()
$domain = DomainResolver::guessDomainFromClass($baseViewModel);
- $name = Str::after($baseViewModel, $domain);
+ $name = str($baseViewModel)
+ ->after($domain)
+ ->replace(['\\', '/'], '/')
+ ->toString();
$this->call(DomainBaseViewModelMakeCommand::class, [
'--domain' => $domain,
diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php
index e37d186..ef06bdb 100644
--- a/src/Commands/InstallCommand.php
+++ b/src/Commands/InstallCommand.php
@@ -3,8 +3,8 @@
namespace Lunarstorm\LaravelDDD\Commands;
use Illuminate\Console\Command;
-use Lunarstorm\LaravelDDD\Support\DomainResolver;
-use Symfony\Component\Process\Process;
+
+use function Laravel\Prompts\confirm;
class InstallCommand extends Command
{
@@ -14,54 +14,15 @@ class InstallCommand extends Command
public function handle(): int
{
- $this->comment('Publishing config...');
- $this->call('vendor:publish', [
- '--tag' => 'ddd-config',
- ]);
+ $this->call('ddd:publish', ['--config' => true]);
- $this->comment('Ensuring domain path is registered in composer.json...');
- $this->registerDomainAutoload();
+ $this->comment('Updating composer.json...');
+ $this->callSilently('ddd:config', ['action' => 'composer']);
- if ($this->confirm('Would you like to publish stubs?')) {
+ if (confirm('Would you like to publish stubs now?', default: false, hint: 'You may do this at any time via ddd:stub')) {
$this->call('ddd:stub');
}
return self::SUCCESS;
}
-
- public function registerDomainAutoload()
- {
- $domainPath = DomainResolver::domainPath();
-
- $domainRootNamespace = str(DomainResolver::domainRootNamespace())
- ->rtrim('/\\')
- ->toString();
-
- $this->comment("Registering domain path `{$domainPath}` in composer.json...");
-
- $composerFile = base_path('composer.json');
- $data = json_decode(file_get_contents($composerFile), true);
- data_fill($data, ['autoload', 'psr-4', $domainRootNamespace.'\\'], $domainPath);
-
- file_put_contents($composerFile, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
-
- $this->composerReload();
- }
-
- protected function composerReload()
- {
- $composer = $this->option('composer');
-
- if ($composer !== 'global') {
- $command = ['php', $composer, 'dump-autoload'];
- } else {
- $command = ['composer', 'dump-autoload'];
- }
-
- (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1']))
- ->setTimeout(null)
- ->run(function ($type, $output) {
- $this->output->write($output);
- });
- }
}
diff --git a/src/Commands/Migration/DomainMigrateMakeCommand.php b/src/Commands/Migration/DomainMigrateMakeCommand.php
index e0dcb73..4dcca4b 100644
--- a/src/Commands/Migration/DomainMigrateMakeCommand.php
+++ b/src/Commands/Migration/DomainMigrateMakeCommand.php
@@ -18,8 +18,8 @@ class DomainMigrateMakeCommand extends BaseMigrateMakeCommand
*/
protected function getMigrationPath()
{
- if ($this->domain) {
- return $this->laravel->basePath($this->domain->migrationPath);
+ if ($this->blueprint) {
+ return $this->laravel->basePath($this->blueprint->getMigrationPath());
}
return $this->laravel->databasePath().DIRECTORY_SEPARATOR.'migrations';
diff --git a/src/Commands/OptimizeCommand.php b/src/Commands/OptimizeCommand.php
index e73462e..58b2245 100644
--- a/src/Commands/OptimizeCommand.php
+++ b/src/Commands/OptimizeCommand.php
@@ -3,7 +3,7 @@
namespace Lunarstorm\LaravelDDD\Commands;
use Illuminate\Console\Command;
-use Lunarstorm\LaravelDDD\Support\DomainAutoloader;
+use Lunarstorm\LaravelDDD\Facades\Autoload;
use Lunarstorm\LaravelDDD\Support\DomainMigration;
class OptimizeCommand extends Command
@@ -24,11 +24,9 @@ protected function configure()
public function handle()
{
$this->components->info('Caching DDD providers, commands, migration paths.');
-
- $this->components->task('domain providers', fn () => DomainAutoloader::cacheProviders());
- $this->components->task('domain commands', fn () => DomainAutoloader::cacheCommands());
+ $this->components->task('domain providers', fn () => Autoload::cacheProviders());
+ $this->components->task('domain commands', fn () => Autoload::cacheCommands());
$this->components->task('domain migration paths', fn () => DomainMigration::cachePaths());
-
$this->newLine();
}
}
diff --git a/src/Commands/UpgradeCommand.php b/src/Commands/UpgradeCommand.php
index e696605..ea0c3c4 100644
--- a/src/Commands/UpgradeCommand.php
+++ b/src/Commands/UpgradeCommand.php
@@ -4,6 +4,7 @@
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Config;
class UpgradeCommand extends Command
{
@@ -19,36 +20,73 @@ public function handle()
return;
}
- $replacements = [
+ $legacyMapping = [
'domain_path' => 'paths.domain',
'domain_namespace' => 'domain_namespace',
- 'namespaces.model' => 'namespaces.models',
- 'namespaces.data_transfer_object' => 'namespaces.data_transfer_objects',
- 'namespaces.view_model' => 'namespaces.view_models',
- 'namespaces.value_object' => 'namespaces.value_objects',
- 'namespaces.action' => 'namespaces.actions',
+ 'application' => null,
+ 'layers' => null,
+ 'namespaces' => [
+ 'model' => 'namespaces.models',
+ 'data_transfer_object' => 'namespaces.data_transfer_objects',
+ 'view_model' => 'namespaces.view_models',
+ 'value_object' => 'namespaces.value_objects',
+ 'action' => 'namespaces.actions',
+ ],
'base_model' => 'base_model',
'base_dto' => 'base_dto',
'base_view_model' => 'base_view_model',
'base_action' => 'base_action',
+ 'autoload' => null,
+ 'autoload_ignore' => null,
+ 'cache_directory' => null,
];
+ $factoryConfig = require __DIR__.'/../../config/ddd.php';
$oldConfig = require config_path('ddd.php');
$oldConfig = Arr::dot($oldConfig);
- // Grab a flesh copy of the new config
- $newConfigContent = file_get_contents(__DIR__.'/../../config/ddd.php.stub');
+ $replacements = [];
+
+ $map = Arr::dot($legacyMapping);
- foreach ($replacements as $dotPath => $legacyKey) {
+ foreach ($map as $dotPath => $legacyKey) {
$value = match (true) {
array_key_exists($dotPath, $oldConfig) => $oldConfig[$dotPath],
array_key_exists($legacyKey, $oldConfig) => $oldConfig[$legacyKey],
default => config("ddd.{$dotPath}"),
};
+ $replacements[$dotPath] = $value ?? data_get($factoryConfig, $dotPath);
+ }
+
+ $replacements = Arr::undot($replacements);
+
+ $freshConfig = $factoryConfig;
+
+ // Grab a fresh copy of the new config
+ $newConfigContent = file_get_contents(__DIR__.'/../../config/ddd.php.stub');
+
+ foreach ($freshConfig as $key => $value) {
+ $resolved = null;
+
+ if (is_array($value)) {
+ $resolved = [
+ ...$value,
+ ...data_get($replacements, $key, []),
+ ];
+
+ if (array_is_list($resolved)) {
+ $resolved = array_unique($resolved);
+ }
+ } else {
+ $resolved = data_get($replacements, $key, $value);
+ }
+
+ $freshConfig[$key] = $resolved;
+
$newConfigContent = str_replace(
- '{{'.$dotPath.'}}',
- var_export($value, true),
+ '{{'.$key.'}}',
+ var_export($resolved, true),
$newConfigContent
);
}
diff --git a/src/ComposerManager.php b/src/ComposerManager.php
new file mode 100755
index 0000000..52d9005
--- /dev/null
+++ b/src/ComposerManager.php
@@ -0,0 +1,179 @@
+composer = app(Composer::class)->setWorkingPath(app()->basePath());
+
+ $this->composerFile = $composerFile ?? app()->basePath('composer.json');
+
+ $this->data = json_decode(file_get_contents($this->composerFile), true);
+ }
+
+ public static function make(?string $composerFile = null): self
+ {
+ return new self($composerFile);
+ }
+
+ public function usingOutput(OutputStyle $output)
+ {
+ $this->output = $output;
+
+ return $this;
+ }
+
+ protected function guessAutoloadPathFromNamespace(string $namespace): string
+ {
+ $rootFolders = [
+ 'src',
+ '',
+ ];
+
+ $relativePath = Str::rtrim(Path::fromNamespace($namespace), '/\\');
+
+ foreach ($rootFolders as $folder) {
+ $path = Path::join($folder, $relativePath);
+
+ if (is_dir($path)) {
+ return $this->normalizePathForComposer($path);
+ }
+ }
+
+ return $this->normalizePathForComposer("src/{$relativePath}");
+ }
+
+ protected function normalizePathForComposer($path): string
+ {
+ $path = Path::normalize($path);
+
+ return str_replace(['\\', '/'], '/', $path);
+ }
+
+ public function hasPsr4Autoload(string $namespace): bool
+ {
+ return collect($this->getPsr4Namespaces())
+ ->hasAny([
+ $namespace,
+ Str::finish($namespace, '\\'),
+ ]);
+ }
+
+ public function registerPsr4Autoload(string $namespace, $path)
+ {
+ $namespace = str($namespace)
+ ->rtrim('/\\')
+ ->finish('\\')
+ ->toString();
+
+ $path = $path ?? $this->guessAutoloadPathFromNamespace($namespace);
+
+ return $this->fill(
+ ['autoload', 'psr-4', $namespace],
+ $this->normalizePathForComposer($path)
+ );
+ }
+
+ public function fill($path, $value)
+ {
+ data_fill($this->data, $path, $value);
+
+ return $this;
+ }
+
+ protected function update($set = [], $forget = [])
+ {
+ foreach ($forget as $key) {
+ $this->forget($key);
+ }
+
+ foreach ($set as $pair) {
+ [$path, $value] = $pair;
+ $this->fill($path, $value);
+ }
+
+ return $this;
+ }
+
+ public function forget($key)
+ {
+ $keys = Arr::wrap($key);
+
+ foreach ($keys as $key) {
+ Arr::forget($this->data, $key);
+ }
+
+ return $this;
+ }
+
+ public function get($path, $default = null)
+ {
+ return data_get($this->data, $path, $default);
+ }
+
+ public function getPsr4Namespaces()
+ {
+ return $this->get(['autoload', 'psr-4'], []);
+ }
+
+ public function getAutoloadPath($namespace)
+ {
+ $namespace = Str::finish($namespace, '\\');
+
+ return $this->get(['autoload', 'psr-4', $namespace]);
+ }
+
+ public function unsetPsr4Autoload($namespace)
+ {
+ $namespace = Str::finish($namespace, '\\');
+
+ return $this->forget(['autoload', 'psr-4', $namespace]);
+ }
+
+ public function reload()
+ {
+ $this->output?->writeLn('Reloading composer (dump-autoload)...');
+
+ $this->composer->dumpAutoloads();
+
+ return $this;
+ }
+
+ public function save()
+ {
+ $this->composer->modify(fn ($composerData) => $this->data);
+
+ return $this;
+ }
+
+ public function saveAndReload()
+ {
+ return $this->save()->reload();
+ }
+
+ public function toJson()
+ {
+ return json_encode($this->data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
+ }
+
+ public function toArray()
+ {
+ return $this->data;
+ }
+}
diff --git a/src/ConfigManager.php b/src/ConfigManager.php
new file mode 100755
index 0000000..39dcdfc
--- /dev/null
+++ b/src/ConfigManager.php
@@ -0,0 +1,156 @@
+configPath = $configPath ?? app()->configPath('ddd.php');
+
+ $this->packageConfig = require DDD::packagePath('config/ddd.php');
+
+ $this->config = file_exists($configPath) ? require ($configPath) : $this->packageConfig;
+
+ $this->stub = file_get_contents(DDD::packagePath('config/ddd.php.stub'));
+ }
+
+ protected function mergeArray($path, $array)
+ {
+ $path = Arr::wrap($path);
+
+ $merged = [];
+
+ foreach ($array as $key => $value) {
+ $merged[$key] = is_array($value)
+ ? $this->mergeArray([...$path, $key], $value)
+ : $this->resolve([...$path, $key], $value);
+ }
+
+ if (array_is_list($merged)) {
+ $merged = array_unique($merged);
+ }
+
+ return $merged;
+ }
+
+ public function resolve($path, $value)
+ {
+ $path = Arr::wrap($path);
+
+ return data_get($this->config, $path, $value);
+ }
+
+ public function syncWithLatest()
+ {
+ $fresh = [];
+
+ foreach ($this->packageConfig as $key => $value) {
+ $resolved = is_array($value)
+ ? $this->mergeArray($key, $value)
+ : $this->resolve($key, $value);
+
+ $fresh[$key] = $resolved;
+ }
+
+ $this->config = $fresh;
+
+ return $this;
+ }
+
+ public function get($key = null)
+ {
+ if (is_null($key)) {
+ return $this->config;
+ }
+
+ return data_get($this->config, $key);
+ }
+
+ public function set($key, $value)
+ {
+ data_set($this->config, $key, $value);
+
+ return $this;
+ }
+
+ public function fill($values)
+ {
+ foreach ($values as $key => $value) {
+ $this->set($key, $value);
+ }
+
+ return $this;
+ }
+
+ public function save()
+ {
+ $content = $this->stub;
+
+ // We will temporary substitute namespace slashes
+ // with a placeholder to avoid double exporter
+ // escaping them as double backslashes.
+ $keysWithNamespaces = [
+ 'domain_namespace',
+ 'application_namespace',
+ 'layers',
+ 'namespaces',
+ 'base_model',
+ 'base_dto',
+ 'base_view_model',
+ 'base_action',
+ ];
+
+ foreach ($keysWithNamespaces as $key) {
+ $value = $this->get($key);
+
+ if (is_string($value)) {
+ $value = str_replace('\\', '[[BACKSLASH]]', $value);
+ }
+
+ if (is_array($value)) {
+ $array = $value;
+ foreach ($array as $k => $v) {
+ $array[$k] = str_replace('\\', '[[BACKSLASH]]', $v);
+ }
+ $value = $array;
+ }
+
+ $this->set($key, $value);
+ }
+
+ foreach ($this->config as $key => $value) {
+ $content = str_replace(
+ '{{'.$key.'}}',
+ VarExporter::export($value),
+ $content
+ );
+ }
+
+ // Restore namespace slashes
+ $content = str_replace('[[BACKSLASH]]', '\\', $content);
+
+ // Write it to a temporary file first
+ $tempPath = sys_get_temp_dir().'/ddd.php';
+ file_put_contents($tempPath, $content);
+
+ // Format it using pint
+ Process::run("./vendor/bin/pint {$tempPath}");
+
+ // Copy the temporary file to the config path
+ copy($tempPath, $this->configPath);
+
+ return $this;
+ }
+}
diff --git a/src/DomainManager.php b/src/DomainManager.php
index 317c453..2eb2ea3 100755
--- a/src/DomainManager.php
+++ b/src/DomainManager.php
@@ -2,10 +2,9 @@
namespace Lunarstorm\LaravelDDD;
-use Illuminate\Console\Command;
-use Lunarstorm\LaravelDDD\Support\Domain;
+use Lunarstorm\LaravelDDD\Support\AutoloadManager;
+use Lunarstorm\LaravelDDD\Support\GeneratorBlueprint;
use Lunarstorm\LaravelDDD\Support\Path;
-use Lunarstorm\LaravelDDD\ValueObjects\DomainCommandContext;
class DomainManager
{
@@ -24,23 +23,44 @@ class DomainManager
protected $applicationLayerFilter;
/**
- * The application layer object resolver callback.
+ * The object schema resolver callback.
*
* @var callable|null
*/
- protected $namespaceResolver;
+ protected $objectSchemaResolver;
- protected ?DomainCommandContext $commandContext;
+ /**
+ * Resolved custom objects.
+ */
+ protected array $resolvedObjects = [];
- protected StubManager $stubs;
+ protected ?GeneratorBlueprint $commandContext;
public function __construct()
{
$this->autoloadFilter = null;
$this->applicationLayerFilter = null;
- $this->namespaceResolver = null;
$this->commandContext = null;
- $this->stubs = new StubManager;
+ }
+
+ public function autoloader(): AutoloadManager
+ {
+ return app(AutoloadManager::class);
+ }
+
+ public function composer(): ComposerManager
+ {
+ return app(ComposerManager::class);
+ }
+
+ public function config(): ConfigManager
+ {
+ return app(ConfigManager::class);
+ }
+
+ public function stubs(): StubManager
+ {
+ return app(StubManager::class);
}
public function filterAutoloadPathsUsing(callable $filter): void
@@ -63,24 +83,14 @@ public function getApplicationLayerFilter(): ?callable
return $this->applicationLayerFilter;
}
- public function resolveNamespaceUsing(callable $resolver): void
- {
- $this->namespaceResolver = $resolver;
- }
-
- public function getNamespaceResolver(): ?callable
- {
- return $this->namespaceResolver;
- }
-
- public function captureCommandContext(Command $command, ?Domain $domain, ?string $type): void
+ public function resolveObjectSchemaUsing(callable $resolver): void
{
- $this->commandContext = DomainCommandContext::fromCommand($command, $domain, $type);
+ $this->objectSchemaResolver = $resolver;
}
- public function getCommandContext(): ?DomainCommandContext
+ public function getObjectSchemaResolver(): ?callable
{
- return $this->commandContext;
+ return $this->objectSchemaResolver;
}
public function packagePath($path = ''): string
@@ -92,9 +102,4 @@ public function laravelVersion($value)
{
return version_compare(app()->version(), $value, '>=');
}
-
- public function stubs(): StubManager
- {
- return $this->stubs;
- }
}
diff --git a/src/Enums/LayerType.php b/src/Enums/LayerType.php
new file mode 100644
index 0000000..65e367a
--- /dev/null
+++ b/src/Enums/LayerType.php
@@ -0,0 +1,10 @@
+app->scoped(DomainManager::class, function () {
- return new DomainManager;
- });
-
- $this->app->bind('ddd', DomainManager::class);
-
/*
* This class is a Package Service Provider
*
@@ -27,6 +23,7 @@ public function configurePackage(Package $package): void
->hasConfigFile()
->hasCommands([
Commands\InstallCommand::class,
+ Commands\ConfigCommand::class,
Commands\PublishCommand::class,
Commands\StubCommand::class,
Commands\UpgradeCommand::class,
@@ -70,10 +67,11 @@ public function configurePackage(Package $package): void
$package->hasCommand(Commands\DomainTraitMakeCommand::class);
}
- // if ($this->laravelVersion('11.30.0')) {
- // $package->hasCommand(Commands\PublishCommand::class);
- // $package->hasCommand(Commands\StubCommand::class);
- // }
+ if ($this->app->runningUnitTests()) {
+ $package->hasRoutes(['testing']);
+ }
+
+ $this->registerBindings();
}
protected function laravelVersion($value)
@@ -83,6 +81,10 @@ protected function laravelVersion($value)
protected function registerMigrations()
{
+ $this->app->when(MigrationCreator::class)
+ ->needs('$customStubPath')
+ ->give(fn () => $this->app->basePath('stubs'));
+
$this->app->singleton(Commands\Migration\DomainMigrateMakeCommand::class, function ($app) {
// Once we have the migration creator registered, we will create the command
// and inject the creator. The creator is responsible for the actual file
@@ -94,13 +96,44 @@ protected function registerMigrations()
});
$this->loadMigrationsFrom(DomainMigration::paths());
+
+ return $this;
+ }
+
+ protected function registerBindings()
+ {
+ $this->app->scoped(DomainManager::class, function () {
+ return new DomainManager;
+ });
+
+ $this->app->scoped(ComposerManager::class, function () {
+ return ComposerManager::make($this->app->basePath('composer.json'));
+ });
+
+ $this->app->scoped(ConfigManager::class, function () {
+ return new ConfigManager($this->app->configPath('ddd.php'));
+ });
+
+ $this->app->scoped(StubManager::class, function () {
+ return new StubManager;
+ });
+
+ $this->app->scoped(AutoloadManager::class, function () {
+ return new AutoloadManager;
+ });
+
+ $this->app->bind('ddd', DomainManager::class);
+ $this->app->bind('ddd.autoloader', AutoloadManager::class);
+ $this->app->bind('ddd.config', ConfigManager::class);
+ $this->app->bind('ddd.composer', ComposerManager::class);
+ $this->app->bind('ddd.stubs', StubManager::class);
+
+ return $this;
}
public function packageBooted()
{
- $this->publishes([
- $this->package->basePath('/../stubs') => $this->app->basePath("stubs/{$this->package->shortName()}"),
- ], "{$this->package->shortName()}-stubs");
+ Autoload::run();
if ($this->app->runningInConsole() && method_exists($this, 'optimizes')) {
$this->optimizes(
@@ -113,8 +146,6 @@ public function packageBooted()
public function packageRegistered()
{
- (new DomainAutoloader)->autoload();
-
$this->registerMigrations();
}
}
diff --git a/src/Support/AutoloadManager.php b/src/Support/AutoloadManager.php
new file mode 100644
index 0000000..90bf8cb
--- /dev/null
+++ b/src/Support/AutoloadManager.php
@@ -0,0 +1,360 @@
+container = $container ?? Container::getInstance();
+
+ $this->app = $this->container->make(Application::class);
+
+ $this->appNamespace = $this->app->getNamespace();
+ }
+
+ public function boot()
+ {
+ $this->booted = true;
+
+ if (! config()->has('ddd.autoload')) {
+ return $this->flush();
+ }
+
+ $this
+ ->flush()
+ ->when(config('ddd.autoload.providers') === true, fn () => $this->handleProviders())
+ ->when($this->app->runningInConsole() && config('ddd.autoload.commands') === true, fn () => $this->handleCommands())
+ ->when(config('ddd.autoload.policies') === true, fn () => $this->handlePolicies())
+ ->when(config('ddd.autoload.factories') === true, fn () => $this->handleFactories());
+
+ return $this;
+ }
+
+ public function isBooted(): bool
+ {
+ return $this->booted;
+ }
+
+ public function isConsoleBooted(): bool
+ {
+ return $this->consoleBooted;
+ }
+
+ public function hasRun(): bool
+ {
+ return $this->ran;
+ }
+
+ protected function flush()
+ {
+ foreach (static::$registeredProviders as $provider) {
+ $this->app?->forgetInstance($provider);
+ }
+
+ static::$registeredProviders = [];
+
+ static::$registeredCommands = [];
+
+ static::$resolvedPolicies = [];
+
+ static::$resolvedFactories = [];
+
+ return $this;
+ }
+
+ protected function normalizePaths($path): array
+ {
+ return collect($path)
+ ->filter(fn ($path) => is_dir($path))
+ ->toArray();
+ }
+
+ public function getAllLayerPaths(): array
+ {
+ return collect([
+ DomainResolver::domainPath(),
+ DomainResolver::applicationLayerPath(),
+ ...array_values(config('ddd.layers', [])),
+ ])->map(fn ($path) => Path::normalize($this->app->basePath($path)))->toArray();
+ }
+
+ protected function getCustomLayerPaths(): array
+ {
+ return collect([
+ ...array_values(config('ddd.layers', [])),
+ ])->map(fn ($path) => Path::normalize($this->app->basePath($path)))->toArray();
+ }
+
+ protected function handleProviders()
+ {
+ $providers = DomainCache::has('domain-providers')
+ ? DomainCache::get('domain-providers')
+ : $this->discoverProviders();
+
+ foreach ($providers as $provider) {
+ static::$registeredProviders[$provider] = $provider;
+ }
+
+ return $this;
+ }
+
+ protected function handleCommands()
+ {
+ $commands = DomainCache::has('domain-commands')
+ ? DomainCache::get('domain-commands')
+ : $this->discoverCommands();
+
+ foreach ($commands as $command) {
+ static::$registeredCommands[$command] = $command;
+ }
+
+ return $this;
+ }
+
+ public function run()
+ {
+ if (! $this->isBooted()) {
+ $this->boot();
+ }
+
+ foreach (static::$registeredProviders as $provider) {
+ $this->app->register($provider);
+ }
+
+ if ($this->app->runningInConsole() && ! $this->isConsoleBooted()) {
+ ConsoleApplication::starting(function (ConsoleApplication $artisan) {
+ foreach (static::$registeredCommands as $command) {
+ $artisan->resolve($command);
+ }
+ });
+
+ $this->consoleBooted = true;
+ }
+
+ $this->ran = true;
+
+ return $this;
+ }
+
+ public function getRegisteredCommands(): array
+ {
+ return static::$registeredCommands;
+ }
+
+ public function getRegisteredProviders(): array
+ {
+ return static::$registeredProviders;
+ }
+
+ public function getResolvedPolicies(): array
+ {
+ return static::$resolvedPolicies;
+ }
+
+ public function getResolvedFactories(): array
+ {
+ return static::$resolvedFactories;
+ }
+
+ protected function handlePolicies()
+ {
+ Gate::guessPolicyNamesUsing(static::$policyResolver = function (string $class): array|string {
+ if ($model = DomainObject::fromClass($class, 'model')) {
+ $resolved = (new Domain($model->domain))
+ ->object('policy', "{$model->name}Policy")
+ ->fullyQualifiedName;
+
+ static::$resolvedPolicies[$class] = $resolved;
+
+ return $resolved;
+ }
+
+ $classDirname = str_replace('/', '\\', dirname(str_replace('\\', '/', $class)));
+
+ $classDirnameSegments = explode('\\', $classDirname);
+
+ return Arr::wrap(Collection::times(count($classDirnameSegments), function ($index) use ($class, $classDirnameSegments) {
+ $classDirname = implode('\\', array_slice($classDirnameSegments, 0, $index));
+
+ return $classDirname.'\\Policies\\'.class_basename($class).'Policy';
+ })->reverse()->values()->first(function ($class) {
+ return class_exists($class);
+ }) ?: [$classDirname.'\\Policies\\'.class_basename($class).'Policy']);
+ });
+
+ return $this;
+ }
+
+ protected function handleFactories()
+ {
+ Factory::guessFactoryNamesUsing(static::$factoryResolver = function (string $modelName) {
+ if ($factoryName = DomainFactory::resolveFactoryName($modelName)) {
+ static::$resolvedFactories[$modelName] = $factoryName;
+
+ return $factoryName;
+ }
+
+ $modelName = Str::startsWith($modelName, $this->appNamespace.'Models\\')
+ ? Str::after($modelName, $this->appNamespace.'Models\\')
+ : Str::after($modelName, $this->appNamespace);
+
+ return 'Database\\Factories\\'.$modelName.'Factory';
+ });
+
+ return $this;
+ }
+
+ protected function finder($paths)
+ {
+ $filter = DDD::getAutoloadFilter() ?? function (SplFileInfo $file) {
+ $pathAfterDomain = str($file->getRelativePath())
+ ->replace('\\', '/')
+ ->after('/')
+ ->finish('/');
+
+ $ignoredFolders = collect(config('ddd.autoload_ignore', []))
+ ->map(fn ($path) => Str::finish($path, '/'));
+
+ if ($pathAfterDomain->startsWith($ignoredFolders)) {
+ return false;
+ }
+ };
+
+ return Finder::create()
+ ->files()
+ ->in($paths)
+ ->filter($filter);
+ }
+
+ public function discoverProviders(): array
+ {
+ $configValue = config('ddd.autoload.providers');
+
+ if ($configValue === false) {
+ return [];
+ }
+
+ $paths = $this->normalizePaths(
+ $configValue === true
+ ? $this->getAllLayerPaths()
+ : $configValue
+ );
+
+ if (empty($paths)) {
+ return [];
+ }
+
+ return Lody::classesFromFinder($this->finder($paths))
+ ->isNotAbstract()
+ ->isInstanceOf(ServiceProvider::class)
+ ->values()
+ ->toArray();
+ }
+
+ public function discoverCommands(): array
+ {
+ $configValue = config('ddd.autoload.commands');
+
+ if ($configValue === false) {
+ return [];
+ }
+
+ $paths = $this->normalizePaths(
+ $configValue === true
+ ? $this->getAllLayerPaths()
+ : $configValue
+ );
+
+ if (empty($paths)) {
+ return [];
+ }
+
+ return Lody::classesFromFinder($this->finder($paths))
+ ->isNotAbstract()
+ ->isInstanceOf(Command::class)
+ ->values()
+ ->toArray();
+ }
+
+ public function cacheCommands()
+ {
+ DomainCache::set('domain-commands', $this->discoverCommands());
+
+ return $this;
+ }
+
+ public function cacheProviders()
+ {
+ DomainCache::set('domain-providers', $this->discoverProviders());
+
+ return $this;
+ }
+
+ protected function resolveAppNamespace()
+ {
+ try {
+ return Container::getInstance()
+ ->make(Application::class)
+ ->getNamespace();
+ } catch (Throwable) {
+ return 'App\\';
+ }
+ }
+
+ public static function partialMock()
+ {
+ $mock = Mockery::mock(AutoloadManager::class, [null])
+ ->makePartial()
+ ->shouldAllowMockingProtectedMethods();
+
+ $mock->shouldReceive('isBooted')->andReturn(false);
+
+ return $mock;
+ }
+}
diff --git a/src/Support/Concerns/InteractsWithComposer.php b/src/Support/Concerns/InteractsWithComposer.php
new file mode 100644
index 0000000..cc1bbee
--- /dev/null
+++ b/src/Support/Concerns/InteractsWithComposer.php
@@ -0,0 +1,30 @@
+rtrim('/\\')
+ ->finish('\\')
+ ->toString();
+
+ $this->composerFill(['autoload', 'psr-4', $namespace], $path);
+
+ return $this;
+ }
+}
diff --git a/src/Support/Domain.php b/src/Support/Domain.php
index e36fab8..d8e608b 100644
--- a/src/Support/Domain.php
+++ b/src/Support/Domain.php
@@ -2,7 +2,6 @@
namespace Lunarstorm\LaravelDDD\Support;
-use Lunarstorm\LaravelDDD\ValueObjects\DomainNamespaces;
use Lunarstorm\LaravelDDD\ValueObjects\DomainObject;
class Domain
@@ -19,7 +18,7 @@ class Domain
public readonly string $domainWithSubdomain;
- public readonly DomainNamespaces $namespace;
+ public readonly Layer $layer;
public static array $objects = [];
@@ -56,9 +55,9 @@ public function __construct(string $domain, ?string $subdomain = null)
? "{$this->domain}.{$this->subdomain}"
: $this->domain;
- $this->namespace = DomainNamespaces::from($this->domain, $this->subdomain);
+ $this->layer = DomainResolver::resolveLayer($this->domainWithSubdomain);
- $this->path = Path::join(DomainResolver::domainPath(), $this->domainWithSubdomain);
+ $this->path = $this->layer->path;
$this->migrationPath = Path::join($this->path, config('ddd.namespaces.migration', 'Database/Migrations'));
}
@@ -74,13 +73,13 @@ public function path(?string $path = null): string
return $this->path;
}
- $path = str($path)
- ->replace($this->namespace->root, '')
+ $resolvedPath = str($path)
+ ->replace($this->layer->namespace, '')
->replace(['\\', '/'], DIRECTORY_SEPARATOR)
->append('.php')
->toString();
- return Path::join($this->path, $path);
+ return Path::join($this->path, $resolvedPath);
}
public function pathInApplicationLayer(?string $path = null): string
@@ -103,6 +102,16 @@ public function relativePath(string $path = ''): string
return collect([$this->domain, $path])->filter()->implode(DIRECTORY_SEPARATOR);
}
+ public function rootNamespace(): string
+ {
+ return $this->layer->namespace;
+ }
+
+ public function intendedLayerFor(string $type)
+ {
+ return DomainResolver::resolveLayer($this->domainWithSubdomain, $type);
+ }
+
public function namespaceFor(string $type, ?string $name = null): string
{
return DomainResolver::getDomainObjectNamespace($this->domainWithSubdomain, $type, $name);
@@ -121,20 +130,12 @@ public function guessNamespaceFromName(string $name): string
public function object(string $type, string $name, bool $absolute = false): DomainObject
{
- $resolvedNamespace = null;
-
- if (DomainResolver::isApplicationLayer($type)) {
- $resolver = app('ddd')->getNamespaceResolver();
-
- $resolvedNamespace = is_callable($resolver)
- ? $resolver($this->domainWithSubdomain, $type, app('ddd')->getCommandContext())
- : null;
- }
+ $layer = $this->intendedLayerFor($type);
- $namespace = $resolvedNamespace ?? match (true) {
- $absolute => $this->namespace->root,
- str($name)->startsWith('\\') => $this->guessNamespaceFromName($name),
- default => $this->namespaceFor($type),
+ $namespace = match (true) {
+ $absolute => $layer->namespace,
+ str($name)->startsWith('\\') => $layer->guessNamespaceFromName($name),
+ default => $layer->namespaceFor($type),
};
$baseName = str($name)->replace($namespace, '')
@@ -150,9 +151,7 @@ public function object(string $type, string $name, bool $absolute = false): Doma
domain: $this->domain,
namespace: $namespace,
fullyQualifiedName: $fullyQualifiedName,
- path: DomainResolver::isApplicationLayer($type)
- ? $this->pathInApplicationLayer($fullyQualifiedName)
- : $this->path($fullyQualifiedName),
+ path: $layer->path($fullyQualifiedName),
type: $type
);
}
diff --git a/src/Support/DomainAutoloader.php b/src/Support/DomainAutoloader.php
deleted file mode 100644
index 9e812d9..0000000
--- a/src/Support/DomainAutoloader.php
+++ /dev/null
@@ -1,216 +0,0 @@
-has('ddd.autoload')) {
- return;
- }
-
- $this->handleProviders();
-
- if (app()->runningInConsole()) {
- $this->handleCommands();
- }
-
- if (config('ddd.autoload.policies') === true) {
- $this->handlePolicies();
- }
-
- if (config('ddd.autoload.factories') === true) {
- $this->handleFactories();
- }
- }
-
- protected static function normalizePaths($path): array
- {
- return collect($path)
- ->filter(fn ($path) => is_dir($path))
- ->toArray();
- }
-
- protected function handleProviders(): void
- {
- $providers = DomainCache::has('domain-providers')
- ? DomainCache::get('domain-providers')
- : static::discoverProviders();
-
- foreach ($providers as $provider) {
- app()->register($provider);
- }
- }
-
- protected function handleCommands(): void
- {
- $commands = DomainCache::has('domain-commands')
- ? DomainCache::get('domain-commands')
- : static::discoverCommands();
-
- foreach ($commands as $command) {
- $this->registerCommand($command);
- }
- }
-
- protected function registerCommand($class)
- {
- ConsoleApplication::starting(function ($artisan) use ($class) {
- $artisan->resolve($class);
- });
- }
-
- protected function handlePolicies(): void
- {
- Gate::guessPolicyNamesUsing(static function (string $class): array|string {
- if ($model = DomainObject::fromClass($class, 'model')) {
- return (new Domain($model->domain))
- ->object('policy', "{$model->name}Policy")
- ->fullyQualifiedName;
- }
-
- $classDirname = str_replace('/', '\\', dirname(str_replace('\\', '/', $class)));
-
- $classDirnameSegments = explode('\\', $classDirname);
-
- return Arr::wrap(Collection::times(count($classDirnameSegments), function ($index) use ($class, $classDirnameSegments) {
- $classDirname = implode('\\', array_slice($classDirnameSegments, 0, $index));
-
- return $classDirname.'\\Policies\\'.class_basename($class).'Policy';
- })->reverse()->values()->first(function ($class) {
- return class_exists($class);
- }) ?: [$classDirname.'\\Policies\\'.class_basename($class).'Policy']);
- });
- }
-
- protected function handleFactories(): void
- {
- Factory::guessFactoryNamesUsing(function (string $modelName) {
- if ($factoryName = DomainFactory::resolveFactoryName($modelName)) {
- return $factoryName;
- }
-
- $appNamespace = static::appNamespace();
-
- $modelName = Str::startsWith($modelName, $appNamespace.'Models\\')
- ? Str::after($modelName, $appNamespace.'Models\\')
- : Str::after($modelName, $appNamespace);
-
- return 'Database\\Factories\\'.$modelName.'Factory';
- });
- }
-
- protected static function finder($paths)
- {
- $filter = app('ddd')->getAutoloadFilter() ?? function (SplFileInfo $file) {
- $pathAfterDomain = str($file->getRelativePath())
- ->replace('\\', '/')
- ->after('/')
- ->finish('/');
-
- $ignoredFolders = collect(config('ddd.autoload_ignore', []))
- ->map(fn ($path) => Str::finish($path, '/'));
-
- if ($pathAfterDomain->startsWith($ignoredFolders)) {
- return false;
- }
- };
-
- return Finder::create()
- ->files()
- ->in($paths)
- ->filter($filter);
- }
-
- protected static function discoverProviders(): array
- {
- $configValue = config('ddd.autoload.providers');
-
- if ($configValue === false) {
- return [];
- }
-
- $paths = static::normalizePaths(
- $configValue === true
- ? app()->basePath(DomainResolver::domainPath())
- : $configValue
- );
-
- if (empty($paths)) {
- return [];
- }
-
- return Lody::classesFromFinder(static::finder($paths))
- ->isNotAbstract()
- ->isInstanceOf(ServiceProvider::class)
- ->toArray();
- }
-
- protected static function discoverCommands(): array
- {
- $configValue = config('ddd.autoload.commands');
-
- if ($configValue === false) {
- return [];
- }
-
- $paths = static::normalizePaths(
- $configValue === true ?
- app()->basePath(DomainResolver::domainPath())
- : $configValue
- );
-
- if (empty($paths)) {
- return [];
- }
-
- return Lody::classesFromFinder(static::finder($paths))
- ->isNotAbstract()
- ->isInstanceOf(Command::class)
- ->toArray();
- }
-
- public static function cacheProviders(): void
- {
- DomainCache::set('domain-providers', static::discoverProviders());
- }
-
- public static function cacheCommands(): void
- {
- DomainCache::set('domain-commands', static::discoverCommands());
- }
-
- protected static function appNamespace()
- {
- try {
- return Container::getInstance()
- ->make(Application::class)
- ->getNamespace();
- } catch (Throwable) {
- return 'App\\';
- }
- }
-}
diff --git a/src/Support/DomainMigration.php b/src/Support/DomainMigration.php
index 9f3a939..b6cd0ba 100644
--- a/src/Support/DomainMigration.php
+++ b/src/Support/DomainMigration.php
@@ -31,7 +31,7 @@ public static function paths(): array
: static::discoverPaths();
}
- protected static function normalizePaths($path): array
+ protected static function filterDirectories($path): array
{
return collect($path)
->filter(fn ($path) => is_dir($path))
@@ -46,7 +46,7 @@ public static function discoverPaths(): array
return [];
}
- $paths = static::normalizePaths([
+ $paths = static::filterDirectories([
app()->basePath(DomainResolver::domainPath()),
]);
diff --git a/src/Support/DomainResolver.php b/src/Support/DomainResolver.php
index 0d9110c..ca0213a 100644
--- a/src/Support/DomainResolver.php
+++ b/src/Support/DomainResolver.php
@@ -3,6 +3,7 @@
namespace Lunarstorm\LaravelDDD\Support;
use Illuminate\Support\Str;
+use Lunarstorm\LaravelDDD\Enums\LayerType;
class DomainResolver
{
@@ -40,7 +41,7 @@ public static function domainRootNamespace(): ?string
*/
public static function applicationLayerPath(): ?string
{
- return config('ddd.application.path');
+ return config('ddd.application_path');
}
/**
@@ -48,7 +49,7 @@ public static function applicationLayerPath(): ?string
*/
public static function applicationLayerRootNamespace(): ?string
{
- return config('ddd.application.namespace');
+ return config('ddd.application_namespace');
}
/**
@@ -67,7 +68,7 @@ public static function getRelativeObjectNamespace(string $type): string
public static function isApplicationLayer(string $type): bool
{
$filter = app('ddd')->getApplicationLayerFilter() ?? function (string $type) {
- $applicationObjects = config('ddd.application.objects', ['controller', 'request']);
+ $applicationObjects = config('ddd.application_objects', ['controller', 'request']);
return in_array($type, $applicationObjects);
};
@@ -87,6 +88,34 @@ public static function resolveRootNamespace(string $type): ?string
: static::domainRootNamespace();
}
+ /**
+ * Resolve the intended layer of a specified domain name keyword.
+ */
+ public static function resolveLayer(string $domain, ?string $type = null): ?Layer
+ {
+ $layers = config('ddd.layers', []);
+
+ // Objects in the application layer take precedence
+ if ($type && static::isApplicationLayer($type)) {
+ return new Layer(
+ static::applicationLayerRootNamespace().'\\'.$domain,
+ Path::join(static::applicationLayerPath(), $domain),
+ LayerType::Application,
+ );
+ }
+
+ return match (true) {
+ array_key_exists($domain, $layers)
+ && is_string($layers[$domain]) => new Layer($domain, $layers[$domain], LayerType::Custom),
+
+ default => new Layer(
+ static::domainRootNamespace().'\\'.$domain,
+ Path::join(static::domainPath(), $domain),
+ LayerType::Domain,
+ )
+ };
+ }
+
/**
* Get the fully qualified namespace for a domain object.
*
@@ -96,20 +125,11 @@ public static function resolveRootNamespace(string $type): ?string
*/
public static function getDomainObjectNamespace(string $domain, string $type, ?string $name = null): string
{
- $customResolver = app('ddd')->getNamespaceResolver();
-
- $resolved = is_callable($customResolver)
- ? $customResolver($domain, $type, app('ddd')->getCommandContext())
- : null;
-
- if (! is_null($resolved)) {
- return $resolved;
- }
-
$resolver = function (string $domain, string $type, ?string $name) {
+ $layer = static::resolveLayer($domain, $type);
+
$namespace = collect([
- static::resolveRootNamespace($type),
- $domain,
+ $layer->namespace,
static::getRelativeObjectNamespace($type),
])->filter()->implode('\\');
diff --git a/src/Support/GeneratorBlueprint.php b/src/Support/GeneratorBlueprint.php
new file mode 100644
index 0000000..00e1354
--- /dev/null
+++ b/src/Support/GeneratorBlueprint.php
@@ -0,0 +1,150 @@
+command = new CommandContext($commandName, $arguments, $options);
+
+ $this->nameInput = str($nameInput)->toString();
+
+ $this->isAbsoluteName = str($this->nameInput)->startsWith('/');
+
+ $this->type = $this->guessObjectType();
+
+ $this->normalizedName = Path::normalizeNamespace(
+ str($nameInput)
+ ->studly()
+ ->replace(['.', '\\', '/'], '\\')
+ ->trim('\\')
+ ->when($this->type === 'factory', fn ($name) => $name->finish('Factory'))
+ ->toString()
+ );
+
+ $this->baseName = class_basename($this->normalizedName);
+
+ $this->domain = new Domain($domainName);
+
+ $this->domainName = $this->domain->domainWithSubdomain;
+
+ $this->layer = DomainResolver::resolveLayer($this->domainName, $this->type);
+
+ $this->schema = $this->resolveSchema();
+ }
+
+ public static function capture(Command $command) {}
+
+ protected function guessObjectType(): string
+ {
+ return match ($this->command->name) {
+ 'ddd:base-view-model' => 'view_model',
+ 'ddd:base-model' => 'model',
+ 'ddd:value' => 'value_object',
+ 'ddd:dto' => 'data_transfer_object',
+ 'ddd:migration' => 'migration',
+ default => str($this->command->name)->after(':')->snake()->toString(),
+ };
+ }
+
+ protected function resolveSchema(): ObjectSchema
+ {
+ $customResolver = app('ddd')->getObjectSchemaResolver();
+
+ $blueprint = is_callable($customResolver)
+ ? App::call($customResolver, [
+ 'domainName' => $this->domainName,
+ 'nameInput' => $this->nameInput,
+ 'type' => $this->type,
+ 'command' => $this->command,
+ ])
+ : null;
+
+ if ($blueprint instanceof ObjectSchema) {
+ return $blueprint;
+ }
+
+ $namespace = match (true) {
+ $this->isAbsoluteName => $this->layer->namespace,
+ str($this->nameInput)->startsWith('\\') => $this->layer->guessNamespaceFromName($this->nameInput),
+ default => $this->layer->namespaceFor($this->type),
+ };
+
+ $fullyQualifiedName = str($this->normalizedName)
+ ->start($namespace.'\\')
+ ->toString();
+
+ return new ObjectSchema(
+ name: $this->normalizedName,
+ namespace: $namespace,
+ fullyQualifiedName: $fullyQualifiedName,
+ path: $this->layer->path($fullyQualifiedName),
+ );
+ }
+
+ public function rootNamespace()
+ {
+ return str($this->schema->namespace)->finish('\\')->toString();
+ }
+
+ public function getDefaultNamespace($rootNamespace)
+ {
+ return $this->schema->namespace;
+ }
+
+ public function getPath($name)
+ {
+ return Path::normalize(app()->basePath($this->schema->path));
+ }
+
+ public function qualifyClass($name)
+ {
+ return $this->schema->fullyQualifiedName;
+ }
+
+ public function getFactoryFor(string $name)
+ {
+ return $this->domain->factory($name);
+ }
+
+ public function getMigrationPath()
+ {
+ return $this->domain->migrationPath;
+ }
+
+ public function getNamespaceFor($type, $name = null)
+ {
+ return $this->domain->namespaceFor($type, $name);
+ }
+}
diff --git a/src/Support/Layer.php b/src/Support/Layer.php
new file mode 100644
index 0000000..1e86c0f
--- /dev/null
+++ b/src/Support/Layer.php
@@ -0,0 +1,76 @@
+namespace = Path::normalizeNamespace(Str::replaceEnd('\\', '', $namespace));
+
+ $this->path = is_null($path)
+ ? Path::fromNamespace($this->namespace)
+ : Path::normalize(Str::replaceEnd('/', '', $path));
+ }
+
+ public static function fromNamespace(string $namespace): self
+ {
+ return new self($namespace);
+ }
+
+ public function path(?string $path = null): string
+ {
+ if (is_null($path)) {
+ return $this->path;
+ }
+
+ $baseName = class_basename($path);
+
+ $relativePath = str($path)
+ ->beforeLast($baseName)
+ ->replaceStart($this->namespace, '')
+ ->replace(['\\', '/'], DIRECTORY_SEPARATOR)
+ ->append($baseName)
+ ->finish('.php')
+ ->toString();
+
+ return Path::join($this->path, $relativePath);
+ }
+
+ public function namespaceFor(string $type, ?string $name = null): string
+ {
+ $namespace = collect([
+ $this->namespace,
+ DomainResolver::getRelativeObjectNamespace($type),
+ ])->filter()->implode('\\');
+
+ if ($name) {
+ $namespace .= "\\{$name}";
+ }
+
+ return Path::normalizeNamespace($namespace);
+ }
+
+ public function guessNamespaceFromName(string $name): string
+ {
+ $baseName = class_basename($name);
+
+ return Path::normalizeNamespace(
+ str($name)
+ ->before($baseName)
+ ->trim('\\')
+ ->prepend($this->namespace.'\\')
+ ->toString()
+ );
+ }
+}
diff --git a/src/Support/Path.php b/src/Support/Path.php
index 7fbdb86..28da83c 100644
--- a/src/Support/Path.php
+++ b/src/Support/Path.php
@@ -18,6 +18,14 @@ public static function join(...$parts)
return implode(DIRECTORY_SEPARATOR, $parts);
}
+ public static function fromNamespace(string $namespace, ?string $classname = null): string
+ {
+ return str($namespace)
+ ->replace(['\\', '/'], DIRECTORY_SEPARATOR)
+ ->when($classname, fn ($s) => $s->append("{$classname}.php"))
+ ->toString();
+ }
+
public static function filePathToNamespace(string $path, string $namespacePath, string $namespace): string
{
return str_replace(
diff --git a/src/ValueObjects/CommandContext.php b/src/ValueObjects/CommandContext.php
new file mode 100644
index 0000000..5ad6032
--- /dev/null
+++ b/src/ValueObjects/CommandContext.php
@@ -0,0 +1,32 @@
+options);
+ }
+
+ public function option(string $key): mixed
+ {
+ return data_get($this->options, $key);
+ }
+
+ public function hasArgument(string $key): bool
+ {
+ return array_key_exists($key, $this->arguments);
+ }
+
+ public function argument(string $key): mixed
+ {
+ return data_get($this->arguments, $key);
+ }
+}
diff --git a/src/ValueObjects/DomainCommandContext.php b/src/ValueObjects/DomainCommandContext.php
deleted file mode 100644
index 692ceed..0000000
--- a/src/ValueObjects/DomainCommandContext.php
+++ /dev/null
@@ -1,51 +0,0 @@
-getName(),
- domain: $domain?->domainWithSubdomain,
- type: $type,
- resource: $command->argument('name'),
- arguments: $command->arguments(),
- options: $command->options(),
- );
- }
-
- public function hasOption(string $key): bool
- {
- return array_key_exists($key, $this->options);
- }
-
- public function option(string $key): mixed
- {
- return data_get($this->options, $key);
- }
-
- public function hasArgument(string $key): bool
- {
- return array_key_exists($key, $this->arguments);
- }
-
- public function argument(string $key): mixed
- {
- return data_get($this->arguments, $key);
- }
-}
diff --git a/src/ValueObjects/ObjectSchema.php b/src/ValueObjects/ObjectSchema.php
new file mode 100644
index 0000000..e3b78aa
--- /dev/null
+++ b/src/ValueObjects/ObjectSchema.php
@@ -0,0 +1,13 @@
+ 'src/Domain',
+ 'domain_namespace' => 'Domain',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application Layer
+ |--------------------------------------------------------------------------
+ |
+ | The path and namespace of the application layer, and the objects
+ | that should be recognized as part of the application layer.
+ |
+ */
+ 'application_path' => 'src/Application',
+ 'application_namespace' => 'Application',
+ 'application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Layers
+ |--------------------------------------------------------------------------
+ |
+ | Additional top-level namespaces and paths that should be recognized as
+ | layers when generating ddd:* objects.
+ |
+ | e.g., 'Infrastructure' => 'src/Infrastructure',
+ |
+ */
+ 'layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Object Namespaces
+ |--------------------------------------------------------------------------
+ |
+ | This value contains the default namespaces of ddd:* generated
+ | objects relative to the layer of which the object belongs to.
+ |
+ */
+ 'namespaces' => [
+ 'model' => 'Models',
+ 'data_transfer_object' => 'Data',
+ 'view_model' => 'ViewModels',
+ 'value_object' => 'ValueObjects',
+ 'action' => 'Actions',
+ 'cast' => 'Casts',
+ 'class' => '',
+ 'channel' => 'Channels',
+ 'command' => 'Commands',
+ 'controller' => 'Controllers',
+ 'enum' => 'Enums',
+ 'event' => 'Events',
+ 'exception' => 'Exceptions',
+ 'factory' => 'Database\Factories',
+ 'interface' => '',
+ 'job' => 'Jobs',
+ 'listener' => 'Listeners',
+ 'mail' => 'Mail',
+ 'middleware' => 'Middleware',
+ 'migration' => 'Database\Migrations',
+ 'notification' => 'Notifications',
+ 'observer' => 'Observers',
+ 'policy' => 'Policies',
+ 'provider' => 'Providers',
+ 'resource' => 'Resources',
+ 'request' => 'Requests',
+ 'rule' => 'Rules',
+ 'scope' => 'Scopes',
+ 'seeder' => 'Database\Seeders',
+ 'trait' => '',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Base Model
+ |--------------------------------------------------------------------------
+ |
+ | The base model class which generated domain models should extend. If
+ | set to null, the generated models will extend Laravel's default.
+ |
+ */
+ 'base_model' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Base DTO
+ |--------------------------------------------------------------------------
+ |
+ | The base class which generated data transfer objects should extend. By
+ | default, generated DTOs will extend `Spatie\LaravelData\Data` from
+ | Spatie's Laravel-data package, a highly recommended data object
+ | package to work with.
+ |
+ */
+ 'base_dto' => 'Spatie\LaravelData\Data',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Base ViewModel
+ |--------------------------------------------------------------------------
+ |
+ | The base class which generated view models should extend. By default,
+ | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`,
+ | which will be created if it doesn't already exist.
+ |
+ */
+ 'base_view_model' => 'Domain\Shared\ViewModels\ViewModel',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Base Action
+ |--------------------------------------------------------------------------
+ |
+ | The base class which generated action objects should extend. By default,
+ | generated actions are based on the `lorisleiva/laravel-actions` package
+ | and do not extend anything.
+ |
+ */
+ 'base_action' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Autoloading
+ |--------------------------------------------------------------------------
+ |
+ | Configure whether domain providers, commands, policies, factories,
+ | and migrations should be auto-discovered and registered.
+ |
+ */
+ 'autoload' => [
+ 'providers' => true,
+ 'commands' => true,
+ 'policies' => true,
+ 'factories' => true,
+ 'migrations' => true,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Autoload Ignore Folders
+ |--------------------------------------------------------------------------
+ |
+ | Folders that should be skipped during autoloading discovery,
+ | relative to the root of each domain.
+ |
+ | e.g., src/Domain/Invoicing/
+ |
+ | If more advanced filtering is needed, a callback can be registered
+ | using `DDD::filterAutoloadPathsUsing(callback $filter)` in
+ | the AppServiceProvider's boot method.
+ |
+ */
+ 'autoload_ignore' => [
+ 'Tests',
+ 'Database/Migrations',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Caching
+ |--------------------------------------------------------------------------
+ |
+ | The folder where the domain cache files will be stored. Used for domain
+ | autoloading.
+ |
+ */
+ 'cache_directory' => 'bootstrap/cache/ddd',
+];
diff --git a/tests/.skeleton/src/Application/Commands/ApplicationSync.php b/tests/.skeleton/src/Application/Commands/ApplicationSync.php
new file mode 100644
index 0000000..d295169
--- /dev/null
+++ b/tests/.skeleton/src/Application/Commands/ApplicationSync.php
@@ -0,0 +1,24 @@
+info('Application state synced!');
+
+ if ($secret = AppSession::getSecret()) {
+ $this->line($secret);
+
+ return;
+ }
+ }
+}
diff --git a/tests/.skeleton/src/Application/Database/Migrations/2024_10_14_215912_application_setup.php b/tests/.skeleton/src/Application/Database/Migrations/2024_10_14_215912_application_setup.php
new file mode 100644
index 0000000..88fa2f3
--- /dev/null
+++ b/tests/.skeleton/src/Application/Database/Migrations/2024_10_14_215912_application_setup.php
@@ -0,0 +1,24 @@
+app->singleton('application-singleton', function (Application $app) {
+ return 'application-singleton';
+ });
+ }
+
+ /**
+ * Bootstrap any application services.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ AppSession::setSecret('application-secret');
+ Clipboard::set('application-secret', 'application-secret');
+ }
+}
diff --git a/tests/.skeleton/src/Domain/Invoicing/Providers/InvoiceServiceProvider.php b/tests/.skeleton/src/Domain/Invoicing/Providers/InvoiceServiceProvider.php
index 5e257e5..7ddb52a 100644
--- a/tests/.skeleton/src/Domain/Invoicing/Providers/InvoiceServiceProvider.php
+++ b/tests/.skeleton/src/Domain/Invoicing/Providers/InvoiceServiceProvider.php
@@ -5,12 +5,13 @@
use Domain\Invoicing\Models\Invoice;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
+use Infrastructure\Support\Clipboard;
class InvoiceServiceProvider extends ServiceProvider
{
public function register()
{
- $this->app->singleton('invoicing', function (Application $app) {
+ $this->app->singleton('invoicing-singleton', function (Application $app) {
return 'invoicing-singleton';
});
}
@@ -23,5 +24,6 @@ public function register()
public function boot()
{
Invoice::setSecret('invoice-secret');
+ Clipboard::set('invoicing-secret', 'invoicing-secret');
}
}
diff --git a/tests/.skeleton/src/Infrastructure/Commands/LogPrune.php b/tests/.skeleton/src/Infrastructure/Commands/LogPrune.php
new file mode 100644
index 0000000..57f7ef7
--- /dev/null
+++ b/tests/.skeleton/src/Infrastructure/Commands/LogPrune.php
@@ -0,0 +1,24 @@
+info('System logs pruned!');
+
+ if ($secret = Clipboard::get('secret')) {
+ $this->line($secret);
+
+ return;
+ }
+ }
+}
diff --git a/tests/.skeleton/src/Infrastructure/Models/AppSession.php b/tests/.skeleton/src/Infrastructure/Models/AppSession.php
new file mode 100644
index 0000000..30f1ee1
--- /dev/null
+++ b/tests/.skeleton/src/Infrastructure/Models/AppSession.php
@@ -0,0 +1,23 @@
+app->singleton('infrastructure-singleton', function (Application $app) {
+ return 'infrastructure-singleton';
+ });
+ }
+
+ /**
+ * Bootstrap any application services.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ Clipboard::set('infrastructure-secret', 'infrastructure-secret');
+ }
+}
diff --git a/tests/.skeleton/src/Infrastructure/Support/Clipboard.php b/tests/.skeleton/src/Infrastructure/Support/Clipboard.php
new file mode 100644
index 0000000..b8eef1e
--- /dev/null
+++ b/tests/.skeleton/src/Infrastructure/Support/Clipboard.php
@@ -0,0 +1,18 @@
+setupTestApplication();
-});
-
-it('can register a custom namespace resolver', function () {
- Config::set('ddd.application', [
- 'path' => 'src/App',
- 'namespace' => 'App',
- ]);
-
- DDD::resolveNamespaceUsing(function (string $domain, string $type, ?DomainCommandContext $context): ?string {
- if ($type == 'controller' && $context->option('api')) {
- return "App\\Api\\Controllers\\{$domain}";
- }
-
- return null;
- });
-
- Artisan::call('ddd:controller', [
- 'name' => 'PaymentApiController',
- '--domain' => 'Invoicing',
- '--api' => true,
- ]);
-
- $output = Artisan::output();
-
- expect($output)
- ->toContainFilepath('src/App/Api/Controllers/Invoicing/PaymentApiController.php');
-
- $expectedPath = base_path('src/App/Api/Controllers/Invoicing/PaymentApiController.php');
-
- expect(file_get_contents($expectedPath))
- ->toContain("namespace App\Api\Controllers\Invoicing;");
-});
diff --git a/tests/Autoload/CommandTest.php b/tests/Autoload/CommandTest.php
index 57650e7..3202d83 100644
--- a/tests/Autoload/CommandTest.php
+++ b/tests/Autoload/CommandTest.php
@@ -1,102 +1,81 @@
commands = [
+ 'application:sync' => 'Application\Commands\ApplicationSync',
+ 'invoice:deliver' => 'Domain\Invoicing\Commands\InvoiceDeliver',
+ 'log:prune' => 'Infrastructure\Commands\LogPrune',
+ ];
- $this->setupTestApplication();
+ $this->setupTestApplication();
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
- });
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
+});
- it('does not register the command', function () {
- expect(class_exists('Domain\Invoicing\Commands\InvoiceDeliver'))->toBeTrue();
- expect(fn () => Artisan::call('invoice:deliver'))->toThrow(CommandNotFoundException::class);
- });
+afterEach(function () {
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
});
-describe('with autoload', function () {
- beforeEach(function () {
- Config::set('ddd.autoload.commands', true);
+describe('when ddd.autoload.commands = false', function () {
+ it('skips handling commands', function () {
+ config()->set('ddd.autoload.commands', false);
- $this->setupTestApplication();
+ $mock = AutoloadManager::partialMock();
+ $mock->shouldNotReceive('handleCommands');
+ $mock->run();
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ expect($mock->getRegisteredCommands())->toBeEmpty();
});
+});
- it('registers existing commands', function () {
- $command = 'invoice:deliver';
+describe('when ddd.autoload.commands = true', function () {
+ it('registers the commands', function () {
+ config()->set('ddd.autoload.commands', true);
- expect(collect(Artisan::all()))
- ->has($command)
- ->toBeTrue();
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
- expect(class_exists('Domain\Invoicing\Commands\InvoiceDeliver'))->toBeTrue();
- Artisan::call($command);
- expect(Artisan::output())->toContain('Invoice delivered!');
+ $expected = array_values($this->commands);
+ $registered = array_values($mock->getRegisteredCommands());
+ expect($expected)->each(fn ($item) => $item->toBeIn($registered));
+ expect($registered)->toHaveCount(count($expected));
});
-
- it('registers newly created commands', function () {
- $command = 'app:invoice-void';
-
- expect(collect(Artisan::all()))
- ->has($command)
- ->toBeFalse();
-
- Artisan::call('ddd:command', [
- 'name' => 'InvoiceVoid',
- '--domain' => 'Invoicing',
- ]);
-
- expect(collect(Artisan::all()))
- ->has($command)
- ->toBeTrue();
-
- $this->artisan($command)->assertSuccessful();
- })->skip("Can't get this to work, might not be test-able without a real app environment.");
});
describe('caching', function () {
- beforeEach(function () {
- Config::set('ddd.autoload.commands', true);
-
- $this->setupTestApplication();
- });
-
it('remembers the last cached state', function () {
DomainCache::set('domain-commands', []);
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ config()->set('ddd.autoload.commands', true);
- // command should not be recognized due to cached empty-state
- expect(fn () => Artisan::call('invoice:deliver'))->toThrow(CommandNotFoundException::class);
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
+ $registered = array_values($mock->getRegisteredCommands());
+ expect($registered)->toHaveCount(0);
});
it('can bust the cache', function () {
DomainCache::set('domain-commands', []);
DomainCache::clear();
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ config()->set('ddd.autoload.commands', true);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
- $this->artisan('invoice:deliver')->assertSuccessful();
+ $expected = array_values($this->commands);
+ $registered = array_values($mock->getRegisteredCommands());
+ expect($expected)->each(fn ($item) => $item->toBeIn($registered));
+ expect($registered)->toHaveCount(count($expected));
});
});
diff --git a/tests/Autoload/FactoryTest.php b/tests/Autoload/FactoryTest.php
index f496b38..2de51c0 100644
--- a/tests/Autoload/FactoryTest.php
+++ b/tests/Autoload/FactoryTest.php
@@ -1,25 +1,39 @@
setupTestApplication();
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
+});
- Config::set('ddd.domain_namespace', 'Domain');
+afterEach(function () {
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
});
-describe('autoload enabled', function () {
- beforeEach(function () {
- Config::set('ddd.autoload.factories', true);
+describe('when ddd.autoload.factories = true', function () {
+ it('handles the factories', function () {
+ config()->set('ddd.autoload.factories', true);
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ $mock = AutoloadManager::partialMock();
+ $mock->shouldReceive('handleFactories')->once();
+ $mock->run();
});
it('can resolve domain factory', function ($modelClass, $expectedFactoryClass) {
+ config()->set('ddd.autoload.factories', true);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
expect($modelClass::factory())->toBeInstanceOf($expectedFactoryClass);
})->with([
// VanillaModel is a vanilla eloquent model in the domain layer
@@ -36,30 +50,42 @@
]);
it('gracefully falls back for non-domain factories', function () {
+ config()->set('ddd.autoload.factories', true);
+
+ $this->refreshApplication();
+
Artisan::call('make:model RegularModel -f');
$modelClass = 'App\Models\RegularModel';
expect(class_exists($modelClass))->toBeTrue();
+ expect(Factory::resolveFactoryName($modelClass))
+ ->toEqual('Database\Factories\RegularModelFactory');
+
expect($modelClass::factory())
->toBeInstanceOf('Database\Factories\RegularModelFactory');
});
});
-describe('autoload disabled', function () {
- beforeEach(function () {
- Config::set('ddd.autoload.factories', false);
+describe('when ddd.autoload.factories = false', function () {
+ it('skips handling factories', function () {
+ config()->set('ddd.autoload.factories', false);
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ $mock = AutoloadManager::partialMock();
+ $mock->shouldNotReceive('handleFactories');
+ $mock->run();
});
- it('cannot resolve factories that rely on autoloading', function ($modelClass) {
+ it('cannot resolve factories that rely on autoloading', function ($modelClass, $correctFactories) {
+ config()->set('ddd.autoload.factories', false);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
expect(fn () => $modelClass::factory())->toThrow(Error::class);
})->with([
- ['Domain\Invoicing\Models\VanillaModel'],
- ['Domain\Internal\Reporting\Models\Report'],
+ ['Domain\Invoicing\Models\VanillaModel', ['Domain\Invoicing\Database\Factories\VanillaModelFactory', 'Database\Factories\Invoicing\VanillaModelFactory']],
+ ['Domain\Internal\Reporting\Models\Report', ['Domain\Internal\Reporting\Database\Factories\ReportFactory', 'Database\Factories\Internal\Reporting\ReportFactory']],
]);
});
diff --git a/tests/Autoload/IgnoreTest.php b/tests/Autoload/IgnoreTest.php
index 0e94256..5be1acf 100644
--- a/tests/Autoload/IgnoreTest.php
+++ b/tests/Autoload/IgnoreTest.php
@@ -5,74 +5,105 @@
use Illuminate\Support\Str;
use Lunarstorm\LaravelDDD\Facades\DDD;
use Lunarstorm\LaravelDDD\Support\DomainCache;
+use Lunarstorm\LaravelDDD\Tests\BootsTestApplication;
use Symfony\Component\Finder\SplFileInfo;
+uses(BootsTestApplication::class);
+
beforeEach(function () {
- Config::set('ddd.domain_path', 'src/Domain');
- Config::set('ddd.domain_namespace', 'Domain');
+ $this->providers = [
+ 'Application\Providers\ApplicationServiceProvider',
+ 'Domain\Invoicing\Providers\InvoiceServiceProvider',
+ 'Infrastructure\Providers\InfrastructureServiceProvider',
+ ];
+
+ $this->commands = [
+ 'application:sync' => 'Application\Commands\ApplicationSync',
+ 'invoice:deliver' => 'Domain\Invoicing\Commands\InvoiceDeliver',
+ 'log:prune' => 'Infrastructure\Commands\LogPrune',
+ ];
+
$this->setupTestApplication();
+
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
+
+ Config::set('ddd.autoload', [
+ 'providers' => true,
+ 'commands' => true,
+ 'factories' => true,
+ 'policies' => true,
+ 'migrations' => true,
+ ]);
+});
+
+afterEach(function () {
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
});
it('can ignore folders when autoloading', function () {
- Artisan::call('ddd:cache');
+ expect(config('ddd.domain_path'))->toEqual('src/Domain');
+ expect(config('ddd.domain_namespace'))->toEqual('Domain');
+ expect(config('ddd.application_path'))->toEqual('src/Application');
+ expect(config('ddd.application_namespace'))->toEqual('Application');
+ expect(config('ddd.layers'))->toContain('src/Infrastructure');
$expected = [
- 'Domain\Invoicing\Providers\InvoiceServiceProvider',
- 'Domain\Invoicing\Commands\InvoiceDeliver',
+ ...array_values($this->providers),
+ ...array_values($this->commands),
];
- $cached = [
- ...DomainCache::get('domain-providers'),
- ...DomainCache::get('domain-commands'),
+ $discovered = [
+ ...DDD::autoloader()->discoverProviders(),
+ ...DDD::autoloader()->discoverCommands(),
];
- expect($cached)->toEqual($expected);
+ expect($expected)->each(fn ($item) => $item->toBeIn($discovered));
+ expect($discovered)->toHaveCount(count($expected));
Config::set('ddd.autoload_ignore', ['Commands']);
- Artisan::call('ddd:cache');
-
$expected = [
- 'Domain\Invoicing\Providers\InvoiceServiceProvider',
+ ...array_values($this->providers),
];
- $cached = [
- ...DomainCache::get('domain-providers'),
- ...DomainCache::get('domain-commands'),
+ $discovered = [
+ ...DDD::autoloader()->discoverProviders(),
+ ...DDD::autoloader()->discoverCommands(),
];
- expect($cached)->toEqual($expected);
+ expect($expected)->each(fn ($item) => $item->toBeIn($discovered));
+ expect($discovered)->toHaveCount(count($expected));
Config::set('ddd.autoload_ignore', ['Providers']);
- Artisan::call('ddd:cache');
-
$expected = [
- 'Domain\Invoicing\Commands\InvoiceDeliver',
+ ...array_values($this->commands),
];
- $cached = [
- ...DomainCache::get('domain-providers'),
- ...DomainCache::get('domain-commands'),
+ $discovered = [
+ ...DDD::autoloader()->discoverProviders(),
+ ...DDD::autoloader()->discoverCommands(),
];
- expect($cached)->toEqual($expected);
+ expect($expected)->each(fn ($item) => $item->toBeIn($discovered));
+ expect($discovered)->toHaveCount(count($expected));
});
it('can register a custom autoload filter', function () {
- Artisan::call('ddd:cache');
-
$expected = [
- 'Domain\Invoicing\Providers\InvoiceServiceProvider',
- 'Domain\Invoicing\Commands\InvoiceDeliver',
+ ...array_values($this->providers),
+ ...array_values($this->commands),
];
- $cached = [
- ...DomainCache::get('domain-providers'),
- ...DomainCache::get('domain-commands'),
+ $discovered = [
+ ...DDD::autoloader()->discoverProviders(),
+ ...DDD::autoloader()->discoverCommands(),
];
- expect($cached)->toEqual($expected);
+ expect($expected)->each(fn ($item) => $item->toBeIn($discovered));
+ expect($discovered)->toHaveCount(count($expected));
$secret = null;
@@ -80,6 +111,10 @@
$ignoredFiles = [
'InvoiceServiceProvider.php',
'InvoiceDeliver.php',
+ 'ApplicationServiceProvider.php',
+ 'ApplicationSync.php',
+ 'InfrastructureServiceProvider.php',
+ 'LogPrune.php',
];
$secret = 'i-was-invoked';
@@ -89,14 +124,12 @@
}
});
- Artisan::call('ddd:cache');
-
- $cached = [
- ...DomainCache::get('domain-providers'),
- ...DomainCache::get('domain-commands'),
+ $discovered = [
+ ...DDD::autoloader()->discoverProviders(),
+ ...DDD::autoloader()->discoverCommands(),
];
- expect($cached)->toEqual([]);
+ expect($discovered)->toHaveCount(0);
expect($secret)->toEqual('i-was-invoked');
});
diff --git a/tests/Autoload/PolicyTest.php b/tests/Autoload/PolicyTest.php
index 160117c..5c1859c 100644
--- a/tests/Autoload/PolicyTest.php
+++ b/tests/Autoload/PolicyTest.php
@@ -1,30 +1,72 @@
policies = [
+ 'Domain\Invoicing\Models\Invoice' => 'Domain\Invoicing\Policies\InvoicePolicy',
+ 'Infrastructure\Models\AppSession' => 'Infrastructure\Policies\AppSessionPolicy',
+ 'Application\Models\Login' => 'Application\Policies\LoginPolicy',
+ ];
+
$this->setupTestApplication();
- Config::set('ddd.domain_namespace', 'Domain');
- Config::set('ddd.autoload.factories', true);
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
+});
+
+afterEach(function () {
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
+});
+
+describe('when ddd.autoload.policies = false', function () {
+ it('skips handling policies', function () {
+ config()->set('ddd.autoload.policies', false);
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
+ $mock = AutoloadManager::partialMock();
+ $mock->shouldNotReceive('handlePolicies');
+ $mock->run();
});
});
-it('can autoload domain policy', function ($class, $expectedPolicy) {
- expect(class_exists($class))->toBeTrue();
- expect(Gate::getPolicyFor($class))->toBeInstanceOf($expectedPolicy);
-})->with([
- ['Domain\Invoicing\Models\Invoice', 'Domain\Invoicing\Policies\InvoicePolicy'],
-]);
-
-it('gracefully falls back for non-domain policies', function ($class, $expectedPolicy) {
- expect(class_exists($class))->toBeTrue();
- expect(Gate::getPolicyFor($class))->toBeInstanceOf($expectedPolicy);
-})->with([
- ['App\Models\Post', 'App\Policies\PostPolicy'],
-]);
+describe('when ddd.autoload.policies = true', function () {
+ it('handles policies', function () {
+ config()->set('ddd.autoload.policies', true);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->shouldReceive('handlePolicies')->once();
+ $mock->run();
+ });
+
+ it('can resolve the policies', function () {
+ config()->set('ddd.autoload.policies', true);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
+ foreach ($this->policies as $class => $expectedPolicy) {
+ $resolvedPolicy = Gate::getPolicyFor($class);
+ expect($mock->getResolvedPolicies())->toHaveKey($class);
+ }
+ })->markTestIncomplete('custom layer policies are not yet supported');
+
+ it('gracefully falls back for non-ddd policies', function ($class, $expectedPolicy) {
+ config()->set('ddd.autoload.policies', true);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
+ expect(class_exists($class))->toBeTrue();
+ expect(Gate::getPolicyFor($class))->toBeInstanceOf($expectedPolicy);
+ expect($mock->getResolvedPolicies())->not->toHaveKey($class);
+ })->with([
+ ['App\Models\Post', 'App\Policies\PostPolicy'],
+ ]);
+});
diff --git a/tests/Autoload/ProviderTest.php b/tests/Autoload/ProviderTest.php
index 6316e54..871eba6 100644
--- a/tests/Autoload/ProviderTest.php
+++ b/tests/Autoload/ProviderTest.php
@@ -1,77 +1,117 @@
providers = [
+ 'Application\Providers\ApplicationServiceProvider',
+ 'Domain\Invoicing\Providers\InvoiceServiceProvider',
+ 'Infrastructure\Providers\InfrastructureServiceProvider',
+ ];
$this->setupTestApplication();
+
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
+
+ expect(config('ddd.autoload_ignore'))->toEqualCanonicalizing([
+ 'Tests',
+ 'Database/Migrations',
+ ]);
+});
+
+afterEach(function () {
+ DomainCache::clear();
+ Artisan::call('ddd:clear');
});
-describe('without autoload', function () {
- beforeEach(function () {
- config([
- 'ddd.autoload.providers' => false,
- ]);
+describe('when ddd.autoload.providers = false', function () {
+ it('skips handling providers', function () {
+ config()->set('ddd.autoload.providers', false);
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ $mock = AutoloadManager::partialMock();
+ $mock->shouldNotReceive('handleProviders');
+ $mock->run();
});
- it('does not register the provider', function () {
- expect(fn () => app('invoicing'))->toThrow(Exception::class);
+ it('does not register the providers', function () {
+ config()->set('ddd.autoload.providers', false);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
+ expect($mock->getRegisteredProviders())->toBeEmpty();
+
+ expect(fn () => app('invoicing-singleton'))->toThrow(Exception::class);
+ expect(fn () => app('application-singleton'))->toThrow(Exception::class);
+ expect(fn () => app('infrastructure-singleton'))->toThrow(Exception::class);
});
});
-describe('with autoload', function () {
- beforeEach(function () {
- config([
- 'ddd.autoload.providers' => true,
- ]);
+describe('when ddd.autoload.providers = true', function () {
+ it('handles the providers', function () {
+ config()->set('ddd.autoload.providers', true);
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ $mock = AutoloadManager::partialMock();
+ $mock->shouldReceive('handleProviders')->once();
+ $mock->run();
});
- it('registers the provider', function () {
- expect(app('invoicing'))->toEqual('invoicing-singleton');
- $this->artisan('invoice:deliver')->expectsOutputToContain('invoice-secret');
- });
-});
+ it('registers the providers', function () {
+ config()->set('ddd.autoload.providers', true);
-describe('caching', function () {
- beforeEach(function () {
- config([
- 'ddd.autoload.providers' => true,
- ]);
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
+ expect(DomainCache::has('domain-providers'))->toBeFalse();
+
+ $expected = array_values($this->providers);
+ $registered = array_values($mock->getRegisteredProviders());
+ expect($expected)->each(fn ($item) => $item->toBeIn($registered));
+ expect($registered)->toHaveCount(count($expected));
- $this->setupTestApplication();
+ expect(app('application-singleton'))->toEqual('application-singleton');
+ expect(app('invoicing-singleton'))->toEqual('invoicing-singleton');
+ expect(app('infrastructure-singleton'))->toEqual('infrastructure-singleton');
});
+});
+describe('caching', function () {
it('remembers the last cached state', function () {
DomainCache::set('domain-providers', []);
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ config()->set('ddd.autoload.providers', true);
- expect(fn () => app('invoicing'))->toThrow(Exception::class);
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
+ expect(DomainCache::has('domain-providers'))->toBeTrue();
+
+ $registered = array_values($mock->getRegisteredProviders());
+ expect($registered)->toHaveCount(0);
});
it('can bust the cache', function () {
DomainCache::set('domain-providers', []);
DomainCache::clear();
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
- });
+ config()->set('ddd.autoload.providers', true);
+
+ $mock = AutoloadManager::partialMock();
+ $mock->run();
+
+ $expected = array_values($this->providers);
+ $registered = array_values($mock->getRegisteredProviders());
+ expect($expected)->each(fn ($item) => $item->toBeIn($registered));
+ expect($registered)->toHaveCount(count($expected));
- expect(app('invoicing'))->toEqual('invoicing-singleton');
- $this->artisan('invoice:deliver')->expectsOutputToContain('invoice-secret');
+ expect(app('application-singleton'))->toEqual('application-singleton');
+ expect(app('invoicing-singleton'))->toEqual('invoicing-singleton');
+ expect(app('infrastructure-singleton'))->toEqual('infrastructure-singleton');
});
});
diff --git a/tests/BootsTestApplication.php b/tests/BootsTestApplication.php
new file mode 100644
index 0000000..d653a3d
--- /dev/null
+++ b/tests/BootsTestApplication.php
@@ -0,0 +1,5 @@
+setupTestApplication();
+
+ // $this->app->when(MigrationCreator::class)
+ // ->needs('$customStubPath')
+ // ->give(fn() => $this->app->basePath('stubs'));
+
+ $this->originalComposerContents = file_get_contents(base_path('composer.json'));
+
+ // $this->artisan('clear-compiled')->assertSuccessful()->execute();
+ // $this->artisan('optimize:clear')->assertSuccessful()->execute();
+});
+
+afterEach(function () {
+ $this->cleanSlate();
+
+ file_put_contents(base_path('composer.json'), $this->originalComposerContents);
+
+ $this->artisan('optimize:clear')->assertSuccessful()->execute();
+});
+
+it('can run the config wizard', function () {
+ $this->artisan('config:cache')->assertSuccessful()->execute();
+
+ expect(config('ddd.domain_path'))->toBe('src/Domain');
+ expect(config('ddd.domain_namespace'))->toBe('Domain');
+ expect(config('ddd.layers'))->toBe([
+ 'Infrastructure' => 'src/Infrastructure',
+ ]);
+
+ $this->reloadApplication();
+
+ $configPath = config_path('ddd.php');
+
+ $this->artisan('ddd:config')
+ ->expectsQuestion('Laravel-DDD Config Utility', 'wizard')
+ ->expectsQuestion('Domain Path', 'src/CustomDomain')
+ ->expectsQuestion('Domain Namespace', 'CustomDomain')
+ ->expectsQuestion('Path to Application Layer', null)
+ ->expectsQuestion('Custom Layers (Optional)', ['Support' => 'src/Support'])
+ ->expectsOutput('Building configuration...')
+ ->expectsOutput("Configuration updated: {$configPath}")
+ ->assertSuccessful()
+ ->execute();
+
+ expect(file_exists($configPath))->toBeTrue();
+
+ $this->artisan('config:cache')->assertSuccessful()->execute();
+
+ expect(config('ddd.domain_path'))->toBe('src/CustomDomain');
+ expect(config('ddd.domain_namespace'))->toBe('CustomDomain');
+ expect(config('ddd.application_path'))->toBe('src/Application');
+ expect(config('ddd.application_namespace'))->toBe('Application');
+ expect(config('ddd.layers'))->toBe([
+ 'Support' => 'src/Support',
+ ]);
+
+ $this->artisan('config:clear')->assertSuccessful()->execute();
+
+ unlink($configPath);
+})->skip(fn () => ! ConfigCommand::hasRequiredVersionOfLaravelPrompts());
+
+it('requires supported version of Laravel Prompts to run the wizard', function () {
+ $this->artisan('ddd:config')
+ ->expectsQuestion('Laravel-DDD Config Utility', 'wizard')
+ ->expectsOutput('This command is not supported with your currently installed version of Laravel Prompts.')
+ ->assertFailed()
+ ->execute();
+})->skip(fn () => ConfigCommand::hasRequiredVersionOfLaravelPrompts());
+
+it('can update and merge ddd.php with latest package version', function () {
+ $configPath = config_path('ddd.php');
+
+ $originalContents = <<<'PHP'
+artisan('ddd:config')
+ ->expectsQuestion('Laravel-DDD Config Utility', 'update')
+ ->expectsQuestion('Are you sure you want to update ddd.php and merge with latest copy from the package?', true)
+ ->expectsOutput('Merging ddd.php...')
+ ->expectsOutput("Configuration updated: {$configPath}")
+ ->expectsOutput('Note: Some values may require manual adjustment.')
+ ->assertSuccessful()
+ ->execute();
+
+ $packageConfigContents = file_get_contents(DDD::packagePath('config/ddd.php'));
+
+ expect($updatedContents = file_get_contents($configPath))
+ ->not->toEqual($originalContents);
+
+ $updatedConfigArray = include $configPath;
+ $packageConfigArray = include DDD::packagePath('config/ddd.php');
+
+ expect($updatedConfigArray)->toHaveKeys(array_keys($packageConfigArray));
+
+ unlink($configPath);
+});
+
+it('can sync composer.json from ddd.php ', function () {
+ $configContent = <<<'PHP'
+ 'src/CustomDomain',
+ 'domain_namespace' => 'CustomDomain',
+ 'application_path' => 'src/CustomApplication',
+ 'application_namespace' => 'CustomApplication',
+ 'application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
+ ],
+ 'layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ 'CustomLayer' => 'src/CustomLayer',
+ ],
+];
+PHP;
+
+ file_put_contents(config_path('ddd.php'), $configContent);
+
+ $this->artisan('config:cache')->assertSuccessful()->execute();
+
+ $composerContents = file_get_contents(base_path('composer.json'));
+
+ $fragments = [
+ '"CustomDomain\\\\": "src/CustomDomain"',
+ '"Infrastructure\\\\": "src/Infrastructure"',
+ '"CustomLayer\\\\": "src/CustomLayer"',
+ '"CustomApplication\\\\": "src/CustomApplication"',
+ ];
+
+ expect($composerContents)->not->toContain(...$fragments);
+
+ $this->artisan('ddd:config')
+ ->expectsQuestion('Laravel-DDD Config Utility', 'composer')
+ ->expectsOutput('Syncing composer.json from ddd.php...')
+ ->expectsOutputToContain(...[
+ 'Namespace',
+ 'Path',
+ 'Status',
+
+ 'CustomDomain',
+ 'src/CustomDomain',
+ 'Added',
+
+ 'CustomApplication',
+ 'src/CustomApplication',
+ 'Added',
+
+ 'Infrastructure',
+ 'src/Infrastructure',
+ 'Added',
+ ])
+ ->assertSuccessful()
+ ->execute();
+
+ $composerContents = file_get_contents(base_path('composer.json'));
+
+ expect($composerContents)->toContain(...$fragments);
+
+ $this->artisan('config:clear')->assertSuccessful()->execute();
+
+ unlink(config_path('ddd.php'));
+});
+
+it('can detect domain namespace from composer.json', function () {
+ $sampleComposer = file_get_contents(__DIR__.'/resources/composer.sample.json');
+
+ file_put_contents(
+ app()->basePath('composer.json'),
+ $sampleComposer
+ );
+
+ $configPath = config_path('ddd.php');
+
+ $this->artisan('ddd:config')
+ ->expectsQuestion('Laravel-DDD Config Utility', 'detect')
+ ->expectsOutputToContain(...[
+ 'Detected configuration:',
+ 'domain_path',
+ 'lib/CustomDomain',
+ 'domain_namespace',
+ 'Domain',
+ ])
+ ->expectsQuestion('Update configuration with these values?', true)
+ ->expectsOutput('Configuration updated: '.$configPath)
+ ->assertSuccessful()
+ ->execute();
+
+ $configValues = DDD::config()->get();
+
+ expect(data_get($configValues, 'domain_path'))->toBe('lib/CustomDomain');
+ expect(data_get($configValues, 'domain_namespace'))->toBe('Domain');
+
+ unlink($configPath);
+});
diff --git a/tests/Command/InstallTest.php b/tests/Command/InstallTest.php
index 2be26aa..54bbc2a 100644
--- a/tests/Command/InstallTest.php
+++ b/tests/Command/InstallTest.php
@@ -3,9 +3,14 @@
use Illuminate\Support\Facades\Config;
beforeEach(function () {
+ $this->originalComposerContents = file_get_contents(base_path('composer.json'));
$this->setupTestApplication();
});
+afterEach(function () {
+ file_put_contents(base_path('composer.json'), $this->originalComposerContents);
+});
+
it('publishes config', function () {
$path = config_path('ddd.php');
@@ -15,11 +20,11 @@
expect(file_exists($path))->toBeFalse();
- $command = $this->artisan('ddd:install');
- $command->expectsOutput('Publishing config...');
- $command->expectsOutput('Ensuring domain path is registered in composer.json...');
- $command->expectsConfirmation('Would you like to publish stubs?', 'no');
- $command->execute();
+ $command = $this->artisan('ddd:install')
+ ->expectsOutput('Publishing config...')
+ ->expectsOutput('Updating composer.json...')
+ ->expectsQuestion('Would you like to publish stubs now?', false)
+ ->execute();
expect(file_exists($path))->toBeTrue();
expect(file_get_contents($path))->toEqual(file_get_contents(__DIR__.'/../../config/ddd.php'));
@@ -42,9 +47,9 @@
$before = data_get($data, ['autoload', 'psr-4', $domainRoot.'\\']);
expect($before)->toBeNull();
- $command = $this->artisan('ddd:install');
- $command->expectsConfirmation('Would you like to publish stubs?', 'no');
- $command->execute();
+ $command = $this->artisan('ddd:install')
+ ->expectsQuestion('Would you like to publish stubs now?', false)
+ ->execute();
$data = json_decode(file_get_contents(base_path('composer.json')), true);
$after = data_get($data, ['autoload', 'psr-4', $domainRoot.'\\']);
@@ -53,7 +58,7 @@
unlink(config_path('ddd.php'));
// Reset composer back to the factory state
- $this->setDomainPathInComposer('Domain', 'src/Domain', reload: true);
+ $this->setAutoloadPathInComposer('Domain', 'src/Domain', reload: true);
})->with([
['src/Domain', 'Domain'],
['src/Domains', 'Domains'],
diff --git a/tests/Command/ListTest.php b/tests/Command/ListTest.php
index 6a1443b..48e067c 100644
--- a/tests/Command/ListTest.php
+++ b/tests/Command/ListTest.php
@@ -1,8 +1,11 @@
setupTestApplication();
+
$this->artisan('ddd:model', [
'name' => 'Invoice',
'--domain' => 'Invoicing',
@@ -38,9 +41,10 @@
$this
->artisan('ddd:list')
- ->expectsTable([
+ ->expectsOutputToContain(...[
'Domain',
'Namespace',
'Path',
- ], $expectedTableContent);
+ ...Arr::flatten($expectedTableContent),
+ ]);
});
diff --git a/tests/Command/CacheTest.php b/tests/Command/OptimizeTest.php
similarity index 76%
rename from tests/Command/CacheTest.php
rename to tests/Command/OptimizeTest.php
index f7aec3f..f2db8a4 100644
--- a/tests/Command/CacheTest.php
+++ b/tests/Command/OptimizeTest.php
@@ -1,28 +1,34 @@
setupTestApplication();
- config(['cache.default' => 'file']);
-
DomainCache::clear();
+
+ $this->originalComposerContents = file_get_contents(base_path('composer.json'));
});
afterEach(function () {
- $this->artisan('optimize:clear')->execute();
+ DomainCache::clear();
+
+ file_put_contents(base_path('composer.json'), $this->originalComposerContents);
+
+ $this->artisan('optimize:clear')->assertSuccessful()->execute();
});
-it('can cache discovered domain providers, commands, migrations', function () {
+it('can optimize discovered domain providers, commands, migrations', function () {
expect(DomainCache::get('domain-providers'))->toBeNull();
expect(DomainCache::get('domain-commands'))->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->toBeNull();
$this
- ->artisan('ddd:cache')
+ ->artisan('ddd:optimize')
->expectsOutputToContain('Caching DDD providers, commands, migration paths.')
->expectsOutputToContain('domain providers')
->expectsOutputToContain('domain commands')
@@ -41,7 +47,7 @@
});
it('can clear the cache', function () {
- Artisan::call('ddd:cache');
+ $this->artisan('ddd:optimize')->assertSuccessful()->execute();
expect(DomainCache::get('domain-providers'))->not->toBeNull();
expect(DomainCache::get('domain-commands'))->not->toBeNull();
@@ -62,20 +68,20 @@
expect(DomainCache::get('domain-commands'))->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->toBeNull();
- $this->artisan('ddd:cache')->execute();
+ $this->artisan('ddd:optimize')->assertSuccessful()->execute();
expect(DomainCache::get('domain-providers'))->not->toBeNull();
expect(DomainCache::get('domain-commands'))->not->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->not->toBeNull();
- $this->artisan('cache:clear')->execute();
+ $this->artisan('cache:clear')->assertSuccessful()->execute();
expect(DomainCache::get('domain-providers'))->not->toBeNull();
expect(DomainCache::get('domain-commands'))->not->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->not->toBeNull();
if (Feature::LaravelPackageOptimizeCommands->missing()) {
- $this->artisan('optimize:clear')->execute();
+ $this->artisan('optimize:clear')->assertSuccessful()->execute();
expect(DomainCache::get('domain-providers'))->not->toBeNull();
expect(DomainCache::get('domain-commands'))->not->toBeNull();
@@ -84,33 +90,33 @@
});
describe('laravel optimize', function () {
- test('optimize will include ddd:cache', function () {
- config(['cache.default' => 'file']);
-
+ test('optimize will include ddd:optimize', function () {
expect(DomainCache::get('domain-providers'))->toBeNull();
expect(DomainCache::get('domain-commands'))->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->toBeNull();
- $this->artisan('optimize')->execute();
+ $this->artisan('optimize')->assertSuccessful()->execute();
expect(DomainCache::get('domain-providers'))->not->toBeNull();
expect(DomainCache::get('domain-commands'))->not->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->not->toBeNull();
+
+ $this->artisan('optimize:clear')->assertSuccessful()->execute();
});
test('optimize:clear will clear ddd cache', function () {
- config(['cache.default' => 'file']);
-
- $this->artisan('ddd:cache')->execute();
+ $this->artisan('ddd:optimize')->assertSuccessful()->execute();
expect(DomainCache::get('domain-providers'))->not->toBeNull();
expect(DomainCache::get('domain-commands'))->not->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->not->toBeNull();
- $this->artisan('optimize:clear')->execute();
+ $this->artisan('optimize:clear')->assertSuccessful()->execute();
expect(DomainCache::get('domain-providers'))->toBeNull();
expect(DomainCache::get('domain-commands'))->toBeNull();
expect(DomainCache::get('domain-migration-paths'))->toBeNull();
+
+ $this->artisan('optimize:clear')->assertSuccessful()->execute();
});
})->skipOnLaravelVersionsBelow(Feature::LaravelPackageOptimizeCommands->value);
diff --git a/tests/Command/UpgradeTest.php b/tests/Command/UpgradeTest.php
index 9d649e6..1ddb156 100644
--- a/tests/Command/UpgradeTest.php
+++ b/tests/Command/UpgradeTest.php
@@ -5,11 +5,11 @@
use Illuminate\Support\Facades\File;
it('can upgrade 0.x config to 1.x', function (string $pathToOldConfig, array $expectedValues) {
- $path = config_path('ddd.php');
+ $configFilePath = config_path('ddd.php');
- File::copy($pathToOldConfig, $path);
+ File::copy($pathToOldConfig, $configFilePath);
- expect(file_exists($path))->toBeTrue();
+ expect(file_exists($configFilePath))->toBeTrue();
$this->artisan('ddd:upgrade')
->expectsOutputToContain('Configuration upgraded successfully.')
@@ -21,10 +21,13 @@
$configAsArray = require config_path('ddd.php');
- foreach ($expectedValues as $path => $value) {
- expect(data_get($configAsArray, $path))
- ->toEqual($value, "Config {$path} does not match expected value.");
+ foreach ($expectedValues as $key => $value) {
+ expect(data_get($configAsArray, $key))
+ ->toEqual($value, "Config {$key} does not match expected value.");
}
+
+ // Delete the config file after the test
+ unlink($configFilePath);
})->with('configUpgrades');
it('skips upgrade if config file was not published', function () {
diff --git a/tests/Command/resources/composer.sample.json b/tests/Command/resources/composer.sample.json
new file mode 100644
index 0000000..61f8be5
--- /dev/null
+++ b/tests/Command/resources/composer.sample.json
@@ -0,0 +1,26 @@
+{
+ "name": "laravel/laravel",
+ "description": "The Laravel Framework.",
+ "keywords": [
+ "framework",
+ "laravel"
+ ],
+ "license": "MIT",
+ "type": "project",
+ "autoload": {
+ "classmap": [
+ "database",
+ "tests/TestCase.php"
+ ],
+ "psr-4": {
+ "App\\": "app/",
+ "Domain\\": "lib/CustomDomain"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "dont-discover": []
+ }
+ },
+ "minimum-stability": "dev"
+}
diff --git a/tests/Config/ManagerTest.php b/tests/Config/ManagerTest.php
new file mode 100644
index 0000000..14f3e35
--- /dev/null
+++ b/tests/Config/ManagerTest.php
@@ -0,0 +1,55 @@
+cleanSlate();
+
+ $this->latestConfig = require DDD::packagePath('config/ddd.php');
+});
+
+afterEach(function () {
+ $this->setupTestApplication();
+ Artisan::call('optimize:clear');
+});
+
+it('can update and merge current config file with latest copy from package', function () {
+ $path = __DIR__.'/resources/config.sparse.php';
+
+ File::copy($path, config_path('ddd.php'));
+
+ expect(file_exists($path))->toBeTrue();
+
+ $originalContents = file_get_contents($path);
+
+ expect(file_get_contents(config_path('ddd.php')))->toEqual($originalContents);
+
+ $original = include $path;
+
+ $config = DDD::config();
+
+ $config->syncWithLatest()->save();
+
+ $updatedContents = file_get_contents(config_path('ddd.php'));
+
+ expect($updatedContents)->not->toEqual($originalContents);
+
+ $updatedConfig = include config_path('ddd.php');
+
+ // Expect original values to be retained
+ foreach ($original as $key => $value) {
+ if (is_array($value)) {
+ // We won't worry about arrays for now
+ continue;
+ }
+
+ expect($updatedConfig[$key])->toEqual($value);
+ }
+
+ // Expect the updated config to have all top-level keys from the latest config
+ expect($updatedConfig)->toHaveKeys(array_keys($this->latestConfig));
+
+ unlink(config_path('ddd.php'));
+});
diff --git a/tests/Config/resources/config.sparse.php b/tests/Config/resources/config.sparse.php
new file mode 100644
index 0000000..78bca52
--- /dev/null
+++ b/tests/Config/resources/config.sparse.php
@@ -0,0 +1,23 @@
+ 'src/CustomDomainFolder',
+ 'domain_namespace' => 'CustomDomainNamespace',
+ 'application_objects' => [
+ 'keepthis',
+ ],
+ 'namespaces' => [
+ 'model' => 'CustomModels',
+ 'data_transfer_object' => 'CustomData',
+ 'view_model' => 'CustomViewModels',
+ 'value_object' => 'CustomValueObjects',
+ 'action' => 'CustomActions',
+ ],
+ 'base_model' => 'Domain\Shared\Models\CustomBaseModel',
+ 'base_dto' => 'Spatie\LaravelData\Data',
+ 'base_view_model' => 'Domain\Shared\ViewModels\CustomViewModel',
+ 'base_action' => null,
+ 'autoload' => [
+ 'migrations' => false,
+ ],
+];
diff --git a/tests/Datasets/GeneratorSchemas.php b/tests/Datasets/GeneratorSchemas.php
new file mode 100644
index 0000000..c899f7e
--- /dev/null
+++ b/tests/Datasets/GeneratorSchemas.php
@@ -0,0 +1,27 @@
+ [
+ 'ddd:model',
+ 'Invoice',
+ 'Invoicing',
+ [
+ 'name' => 'Invoice',
+ 'namespace' => 'Domain\Invoicing\Models',
+ 'fullyQualifiedName' => 'Domain\Invoicing\Models\Invoice',
+ 'path' => 'src/Domain/Invoicing/Models/Invoice.php',
+ ],
+ ],
+
+ 'ddd:model Invoicing:Invoice' => [
+ 'ddd:model',
+ 'InvoicingEntry',
+ 'Invoicing',
+ [
+ 'name' => 'Invoice',
+ 'namespace' => 'Domain\Invoicing\Models',
+ 'fullyQualifiedName' => 'Domain\Invoicing\Models\Invoice',
+ 'path' => 'src/Domain/Invoicing/Models/Invoice.php',
+ ],
+ ],
+]);
diff --git a/tests/Datasets/resources/config.0.10.0.php b/tests/Datasets/resources/config.0.10.0.php
index 59e9940..ea52a19 100644
--- a/tests/Datasets/resources/config.0.10.0.php
+++ b/tests/Datasets/resources/config.0.10.0.php
@@ -2,41 +2,41 @@
return [
/*
- |--------------------------------------------------------------------------
- | Domain Path
- |--------------------------------------------------------------------------
- |
- | The path to the domain folder relative to the application root.
- |
- */
+ |--------------------------------------------------------------------------
+ | Domain Path
+ |--------------------------------------------------------------------------
+ |
+ | The path to the domain folder relative to the application root.
+ |
+ */
'domain_path' => 'src/CustomDomainFolder',
/*
- |--------------------------------------------------------------------------
- | Domain Namespace
- |--------------------------------------------------------------------------
- |
- | The root domain namespace.
- |
- */
+ |--------------------------------------------------------------------------
+ | Domain Namespace
+ |--------------------------------------------------------------------------
+ |
+ | The root domain namespace.
+ |
+ */
'domain_namespace' => 'CustomDomainNamespace',
/*
- |--------------------------------------------------------------------------
- | Domain Object Namespaces
- |--------------------------------------------------------------------------
- |
- | This value contains the default namespaces of generated domain
- | objects relative to the domain namespace of which the object
- | belongs to.
- |
- | e.g., Domain/Invoicing/Models/*
- | Domain/Invoicing/Data/*
- | Domain/Invoicing/ViewModels/*
- | Domain/Invoicing/ValueObjects/*
- | Domain/Invoicing/Actions/*
- |
- */
+ |--------------------------------------------------------------------------
+ | Domain Object Namespaces
+ |--------------------------------------------------------------------------
+ |
+ | This value contains the default namespaces of generated domain
+ | objects relative to the domain namespace of which the object
+ | belongs to.
+ |
+ | e.g., Domain/Invoicing/Models/*
+ | Domain/Invoicing/Data/*
+ | Domain/Invoicing/ViewModels/*
+ | Domain/Invoicing/ValueObjects/*
+ | Domain/Invoicing/Actions/*
+ |
+ */
'namespaces' => [
'models' => 'CustomModels',
'data_transfer_objects' => 'CustomData',
@@ -46,51 +46,51 @@
],
/*
- |--------------------------------------------------------------------------
- | Base Model
- |--------------------------------------------------------------------------
- |
- | The base class which generated domain models should extend. By default,
- | generated domain models will extend `Domain\Shared\Models\BaseModel`,
- | which will be created if it doesn't already exist.
- |
- */
+ |--------------------------------------------------------------------------
+ | Base Model
+ |--------------------------------------------------------------------------
+ |
+ | The base class which generated domain models should extend. By default,
+ | generated domain models will extend `Domain\Shared\Models\BaseModel`,
+ | which will be created if it doesn't already exist.
+ |
+ */
'base_model' => 'Domain\Shared\Models\CustomBaseModel',
/*
- |--------------------------------------------------------------------------
- | Base DTO
- |--------------------------------------------------------------------------
- |
- | The base class which generated data transfer objects should extend. By
- | default, generated DTOs will extend `Spatie\LaravelData\Data` from
- | Spatie's Laravel-data package, a highly recommended data object
- | package to work with.
- |
- */
+ |--------------------------------------------------------------------------
+ | Base DTO
+ |--------------------------------------------------------------------------
+ |
+ | The base class which generated data transfer objects should extend. By
+ | default, generated DTOs will extend `Spatie\LaravelData\Data` from
+ | Spatie's Laravel-data package, a highly recommended data object
+ | package to work with.
+ |
+ */
'base_dto' => 'Spatie\LaravelData\Data',
/*
- |--------------------------------------------------------------------------
- | Base ViewModel
- |--------------------------------------------------------------------------
- |
- | The base class which generated view models should extend. By default,
- | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`,
- | which will be created if it doesn't already exist.
- |
- */
+ |--------------------------------------------------------------------------
+ | Base ViewModel
+ |--------------------------------------------------------------------------
+ |
+ | The base class which generated view models should extend. By default,
+ | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`,
+ | which will be created if it doesn't already exist.
+ |
+ */
'base_view_model' => 'Domain\Shared\ViewModels\CustomViewModel',
/*
- |--------------------------------------------------------------------------
- | Base Action
- |--------------------------------------------------------------------------
- |
- | The base class which generated action objects should extend. By default,
- | generated actions are based on the `lorisleiva/laravel-actions` package
- | and do not extend anything.
- |
- */
+ |--------------------------------------------------------------------------
+ | Base Action
+ |--------------------------------------------------------------------------
+ |
+ | The base class which generated action objects should extend. By default,
+ | generated actions are based on the `lorisleiva/laravel-actions` package
+ | and do not extend anything.
+ |
+ */
'base_action' => null,
];
diff --git a/tests/Expectations.php b/tests/Expectations.php
index f2cdd6d..8e4d405 100644
--- a/tests/Expectations.php
+++ b/tests/Expectations.php
@@ -15,6 +15,10 @@
return $this->toContain(Path::normalize($path));
});
+expect()->extend('toEqualPath', function ($path) {
+ return $this->toEqual(Path::normalize($path));
+});
+
expect()->extend('toGenerateFileWithNamespace', function ($expectedPath, $expectedNamespace) {
$command = $this->value;
diff --git a/tests/Factory/DomainFactoryTest.php b/tests/Factory/DomainFactoryTest.php
index 82e6e54..217ad26 100644
--- a/tests/Factory/DomainFactoryTest.php
+++ b/tests/Factory/DomainFactoryTest.php
@@ -3,9 +3,10 @@
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Artisan;
-use Illuminate\Support\Facades\Config;
use Lunarstorm\LaravelDDD\Factories\DomainFactory;
-use Lunarstorm\LaravelDDD\Support\DomainAutoloader;
+use Lunarstorm\LaravelDDD\Tests\BootsTestApplication;
+
+uses(BootsTestApplication::class);
it('can resolve the factory name of a domain model', function ($modelClass, $expectedFactoryClass) {
$this->setupTestApplication();
@@ -28,13 +29,17 @@
]);
it('can instantiate a domain model factory', function ($domainParameter, $modelName, $modelClass) {
- $this->afterApplicationCreated(function () {
- (new DomainAutoloader)->autoload();
+ $this->setupTestApplication();
+
+ $this->afterApplicationRefreshed(function () {
+ app('ddd.autoloader')->boot();
});
- $this->setupTestApplication();
+ $this->refreshApplicationWithConfig([
+ 'ddd.base_model' => 'Lunarstorm\LaravelDDD\Models\DomainModel',
+ 'ddd.autoload.factories' => true,
+ ]);
- Config::set('ddd.base_model', 'Lunarstorm\LaravelDDD\Models\DomainModel');
Artisan::call("ddd:model -f {$domainParameter}:{$modelName}");
expect(class_exists($modelClass))->toBeTrue();
diff --git a/tests/Generator/ActionMakeTest.php b/tests/Generator/ActionMakeTest.php
index 2bdb1f2..52148fb 100644
--- a/tests/Generator/ActionMakeTest.php
+++ b/tests/Generator/ActionMakeTest.php
@@ -57,7 +57,7 @@
Artisan::call("ddd:action {$domain}:{$given}");
- expect(file_exists($expectedPath))->toBeTrue();
+ expect(file_exists($expectedPath))->toBeTrue("ddd:action {$domain}:{$given} -> expected {$expectedPath} to exist.");
})->with('makeActionInputs');
it('extends a base action if specified in config', function ($baseAction) {
diff --git a/tests/Generator/ControllerMakeTest.php b/tests/Generator/ControllerMakeTest.php
index e814913..8f41808 100644
--- a/tests/Generator/ControllerMakeTest.php
+++ b/tests/Generator/ControllerMakeTest.php
@@ -9,13 +9,12 @@
$this->cleanSlate();
$this->setupTestApplication();
- Config::set('ddd.domain_path', 'src/Domain');
- Config::set('ddd.domain_namespace', 'Domain');
-
- Config::set('ddd.application', [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
- 'objects' => ['controller', 'request'],
+ Config::set([
+ 'ddd.domain_path' => 'src/Domain',
+ 'ddd.domain_namespace' => 'Domain',
+ 'ddd.application_path' => 'app/Modules',
+ 'ddd.application_namespace' => 'App\Modules',
+ 'ddd.application_objects' => ['controller', 'request'],
]);
});
diff --git a/tests/Generator/CustomLayerTest.php b/tests/Generator/CustomLayerTest.php
new file mode 100644
index 0000000..8c470ca
--- /dev/null
+++ b/tests/Generator/CustomLayerTest.php
@@ -0,0 +1,92 @@
+cleanSlate();
+
+ Config::set('ddd.layers', [
+ 'CustomLayer' => 'src/CustomLayer',
+ ]);
+});
+
+it('can generate objects into custom layers', function ($type, $objectName, $expectedNamespace, $expectedPath) {
+ if (in_array($type, ['class', 'enum', 'interface', 'trait'])) {
+ skipOnLaravelVersionsBelow('11');
+ }
+
+ $relativePath = $expectedPath;
+ $expectedPath = base_path($relativePath);
+
+ if (file_exists($expectedPath)) {
+ unlink($expectedPath);
+ }
+
+ expect(file_exists($expectedPath))->toBeFalse();
+
+ $command = "ddd:{$type} CustomLayer:{$objectName}";
+
+ Artisan::call($command);
+
+ expect(Artisan::output())->toContainFilepath($relativePath);
+
+ expect(file_exists($expectedPath))->toBeTrue();
+
+ expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};");
+})->with([
+ 'action' => ['action', 'CustomLayerAction', 'CustomLayer\Actions', 'src/CustomLayer/Actions/CustomLayerAction.php'],
+ 'cast' => ['cast', 'CustomLayerCast', 'CustomLayer\Casts', 'src/CustomLayer/Casts/CustomLayerCast.php'],
+ 'channel' => ['channel', 'CustomLayerChannel', 'CustomLayer\Channels', 'src/CustomLayer/Channels/CustomLayerChannel.php'],
+ 'command' => ['command', 'CustomLayerCommand', 'CustomLayer\Commands', 'src/CustomLayer/Commands/CustomLayerCommand.php'],
+ 'event' => ['event', 'CustomLayerEvent', 'CustomLayer\Events', 'src/CustomLayer/Events/CustomLayerEvent.php'],
+ 'exception' => ['exception', 'CustomLayerException', 'CustomLayer\Exceptions', 'src/CustomLayer/Exceptions/CustomLayerException.php'],
+ 'job' => ['job', 'CustomLayerJob', 'CustomLayer\Jobs', 'src/CustomLayer/Jobs/CustomLayerJob.php'],
+ 'listener' => ['listener', 'CustomLayerListener', 'CustomLayer\Listeners', 'src/CustomLayer/Listeners/CustomLayerListener.php'],
+ 'mail' => ['mail', 'CustomLayerMail', 'CustomLayer\Mail', 'src/CustomLayer/Mail/CustomLayerMail.php'],
+ 'notification' => ['notification', 'CustomLayerNotification', 'CustomLayer\Notifications', 'src/CustomLayer/Notifications/CustomLayerNotification.php'],
+ 'observer' => ['observer', 'CustomLayerObserver', 'CustomLayer\Observers', 'src/CustomLayer/Observers/CustomLayerObserver.php'],
+ 'policy' => ['policy', 'CustomLayerPolicy', 'CustomLayer\Policies', 'src/CustomLayer/Policies/CustomLayerPolicy.php'],
+ 'provider' => ['provider', 'CustomLayerServiceProvider', 'CustomLayer\Providers', 'src/CustomLayer/Providers/CustomLayerServiceProvider.php'],
+ 'resource' => ['resource', 'CustomLayerResource', 'CustomLayer\Resources', 'src/CustomLayer/Resources/CustomLayerResource.php'],
+ 'rule' => ['rule', 'CustomLayerRule', 'CustomLayer\Rules', 'src/CustomLayer/Rules/CustomLayerRule.php'],
+ 'scope' => ['scope', 'CustomLayerScope', 'CustomLayer\Scopes', 'src/CustomLayer/Scopes/CustomLayerScope.php'],
+ 'seeder' => ['seeder', 'CustomLayerSeeder', 'CustomLayer\Database\Seeders', 'src/CustomLayer/Database/Seeders/CustomLayerSeeder.php'],
+ 'class' => ['class', 'CustomLayerClass', 'CustomLayer', 'src/CustomLayer/CustomLayerClass.php'],
+ 'enum' => ['enum', 'CustomLayerEnum', 'CustomLayer\Enums', 'src/CustomLayer/Enums/CustomLayerEnum.php'],
+ 'interface' => ['interface', 'CustomLayerInterface', 'CustomLayer', 'src/CustomLayer/CustomLayerInterface.php'],
+ 'trait' => ['trait', 'CustomLayerTrait', 'CustomLayer', 'src/CustomLayer/CustomLayerTrait.php'],
+]);
+
+it('ignores custom layer if object belongs in the application layer', function ($type, $objectName, $expectedNamespace, $expectedPath) {
+ Config::set([
+ 'ddd.application_namespace' => 'Application',
+ 'ddd.application_path' => 'src/Application',
+ 'ddd.application_objects' => [
+ $type,
+ ],
+ ]);
+
+ $relativePath = $expectedPath;
+ $expectedPath = base_path($relativePath);
+
+ if (file_exists($expectedPath)) {
+ unlink($expectedPath);
+ }
+
+ expect(file_exists($expectedPath))->toBeFalse();
+
+ $command = "ddd:{$type} CustomLayer:{$objectName}";
+
+ Artisan::call($command);
+
+ expect(Artisan::output())->toContainFilepath($relativePath);
+
+ expect(file_exists($expectedPath))->toBeTrue();
+
+ expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};");
+})->with([
+ 'request' => ['request', 'CustomLayerRequest', 'Application\CustomLayer\Requests', 'src/Application/CustomLayer/Requests/CustomLayerRequest.php'],
+ 'controller' => ['controller', 'CustomLayerController', 'Application\CustomLayer\Controllers', 'src/Application/CustomLayer/Controllers/CustomLayerController.php'],
+ 'middleware' => ['middleware', 'CustomLayerMiddleware', 'Application\CustomLayer\Middleware', 'src/Application/CustomLayer/Middleware/CustomLayerMiddleware.php'],
+]);
diff --git a/tests/Generator/DtoMakeTestTest.php b/tests/Generator/DtoMakeTest.php
similarity index 100%
rename from tests/Generator/DtoMakeTestTest.php
rename to tests/Generator/DtoMakeTest.php
diff --git a/tests/Generator/ExtendedCommandsTest.php b/tests/Generator/ExtendedMakeTest.php
similarity index 95%
rename from tests/Generator/ExtendedCommandsTest.php
rename to tests/Generator/ExtendedMakeTest.php
index 52d8959..ce8a0be 100644
--- a/tests/Generator/ExtendedCommandsTest.php
+++ b/tests/Generator/ExtendedMakeTest.php
@@ -4,7 +4,7 @@
use Illuminate\Support\Facades\Config;
use Lunarstorm\LaravelDDD\Support\Domain;
-it('can generate extended objects', function ($type, $objectName, $domainPath, $domainRoot) {
+it('can generate other objects', function ($type, $objectName, $domainPath, $domainRoot) {
if (in_array($type, ['class', 'enum', 'interface', 'trait'])) {
skipOnLaravelVersionsBelow('11');
}
diff --git a/tests/Generator/Model/MakeWithControllerTest.php b/tests/Generator/Model/MakeWithControllerTest.php
index c5f1077..2418ad3 100644
--- a/tests/Generator/Model/MakeWithControllerTest.php
+++ b/tests/Generator/Model/MakeWithControllerTest.php
@@ -1,15 +1,21 @@
'src/Domain',
+ 'ddd.domain_namespace' => 'Domain',
+ 'ddd.base_model' => DomainModel::class,
+ 'ddd.application_namespace' => 'App\Modules',
+ 'ddd.application_path' => 'app/Modules',
+ 'ddd.application_objects' => [
+ 'controller',
+ ],
+ ]);
$this->setupTestApplication();
});
diff --git a/tests/Generator/Model/MakeWithOptionsTest.php b/tests/Generator/Model/MakeWithOptionsTest.php
index 09cb201..aae2c98 100644
--- a/tests/Generator/Model/MakeWithOptionsTest.php
+++ b/tests/Generator/Model/MakeWithOptionsTest.php
@@ -10,47 +10,47 @@
Config::set('ddd.domain_namespace', 'Domain');
});
-// it('can generate a domain model with factory', function () {
-// $domainName = 'Invoicing';
-// $modelName = 'Record';
+it('can generate a domain model with factory', function () {
+ $domainName = 'Invoicing';
+ $modelName = 'Record';
-// $domain = new Domain($domainName);
+ $domain = new Domain($domainName);
-// $factoryName = "{$modelName}Factory";
+ $factoryName = "{$modelName}Factory";
-// $domainModel = $domain->model($modelName);
+ $domainModel = $domain->model($modelName);
-// $domainFactory = $domain->factory($factoryName);
+ $domainFactory = $domain->factory($factoryName);
-// $expectedModelPath = base_path($domainModel->path);
+ $expectedModelPath = base_path($domainModel->path);
-// if (file_exists($expectedModelPath)) {
-// unlink($expectedModelPath);
-// }
+ if (file_exists($expectedModelPath)) {
+ unlink($expectedModelPath);
+ }
-// $expectedFactoryPath = base_path($domainFactory->path);
+ $expectedFactoryPath = base_path($domainFactory->path);
-// if (file_exists($expectedFactoryPath)) {
-// unlink($expectedFactoryPath);
-// }
+ if (file_exists($expectedFactoryPath)) {
+ unlink($expectedFactoryPath);
+ }
-// Artisan::call('ddd:model', [
-// 'name' => $modelName,
-// '--domain' => $domain->dotName,
-// '--factory' => true,
-// ]);
+ Artisan::call('ddd:model', [
+ 'name' => $modelName,
+ '--domain' => $domain->dotName,
+ '--factory' => true,
+ ]);
-// $output = Artisan::output();
+ $output = Artisan::output();
-// expect($output)->toContainFilepath($domainModel->path);
+ expect($output)->toContainFilepath($domainModel->path);
-// expect(file_exists($expectedModelPath))->toBeTrue("Expecting model file to be generated at {$expectedModelPath}");
-// expect(file_exists($expectedFactoryPath))->toBeTrue("Expecting factory file to be generated at {$expectedFactoryPath}");
+ expect(file_exists($expectedModelPath))->toBeTrue("Expecting model file to be generated at {$expectedModelPath}");
+ expect(file_exists($expectedFactoryPath))->toBeTrue("Expecting factory file to be generated at {$expectedFactoryPath}");
-// expect(file_get_contents($expectedFactoryPath))
-// ->toContain("use {$domainModel->fullyQualifiedName};")
-// ->toContain("protected \$model = {$modelName}::class;");
-// });
+ expect(file_get_contents($expectedFactoryPath))
+ ->toContain("use {$domainModel->fullyQualifiedName};")
+ ->toContain("protected \$model = {$modelName}::class;");
+});
it('can generate domain model with options', function ($options, $objectType, $objectName, $expectedObjectPath) {
$domainName = 'Invoicing';
diff --git a/tests/Generator/RequestMakeTest.php b/tests/Generator/RequestMakeTest.php
index cc24949..5a584b1 100644
--- a/tests/Generator/RequestMakeTest.php
+++ b/tests/Generator/RequestMakeTest.php
@@ -5,13 +5,12 @@
use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature;
beforeEach(function () {
- Config::set('ddd.domain_path', 'src/Domain');
- Config::set('ddd.domain_namespace', 'Domain');
-
- Config::set('ddd.application', [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
- 'objects' => ['controller', 'request'],
+ Config::set([
+ 'ddd.domain_path' => 'src/Domain',
+ 'ddd.domain_namespace' => 'Domain',
+ 'ddd.application_path' => 'app/Modules',
+ 'ddd.application_namespace' => 'App\Modules',
+ 'ddd.application_objects' => ['controller', 'request'],
]);
$this->setupTestApplication();
diff --git a/tests/Pest.php b/tests/Pest.php
index d31d21e..da75808 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -13,6 +13,15 @@ function skipOnLaravelVersionsBelow($minimumVersion)
}
}
+function onlyOnLaravelVersionsBelow($minimumVersion)
+{
+ $version = app()->version();
+
+ if (! version_compare($version, $minimumVersion, '<')) {
+ test()->markTestSkipped("Does not apply to Laravel {$minimumVersion}+ (Current version: {$version}).");
+ }
+}
+
function setConfigValues(array $values)
{
TestCase::configValues($values);
diff --git a/tests/Setup/PublishTest.php b/tests/Setup/PublishTest.php
index 69858bc..c84afdd 100644
--- a/tests/Setup/PublishTest.php
+++ b/tests/Setup/PublishTest.php
@@ -17,6 +17,9 @@
]);
expect(file_exists($expectedPath))->toBeTrue();
+
+ // Delete it
+ unlink($expectedPath);
});
it('can publish stubs', function () {
@@ -34,4 +37,4 @@
expect(File::exists($dir))->toBeTrue();
expect(File::isEmptyDirectory($dir))->toBeFalse();
-});
+})->markTestSkipped('Deprecated');
diff --git a/tests/Support/AutoloaderTest.php b/tests/Support/AutoloaderTest.php
index aec470b..7f381df 100644
--- a/tests/Support/AutoloaderTest.php
+++ b/tests/Support/AutoloaderTest.php
@@ -1,13 +1,48 @@
setupTestApplication();
});
-it('can run', function () {
- $autoloader = new DomainAutoloader;
+beforeEach(function () {
+ config([
+ 'ddd.domain_path' => 'src/Domain',
+ 'ddd.domain_namespace' => 'Domain',
+ 'ddd.application_namespace' => 'Application',
+ 'ddd.application_path' => 'src/Application',
+ 'ddd.application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
+ ],
+ 'ddd.layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ 'Support' => 'src/Support',
+ 'Library' => 'lib',
+ ],
+ 'ddd.autoload_ignore' => [
+ 'Tests',
+ 'Database/Migrations',
+ ],
+ 'cache.default' => 'file',
+ ]);
+
+ $this->expectedPaths = collect([
+ app()->basePath('src/Domain'),
+ app()->basePath('src/Application'),
+ app()->basePath('src/Infrastructure'),
+ app()->basePath('src/Support'),
+ app()->basePath('lib'),
+ ])->map(fn ($path) => Path::normalize($path))->toArray();
- $autoloader->autoload();
-})->throwsNoExceptions();
+ $this->setupTestApplication();
+});
+
+it('can discover paths to all layers', function () {
+ $autoloader = app(AutoloadManager::class);
+
+ expect($autoloader->getAllLayerPaths())->toEqualCanonicalizing($this->expectedPaths);
+});
diff --git a/tests/Support/BlueprintTest.php b/tests/Support/BlueprintTest.php
new file mode 100644
index 0000000..23b72c0
--- /dev/null
+++ b/tests/Support/BlueprintTest.php
@@ -0,0 +1,123 @@
+set([
+ 'ddd.domain_path' => 'src/Domain',
+ 'ddd.domain_namespace' => 'Domain',
+ 'ddd.application_namespace' => 'Application',
+ 'ddd.application_path' => 'src/Application',
+ 'ddd.application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
+ ],
+ 'ddd.layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ 'NestedLayer' => 'src/Nested/Layer',
+ 'AppNested' => 'app/Nested',
+ ],
+ ]);
+});
+
+it('handles nested objects', function ($nameInput, $normalized) {
+ $blueprint = new GeneratorBlueprint(
+ commandName: 'ddd:model',
+ nameInput: $nameInput,
+ domainName: 'SomeDomain',
+ );
+
+ expect($blueprint->schema)
+ ->name->toBe($normalized)
+ ->namespace->toBe('Domain\SomeDomain\Models');
+})->with([
+ ['Nested\\Thing', 'Nested\\Thing'],
+ ['Nested/Thing', 'Nested\\Thing'],
+ ['Nested/Thing/Deeply', 'Nested\\Thing\\Deeply'],
+ ['Nested\\Thing/Deeply', 'Nested\\Thing\\Deeply'],
+]);
+
+it('handles objects in the application layer', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) {
+ $blueprint = new GeneratorBlueprint(
+ commandName: $command,
+ nameInput: $nameInput,
+ domainName: $domainName,
+ );
+
+ expect($blueprint->schema)
+ ->name->toBe($expectedName)
+ ->namespace->toBe($expectedNamespace)
+ ->fullyQualifiedName->toBe($expectedFqn)
+ ->path->toEqualPath($expectedPath);
+})->with([
+ ['ddd:controller', 'SomeDomain', 'ApplicationController', 'ApplicationController', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\ApplicationController', 'src/Application/SomeDomain/Controllers/ApplicationController.php'],
+ ['ddd:controller', 'SomeDomain', 'Application', 'Application', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\Application', 'src/Application/SomeDomain/Controllers/Application.php'],
+ ['ddd:middleware', 'SomeDomain', 'CrazyMiddleware', 'CrazyMiddleware', 'Application\\SomeDomain\\Middleware', 'Application\\SomeDomain\\Middleware\\CrazyMiddleware', 'src/Application/SomeDomain/Middleware/CrazyMiddleware.php'],
+ ['ddd:request', 'SomeDomain', 'LazyRequest', 'LazyRequest', 'Application\\SomeDomain\\Requests', 'Application\\SomeDomain\\Requests\\LazyRequest', 'src/Application/SomeDomain/Requests/LazyRequest.php'],
+]);
+
+it('handles objects in custom layers', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) {
+ $blueprint = new GeneratorBlueprint(
+ commandName: $command,
+ nameInput: $nameInput,
+ domainName: $domainName,
+ );
+
+ expect($blueprint->schema)
+ ->name->toBe($expectedName)
+ ->namespace->toBe($expectedNamespace)
+ ->fullyQualifiedName->toBe($expectedFqn)
+ ->path->toEqualPath($expectedPath);
+})->with([
+ ['ddd:model', 'Infrastructure', 'System', 'System', 'Infrastructure\\Models', 'Infrastructure\\Models\\System', 'src/Infrastructure/Models/System.php'],
+ ['ddd:factory', 'Infrastructure', 'System', 'SystemFactory', 'Infrastructure\\Database\\Factories', 'Infrastructure\\Database\\Factories\\SystemFactory', 'src/Infrastructure/Database/Factories/SystemFactory.php'],
+ ['ddd:provider', 'Infrastructure', 'InfrastructureServiceProvider', 'InfrastructureServiceProvider', 'Infrastructure\\Providers', 'Infrastructure\\Providers\\InfrastructureServiceProvider', 'src/Infrastructure/Providers/InfrastructureServiceProvider.php'],
+ ['ddd:provider', 'Infrastructure', 'Infrastructure\\InfrastructureServiceProvider', 'Infrastructure\\InfrastructureServiceProvider', 'Infrastructure\\Providers', 'Infrastructure\\Providers\\Infrastructure\\InfrastructureServiceProvider', 'src/Infrastructure/Providers/Infrastructure/InfrastructureServiceProvider.php'],
+ ['ddd:provider', 'Infrastructure', 'InfrastructureServiceProvider', 'InfrastructureServiceProvider', 'Infrastructure\\Providers', 'Infrastructure\\Providers\\InfrastructureServiceProvider', 'src/Infrastructure/Providers/InfrastructureServiceProvider.php'],
+ ['ddd:provider', 'AppNested', 'CrazyServiceProvider', 'CrazyServiceProvider', 'AppNested\\Providers', 'AppNested\\Providers\\CrazyServiceProvider', 'app/Nested/Providers/CrazyServiceProvider.php'],
+ ['ddd:provider', 'NestedLayer', 'CrazyServiceProvider', 'CrazyServiceProvider', 'NestedLayer\\Providers', 'NestedLayer\\Providers\\CrazyServiceProvider', 'src/Nested/Layer/Providers/CrazyServiceProvider.php'],
+]);
+
+it('handles objects whose name contains the domain name', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) {
+ $blueprint = new GeneratorBlueprint(
+ commandName: $command,
+ nameInput: $nameInput,
+ domainName: $domainName,
+ );
+
+ expect($blueprint->schema)
+ ->name->toBe($expectedName)
+ ->namespace->toBe($expectedNamespace)
+ ->fullyQualifiedName->toBe($expectedFqn)
+ ->path->toEqualPath($expectedPath);
+})->with([
+ ['ddd:model', 'SomeDomain', 'SomeDomain', 'SomeDomain', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomain', 'src/Domain/SomeDomain/Models/SomeDomain.php'],
+ ['ddd:model', 'SomeDomain', 'SomeDomainModel', 'SomeDomainModel', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomainModel', 'src/Domain/SomeDomain/Models/SomeDomainModel.php'],
+ ['ddd:model', 'SomeDomain', 'Nested\\SomeDomain', 'Nested\\SomeDomain', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\Nested\\SomeDomain', 'src/Domain/SomeDomain/Models/Nested/SomeDomain.php'],
+ ['ddd:model', 'SomeDomain', 'SomeDomain\\SomeDomain', 'SomeDomain\\SomeDomain', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomain\\SomeDomain', 'src/Domain/SomeDomain/Models/SomeDomain/SomeDomain.php'],
+ ['ddd:model', 'SomeDomain', 'SomeDomain\\SomeDomainModel', 'SomeDomain\\SomeDomainModel', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomain\\SomeDomainModel', 'src/Domain/SomeDomain/Models/SomeDomain/SomeDomainModel.php'],
+ ['ddd:model', 'Infrastructure', 'Infrastructure', 'Infrastructure', 'Infrastructure\\Models', 'Infrastructure\\Models\\Infrastructure', 'src/Infrastructure/Models/Infrastructure.php'],
+ ['ddd:model', 'Infrastructure', 'Nested\\Infrastructure', 'Nested\\Infrastructure', 'Infrastructure\\Models', 'Infrastructure\\Models\\Nested\\Infrastructure', 'src/Infrastructure/Models/Nested/Infrastructure.php'],
+ ['ddd:controller', 'SomeDomain', 'SomeDomain', 'SomeDomain', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\SomeDomain', 'src/Application/SomeDomain/Controllers/SomeDomain.php'],
+ ['ddd:controller', 'SomeDomain', 'SomeDomainController', 'SomeDomainController', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\SomeDomainController', 'src/Application/SomeDomain/Controllers/SomeDomainController.php'],
+ ['ddd:controller', 'SomeDomain', 'SomeDomain\\SomeDomain', 'SomeDomain\\SomeDomain', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\SomeDomain\\SomeDomain', 'src/Application/SomeDomain/Controllers/SomeDomain/SomeDomain.php'],
+]);
+
+it('handles absolute-path names', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) {
+ $blueprint = new GeneratorBlueprint(
+ commandName: $command,
+ nameInput: $nameInput,
+ domainName: $domainName,
+ );
+
+ expect($blueprint->schema)
+ ->name->toBe($expectedName)
+ ->namespace->toBe($expectedNamespace)
+ ->fullyQualifiedName->toBe($expectedFqn)
+ ->path->toEqualPath($expectedPath);
+})->with([
+ ['ddd:model', 'SomeDomain', '/RootModel', 'RootModel', 'Domain\\SomeDomain', 'Domain\\SomeDomain\\RootModel', 'src/Domain/SomeDomain/RootModel.php'],
+ ['ddd:model', 'SomeDomain', '/CustomLocation/Thing', 'CustomLocation\\Thing', 'Domain\\SomeDomain', 'Domain\\SomeDomain\\CustomLocation\\Thing', 'src/Domain/SomeDomain/CustomLocation/Thing.php'],
+ ['ddd:model', 'SomeDomain', '/Custom/Nested/Thing', 'Custom\\Nested\\Thing', 'Domain\\SomeDomain', 'Domain\\SomeDomain\\Custom\\Nested\\Thing', 'src/Domain/SomeDomain/Custom/Nested/Thing.php'],
+]);
diff --git a/tests/Support/DomainTest.php b/tests/Support/DomainTest.php
index 8a701ae..accef75 100644
--- a/tests/Support/DomainTest.php
+++ b/tests/Support/DomainTest.php
@@ -28,6 +28,8 @@
->path->toBe(Path::normalize($expectedPath));
})->with([
['Reporting', 'InvoiceReport', 'Domain\\Reporting\\Models\\InvoiceReport', 'src/Domain/Reporting/Models/InvoiceReport.php'],
+ ['Reporting', 'ReportingLog', 'Domain\\Reporting\\Models\\ReportingLog', 'src/Domain/Reporting/Models/ReportingLog.php'],
+ ['Reporting', 'Reporting\Log', 'Domain\\Reporting\\Models\\Reporting\\Log', 'src/Domain/Reporting/Models/Reporting/Log.php'],
['Reporting.Internal', 'InvoiceReport', 'Domain\\Reporting\\Internal\\Models\\InvoiceReport', 'src/Domain/Reporting/Internal/Models/InvoiceReport.php'],
]);
@@ -93,10 +95,10 @@
describe('application layer', function () {
beforeEach(function () {
- Config::set('ddd.application', [
- 'path' => 'app/Modules',
- 'namespace' => 'App\Modules',
- 'objects' => ['controller', 'request'],
+ Config::set([
+ 'ddd.application_path' => 'app/Modules',
+ 'ddd.application_namespace' => 'App\Modules',
+ 'ddd.application_objects' => ['controller', 'request'],
]);
});
@@ -113,6 +115,25 @@
]);
});
+describe('custom layers', function () {
+ beforeEach(function () {
+ Config::set('ddd.layers', [
+ 'Support' => 'src/Support',
+ ]);
+ });
+
+ it('can map domains to custom layers', function ($domainName, $objectType, $objectName, $expectedFQN, $expectedPath) {
+ expect((new Domain($domainName))->object($objectType, $objectName))
+ ->name->toBe($objectName)
+ ->fullyQualifiedName->toBe($expectedFQN)
+ ->path->toBe(Path::normalize($expectedPath));
+ })->with([
+ ['Support', 'class', 'ExchangeRate', 'Support\\ExchangeRate', 'src/Support/ExchangeRate.php'],
+ ['Support', 'trait', 'Concerns\\HasOptions', 'Support\\Concerns\\HasOptions', 'src/Support/Concerns/HasOptions.php'],
+ ['Support', 'exception', 'InvalidExchangeRate', 'Support\\Exceptions\\InvalidExchangeRate', 'src/Support/Exceptions/InvalidExchangeRate.php'],
+ ]);
+});
+
it('normalizes slashes in nested objects', function ($nameInput, $normalized) {
expect((new Domain('Invoicing'))->object('class', $nameInput))
->name->toBe($normalized);
diff --git a/tests/Support/ResolveLayerTest.php b/tests/Support/ResolveLayerTest.php
new file mode 100644
index 0000000..5060177
--- /dev/null
+++ b/tests/Support/ResolveLayerTest.php
@@ -0,0 +1,44 @@
+setupTestApplication();
+});
+
+it('can resolve domains to custom layers', function ($domainName, $namespace, $path) {
+ Config::set('ddd.layers', [
+ 'Support' => 'src/Support',
+ ]);
+
+ $layer = DomainResolver::resolveLayer($domainName);
+
+ expect($layer)
+ ->namespace->toBe($namespace)
+ ->path->toBe(Path::normalize($path));
+})->with([
+ ['Support', 'Support', 'src/Support'],
+ ['Invoicing', 'Domain\\Invoicing', 'src/Domain/Invoicing'],
+ ['Reporting\\Internal', 'Domain\\Reporting\\Internal', 'src/Domain/Reporting/Internal'],
+]);
+
+it('resolves normally when no custom layer is found', function ($domainName, $namespace, $path) {
+ Config::set('ddd.layers', [
+ 'SupportNotMatching' => 'src/Support',
+ ]);
+
+ $layer = DomainResolver::resolveLayer($domainName);
+
+ expect($layer)
+ ->namespace->toBe($namespace)
+ ->path->toBe(Path::normalize($path));
+})->with([
+ ['Support', 'Domain\\Support', 'src/Domain/Support'],
+ ['Invoicing', 'Domain\\Invoicing', 'src/Domain/Invoicing'],
+ ['Reporting\\Internal', 'Domain\\Reporting\\Internal', 'src/Domain/Reporting/Internal'],
+]);
diff --git a/tests/Support/ResolveObjectSchemaUsingTest.php b/tests/Support/ResolveObjectSchemaUsingTest.php
new file mode 100644
index 0000000..4d7abfe
--- /dev/null
+++ b/tests/Support/ResolveObjectSchemaUsingTest.php
@@ -0,0 +1,53 @@
+setupTestApplication();
+
+ Config::set('ddd.domain_path', 'src/Domain');
+ Config::set('ddd.domain_namespace', 'Domain');
+});
+
+it('can register a custom object schema resolver', function () {
+ Config::set([
+ 'ddd.application_path' => 'src/App',
+ 'ddd.application_namespace' => 'App',
+ ]);
+
+ DDD::resolveObjectSchemaUsing(function (string $domainName, string $nameInput, string $type, CommandContext $command): ?ObjectSchema {
+ if ($type === 'controller' && $command->option('api')) {
+ return new ObjectSchema(
+ name: $name = str($nameInput)->replaceEnd('Controller', '')->finish('ApiController')->toString(),
+ namespace: "App\\Api\\Controllers\\{$domainName}",
+ fullyQualifiedName: "App\\Api\\Controllers\\{$domainName}\\{$name}",
+ path: "src/App/Api/Controllers/{$domainName}/{$name}.php",
+ );
+ }
+
+ return null;
+ });
+
+ Artisan::call('ddd:controller', [
+ 'name' => 'PaymentController',
+ '--domain' => 'Invoicing',
+ '--api' => true,
+ ]);
+
+ $output = Artisan::output();
+
+ expect($output)
+ ->toContainFilepath('src/App/Api/Controllers/Invoicing/PaymentApiController.php');
+
+ $expectedPath = base_path('src/App/Api/Controllers/Invoicing/PaymentApiController.php');
+
+ expect(file_get_contents($expectedPath))
+ ->toContain(...[
+ "namespace App\Api\Controllers\Invoicing;",
+ 'class PaymentApiController',
+ ]);
+});
diff --git a/tests/TestCase.php b/tests/TestCase.php
index f2baed7..a7ba0a9 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -5,8 +5,10 @@
use Illuminate\Contracts\Config\Repository;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Lunarstorm\LaravelDDD\LaravelDDDServiceProvider;
+use Lunarstorm\LaravelDDD\Support\DomainCache;
use Orchestra\Testbench\TestCase as Orchestra;
use Symfony\Component\Process\Process;
@@ -14,27 +16,30 @@ class TestCase extends Orchestra
{
public static $configValues = [];
+ public $appConfig = [];
+
+ protected $originalComposerContents;
+
protected function setUp(): void
{
+ $this->originalComposerContents = $this->getComposerFileContents();
+
$this->afterApplicationCreated(function () {
$this->cleanSlate();
- // $this->updateComposer(
- // set: [
- // [['autoload', 'psr-4', 'App\\'], 'vendor/orchestra/testbench-core/laravel/app'],
- // [['autoload', 'psr-4', 'Database\\Factories\\'], 'vendor/orchestra/testbench-core/laravel/database/factories'],
- // [['autoload', 'psr-4', 'Database\\Seeders\\'], 'vendor/orchestra/testbench-core/laravel/database/seeders'],
- // [['autoload', 'psr-4', 'Domain\\'], 'vendor/orchestra/testbench-core/laravel/src/Domain'],
- // ],
- // forget: [
- // ['autoload', 'psr-4', 'Domains\\'],
- // ['autoload', 'psr-4', 'Domain\\'],
- // ]
- // );
-
Factory::guessFactoryNamesUsing(
fn (string $modelName) => 'Lunarstorm\\LaravelDDD\\Database\\Factories\\'.class_basename($modelName).'Factory'
);
+
+ DomainCache::clear();
+
+ config()->set('data.structure_caching.enabled', false);
+
+ Artisan::command('data:cache-structures', function () {});
+ });
+
+ $this->afterApplicationRefreshed(function () {
+ config()->set('data.structure_caching.enabled', false);
});
$this->beforeApplicationDestroyed(function () {
@@ -44,33 +49,99 @@ protected function setUp(): void
parent::setUp();
}
+ protected function tearDown(): void
+ {
+ $basePath = $this->getBasePath();
+
+ $this->cleanSlate();
+
+ file_put_contents($basePath.'/composer.json', $this->originalComposerContents);
+
+ parent::tearDown();
+ }
+
public static function configValues(array $values)
{
static::$configValues = $values;
}
+ public static function resetConfig()
+ {
+ static::$configValues = [];
+ }
+
protected function defineEnvironment($app)
{
+ if (in_array(BootsTestApplication::class, class_uses_recursive($this))) {
+ static::$configValues = [
+ 'ddd.domain_path' => 'src/Domain',
+ 'ddd.domain_namespace' => 'Domain',
+ 'ddd.application_path' => 'src/Application',
+ 'ddd.application_namespace' => 'Application',
+ 'ddd.application_objects' => [
+ 'controller',
+ 'request',
+ 'middleware',
+ ],
+ 'ddd.layers' => [
+ 'Infrastructure' => 'src/Infrastructure',
+ ],
+ 'ddd.autoload' => [
+ 'providers' => true,
+ 'commands' => true,
+ 'policies' => true,
+ 'factories' => true,
+ 'migrations' => true,
+ ],
+ 'ddd.autoload_ignore' => [
+ 'Tests',
+ 'Database/Migrations',
+ ],
+ 'ddd.cache_directory' => 'bootstrap/cache/ddd',
+ 'cache.default' => 'file',
+ 'data.structure_caching.enabled' => false,
+ ...static::$configValues,
+ ];
+ }
+
tap($app['config'], function (Repository $config) {
foreach (static::$configValues as $key => $value) {
$config->set($key, $value);
}
+
+ foreach ($this->appConfig as $key => $value) {
+ $config->set($key, $value);
+ }
+
+ $config->set('data.structure_caching.enabled', false);
});
+ }
+
+ protected function refreshApplicationWithConfig(array $config)
+ {
+ $this->appConfig = $config;
+
+ // $this->afterApplicationRefreshed(fn () => $this->appConfig = []);
+
+ $this->reloadApplication();
+
+ $this->appConfig = [];
- // $this->updateComposer(
- // set: [
- // [['autoload', 'psr-4', 'App\\'], 'vendor/orchestra/testbench-core/laravel/app'],
- // ],
- // forget: [
- // ['autoload', 'psr-4', 'Domains\\'],
- // ['autoload', 'psr-4', 'Domain\\'],
- // ]
- // );
+ return $this;
+ }
+
+ protected function withConfig(array $config)
+ {
+ $this->appConfig = $config;
+
+ return $this;
}
protected function getComposerFileContents()
{
- return file_get_contents(base_path('composer.json'));
+ $basePath = $this->getBasePath();
+
+ return file_get_contents($basePath.'/composer.json');
}
protected function getComposerFileAsArray()
@@ -142,23 +213,24 @@ protected function composerReload()
protected function cleanSlate()
{
- File::copy(__DIR__.'/.skeleton/composer.json', base_path('composer.json'));
+ $basePath = $this->getBasePath();
- File::delete(base_path('config/ddd.php'));
+ File::delete($basePath.'/config/ddd.php');
- File::cleanDirectory(app_path());
- File::cleanDirectory(base_path('database/factories'));
+ File::cleanDirectory($basePath.'/app/Models');
+ File::cleanDirectory($basePath.'/database/factories');
+ File::cleanDirectory($basePath.'/bootstrap/cache');
+ File::cleanDirectory($basePath.'/bootstrap/cache/ddd');
- File::deleteDirectory(resource_path('stubs/ddd'));
- File::deleteDirectory(base_path('stubs'));
- File::deleteDirectory(base_path('Custom'));
- File::deleteDirectory(base_path('src/Domain'));
- File::deleteDirectory(base_path('src/Domains'));
- File::deleteDirectory(base_path('src/App'));
- File::deleteDirectory(app_path('Modules'));
- File::deleteDirectory(app_path('Models'));
+ File::deleteDirectory($basePath.'/src');
+ File::deleteDirectory($basePath.'/resources/stubs/ddd');
+ File::deleteDirectory($basePath.'/stubs');
+ File::deleteDirectory($basePath.'/Custom');
+ File::deleteDirectory($basePath.'/Other');
+ File::deleteDirectory($basePath.'/app/Policies');
+ File::deleteDirectory($basePath.'/app/Modules');
- File::deleteDirectory(base_path('bootstrap/cache/ddd'));
+ // File::copy(__DIR__.'/.skeleton/composer.json', $basePath.'/composer.json');
return $this;
}
@@ -172,22 +244,45 @@ protected function cleanStubs()
protected function setupTestApplication()
{
- File::copyDirectory(__DIR__.'/.skeleton/app', app_path());
+ $this->cleanSlate();
+
+ $basePath = $this->getBasePath();
+
+ File::ensureDirectoryExists(app_path());
+ File::ensureDirectoryExists(app_path('Models'));
+ File::ensureDirectoryExists(database_path('factories'));
+ File::ensureDirectoryExists($basePath.'/bootstrap/cache/ddd');
+
+ $skeletonAppFolders = glob(__DIR__.'/.skeleton/app/*', GLOB_ONLYDIR);
+
+ foreach ($skeletonAppFolders as $folder) {
+ File::copyDirectory($folder, app_path(basename($folder)));
+ }
+
File::copyDirectory(__DIR__.'/.skeleton/database', base_path('database'));
- File::copyDirectory(__DIR__.'/.skeleton/src/Domain', base_path('src/Domain'));
+ File::copyDirectory(__DIR__.'/.skeleton/src', base_path('src'));
File::copy(__DIR__.'/.skeleton/bootstrap/providers.php', base_path('bootstrap/providers.php'));
- File::ensureDirectoryExists(app_path('Models'));
+ File::copy(__DIR__.'/.skeleton/config/ddd.php', config_path('ddd.php'));
+ File::copy(__DIR__.'/.skeleton/composer.json', $basePath.'/composer.json');
+
+ $this->composerReload();
+
+ // $this->setAutoloadPathInComposer('Domain', 'src/Domain');
+ // $this->setAutoloadPathInComposer('Application', 'src/Application');
+ // $this->setAutoloadPathInComposer('Infrastructure', 'src/Infrastructure');
+
+ DomainCache::clear();
- $this->setDomainPathInComposer('Domain', 'src/Domain');
+ config()->set('data.structure_caching.enabled', false);
return $this;
}
- protected function setDomainPathInComposer($domainNamespace, $domainPath, bool $reload = true)
+ protected function setAutoloadPathInComposer($namespace, $path, bool $reload = true)
{
$this->updateComposer(
set: [
- [['autoload', 'psr-4', $domainNamespace.'\\'], $domainPath],
+ [['autoload', 'psr-4', $namespace.'\\'], $path],
],
);