1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 19:54:04 +00:00

Merge branch 'master' into 515-complete

This commit is contained in:
Samuel Štancl 2022-10-25 12:54:21 +02:00
commit b3902bcf29
34 changed files with 453 additions and 144 deletions

View file

@ -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 [

View file

@ -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.');
}
}

View file

@ -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.');
}
}

View file

@ -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));

View file

@ -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;
}
}

View 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();
}
}

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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));

View file

@ -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

View file

@ -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(' / ')}</>";
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Stancl\Tenancy\Database\Contracts;
use Illuminate\Database\Connection;
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
/**
* Tenant database manager with a persistent connection.
*/
interface StatefulTenantDatabaseManager extends TenantDatabaseManager
{
/** Get the DB connection used by the tenant database manager. */
public function database(): Connection; // todo rename to connection()
/**
* Set the DB connection that should be used by the tenant database manager.
*
* @throws NoConnectionSetException
*/
public function setConnection(string $connection): void;
}

View file

@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Contracts;
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
interface TenantDatabaseManager
{
/** Create a database. */
@ -19,11 +17,4 @@ interface TenantDatabaseManager
/** Construct a DB connection config array. */
public function makeConnectionConfig(array $baseConfig, string $databaseName): array;
/**
* Set the DB connection that should be used by the tenant database manager.
*
* @throws NoConnectionSetException
*/
public function setConnection(string $connection): void;
}

View file

@ -195,7 +195,9 @@ class DatabaseConfig
/** @var Contracts\TenantDatabaseManager $databaseManager */
$databaseManager = app($databaseManagers[$driver]);
$databaseManager->setConnection($this->getTemplateConnectionName());
if ($databaseManager instanceof Contracts\StatefulTenantDatabaseManager) {
$databaseManager->setConnection($this->getTemplateConnectionName());
}
return $databaseManager;
}

View file

@ -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);

View file

@ -6,15 +6,15 @@ namespace Stancl\Tenancy\Database\TenantDatabaseManagers;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager as Contract;
use Stancl\Tenancy\Database\Contracts\StatefulTenantDatabaseManager;
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
abstract class TenantDatabaseManager implements Contract // todo better naming?
abstract class TenantDatabaseManager implements StatefulTenantDatabaseManager
{
/** The database connection to the server. */
protected string $connection;
protected function database(): Connection
public function database(): Connection
{
if (! isset($this->connection)) {
throw new NoConnectionSetException(static::class);

View file

@ -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;

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class TenantMaintenanceModeDisabled extends Contracts\TenantEvent
{
//
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class TenantMaintenanceModeEnabled extends Contracts\TenantEvent
{
//
}

View file

@ -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;
}
}

View file

@ -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');