diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db2b0ffc..26de6a18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,3 +102,4 @@ jobs: author_name: "PHP CS Fixer" author_email: "phpcsfixer@example.com" message: Fix code style (php-cs-fixer) + diff --git a/assets/config.php b/assets/config.php index cd0a6c42..eb68d9b0 100644 --- a/assets/config.php +++ b/assets/config.php @@ -2,16 +2,14 @@ declare(strict_types=1); -use Stancl\Tenancy\Database\Models\Domain; -use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Middleware; use Stancl\Tenancy\Resolvers; return [ - 'tenant_model' => Tenant::class, - 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, + 'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class, + 'domain_model' => Stancl\Tenancy\Database\Models\Domain::class, - 'domain_model' => Domain::class, + 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, /** * The list of domains hosting your central app. diff --git a/src/Commands/Down.php b/src/Commands/Down.php index 6b390957..e7341d7f 100644 --- a/src/Commands/Down.php +++ b/src/Commands/Down.php @@ -22,24 +22,23 @@ class Down extends DownCommand public function handle(): int { - // The base down command is heavily used. Instead of saving the data inside a file, - // the data is stored the tenant database, which means some Laravel features - // are not available with tenants. - $payload = $this->getDownDatabasePayload(); - // This runs for all tenants if no --tenants are specified tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($payload) { - $this->line("Tenant: {$tenant['id']}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $tenant->putDownForMaintenance($payload); }); - $this->comment('Tenants are now in maintenance mode.'); + $this->components->info('Tenants are now in maintenance mode.'); return 0; } - /** Get the payload to be placed in the "down" file. */ + /** + * Get the payload to be placed in the "down" file. This + * payload is the same as the original function + * but without the 'template' option. + */ protected function getDownDatabasePayload(): array { return [ diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 12a2c2c9..424aa261 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -4,50 +4,136 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; +use Closure; use Illuminate\Console\Command; class Install extends Command { protected $signature = 'tenancy:install'; - protected $description = 'Install stancl/tenancy.'; + protected $description = 'Install Tenancy for Laravel.'; - public function handle(): void + public function handle(): int { - $this->comment('Installing stancl/tenancy...'); - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'config', - ]); - $this->info('✔️ Created config/tenancy.php'); + $this->step( + name: 'Publishing config file', + tag: 'config', + file: 'config/tenancy.php', + newLineBefore: true, + ); - if (! file_exists(base_path('routes/tenant.php'))) { - $this->callSilent('vendor:publish', [ + $this->step( + name: 'Publishing routes [routes/tenant.php]', + tag: 'routes', + file: 'routes/tenant.php', + ); + + $this->step( + name: 'Publishing service provider', + tag: 'providers', + file: 'app/Providers/TenancyServiceProvider.php', + ); + + $this->step( + name: 'Publishing migrations', + tag: 'migrations', + files: [ + 'database/migrations/2019_09_15_000010_create_tenants_table.php', + 'database/migrations/2019_09_15_000020_create_domains_table.php', + ], + warning: 'Migrations already exist', + ); + + $this->step( + name: 'Creating [database/migrations/tenant] folder', + task: fn () => mkdir(database_path('migrations/tenant')), + unless: is_dir(database_path('migrations/tenant')), + warning: 'Folder [database/migrations/tenant] already exists.', + newLineAfter: true, + ); + + $this->components->info('✨️ Tenancy for Laravel successfully installed.'); + + $this->askForSupport(); + + return 0; + } + + /** + * Run a step of the installation process. + * + * @param string $name The name of the step. + * @param Closure|null $task The task code. + * @param bool $unless Condition specifying when the task should NOT run. + * @param string|null $warning Warning shown when the $unless condition is true. + * @param string|null $file Name of the file being added. + * @param string|null $tag The tag being published. + * @param array|null $files Names of files being added. + * @param bool $newLineBefore Should a new line be printed after the step. + * @param bool $newLineAfter Should a new line be printed after the step. + */ + protected function step( + string $name, + Closure $task = null, + bool $unless = false, + string $warning = null, + string $file = null, + string $tag = null, + array $files = null, + bool $newLineBefore = false, + bool $newLineAfter = false, + ): void { + if ($file) { + $name .= " [$file]"; // Append clickable path to the task name + $unless = file_exists(base_path($file)); // Make the condition a check for the file's existence + $warning = "File [$file] already exists."; // Make the warning a message about the file already existing + } + + if ($tag) { + $task = fn () => $this->callSilent('vendor:publish', [ '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'routes', + '--tag' => $tag, ]); - $this->info('✔️ Created routes/tenant.php'); + } + + if ($files) { + // Show a warning if any of the files already exist + $unless = count(array_filter($files, fn ($file) => file_exists(base_path($file)))) !== 0; + } + + if (! $unless) { + if ($newLineBefore) { + $this->newLine(); + } + + $this->components->task($name, $task ?? fn () => null); + + if ($files) { + // Print out a clickable list of the added files + $this->components->bulletList(array_map(fn (string $file) => "[$file]", $files)); + } + + if ($newLineAfter) { + $this->newLine(); + } } else { - $this->info('Found routes/tenant.php.'); + $this->components->warn($warning); } + } - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'providers', - ]); - $this->info('✔️ Created TenancyServiceProvider.php'); - - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'migrations', - ]); - $this->info('✔️ Created migrations. Remember to run [php artisan migrate]!'); - - if (! is_dir(database_path('migrations/tenant'))) { - mkdir(database_path('migrations/tenant')); - $this->info('✔️ Created database/migrations/tenant folder.'); + /** If the user accepts, opens the GitHub project in the browser. */ + public function askForSupport(): void + { + if ($this->components->confirm('Would you like to show your support by starring the project on GitHub?', true)) { + if (PHP_OS_FAMILY === 'Darwin') { + exec('open https://github.com/archtechx/tenancy'); + } + if (PHP_OS_FAMILY === 'Windows') { + exec('start https://github.com/archtechx/tenancy'); + } + if (PHP_OS_FAMILY === 'Linux') { + exec('xdg-open https://github.com/archtechx/tenancy'); + } } - - $this->comment('✨️ stancl/tenancy installed successfully.'); } } diff --git a/src/Commands/Link.php b/src/Commands/Link.php index 53f3cf6f..0a587122 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -23,7 +23,7 @@ class Link extends Command protected $description = 'Create or remove tenant symbolic links.'; - public function handle(): void + public function handle(): int { $tenants = $this->getTenants(); @@ -35,14 +35,18 @@ class Link extends Command } } catch (Exception $exception) { $this->error($exception->getMessage()); + + return 1; } + + return 0; } protected function removeLinks(LazyCollection $tenants): void { RemoveStorageSymlinksAction::handle($tenants); - $this->info('The links have been removed.'); + $this->components->info('The links have been removed.'); } protected function createLinks(LazyCollection $tenants): void @@ -53,6 +57,6 @@ class Link extends Command (bool) ($this->option('force') ?? false), ); - $this->info('The links have been created.'); + $this->components->info('The links have been created.'); } } diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 739b56de..82395fcc 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -43,7 +43,7 @@ class Migrate extends MigrateCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new MigratingDatabase($tenant)); diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 32dc6ee5..657c4990 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -23,23 +23,27 @@ class MigrateFresh extends Command $this->setName('tenants:migrate-fresh'); } - public function handle(): void + public function handle(): int { tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->info('Dropping tables.'); - $this->call('db:wipe', array_filter([ - '--database' => 'tenant', - '--drop-views' => $this->option('drop-views'), - '--force' => true, - ])); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); - $this->info('Migrating.'); - $this->callSilent('tenants:migrate', [ - '--tenants' => [$tenant->getTenantKey()], - '--force' => true, - ]); + $this->components->task('Dropping tables', function () { + $this->callSilently('db:wipe', array_filter([ + '--database' => 'tenant', + '--drop-views' => $this->option('drop-views'), + '--force' => true, + ])); + }); + + $this->components->task('Migrating', function () use ($tenant) { + $this->callSilent('tenants:migrate', [ + '--tenants' => [$tenant->getTenantKey()], + '--force' => true, + ]); + }); }); - $this->info('Done.'); + return 0; } } diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index d3989cc0..1e84ab12 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -37,7 +37,7 @@ class Rollback extends RollbackCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new RollingBackDatabase($tenant)); diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 9bb04716..5ecc7c77 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -19,30 +19,32 @@ class Run extends Command protected $signature = 'tenants:run {commandname : The artisan command.} {--tenants=* : The tenant(s) to run the command for. Default: all}'; - public function handle(): void + public function handle(): int { $argvInput = $this->argvInput(); tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $this->getLaravel() ->make(Kernel::class) ->handle($argvInput, new ConsoleOutput); }); + + return 0; } protected function argvInput(): ArgvInput { - /** @var string $commandname */ - $commandname = $this->argument('commandname'); + /** @var string $commandName */ + $commandName = $this->argument('commandname'); // Convert string command to array - $subcommand = explode(' ', $commandname); + $subCommand = explode(' ', $commandName); // Add "artisan" as first parameter because ArgvInput expects "artisan" as first parameter and later removes it - array_unshift($subcommand, 'artisan'); + array_unshift($subCommand, 'artisan'); - return new ArgvInput($subcommand); + return new ArgvInput($subCommand); } } diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 496c04e6..8ed0b6d9 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -36,7 +36,7 @@ class Seed extends SeedCommand } tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant->getTenantKey()}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); event(new SeedingDatabase($tenant)); diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 9c8698c6..6edae6b0 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; -use Illuminate\Console\Command; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\DumpCommand; @@ -22,13 +21,6 @@ class TenantDump extends DumpCommand } public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int - { - $this->tenant()->run(fn () => parent::handle($connections, $dispatcher)); - - return Command::SUCCESS; - } - - public function tenant(): Tenant { $tenant = $this->option('tenant') ?? tenant() @@ -39,9 +31,15 @@ class TenantDump extends DumpCommand $tenant = tenancy()->find($tenant); } - throw_if(! $tenant, 'Could not identify the tenant to use for dumping the schema.'); + if ($tenant === null) { + $this->components->error('Could not find tenant to use for dumping the schema.'); - return $tenant; + return 1; + } + + parent::handle($connections, $dispatcher); + + return 0; } protected function getOptions(): array diff --git a/src/Commands/TenantList.php b/src/Commands/TenantList.php index 9fd3f8bd..c008ba59 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Tenant; @@ -14,19 +15,35 @@ class TenantList extends Command protected $description = 'List tenants.'; - public function handle(): void + public function handle(): int { - $this->info('Listing all tenants.'); - $tenants = tenancy()->query()->cursor(); + $this->components->info("Listing {$tenants->count()} tenants."); + foreach ($tenants as $tenant) { /** @var Model&Tenant $tenant */ - if ($tenant->domains) { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); - } else { - $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); - } + $this->components->twoColumnDetail($this->tenantCLI($tenant), $this->domainsCLI($tenant->domains)); } + + $this->newLine(); + + return 0; + } + + /** Generate the visual CLI output for the tenant name. */ + protected function tenantCLI(Model&Tenant $tenant): string + { + return "{$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"; + } + + /** Generate the visual CLI output for the domain names. */ + protected function domainsCLI(?Collection $domains): ?string + { + if (! $domains) { + return null; + } + + return "{$domains->pluck('domain')->implode(' / ')}"; } } diff --git a/src/Commands/Up.php b/src/Commands/Up.php index a3f690c2..08c935c3 100644 --- a/src/Commands/Up.php +++ b/src/Commands/Up.php @@ -15,13 +15,15 @@ class Up extends Command protected $description = 'Put tenants out of maintenance mode.'; - public function handle(): void + public function handle(): int { tenancy()->runForMultiple($this->getTenants(), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->components->info("Tenant: {$tenant->getTenantKey()}"); $tenant->bringUpFromMaintenance(); }); - $this->comment('Tenants are now out of maintenance mode.'); + $this->components->info('Tenants are now out of maintenance mode.'); + + return 0; } } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index bac5005d..793dca30 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -171,7 +171,9 @@ test('install command works', function () { mkdir($dir, 0777, true); } - pest()->artisan('tenancy:install'); + pest()->artisan('tenancy:install') + ->expectsConfirmation('Would you like to show your support by starring the project on GitHub?', 'no') + ->assertExitCode(0); expect(base_path('routes/tenant.php'))->toBeFile(); expect(base_path('config/tenancy.php'))->toBeFile(); expect(app_path('Providers/TenancyServiceProvider.php'))->toBeFile(); @@ -214,7 +216,8 @@ test('run command with array of tenants works', function () { test('link command works', function() { $tenantId1 = Tenant::create()->getTenantKey(); $tenantId2 = Tenant::create()->getTenantKey(); - pest()->artisan('tenants:link'); + pest()->artisan('tenants:link') + ->assertExitCode(0); $this->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public")); $this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1"))); @@ -224,7 +227,7 @@ test('link command works', function() { pest()->artisan('tenants:link', [ '--remove' => true, - ]); + ])->assertExitCode(0); $this->assertDirectoryDoesNotExist(public_path("public-$tenantId1")); $this->assertDirectoryDoesNotExist(public_path("public-$tenantId2")); @@ -256,8 +259,9 @@ test('run command works when sub command asks questions and accepts arguments', pest()->artisan("tenants:run --tenants=$id 'user:addwithname Abrar' ") ->expectsQuestion('What is your email?', 'email@localhost') - ->expectsOutput("Tenant: $id") - ->expectsOutput("User created: Abrar(email@localhost)"); + ->expectsOutputToContain("Tenant: $id.") + ->expectsOutput("User created: Abrar(email@localhost)") + ->assertExitCode(0); // Assert we are in central context expect(tenancy()->initialized)->toBeFalse(); diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index e31fe026..86366e2f 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -59,12 +59,18 @@ test('tenants can be put into maintenance mode using artisan commands', function pest()->get('http://acme.localhost/foo')->assertStatus(200); + pest()->artisan('tenants:down') + ->expectsOutputToContain('Tenants are now in maintenance mode.') + ->assertExitCode(0); + Artisan::call('tenants:down'); tenancy()->end(); // End tenancy before making a request pest()->get('http://acme.localhost/foo')->assertStatus(503); - Artisan::call('tenants:up'); + pest()->artisan('tenants:up') + ->expectsOutputToContain('Tenants are now out of maintenance mode.') + ->assertExitCode(0); tenancy()->end(); // End tenancy before making a request pest()->get('http://acme.localhost/foo')->assertStatus(200);