diff --git a/src/Commands/PublishCommand.php b/src/Commands/PublishCommand.php index 3f0f385..a561f5d 100644 --- a/src/Commands/PublishCommand.php +++ b/src/Commands/PublishCommand.php @@ -18,6 +18,7 @@ protected function getOptions() return [ ['config', 'c', InputOption::VALUE_NONE, 'Publish the config file'], ['stubs', 's', InputOption::VALUE_NONE, 'Publish the stubs'], + ['all', 'a', InputOption::VALUE_NONE, 'Publish both the config file and stubs'], ]; } @@ -40,6 +41,7 @@ public function handle(): int $thingsToPublish = [ ...$this->option('config') ? ['config'] : [], ...$this->option('stubs') ? ['stubs'] : [], + ...$this->option('all') ? ['config', 'stubs'] : [], ] ?: $this->askForThingsToPublish(); if (in_array('config', $thingsToPublish)) { @@ -51,10 +53,7 @@ public function handle(): int if (in_array('stubs', $thingsToPublish)) { $this->comment('Publishing stubs...'); - - $this->callSilently('vendor:publish', [ - '--tag' => 'ddd-stubs', - ]); + $this->call('ddd:stub --all'); } return self::SUCCESS; diff --git a/src/Commands/StubCommand.php b/src/Commands/StubCommand.php index 82bb60d..e6f4001 100644 --- a/src/Commands/StubCommand.php +++ b/src/Commands/StubCommand.php @@ -3,11 +3,9 @@ namespace Lunarstorm\LaravelDDD\Commands; use Illuminate\Console\Command; -use Illuminate\Filesystem\Filesystem; -use Illuminate\Foundation\Console\StubPublishCommand; use Illuminate\Foundation\Events\PublishingStubs; +use Illuminate\Support\Facades\File; use Illuminate\Support\Str; -use ReflectionClass; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -36,81 +34,17 @@ protected function getOptions() ]; } - protected function getNativeLaravelStubs() + protected function getStubChoices() { - $laravelStubCommand = new ReflectionClass(new StubPublishCommand); - - $dir = dirname($laravelStubCommand->getFileName()); - return [ - $dir.'/stubs/cast.inbound.stub' => 'cast.inbound.stub', - $dir.'/stubs/cast.stub' => 'cast.stub', - $dir.'/stubs/class.stub' => 'class.stub', - $dir.'/stubs/class.invokable.stub' => 'class.invokable.stub', - $dir.'/stubs/console.stub' => 'console.stub', - $dir.'/stubs/enum.stub' => 'enum.stub', - $dir.'/stubs/enum.backed.stub' => 'enum.backed.stub', - $dir.'/stubs/event.stub' => 'event.stub', - $dir.'/stubs/job.queued.stub' => 'job.queued.stub', - $dir.'/stubs/job.stub' => 'job.stub', - $dir.'/stubs/listener.typed.queued.stub' => 'listener.typed.queued.stub', - $dir.'/stubs/listener.queued.stub' => 'listener.queued.stub', - $dir.'/stubs/listener.typed.stub' => 'listener.typed.stub', - $dir.'/stubs/listener.stub' => 'listener.stub', - $dir.'/stubs/mail.stub' => 'mail.stub', - $dir.'/stubs/markdown-mail.stub' => 'markdown-mail.stub', - $dir.'/stubs/markdown-notification.stub' => 'markdown-notification.stub', - $dir.'/stubs/model.pivot.stub' => 'model.pivot.stub', - $dir.'/stubs/model.stub' => 'model.stub', - $dir.'/stubs/notification.stub' => 'notification.stub', - $dir.'/stubs/observer.plain.stub' => 'observer.plain.stub', - $dir.'/stubs/observer.stub' => 'observer.stub', - $dir.'/stubs/pest.stub' => 'pest.stub', - $dir.'/stubs/pest.unit.stub' => 'pest.unit.stub', - $dir.'/stubs/policy.plain.stub' => 'policy.plain.stub', - $dir.'/stubs/policy.stub' => 'policy.stub', - $dir.'/stubs/provider.stub' => 'provider.stub', - $dir.'/stubs/request.stub' => 'request.stub', - $dir.'/stubs/resource.stub' => 'resource.stub', - $dir.'/stubs/resource-collection.stub' => 'resource-collection.stub', - $dir.'/stubs/rule.stub' => 'rule.stub', - $dir.'/stubs/scope.stub' => 'scope.stub', - $dir.'/stubs/test.stub' => 'test.stub', - $dir.'/stubs/test.unit.stub' => 'test.unit.stub', - $dir.'/stubs/trait.stub' => 'trait.stub', - $dir.'/stubs/view-component.stub' => 'view-component.stub', - // realpath($dir . '/../../Database/Console/Factories/stubs/factory.stub') => 'factory.stub', - realpath($dir.'/../../Database/Console/Seeds/stubs/seeder.stub') => 'seeder.stub', - realpath($dir.'/../../Database/Migrations/stubs/migration.create.stub') => 'migration.create.stub', - realpath($dir.'/../../Database/Migrations/stubs/migration.stub') => 'migration.stub', - realpath($dir.'/../../Database/Migrations/stubs/migration.update.stub') => 'migration.update.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.api.stub') => 'controller.api.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.invokable.stub') => 'controller.invokable.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.model.api.stub') => 'controller.model.api.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.model.stub') => 'controller.model.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.nested.api.stub') => 'controller.nested.api.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.api.stub') => 'controller.nested.singleton.api.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.stub') => 'controller.nested.singleton.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.nested.stub') => 'controller.nested.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.plain.stub') => 'controller.plain.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.singleton.api.stub') => 'controller.singleton.api.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.singleton.stub') => 'controller.singleton.stub', - realpath($dir.'/../../Routing/Console/stubs/controller.stub') => 'controller.stub', - realpath($dir.'/../../Routing/Console/stubs/middleware.stub') => 'middleware.stub', + ...app('ddd')->stubs()->dddStubs(), + ...app('ddd')->stubs()->frameworkStubs(), ]; } protected function resolveSelectedStubs(array $names = []) { - $stubs = [ - realpath(__DIR__.'/../../stubs/action.stub') => 'action.stub', - realpath(__DIR__.'/../../stubs/dto.stub') => 'dto.stub', - realpath(__DIR__.'/../../stubs/value-object.stub') => 'value-object.stub', - realpath(__DIR__.'/../../stubs/view-model.stub') => 'view-model.stub', - realpath(__DIR__.'/../../stubs/base-view-model.stub') => 'base-view-model.stub', - realpath(__DIR__.'/../../stubs/factory.stub') => 'factory.stub', - ...$this->getNativeLaravelStubs(), - ]; + $stubs = $this->getStubChoices(); if ($names) { return collect($stubs) @@ -121,7 +55,7 @@ protected function resolveSelectedStubs(array $names = []) ->all(); } - return multisearch( + $selected = multisearch( label: 'Which stub should be published?', placeholder: 'Search for a stub...', options: fn (string $value) => strlen($value) > 0 @@ -129,6 +63,10 @@ protected function resolveSelectedStubs(array $names = []) : $stubs, required: true ); + + return collect($stubs) + ->filter(fn ($stub, $path) => in_array($stub, $selected)) + ->all(); } public function handle(): int @@ -139,29 +77,17 @@ public function handle(): int default => select( label: 'What do you want to do?', options: [ - 'all' => 'Publish all stubs', 'some' => 'Choose stubs to publish', + 'all' => 'Publish all stubs', ], required: true, - default: 'all' + default: 'some' ) }; - if ($option === 'all') { - $this->comment('Publishing all stubs...'); - - $this->call('vendor:publish', [ - '--tag' => 'ddd-stubs', - ]); - - return self::SUCCESS; - } - - $stubs = $this->resolveSelectedStubs($this->argument('name')); - - if (! is_dir($stubsPath = $this->laravel->basePath('stubs/ddd'))) { - (new Filesystem)->makeDirectory($stubsPath, recursive: true); - } + $stubs = $option === 'all' + ? $this->getStubChoices() + : $this->resolveSelectedStubs($this->argument('name')); if (empty($stubs)) { $this->warn('No matching stubs found.'); @@ -169,6 +95,8 @@ public function handle(): int return self::INVALID; } + File::ensureDirectoryExists($stubsPath = $this->laravel->basePath('stubs/ddd')); + $this->laravel['events']->dispatch($event = new PublishingStubs($stubs)); foreach ($event->stubs as $from => $to) { diff --git a/src/DomainManager.php b/src/DomainManager.php index 767d185..9e38544 100755 --- a/src/DomainManager.php +++ b/src/DomainManager.php @@ -32,12 +32,15 @@ class DomainManager protected ?DomainCommandContext $commandContext; + protected StubManager $stubs; + public function __construct() { $this->autoloadFilter = null; $this->applicationLayerFilter = null; $this->namespaceResolver = null; $this->commandContext = null; + $this->stubs = new StubManager; } public function filterAutoloadPathsUsing(callable $filter): void @@ -84,4 +87,9 @@ public function packagePath($path = ''): string { return Path::normalize(__DIR__.'/../'.$path); } + + public function stubs(): StubManager + { + return $this->stubs; + } } diff --git a/src/Facades/DDD.php b/src/Facades/DDD.php index bd16c3a..93ad644 100644 --- a/src/Facades/DDD.php +++ b/src/Facades/DDD.php @@ -3,6 +3,7 @@ namespace Lunarstorm\LaravelDDD\Facades; use Illuminate\Support\Facades\Facade; +use Lunarstorm\LaravelDDD\StubManager; /** * @see \Lunarstorm\LaravelDDD\DomainManager @@ -10,6 +11,7 @@ * @method static void filterAutoloadPathsUsing(callable $filter) * @method static void resolveNamespaceUsing(callable $resolver) * @method static string packagePath(string $path = '') + * @method static StubManager stubs() */ class DDD extends Facade { diff --git a/src/StubManager.php b/src/StubManager.php new file mode 100755 index 0000000..8c6320d --- /dev/null +++ b/src/StubManager.php @@ -0,0 +1,96 @@ +dddStubs(), + ...$this->frameworkStubs(), + ]; + } + + public function dddStubs() + { + return [ + realpath(__DIR__.'/../stubs/action.stub') => 'action.stub', + realpath(__DIR__.'/../stubs/dto.stub') => 'dto.stub', + realpath(__DIR__.'/../stubs/value-object.stub') => 'value-object.stub', + realpath(__DIR__.'/../stubs/view-model.stub') => 'view-model.stub', + realpath(__DIR__.'/../stubs/base-view-model.stub') => 'base-view-model.stub', + realpath(__DIR__.'/../stubs/factory.stub') => 'factory.stub', + ]; + } + + public function frameworkStubs() + { + $laravelStubCommand = new ReflectionClass(new StubPublishCommand); + + $dir = dirname($laravelStubCommand->getFileName()); + + return [ + $dir.'/stubs/cast.inbound.stub' => 'cast.inbound.stub', + $dir.'/stubs/cast.stub' => 'cast.stub', + $dir.'/stubs/class.stub' => 'class.stub', + $dir.'/stubs/class.invokable.stub' => 'class.invokable.stub', + $dir.'/stubs/console.stub' => 'console.stub', + $dir.'/stubs/enum.stub' => 'enum.stub', + $dir.'/stubs/enum.backed.stub' => 'enum.backed.stub', + $dir.'/stubs/event.stub' => 'event.stub', + $dir.'/stubs/job.queued.stub' => 'job.queued.stub', + $dir.'/stubs/job.stub' => 'job.stub', + $dir.'/stubs/listener.typed.queued.stub' => 'listener.typed.queued.stub', + $dir.'/stubs/listener.queued.stub' => 'listener.queued.stub', + $dir.'/stubs/listener.typed.stub' => 'listener.typed.stub', + $dir.'/stubs/listener.stub' => 'listener.stub', + $dir.'/stubs/mail.stub' => 'mail.stub', + $dir.'/stubs/markdown-mail.stub' => 'markdown-mail.stub', + $dir.'/stubs/markdown-notification.stub' => 'markdown-notification.stub', + $dir.'/stubs/model.pivot.stub' => 'model.pivot.stub', + $dir.'/stubs/model.stub' => 'model.stub', + $dir.'/stubs/notification.stub' => 'notification.stub', + $dir.'/stubs/observer.plain.stub' => 'observer.plain.stub', + $dir.'/stubs/observer.stub' => 'observer.stub', + // $dir . '/stubs/pest.stub' => 'pest.stub', + // $dir . '/stubs/pest.unit.stub' => 'pest.unit.stub', + $dir.'/stubs/policy.plain.stub' => 'policy.plain.stub', + $dir.'/stubs/policy.stub' => 'policy.stub', + $dir.'/stubs/provider.stub' => 'provider.stub', + $dir.'/stubs/request.stub' => 'request.stub', + $dir.'/stubs/resource.stub' => 'resource.stub', + $dir.'/stubs/resource-collection.stub' => 'resource-collection.stub', + $dir.'/stubs/rule.stub' => 'rule.stub', + $dir.'/stubs/scope.stub' => 'scope.stub', + $dir.'/stubs/test.stub' => 'test.stub', + $dir.'/stubs/test.unit.stub' => 'test.unit.stub', + $dir.'/stubs/trait.stub' => 'trait.stub', + $dir.'/stubs/view-component.stub' => 'view-component.stub', + // Factories will use a ddd-specific stub + // realpath($dir . '/../../Database/Console/Factories/stubs/factory.stub') => 'factory.stub', + realpath($dir.'/../../Database/Console/Seeds/stubs/seeder.stub') => 'seeder.stub', + realpath($dir.'/../../Database/Migrations/stubs/migration.create.stub') => 'migration.create.stub', + realpath($dir.'/../../Database/Migrations/stubs/migration.stub') => 'migration.stub', + realpath($dir.'/../../Database/Migrations/stubs/migration.update.stub') => 'migration.update.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.api.stub') => 'controller.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.invokable.stub') => 'controller.invokable.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.model.api.stub') => 'controller.model.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.model.stub') => 'controller.model.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.api.stub') => 'controller.nested.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.api.stub') => 'controller.nested.singleton.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.singleton.stub') => 'controller.nested.singleton.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.nested.stub') => 'controller.nested.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.plain.stub') => 'controller.plain.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.singleton.api.stub') => 'controller.singleton.api.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.singleton.stub') => 'controller.singleton.stub', + realpath($dir.'/../../Routing/Console/stubs/controller.stub') => 'controller.stub', + realpath($dir.'/../../Routing/Console/stubs/middleware.stub') => 'middleware.stub', + ]; + } +} diff --git a/tests/Command/PublishTest.php b/tests/Command/PublishTest.php new file mode 100644 index 0000000..76dac3c --- /dev/null +++ b/tests/Command/PublishTest.php @@ -0,0 +1,116 @@ +cleanSlate(); + + $path = app()->configPath('ddd.php'); + + if (file_exists($path)) { + unlink($path); + } + + expect(file_exists($path))->toBeFalse(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + File::deleteDirectory($publishedStubFolder); + + assertDirectoryDoesNotExist($publishedStubFolder); +}); + +afterEach(function () { + $this->cleanSlate(); +}); + +it('can publish config using --config option', function () { + $path = app()->configPath('ddd.php'); + + $this + ->artisan('ddd:publish --config') + ->expectsOutputToContain('Publishing config...') + ->doesntExpectOutput('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); +}); + +it('can publish everything', function ($options) { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:publish', $options) + ->expectsOutputToContain('Publishing config...') + ->expectsOutputToContain('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); +})->with([ + '--all' => [['--all' => true]], + '--config --stubs' => [['--config' => true, '--stubs' => true]], +]); + +it('can publish config interactively', function () { + $path = app()->configPath('ddd.php'); + + $this + ->artisan('ddd:publish') + ->expectsQuestion('What should be published?', ['config']) + ->expectsOutputToContain('Publishing config...') + ->doesntExpectOutput('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); +}); + +it('can publish stubs interactively', function () { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:publish') + ->expectsQuestion('What should be published?', ['stubs']) + ->expectsOutputToContain('Publishing stubs...') + ->doesntExpectOutput('Publishing config...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeFalse(); + + assertDirectoryExists($publishedStubFolder); +}); + +it('can publish everything interactively', function () { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:publish') + ->expectsQuestion('What should be published?', ['config', 'stubs']) + ->expectsOutputToContain('Publishing config...') + ->expectsOutputToContain('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeTrue(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); +}); diff --git a/tests/Command/StubTest.php b/tests/Command/StubTest.php new file mode 100644 index 0000000..ac9a404 --- /dev/null +++ b/tests/Command/StubTest.php @@ -0,0 +1,127 @@ +cleanSlate(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + File::deleteDirectory($publishedStubFolder); + + assertDirectoryDoesNotExist($publishedStubFolder); +}); + +afterEach(function () { + $this->cleanSlate(); +}); + +it('can publish all stubs using --all option', function () { + $this + ->artisan('ddd:stub --all') + ->doesntExpectOutput('Publishing stubs...') + ->assertSuccessful() + ->execute(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); + + expect(count($stubFiles))->toEqual(count([ + ...app('ddd')->stubs()->dddStubs(), + ...app('ddd')->stubs()->frameworkStubs(), + ])); +}); + +it('can publish all stubs interactively', function () { + $path = app()->configPath('ddd.php'); + $publishedStubFolder = app()->basePath('stubs/ddd'); + + $this + ->artisan('ddd:stub') + ->expectsQuestion('What do you want to do?', 'all') + ->assertSuccessful() + ->execute(); + + expect(file_exists($path))->toBeFalse(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toBeGreaterThan(0); + + expect(count($stubFiles))->toEqual(count([ + ...app('ddd')->stubs()->dddStubs(), + ...app('ddd')->stubs()->frameworkStubs(), + ])); +}); + +it('can publish specific stubs using arguments', function ($stubsToPublish) { + $expectedStubFilenames = collect($stubsToPublish) + ->map(fn ($stub) => $stub.'.stub') + ->all(); + + $arguments = collect($stubsToPublish)->join(' '); + + $this + ->artisan("ddd:stub {$arguments}") + ->assertSuccessful() + ->execute(); + + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toEqual(count($stubsToPublish)); + + foreach ($stubFiles as $file) { + expect($file->getFilename())->toBeIn($expectedStubFilenames); + } +})->with([ + 'model' => [['model']], + 'model/action/dto' => [['model', 'action', 'dto']], + 'model/model.pivot' => [['model', 'model.pivot']], + 'controller' => [['controller']], +]); + +it('can publish specific stubs interactively', function () { + $publishedStubFolder = app()->basePath('stubs/ddd'); + + assertDirectoryDoesNotExist($publishedStubFolder); + + $options = app('ddd')->stubs()->allStubs(); + + $matches = collect($options) + ->filter(fn ($stub, $path) => str($stub)->contains('class')) + ->all(); + + $this + ->artisan('ddd:stub') + ->expectsQuestion('What do you want to do?', 'some') + ->expectsSearch( + 'Which stub should be published?', + search: 'class', + answers: $matches, + answer: ['class.stub'] + ) + ->assertSuccessful() + ->execute(); + + assertDirectoryExists($publishedStubFolder); + + $stubFiles = File::files($publishedStubFolder); + + expect(count($stubFiles))->toEqual(1); + + expect($stubFiles[0]->getFilename())->toEqual('class.stub'); +});