1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 17:04:04 +00:00

[4.x] Update commands CLI outputs (#968)

* Using laravel components

* Ensure commands returns success

* update tests

* clean

* bump EndBug version

* Update ci.yml

* Update ci.yml

* Update ci.yml

* revert CI changes

* Update ci.yml

* Update ci.yml

* Update ci.yml

* revert CI changes to it's original state

* fix typos, improve code

* improve Install & TenantList commands

* php-cs-fixer

* type GitHub properly

Co-authored-by: Abrar Ahmad <abrar.dev99@gmail.com>
Co-authored-by: Samuel Štancl <samuel@archte.ch>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
Jori Stein 2022-10-18 13:11:57 -04:00 committed by GitHub
parent 05f1b2d6f5
commit e4f5b92485
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 217 additions and 96 deletions

View file

@ -102,3 +102,4 @@ jobs:
author_name: "PHP CS Fixer" author_name: "PHP CS Fixer"
author_email: "phpcsfixer@example.com" author_email: "phpcsfixer@example.com"
message: Fix code style (php-cs-fixer) message: Fix code style (php-cs-fixer)

View file

@ -2,16 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
use Stancl\Tenancy\Database\Models\Domain;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Middleware; use Stancl\Tenancy\Middleware;
use Stancl\Tenancy\Resolvers; use Stancl\Tenancy\Resolvers;
return [ return [
'tenant_model' => Tenant::class, 'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class,
'id_generator' => Stancl\Tenancy\UUIDGenerator::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. * The list of domains hosting your central app.

View file

@ -22,24 +22,23 @@ class Down extends DownCommand
public function handle(): int 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(); $payload = $this->getDownDatabasePayload();
// This runs for all tenants if no --tenants are specified
tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($payload) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($payload) {
$this->line("Tenant: {$tenant['id']}"); $this->components->info("Tenant: {$tenant->getTenantKey()}");
$tenant->putDownForMaintenance($payload); $tenant->putDownForMaintenance($payload);
}); });
$this->comment('Tenants are now in maintenance mode.'); $this->components->info('Tenants are now in maintenance mode.');
return 0; 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 protected function getDownDatabasePayload(): array
{ {
return [ return [

View file

@ -4,50 +4,136 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Commands; namespace Stancl\Tenancy\Commands;
use Closure;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class Install extends Command class Install extends Command
{ {
protected $signature = 'tenancy:install'; 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->step(
$this->callSilent('vendor:publish', [ name: 'Publishing config file',
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider', tag: 'config',
'--tag' => 'config', file: 'config/tenancy.php',
]); newLineBefore: true,
$this->info('✔️ Created config/tenancy.php'); );
if (! file_exists(base_path('routes/tenant.php'))) { $this->step(
$this->callSilent('vendor:publish', [ 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', '--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 { } else {
$this->info('Found routes/tenant.php.'); $this->components->warn($warning);
} }
}
$this->callSilent('vendor:publish', [ /** If the user accepts, opens the GitHub project in the browser. */
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider', public function askForSupport(): void
'--tag' => 'providers', {
]); if ($this->components->confirm('Would you like to show your support by starring the project on GitHub?', true)) {
$this->info('✔️ Created TenancyServiceProvider.php'); if (PHP_OS_FAMILY === 'Darwin') {
exec('open https://github.com/archtechx/tenancy');
$this->callSilent('vendor:publish', [ }
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider', if (PHP_OS_FAMILY === 'Windows') {
'--tag' => 'migrations', exec('start https://github.com/archtechx/tenancy');
]); }
$this->info('✔️ Created migrations. Remember to run [php artisan migrate]!'); if (PHP_OS_FAMILY === 'Linux') {
exec('xdg-open https://github.com/archtechx/tenancy');
if (! is_dir(database_path('migrations/tenant'))) { }
mkdir(database_path('migrations/tenant'));
$this->info('✔️ Created database/migrations/tenant folder.');
} }
$this->comment('✨️ stancl/tenancy installed successfully.');
} }
} }

View file

@ -23,7 +23,7 @@ class Link extends Command
protected $description = 'Create or remove tenant symbolic links.'; protected $description = 'Create or remove tenant symbolic links.';
public function handle(): void public function handle(): int
{ {
$tenants = $this->getTenants(); $tenants = $this->getTenants();
@ -35,14 +35,18 @@ class Link extends Command
} }
} catch (Exception $exception) { } catch (Exception $exception) {
$this->error($exception->getMessage()); $this->error($exception->getMessage());
return 1;
} }
return 0;
} }
protected function removeLinks(LazyCollection $tenants): void protected function removeLinks(LazyCollection $tenants): void
{ {
RemoveStorageSymlinksAction::handle($tenants); RemoveStorageSymlinksAction::handle($tenants);
$this->info('The links have been removed.'); $this->components->info('The links have been removed.');
} }
protected function createLinks(LazyCollection $tenants): void protected function createLinks(LazyCollection $tenants): void
@ -53,6 +57,6 @@ class Link extends Command
(bool) ($this->option('force') ?? false), (bool) ($this->option('force') ?? false),
); );
$this->info('The links have been created.'); $this->components->info('The links have been created.');
} }
} }

View file

@ -43,7 +43,7 @@ class Migrate extends MigrateCommand
} }
tenancy()->runForMultiple($this->getTenants(), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->components->info("Tenant: {$tenant->getTenantKey()}");
event(new MigratingDatabase($tenant)); event(new MigratingDatabase($tenant));

View file

@ -23,23 +23,27 @@ class MigrateFresh extends Command
$this->setName('tenants:migrate-fresh'); $this->setName('tenants:migrate-fresh');
} }
public function handle(): void public function handle(): int
{ {
tenancy()->runForMultiple($this->getTenants(), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->info('Dropping tables.'); $this->components->info("Tenant: {$tenant->getTenantKey()}");
$this->call('db:wipe', array_filter([
'--database' => 'tenant',
'--drop-views' => $this->option('drop-views'),
'--force' => true,
]));
$this->info('Migrating.'); $this->components->task('Dropping tables', function () {
$this->callSilent('tenants:migrate', [ $this->callSilently('db:wipe', array_filter([
'--tenants' => [$tenant->getTenantKey()], '--database' => 'tenant',
'--force' => true, '--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;
} }
} }

View file

@ -37,7 +37,7 @@ class Rollback extends RollbackCommand
} }
tenancy()->runForMultiple($this->getTenants(), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->components->info("Tenant: {$tenant->getTenantKey()}");
event(new RollingBackDatabase($tenant)); event(new RollingBackDatabase($tenant));

View file

@ -19,30 +19,32 @@ class Run extends Command
protected $signature = 'tenants:run {commandname : The artisan command.} protected $signature = 'tenants:run {commandname : The artisan command.}
{--tenants=* : The tenant(s) to run the command for. Default: all}'; {--tenants=* : The tenant(s) to run the command for. Default: all}';
public function handle(): void public function handle(): int
{ {
$argvInput = $this->argvInput(); $argvInput = $this->argvInput();
tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) use ($argvInput) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->components->info("Tenant: {$tenant->getTenantKey()}");
$this->getLaravel() $this->getLaravel()
->make(Kernel::class) ->make(Kernel::class)
->handle($argvInput, new ConsoleOutput); ->handle($argvInput, new ConsoleOutput);
}); });
return 0;
} }
protected function argvInput(): ArgvInput protected function argvInput(): ArgvInput
{ {
/** @var string $commandname */ /** @var string $commandName */
$commandname = $this->argument('commandname'); $commandName = $this->argument('commandname');
// Convert string command to array // 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 // 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);
} }
} }

View file

@ -36,7 +36,7 @@ class Seed extends SeedCommand
} }
tenancy()->runForMultiple($this->getTenants(), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant->getTenantKey()}"); $this->components->info("Tenant: {$tenant->getTenantKey()}");
event(new SeedingDatabase($tenant)); event(new SeedingDatabase($tenant));

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Commands; namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Console\DumpCommand; use Illuminate\Database\Console\DumpCommand;
@ -22,13 +21,6 @@ class TenantDump extends DumpCommand
} }
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int 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 = $this->option('tenant')
?? tenant() ?? tenant()
@ -39,9 +31,15 @@ class TenantDump extends DumpCommand
$tenant = tenancy()->find($tenant); $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 protected function getOptions(): array

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Commands; namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
@ -14,19 +15,35 @@ class TenantList extends Command
protected $description = 'List tenants.'; protected $description = 'List tenants.';
public function handle(): void public function handle(): int
{ {
$this->info('Listing all tenants.');
$tenants = tenancy()->query()->cursor(); $tenants = tenancy()->query()->cursor();
$this->components->info("Listing {$tenants->count()} tenants.");
foreach ($tenants as $tenant) { foreach ($tenants as $tenant) {
/** @var Model&Tenant $tenant */ /** @var Model&Tenant $tenant */
if ($tenant->domains) { $this->components->twoColumnDetail($this->tenantCLI($tenant), $this->domainsCLI($tenant->domains));
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? []));
} else {
$this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}");
}
} }
$this->newLine();
return 0;
}
/** Generate the visual CLI output for the tenant name. */
protected function tenantCLI(Model&Tenant $tenant): string
{
return "<fg=yellow>{$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}</>";
}
/** Generate the visual CLI output for the domain names. */
protected function domainsCLI(?Collection $domains): ?string
{
if (! $domains) {
return null;
}
return "<fg=blue;options=bold>{$domains->pluck('domain')->implode(' / ')}</>";
} }
} }

View file

@ -15,13 +15,15 @@ class Up extends Command
protected $description = 'Put tenants out of maintenance mode.'; protected $description = 'Put tenants out of maintenance mode.';
public function handle(): void public function handle(): int
{ {
tenancy()->runForMultiple($this->getTenants(), function ($tenant) { tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant['id']}"); $this->components->info("Tenant: {$tenant->getTenantKey()}");
$tenant->bringUpFromMaintenance(); $tenant->bringUpFromMaintenance();
}); });
$this->comment('Tenants are now out of maintenance mode.'); $this->components->info('Tenants are now out of maintenance mode.');
return 0;
} }
} }

View file

@ -171,7 +171,9 @@ test('install command works', function () {
mkdir($dir, 0777, true); 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('routes/tenant.php'))->toBeFile();
expect(base_path('config/tenancy.php'))->toBeFile(); expect(base_path('config/tenancy.php'))->toBeFile();
expect(app_path('Providers/TenancyServiceProvider.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() { test('link command works', function() {
$tenantId1 = Tenant::create()->getTenantKey(); $tenantId1 = Tenant::create()->getTenantKey();
$tenantId2 = 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->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public"));
$this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1"))); $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', [ pest()->artisan('tenants:link', [
'--remove' => true, '--remove' => true,
]); ])->assertExitCode(0);
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId1")); $this->assertDirectoryDoesNotExist(public_path("public-$tenantId1"));
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId2")); $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' ") pest()->artisan("tenants:run --tenants=$id 'user:addwithname Abrar' ")
->expectsQuestion('What is your email?', 'email@localhost') ->expectsQuestion('What is your email?', 'email@localhost')
->expectsOutput("Tenant: $id") ->expectsOutputToContain("Tenant: $id.")
->expectsOutput("User created: Abrar(email@localhost)"); ->expectsOutput("User created: Abrar(email@localhost)")
->assertExitCode(0);
// Assert we are in central context // Assert we are in central context
expect(tenancy()->initialized)->toBeFalse(); expect(tenancy()->initialized)->toBeFalse();

View file

@ -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()->get('http://acme.localhost/foo')->assertStatus(200);
pest()->artisan('tenants:down')
->expectsOutputToContain('Tenants are now in maintenance mode.')
->assertExitCode(0);
Artisan::call('tenants:down'); Artisan::call('tenants:down');
tenancy()->end(); // End tenancy before making a request tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(503); 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 tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(200); pest()->get('http://acme.localhost/foo')->assertStatus(200);