-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Domain autoloader #49
Changes from 7 commits
5e32899
a0a5c06
a418b1d
242ef15
edf44d6
5d67e04
d2d5130
4ff70cb
36a993a
da51867
d808673
e943554
4168b29
94112cb
c79cb8b
0f2fc85
bde21b0
62d4bcb
fea680f
bd1a5fa
c6e096a
91fe23f
89354b5
cfb5073
cbdb804
73a27ac
4159b64
b0232a4
4d25561
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,8 +60,47 @@ | |
'resource' => 'Resources', | ||
'rule' => 'Rules', | ||
'scope' => 'Scopes', | ||
'factory' => 'Database\Factories', | ||
], | ||
|
||
/* | ||
* The folder where the domain cache files will be stored. | ||
*/ | ||
'cache_directory' => 'bootstrap/cache', | ||
|
||
'autoload' => [ | ||
/* | ||
| Autoload service providers from the domain namespace. | ||
| By default, it loads any file that ends with 'ServiceProvider.php' inside your domain. | ||
| For example: Domain/Invoicing/Providers/InvoicingServiceProvider.php or Domain/Invoicing/InvoicingServiceProvider.php | ||
*/ | ||
// To customize the pattern, you can use a glob pattern like '*/Providers/*.php' | ||
'service_providers' => '*/*ServiceProvider.php', | ||
|
||
/* | ||
| Autoload commands from the domain namespace. | ||
| By default, it loads any file inside the /Commands folder that ends '.php', extends Illuminate\Console\Command and is not abstract. | ||
| For example: Domain/Invoicing/Commands/CreateInvoiceCommand.php | ||
*/ | ||
// To customize the pattern, you can use a glob pattern like '*/Commands/*.php' | ||
'commands' => '*/Commands/*.php', | ||
|
||
/* | ||
| Autoload policies from the domain namespace. | ||
| By default, it loads any file inside the /Policies folder that ends 'Policy.php' and is not abstract. | ||
| For example: Domain/Invoicing/Policies/InvoicePolicy.php | ||
*/ | ||
'policies' => 'Policies\\{model}Policy', | ||
|
||
/* | ||
| Autoload factories from the domain namespace. | ||
| By default, it loads any file inside the /Database/Factories folder that ends 'Factory.php' and is not abstract. | ||
| For example: Domain/Invoicing/Database/Factories/InvoiceFactory.php | ||
*/ | ||
'factories' => 'Database\\Factories\\{model}Factory', | ||
], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering, are these autoload config strings necessary, now that Instead, each There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I actually made them as booleans first, but I thought it would be good to be able to customize. It also still works with booleans,. With
That might be a bit over-engineered though. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked into how e.g., $finder = Finder::create()->files()->in($paths);
return Lody::classesFromFinder($finder)
->isNotAbstract()
->isInstanceOf(ServiceProvider::class)
->toArray(); I will push my updates soon and summarize everything in more detail there. |
||
|
||
|
||
/* | ||
|-------------------------------------------------------------------------- | ||
| Base Model | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
namespace Lunarstorm\LaravelDDD\Support; | ||
|
||
use ErrorException; | ||
use Illuminate\Events\Dispatcher; | ||
|
||
class CacheClearSubscriber | ||
{ | ||
public function __construct() | ||
{ | ||
} | ||
|
||
public function handle(): void | ||
{ | ||
$files = glob(base_path(config('ddd.cache_directory').'/ddd-*.php')); | ||
|
||
foreach ($files as $file) { | ||
try { | ||
unlink($file); | ||
} catch (ErrorException $exception) { | ||
if (! str_contains($exception->getMessage(), 'No such file or directory')) { | ||
dump($exception->getMessage()); | ||
throw $exception; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Register the listeners for the subscriber. | ||
* | ||
* @param Dispatcher $events | ||
* | ||
* @return array | ||
*/ | ||
public function subscribe(Dispatcher $events): void | ||
{ | ||
$events->listen('cache:clearing', [$this, 'handle']); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
<?php | ||
namespace Lunarstorm\LaravelDDD\Support; | ||
|
||
use Illuminate\Console\Application as ConsoleApplication; | ||
use Illuminate\Console\Command; | ||
use Illuminate\Database\Eloquent\Factories\Factory; | ||
use Illuminate\Support\Arr; | ||
use Illuminate\Support\Facades\Cache; | ||
use Illuminate\Support\Facades\Gate; | ||
use ReflectionClass; | ||
|
||
class DomainAutoloader | ||
{ | ||
protected string $cacheDirectory; | ||
|
||
protected mixed $config; | ||
|
||
public function __construct() | ||
{ | ||
$this->config = config('ddd'); | ||
$this->cacheDirectory = $this->config['cache_directory'] ?? 'bootstrap/cache/ddd'; | ||
} | ||
|
||
public function autoload(): void | ||
{ | ||
if(isset($this->config['autoload']['service_providers'])) { | ||
$this->registerDomainServiceProviders($this->config['autoload']['service_providers']); | ||
} | ||
if(isset($this->config['autoload']['commands'])) { | ||
$this->registerDomainCommands($this->config['autoload']['commands']); | ||
} | ||
if(isset($this->config['autoload']['policies'])) { | ||
$this->registerPolicies($this->config['autoload']['policies']); | ||
} | ||
if(isset($this->config['autoload']['factories'])) { | ||
$this->registerFactories($this->config['autoload']['factories']); | ||
} | ||
} | ||
|
||
protected function registerDomainServiceProviders(bool|string $domainPath = null): void | ||
{ | ||
$domainPath = is_string($domainPath) ? $domainPath : '*/*ServiceProvider.php'; | ||
|
||
$serviceProviders = $this->remember('ddd-domain-service-providers', static function () use ($domainPath){ | ||
return Arr::map( | ||
glob(base_path(DomainResolver::domainPath().'/'.$domainPath)), | ||
(static function ($serviceProvider) { | ||
|
||
return Path::filePathToNamespace( | ||
$serviceProvider, | ||
DomainResolver::domainPath(), | ||
DomainResolver::domainRootNamespace() | ||
); | ||
})); | ||
}); | ||
|
||
$app = app(); | ||
foreach ($serviceProviders as $serviceProvider) { | ||
$app->register($serviceProvider); | ||
} | ||
} | ||
|
||
protected function registerDomainCommands(bool|string $domainPath = null): void | ||
{ | ||
$domainPath = is_string($domainPath) ? $domainPath : '*/Commands/*.php'; | ||
$commands = $this->remember('ddd-domain-commands', static function () use ($domainPath){ | ||
$commands = Arr::map( | ||
glob(base_path(DomainResolver::domainPath().'/'.$domainPath)), | ||
static function ($command) { | ||
return Path::filePathToNamespace( | ||
$command, | ||
DomainResolver::domainPath(), | ||
DomainResolver::domainRootNamespace() | ||
); | ||
}); | ||
|
||
// Filter out invalid commands (Abstract classes and classes not extending Illuminate\Console\Command) | ||
return Arr::where($commands, static function($command) { | ||
if (is_subclass_of($command, Command::class) && | ||
! (new ReflectionClass($command))->isAbstract()) { | ||
ConsoleApplication::starting(static function ($artisan) use ($command): void { | ||
$artisan->resolve($command); | ||
}); | ||
} | ||
}); | ||
}); | ||
ConsoleApplication::starting(static function ($artisan) use ($commands): void { | ||
foreach ($commands as $command) { | ||
$artisan->resolve($command); | ||
} | ||
}); | ||
} | ||
|
||
protected function registerPolicies(bool|string $domainPath = null): void | ||
{ | ||
$domainPath = is_string($domainPath) ? $domainPath : 'Policies\\{model}Policy'; | ||
|
||
Gate::guessPolicyNamesUsing(static function (string $modelClass) use ($domainPath): ?string { | ||
|
||
[$domain, $model] = static::extractDomainAndModelFromModelNamespace($modelClass); | ||
|
||
if (is_null($domain)) { | ||
return null; | ||
} | ||
|
||
$policy = DomainResolver::domainRootNamespace().'\\'.$domain.'\\'.str_replace('{model}', $model, $domainPath); | ||
|
||
return $policy; | ||
}); | ||
} | ||
|
||
protected function registerFactories(bool|string $domainPath = null): void | ||
{ | ||
$domainPath = is_string($domainPath) ? $domainPath : 'Database\\Factories\\{model}Factory'; | ||
|
||
Factory::guessFactoryNamesUsing( function (string $modelClass) use ($domainPath){ | ||
|
||
[$domain, $model] = $this->extractDomainAndModelFromModelNamespace($modelClass); | ||
|
||
if (is_null($domain)) { | ||
return null; | ||
} | ||
|
||
// Look for domain model factory in \<DomainNamespace>\Database\\Factories\<model>Factory.php | ||
$classPath = 'Domain\\'.$domain.'\\'.str_replace('{model}', $model, $domainPath); | ||
if (class_exists($classPath)) { | ||
return $classPath; | ||
} | ||
|
||
// Look for domain factory in /database/factories/<domain>/<model>Factory.php | ||
$classPath = 'Database\\Factories\\'.$domain.'\\'.$model.'Factory'; | ||
if (class_exists($classPath)) { | ||
return $classPath; | ||
} | ||
|
||
// Default Laravel factory location | ||
return 'Database\Factories\\'.class_basename($modelClass).'Factory'; | ||
}); | ||
} | ||
|
||
protected function extractDomainAndModelFromModelNamespace(string $modelName): array | ||
{ | ||
// Matches <DomainNamespace>\{domain}\<ModelNamespace>\{model} and extracts domain and model | ||
// For example: Domain\Invoicing\Models\Invoice gives ['domain' => 'Invoicing', 'model' => 'Invoice'] | ||
$regex = '/'.DomainResolver::domainRootNamespace().'\\\\(?<domain>.+)\\\\'.$this->config['namespaces.models'].'\\\\(?<model>.+)/'; | ||
|
||
if (preg_match($regex, $modelName, $matches, PREG_OFFSET_CAPTURE, 0)) { | ||
return [ | ||
'domain' => $matches['domain'][0], | ||
'model' => $matches['model'][0] | ||
]; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
protected function remember($fileName, $callback) | ||
{ | ||
// The cache is not available during booting, so we need to roll our own file based cache | ||
$cacheFilePath = base_path($this->cacheDirectory.'/'.$fileName.'.php'); | ||
|
||
$data = file_exists($cacheFilePath) ? include $cacheFilePath : null; | ||
|
||
if (is_null($data)) { | ||
$data = $callback(); | ||
|
||
file_put_contents( | ||
$cacheFilePath, | ||
'<?php ' . PHP_EOL . 'return ' . var_export($data, true) . ';' | ||
); | ||
} | ||
|
||
return $data; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?php | ||
|
||
use Illuminate\Support\Facades\Artisan; | ||
use Illuminate\Support\Facades\Config; | ||
use Illuminate\Support\Str; | ||
use Lunarstorm\LaravelDDD\Tests\Fixtures\Enums\Feature; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on simplifying this namespace to
Factories
instead ofDatabase\Factories
? Or is this following a certain pattern?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is where I have been placing my factories, and from what I have seen in other projects that seems pretty common, although from a small sample size. The
Database
folder usually also contain folders for migrations and seeders for that domain, just like the/database
folder in a vanilla Laravel app.I think that is a sensible default, but because this is easy configure how you want it I have no strong feelings. I would be fine with
Factories
as well, and that looks nicer/cleaner in the config.