mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 12:24:04 +00:00
Merge branch 'master' of https://github.com/archtechx/tenancy into add-skip-failing-options-to-migrate
This commit is contained in:
commit
b69c523f08
27 changed files with 408 additions and 116 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -102,3 +102,4 @@ jobs:
|
|||
author_name: "PHP CS Fixer"
|
||||
author_email: "phpcsfixer@example.com"
|
||||
message: Fix code style (php-cs-fixer)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
return $event->tenant;
|
||||
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
||||
],
|
||||
Events\TenantMaintenanceModeEnabled::class => [],
|
||||
Events\TenantMaintenanceModeDisabled::class => [],
|
||||
|
||||
// Domain events
|
||||
Events\CreatingDomain::class => [],
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -128,6 +126,9 @@ return [
|
|||
*/
|
||||
// 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database
|
||||
],
|
||||
|
||||
// todo docblock
|
||||
'drop_tenant_databases_on_migrate_fresh' => false,
|
||||
],
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
"doctrine/dbal": "^2.10",
|
||||
"spatie/valuestore": "^1.2.5",
|
||||
"pestphp/pest": "^1.21",
|
||||
"nunomaduro/larastan": "^1.0"
|
||||
"nunomaduro/larastan": "^1.0",
|
||||
"spatie/invade": "^1.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
includes:
|
||||
- ./vendor/nunomaduro/larastan/extension.neon
|
||||
- ./vendor/spatie/invade/phpstan-extension.neon
|
||||
|
||||
parameters:
|
||||
paths:
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use Illuminate\Console\Command;
|
|||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
final class MigrateFresh extends Command
|
||||
class MigrateFresh extends Command
|
||||
{
|
||||
use HasATenantsOption;
|
||||
|
||||
|
|
@ -23,23 +23,27 @@ final 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([
|
||||
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||
|
||||
$this->components->task('Dropping tables', function () {
|
||||
$this->callSilently('db:wipe', array_filter([
|
||||
'--database' => 'tenant',
|
||||
'--drop-views' => $this->option('drop-views'),
|
||||
'--force' => true,
|
||||
]));
|
||||
});
|
||||
|
||||
$this->info('Migrating.');
|
||||
$this->components->task('Migrating', function () use ($tenant) {
|
||||
$this->callSilent('tenants:migrate', [
|
||||
'--tenants' => [$tenant->getTenantKey()],
|
||||
'--force' => true,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
$this->info('Done.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
src/Commands/MigrateFreshOverride.php
Normal file
19
src/Commands/MigrateFreshOverride.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||
|
||||
class MigrateFreshOverride extends FreshCommand
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
if (config('tenancy.database.drop_tenant_databases_on_migrate_fresh')) {
|
||||
tenancy()->model()::cursor()->each->delete();
|
||||
}
|
||||
|
||||
return parent::handle();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "<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(' / ')}</>";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Stancl\Tenancy\Events\TenantMaintenanceModeDisabled;
|
||||
use Stancl\Tenancy\Events\TenantMaintenanceModeEnabled;
|
||||
|
||||
/**
|
||||
* @mixin \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
|
|
@ -21,10 +24,14 @@ trait MaintenanceMode
|
|||
'status' => $data['status'] ?? 503,
|
||||
],
|
||||
]);
|
||||
|
||||
event(new TenantMaintenanceModeEnabled($this));
|
||||
}
|
||||
|
||||
public function bringUpFromMaintenance(): void
|
||||
{
|
||||
$this->update(['maintenance_mode' => null]);
|
||||
|
||||
event(new TenantMaintenanceModeDisabled($this));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts;
|
|||
use Stancl\Tenancy\Database\Concerns;
|
||||
use Stancl\Tenancy\Database\TenantCollection;
|
||||
use Stancl\Tenancy\Events;
|
||||
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
||||
|
||||
/**
|
||||
* @property string|int $id
|
||||
|
|
@ -45,6 +46,17 @@ class Tenant extends Model implements Contracts\Tenant
|
|||
return $this->getAttribute($this->getTenantKeyName());
|
||||
}
|
||||
|
||||
public static function current(): static|null
|
||||
{
|
||||
return tenant();
|
||||
}
|
||||
|
||||
/** @throws TenancyNotInitializedException */
|
||||
public static function currentOrFail(): static
|
||||
{
|
||||
return static::current() ?? throw new TenancyNotInitializedException;
|
||||
}
|
||||
|
||||
public function newCollection(array $models = []): TenantCollection
|
||||
{
|
||||
return new TenantCollection($models);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Events\Contracts;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
abstract class TenantEvent
|
||||
abstract class TenantEvent // todo we could add a feature to JobPipeline that automatically gets data for the send() from here
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
|
|
|
|||
10
src/Events/TenantMaintenanceModeDisabled.php
Normal file
10
src/Events/TenantMaintenanceModeDisabled.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class TenantMaintenanceModeDisabled extends Contracts\TenantEvent
|
||||
{
|
||||
//
|
||||
}
|
||||
10
src/Events/TenantMaintenanceModeEnabled.php
Normal file
10
src/Events/TenantMaintenanceModeEnabled.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
class TenantMaintenanceModeEnabled extends Contracts\TenantEvent
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ use Stancl\Tenancy\Tenancy;
|
|||
class InitializeTenancyByRequestData extends IdentificationMiddleware
|
||||
{
|
||||
public static string $header = 'X-Tenant';
|
||||
public static string $cookie = 'X-Tenant';
|
||||
public static string $queryParameter = 'tenant';
|
||||
public static ?Closure $onFail = null;
|
||||
|
||||
|
|
@ -33,13 +34,18 @@ class InitializeTenancyByRequestData extends IdentificationMiddleware
|
|||
|
||||
protected function getPayload(Request $request): ?string
|
||||
{
|
||||
$tenant = null;
|
||||
if (static::$header && $request->hasHeader(static::$header)) {
|
||||
$tenant = $request->header(static::$header);
|
||||
} elseif (static::$queryParameter && $request->has(static::$queryParameter)) {
|
||||
$tenant = $request->get(static::$queryParameter);
|
||||
return $request->header(static::$header);
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
if (static::$queryParameter && $request->has(static::$queryParameter)) {
|
||||
return $request->get(static::$queryParameter);
|
||||
}
|
||||
|
||||
if (static::$cookie && $request->hasCookie(static::$cookie)) {
|
||||
return $request->cookie(static::$cookie);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy;
|
||||
|
||||
use Illuminate\Cache\CacheManager;
|
||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
|
|
@ -90,6 +91,10 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
Commands\Up::class,
|
||||
]);
|
||||
|
||||
$this->app->extend(FreshCommand::class, function () {
|
||||
return new Commands\MigrateFreshOverride;
|
||||
});
|
||||
|
||||
$this->publishes([
|
||||
__DIR__ . '/../assets/config.php' => config_path('tenancy.php'),
|
||||
], 'config');
|
||||
|
|
|
|||
|
|
@ -8,12 +8,16 @@ use Stancl\JobPipeline\JobPipeline;
|
|||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Jobs\DeleteDomains;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Jobs\DeleteDatabase;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\TenantDeleted;
|
||||
use Stancl\Tenancy\Tests\Etc\TestSeeder;
|
||||
use Stancl\Tenancy\Events\DeletingTenant;
|
||||
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
|
|
@ -181,7 +185,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();
|
||||
|
|
@ -224,7 +230,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")));
|
||||
|
|
@ -234,7 +241,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"));
|
||||
|
|
@ -266,8 +273,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();
|
||||
|
|
@ -281,6 +289,47 @@ test('run command works when sub command asks questions and accepts arguments',
|
|||
expect($user->email)->toBe('email@localhost');
|
||||
});
|
||||
|
||||
test('migrate fresh command only deletes tenant databases if drop_tenant_databases_on_migrate_fresh is true', function (bool $dropTenantDBsOnMigrateFresh) {
|
||||
Event::listen(DeletingTenant::class,
|
||||
JobPipeline::make([DeleteDomains::class])->send(function (DeletingTenant $event) {
|
||||
return $event->tenant;
|
||||
})->shouldBeQueued(false)->toListener()
|
||||
);
|
||||
|
||||
Event::listen(
|
||||
TenantDeleted::class,
|
||||
JobPipeline::make([DeleteDatabase::class])->send(function (TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->shouldBeQueued(false)->toListener()
|
||||
);
|
||||
|
||||
config(['tenancy.database.drop_tenant_databases_on_migrate_fresh' => $dropTenantDBsOnMigrateFresh]);
|
||||
$shouldHaveDBAfterMigrateFresh = ! $dropTenantDBsOnMigrateFresh;
|
||||
|
||||
/** @var Tenant[] $tenants */
|
||||
$tenants = [
|
||||
Tenant::create(),
|
||||
Tenant::create(),
|
||||
Tenant::create(),
|
||||
];
|
||||
|
||||
$tenantHasDatabase = fn (Tenant $tenant) => $tenant->database()->manager()->databaseExists($tenant->database()->getName());
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
expect($tenantHasDatabase($tenant))->toBeTrue();
|
||||
}
|
||||
|
||||
pest()->artisan('migrate:fresh', [
|
||||
'--force' => true,
|
||||
'--path' => __DIR__ . '/../assets/migrations',
|
||||
'--realpath' => true,
|
||||
]);
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
expect($tenantHasDatabase($tenant))->toBe($shouldHaveDBAfterMigrateFresh);
|
||||
}
|
||||
})->with([true, false]);
|
||||
|
||||
// todo@tests
|
||||
function runCommandWorks(): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
|
||||
|
|
@ -32,6 +33,20 @@ test('tenants can be in maintenance mode', function () {
|
|||
pest()->get('http://acme.localhost/foo')->assertStatus(200);
|
||||
});
|
||||
|
||||
test('maintenance mode events are fired', function () {
|
||||
$tenant = MaintenanceTenant::create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$tenant->putDownForMaintenance();
|
||||
|
||||
Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeEnabled::class);
|
||||
|
||||
$tenant->bringUpFromMaintenance();
|
||||
|
||||
Event::assertDispatched(\Stancl\Tenancy\Events\TenantMaintenanceModeDisabled::class);
|
||||
});
|
||||
|
||||
test('tenants can be put into maintenance mode using artisan commands', function() {
|
||||
Route::get('/foo', function () {
|
||||
return 'bar';
|
||||
|
|
@ -44,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);
|
||||
|
|
|
|||
|
|
@ -20,19 +20,18 @@ beforeEach(function () {
|
|||
|
||||
afterEach(function () {
|
||||
InitializeTenancyByRequestData::$header = 'X-Tenant';
|
||||
InitializeTenancyByRequestData::$cookie = 'X-Tenant';
|
||||
InitializeTenancyByRequestData::$queryParameter = 'tenant';
|
||||
});
|
||||
|
||||
test('header identification works', function () {
|
||||
InitializeTenancyByRequestData::$header = 'X-Tenant';
|
||||
$tenant = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
$this
|
||||
->withoutExceptionHandling()
|
||||
->get('test', [
|
||||
'X-Tenant' => $tenant->id,
|
||||
])
|
||||
->withHeader('X-Tenant', $tenant->id)
|
||||
->get('test')
|
||||
->assertSee($tenant->id);
|
||||
});
|
||||
|
||||
|
|
@ -40,10 +39,20 @@ test('query parameter identification works', function () {
|
|||
InitializeTenancyByRequestData::$queryParameter = 'tenant';
|
||||
|
||||
$tenant = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
$this
|
||||
->withoutExceptionHandling()
|
||||
->get('test?tenant=' . $tenant->id)
|
||||
->assertSee($tenant->id);
|
||||
});
|
||||
|
||||
test('cookie identification works', function () {
|
||||
InitializeTenancyByRequestData::$cookie = 'X-Tenant';
|
||||
$tenant = Tenant::create();
|
||||
|
||||
$this
|
||||
->withoutExceptionHandling()
|
||||
->withUnencryptedCookie('X-Tenant', $tenant->id)
|
||||
->get('test',)
|
||||
->assertSee($tenant->id);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
|
|||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\UUIDGenerator;
|
||||
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
||||
|
||||
test('created event is dispatched', function () {
|
||||
Event::fake([TenantCreated::class]);
|
||||
|
|
@ -141,6 +142,31 @@ test('a command can be run on a collection of tenants', function () {
|
|||
expect(Tenant::find('t2')->foo)->toBe('xyz');
|
||||
});
|
||||
|
||||
test('the current method returns the currently initialized tenant', function() {
|
||||
tenancy()->initialize($tenant = Tenant::create());
|
||||
|
||||
expect(Tenant::current())->toBe($tenant);
|
||||
});
|
||||
|
||||
test('the current method returns null if there is no currently initialized tenant', function() {
|
||||
tenancy()->end();
|
||||
|
||||
expect(Tenant::current())->toBeNull();
|
||||
});
|
||||
|
||||
test('currentOrFail method returns the currently initialized tenant', function() {
|
||||
tenancy()->initialize($tenant = Tenant::create());
|
||||
|
||||
expect(Tenant::currentOrFail())->toBe($tenant);
|
||||
});
|
||||
|
||||
test('currentOrFail method throws an exception if there is no currently initialized tenant', function() {
|
||||
tenancy()->end();
|
||||
|
||||
expect(fn() => Tenant::currentOrFail())->toThrow(TenancyNotInitializedException::class);
|
||||
});
|
||||
|
||||
|
||||
class MyTenant extends Tenant
|
||||
{
|
||||
protected $table = 'tenants';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue