Skip to content

Commit

Permalink
Merge pull request #31 from lunarstorm/feat-sub-domain-factories
Browse files Browse the repository at this point in the history
feat: Add support for subdomains in newFactory method (#29)
  • Loading branch information
JasperTey authored Oct 23, 2023
2 parents 2ec0428 + 1695363 commit 5e7a8bb
Show file tree
Hide file tree
Showing 19 changed files with 483 additions and 81 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to `laravel-ddd` will be documented in this file.

## [Unversioned]
### Added
- Formal support for subdomains (nested domains). For example, to generate model `Domain\Reporting\Internal\Models\InvoiceReport`, the domain argument can be specified in any of the following ways:
- `ddd:model Reporting\\Internal InvoiceReport`
- `ddd:model Reporting/Internal InvoiceReport`
- `ddd:model Reporting.Internal InvoiceReport`
- Implement abstract `Lunarstorm\LaravelDDD\Factories\DomainFactory` extension of `Illuminate\Database\Eloquent\Factories\Factory`:
- Implements `DomainFactory::resolveFactoryName()` to resolve the corresponding factory for a domain model.
- Will resolve the correct factory if the model belongs to a subdomain; `Domain\Reporting\Internal\Models\InvoiceReport` will correctly resolve to `Database\Factories\Reporting\Internal\InvoiceReportFactory`.

### Changed
- Default base model implementation in `base-model.php.stub` now uses using `DomainFactory::factoryForModel()` inside the `newFactory` method to resolve the model factory.

## [0.6.1] - 2023-08-14
### Fixed
- Ensure generated domain factories set the `protected $model` property.
Expand Down
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,19 @@
},
"autoload-dev": {
"psr-4": {
"Lunarstorm\\LaravelDDD\\Tests\\": "tests"
"Lunarstorm\\LaravelDDD\\Tests\\": "tests",
"App\\": "vendor/orchestra/testbench-core/laravel/app/",
"Database\\Factories\\": "vendor/orchestra/testbench-core/laravel/database/factories/",
"Domain\\": "vendor/orchestra/testbench-core/laravel/src/Domain/"
}
},
"scripts": {
"post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi",
"analyse": "vendor/bin/phpstan analyse",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/pint"
"format": "vendor/bin/pint",
"lint": "vendor/bin/pint"
},
"config": {
"sort-packages": true,
Expand Down
1 change: 1 addition & 0 deletions src/Commands/DomainGeneratorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ protected function getDomain()
{
return str($this->getDomainInput())
->trim()
->replace(['.', '/'], '\\')
->studly()
->toString();
}
Expand Down
15 changes: 9 additions & 6 deletions src/Commands/MakeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,17 @@ protected function preparePlaceholders(): array

$name = $this->getNameInput();

$namespacedModel = $this->option('model')
? $domain->namespacedModel($this->option('model'))
: $domain->namespacedModel($this->guessModelName($name));
$modelName = $this->option('model') ?: $this->guessModelName($name);

$domainModel = $domain->model($modelName);

$domainFactory = $domain->factory($name);

return [
'namespacedModel' => $namespacedModel,
'model' => class_basename($namespacedModel),
'namespacedModel' => $domainModel->fqn,
'model' => class_basename($domainModel->fqn),
'factory' => $this->getFactoryName(),
'namespace' => $domainFactory->namespace,
];
}

Expand All @@ -109,6 +112,6 @@ protected function guessModelName($name)
$name = substr($name, 0, -7);
}

return (new Domain($this->getDomain()))->namespacedModel($name);
return (new Domain($this->getDomain()))->model($name)->name;
}
}
53 changes: 53 additions & 0 deletions src/Factories/DomainFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Lunarstorm\LaravelDDD\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

abstract class DomainFactory extends Factory
{
/**
* Get the domain namespace.
*
* @return string
*/
protected static function domainNamespace()
{
return basename(config('ddd.paths.domains')).'\\';
}

/**
* Get the factory name for the given domain model name.
*
* @param class-string<\Illuminate\Database\Eloquent\Model> $modelName
* @return null|class-string<\Illuminate\Database\Eloquent\Factories\Factory>
*/
public static function resolveFactoryName(string $modelName)
{
$resolver = function (string $modelName) {
$domainNamespace = static::domainNamespace();
$modelNamespace = config('ddd.namespaces.models');

// Expected domain model FQN:
// {DomainNamespace}\{Domain}\{ModelNamespace}\{Model}

if (! Str::startsWith($modelName, $domainNamespace)) {
// Not a domain model
return null;
}

$domain = str($modelName)
->after($domainNamespace)
->beforeLast($modelNamespace)
->trim('\\')
->toString();

$modelBaseName = class_basename($modelName);

return static::$namespace."{$domain}\\{$modelBaseName}Factory";
};

return $resolver($modelName);
}
}
19 changes: 19 additions & 0 deletions src/Models/DomainModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Lunarstorm\LaravelDDD\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Lunarstorm\LaravelDDD\Factories\DomainFactory;

abstract class DomainModel extends Model
{
use HasFactory;

protected $guarded = [];

protected static function newFactory()
{
return DomainFactory::factoryForModel(get_called_class());
}
}
162 changes: 144 additions & 18 deletions src/Support/Domain.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,162 @@

namespace Lunarstorm\LaravelDDD\Support;

use Lunarstorm\LaravelDDD\ValueObjects\DomainNamespaces;
use Lunarstorm\LaravelDDD\ValueObjects\DomainObject;

class Domain
{
public string $domainRoot;
public readonly string $dotName;

public readonly string $path;

public readonly string $domain;

public readonly ?string $subdomain;

public function __construct(public string $domain)
public readonly string $domainWithSubdomain;

public readonly DomainNamespaces $namespace;

public function __construct(string $domain, string $subdomain = null)
{
$this->domainRoot = basename(config('ddd.paths.domains'));
if (is_null($subdomain)) {
// If a subdomain isn't explicitly specified, we
// will attempt to parse it from the domain.
$parts = str($domain)
->replace(['\\', '/'], '.')
->explode('.')
->filter();

$domain = $parts->shift();

if ($parts->count() > 0) {
$subdomain = $parts->implode('.');
}
}

$domain = str($domain)->trim('\\/')->toString();

$subdomain = str($subdomain)->trim('\\/')->toString();

$this->domainWithSubdomain = str($domain)
->when($subdomain, fn ($domain) => $domain->append("\\{$subdomain}"))
->toString();

$this->domain = $domain;

$this->subdomain = $subdomain ?: null;

$this->dotName = $this->subdomain
? "{$this->domain}.{$this->subdomain}"
: $this->domain;

$this->namespace = DomainNamespaces::from($this->domain, $this->subdomain);

$this->path = Path::join(config('ddd.paths.domains'), $this->domainWithSubdomain);
}

public function relativePath(string $path = ''): string
protected function getDomainBasePath()
{
return implode(DIRECTORY_SEPARATOR, [
$this->domain,
$path,
]);
return app()->basePath(config('ddd.paths.domains'));
}

public function namespacedModel(?string $model): string
public function path(string $path = null): string
{
$prefix = implode('\\', [
$this->domainRoot,
$this->domain,
config('ddd.namespaces.models'),
]);
if (is_null($path)) {
return $this->path;
}

$model = str($model)
->replace($prefix, '')
->ltrim('\\')
$path = str($path)
->replace($this->namespace->root, '')
->replace(['\\', '/'], DIRECTORY_SEPARATOR)
->append('.php')
->toString();

return implode('\\', [$prefix, $model]);
return Path::join($this->path, $path);
}

public function relativePath(string $path = ''): string
{
return collect([$this->domain, $path])->filter()->implode(DIRECTORY_SEPARATOR);
}

public function model(string $name): DomainObject
{
$name = str_replace($this->namespace->models.'\\', '', $name);

return new DomainObject(
name: $name,
namespace: $this->namespace->models,
fqn: $this->namespace->models.'\\'.$name,
path: $this->path($this->namespace->models.'\\'.$name),
);
}

public function factory(string $name): DomainObject
{
$name = str_replace($this->namespace->factories.'\\', '', $name);

return new DomainObject(
name: $name,
namespace: $this->namespace->factories,
fqn: $this->namespace->factories.'\\'.$name,
path: str("database/factories/{$this->domainWithSubdomain}/{$name}.php")
->replace(['\\', '/'], DIRECTORY_SEPARATOR)
->toString()
);
}

public function dataTransferObject(string $name): DomainObject
{
$name = str_replace($this->namespace->dataTransferObjects.'\\', '', $name);

return new DomainObject(
name: $name,
namespace: $this->namespace->dataTransferObjects,
fqn: $this->namespace->dataTransferObjects.'\\'.$name,
path: $this->path($this->namespace->dataTransferObjects.'\\'.$name),
);
}

public function dto(string $name): DomainObject
{
return $this->dataTransferObject($name);
}

public function viewModel(string $name): DomainObject
{
$name = str_replace($this->namespace->viewModels.'\\', '', $name);

return new DomainObject(
name: $name,
namespace: $this->namespace->viewModels,
fqn: $this->namespace->viewModels.'\\'.$name,
path: $this->path($this->namespace->viewModels.'\\'.$name),
);
}

public function valueObject(string $name): DomainObject
{
$name = str_replace($this->namespace->valueObjects.'\\', '', $name);

return new DomainObject(
name: $name,
namespace: $this->namespace->valueObjects,
fqn: $this->namespace->valueObjects.'\\'.$name,
path: $this->path($this->namespace->valueObjects.'\\'.$name),
);
}

public function action(string $name): DomainObject
{
$name = str_replace($this->namespace->actions.'\\', '', $name);

return new DomainObject(
name: $name,
namespace: $this->namespace->actions,
fqn: $this->namespace->actions.'\\'.$name,
path: $this->path($this->namespace->actions.'\\'.$name),
);
}
}
10 changes: 10 additions & 0 deletions src/Support/DomainResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Lunarstorm\LaravelDDD\Support;

class DomainResolver
{
public static function fromModelClass(string $modelClass)
{
}
}
20 changes: 20 additions & 0 deletions src/Support/Path.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Lunarstorm\LaravelDDD\Support;

class Path
{
public static function normalize($path)
{
return str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path);
}

public static function join(...$parts)
{
$parts = array_map(function ($part) {
return trim(static::normalize($part), DIRECTORY_SEPARATOR);
}, $parts);

return implode(DIRECTORY_SEPARATOR, $parts);
}
}
Loading

0 comments on commit 5e7a8bb

Please sign in to comment.