Skip to content

Commit

Permalink
Address base-view-model generation issues.
Browse files Browse the repository at this point in the history
  • Loading branch information
jaspertey committed Mar 26, 2024
1 parent 2363483 commit 2775e5e
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ All notable changes to `laravel-ddd` will be documented in this file.
- `ddd:action CreateInvoice --domain=Invoicing` (this takes precedence).
- Shorthand syntax: `ddd:action Invoicing:CreateInvoice`.
- Or simply `ddd:action CreateInvoice` to be prompted for the domain.
- Improve the reliability of generating base view models when `ddd.base_view_model` is something other than the default `Domain\Shared\ViewModels\ViewModel`.

### Chore
- Dropped Laravel 9 support.
Expand Down
10 changes: 10 additions & 0 deletions src/Commands/DomainDtoMakeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ class DomainDtoMakeCommand extends DomainGeneratorCommand

protected $type = 'Data Transfer Object';

protected function configure()
{
$this->setAliases([
'ddd:data-transfer-object',
'ddd:datatransferobject',
]);

parent::configure();
}

protected function getStub()
{
return $this->resolveStubPath('dto.php.stub');
Expand Down
10 changes: 10 additions & 0 deletions src/Commands/DomainValueObjectMakeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ class DomainValueObjectMakeCommand extends DomainGeneratorCommand

protected $type = 'Value Object';

protected function configure()
{
$this->setAliases([
'ddd:value-object',
'ddd:valueobject',
]);

parent::configure();
}

protected function getStub()
{
return $this->resolveStubPath('value-object.php.stub');
Expand Down
53 changes: 45 additions & 8 deletions src/Commands/DomainViewModelMakeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Lunarstorm\LaravelDDD\Commands;

use Illuminate\Support\Str;
use Lunarstorm\LaravelDDD\Support\DomainResolver;

class DomainViewModelMakeCommand extends DomainGeneratorCommand
{
protected $name = 'ddd:view-model';
Expand All @@ -20,23 +23,57 @@ protected function getStub()
return $this->resolveStubPath('view-model.php.stub');
}

public function handle()
protected function preparePlaceholders(): array
{
$baseViewModel = config('ddd.base_view_model');
$baseClass = config('ddd.base_view_model');
$baseClassName = class_basename($baseClass);

return [
'extends' => filled($baseClass) ? " extends {$baseClassName}" : '',
'baseClassImport' => filled($baseClass) ? "use {$baseClass};" : '',
];
}

$parts = str($baseViewModel)->explode('\\');
$baseName = $parts->last();
$basePath = $this->getPath($baseViewModel);
public function handle()
{
if ($this->shouldCreateBaseViewModel()) {
$baseViewModel = config('ddd.base_view_model');

if (! file_exists($basePath)) {
$this->warn("Base view model {$baseViewModel} doesn't exist, generating...");

$domain = DomainResolver::guessDomainFromClass($baseViewModel);

$name = Str::after($baseViewModel, "{$domain}\\");

$this->call(DomainBaseViewModelMakeCommand::class, [
'--domain' => 'Shared',
'name' => $baseName,
'--domain' => $domain,
'name' => $name,
]);
}

parent::handle();
}

protected function shouldCreateBaseViewModel(): bool
{
$baseViewModel = config('ddd.base_view_model');

// If the class exists, we don't need to create it.
if (class_exists($baseViewModel)) {
return false;
}

// If the class is outside of the domain layer, we won't attempt to create it.
if (! DomainResolver::isDomainClass($baseViewModel)) {
return false;
}

// At this point the class is probably a domain object, but we should
// check if the expected path exists.
if (file_exists(app()->basePath(DomainResolver::guessPathFromClass($baseViewModel)))) {
return false;
}

return true;
}
}
2 changes: 2 additions & 0 deletions src/Support/Domain.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ public function __construct(string $domain, ?string $subdomain = null)

protected function registerDomainObjects()
{
// WIP
}

protected function registerDomainObject()
{
// WIP
}

protected function getDomainBasePath()
Expand Down
54 changes: 47 additions & 7 deletions src/Support/DomainResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,40 @@

class DomainResolver
{
/**
* Get the list of current domain choices.
*/
public static function domainChoices(): array
{
$folders = glob(app()->basePath(static::domainPath().'/*'), GLOB_ONLYDIR);

return collect($folders)
->map(function ($folder) {
return basename($folder);
})
->map(fn ($path) => basename($path))
->sort()
->toArray();
}

/**
* Get the current configured domain path.
*/
public static function domainPath(): ?string
{
return config('ddd.domain_path');
}

/**
* Get the current configured root domain namespace.
*/
public static function domainRootNamespace(): ?string
{
return config('ddd.domain_namespace');
}

/**
* Resolve the relative domain object namespace.
*
* @param string $type The domain object type.
*/
public static function getRelativeObjectNamespace(string $type): string
{
return config("ddd.namespaces.{$type}", str($type)->plural()->studly()->toString());
Expand All @@ -38,20 +50,48 @@ public static function getDomainObjectNamespace(string $domain, string $type): s
return implode('\\', [static::domainRootNamespace(), $domain, static::getRelativeObjectNamespace($type)]);
}

/**
* Attempt to resolve the domain of a given domain class.
*/
public static function guessDomainFromClass(string $class): ?string
{
$domainNamespace = Str::finish(DomainResolver::domainRootNamespace(), '\\');

if (! str($class)->startsWith($domainNamespace)) {
if (! static::isDomainClass($class)) {
// Not a domain object
return null;
}

$domain = str($class)
->after($domainNamespace)
->after(Str::finish(static::domainRootNamespace(), '\\'))
->before('\\')
->toString();

return $domain;
}

/**
* Attempt to resolve the file path of a given domain class.
*/
public static function guessPathFromClass(string $class): ?string
{
if (! static::isDomainClass($class)) {
// Not a domain object
return null;
}

$classWithoutDomainRoot = str($class)
->after(Str::finish(static::domainRootNamespace(), '\\'))
->toString();

return Path::join(...[static::domainPath(), "{$classWithoutDomainRoot}.php"]);
}

/**
* Determine whether a class is an object within the domain layer.
*
* @param string $class The fully qualified class name.
*/
public static function isDomainClass(string $class): bool
{
return str($class)->startsWith(Str::finish(static::domainRootNamespace(), '\\'));
}
}
4 changes: 2 additions & 2 deletions stubs/view-model.php.stub
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace {{ namespace }};

use {{ rootNamespace }}\Shared\ViewModels\ViewModel;
{{ baseClassImport }}

class {{ class }} extends ViewModel
class {{ class }}{{ extends }}
{
public function __construct()
{
Expand Down
60 changes: 52 additions & 8 deletions tests/Generator/MakeViewModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
it('can generate view models', function ($domainPath, $domainRoot) {
Config::set('ddd.domain_path', $domainPath);
Config::set('ddd.domain_namespace', $domainRoot);
Config::set('ddd.base_view_model', 'Domain\Shared\ViewModels\MyBaseViewModel');

$viewModelName = Str::studly(fake()->word());
$viewModelName = Str::studly(fake()->word().'ViewModel');
$domain = Str::studly(fake()->word());

$relativePath = implode('/', [
Expand Down Expand Up @@ -38,7 +39,14 @@
config('ddd.namespaces.view_model'),
]);

expect(file_get_contents($expectedPath))->toContain("namespace {$expectedNamespace};");
$fileContent = file_get_contents($expectedPath);

expect($fileContent)
->toContain(
"namespace {$expectedNamespace};",
"use Domain\Shared\ViewModels\MyBaseViewModel;",
"class {$viewModelName} extends MyBaseViewModel",
);
})->with('domainPaths');

it('normalizes generated view model to pascal case', function ($given, $normalized) {
Expand All @@ -56,9 +64,11 @@
expect(file_exists($expectedPath))->toBeTrue();
})->with('makeViewModelInputs');

it('generates the base view model if needed', function () {
$className = Str::studly(fake()->word());
$domain = Str::studly(fake()->word());
it('generates the base view model if needed', function ($baseViewModel, $baseViewModelPath) {
$className = 'ShowInvoiceViewModel';
$domain = 'Invoicing';

Config::set('ddd.base_view_model', $baseViewModel);

$expectedPath = base_path(implode('/', [
config('ddd.domain_path'),
Expand All @@ -73,8 +83,7 @@

expect(file_exists($expectedPath))->toBeFalse();

// This currently only tests for the default base model
$expectedBaseViewModelPath = base_path(config('ddd.domain_path').'/Shared/ViewModels/ViewModel.php');
$expectedBaseViewModelPath = app()->basePath($baseViewModelPath);

if (file_exists($expectedBaseViewModelPath)) {
unlink($expectedBaseViewModelPath);
Expand All @@ -84,5 +93,40 @@

Artisan::call("ddd:view-model {$domain}:{$className}");

expect(Artisan::output())->toContain("Base view model {$baseViewModel} doesn't exist, generating");

expect(file_exists($expectedBaseViewModelPath))->toBeTrue();
});

// Subsequent calls should not attempt to generate a base view model again
Artisan::call("ddd:view-model {$domain}:EditInvoiceViewModel");

expect(Artisan::output())->not->toContain("Base view model {$baseViewModel} doesn't exist, generating");
})->with([
"Domain\Shared\ViewModels\ViewModel" => ["Domain\Shared\ViewModels\ViewModel", 'src/Domain/Shared/ViewModels/ViewModel.php'],
"Domain\SomewhereElse\ViewModels\BaseViewModel" => ["Domain\SomewhereElse\ViewModels\BaseViewModel", 'src/Domain/SomewhereElse/ViewModels/BaseViewModel.php'],
]);

it('does not attempt to generate base view models outside the domain layer', function ($baseViewModel) {
$className = 'ShowInvoiceViewModel';
$domain = 'Invoicing';

Config::set('ddd.base_view_model', $baseViewModel);

$expectedPath = base_path(implode('/', [
config('ddd.domain_path'),
$domain,
config('ddd.namespaces.view_model'),
"{$className}.php",
]));

if (file_exists($expectedPath)) {
unlink($expectedPath);
}

Artisan::call("ddd:view-model {$domain}:{$className}");

expect(Artisan::output())->not->toContain("Base view model {$baseViewModel} doesn't exist, generating");
})->with([
"Vendor\External\ViewModels\ViewModel" => ["Vendor\External\ViewModels\ViewModel"],
"Illuminate\Support\Str" => ["Illuminate\Support\Str"],
]);

0 comments on commit 2775e5e

Please sign in to comment.