mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 14:54:03 +00:00
Merge branch 'master' into fix-url-bootstrappers
This commit is contained in:
commit
58cdae908a
44 changed files with 584 additions and 260 deletions
|
|
@ -27,6 +27,7 @@ class JobBatchBootstrapper implements TenancyBootstrapper
|
|||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
// todo@revisit
|
||||
// Update batch repository connection to use the tenant connection
|
||||
$this->previousConnection = $this->batchRepository->getConnection();
|
||||
$this->batchRepository->setConnection($this->databaseManager->connection('tenant'));
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class CreateUserWithRLSPolicies extends Command
|
|||
$manager->setConnection($tenantModel->database()->getTenantHostConnectionName());
|
||||
|
||||
// Set the database name (= central schema name/search_path in this case), username, and password
|
||||
$tenantModel->setInternal('db_name', $manager->database()->getConfig('search_path'));
|
||||
$tenantModel->setInternal('db_name', $manager->connection()->getConfig('search_path'));
|
||||
$tenantModel->setInternal('db_username', $username);
|
||||
$tenantModel->setInternal('db_password', $password);
|
||||
|
||||
|
|
@ -142,9 +142,9 @@ class CreateUserWithRLSPolicies extends Command
|
|||
|
||||
$this->components->bulletList($createdPolicies);
|
||||
|
||||
$this->components->info('RLS policies updated successfully.');
|
||||
$this->components->success('RLS policies updated successfully.');
|
||||
} else {
|
||||
$this->components->info('All RLS policies are up to date.');
|
||||
$this->components->success('All RLS policies are up to date.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ class Install extends Command
|
|||
newLineAfter: true,
|
||||
);
|
||||
|
||||
$this->components->info('✨️ Tenancy for Laravel successfully installed.');
|
||||
$this->components->success('✨️ Tenancy for Laravel successfully installed.');
|
||||
|
||||
$this->askForSupport();
|
||||
if (! $this->option('no-interaction')) {
|
||||
$this->askForSupport();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use Stancl\Tenancy\Concerns\ParallelCommand;
|
|||
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||
use Stancl\Tenancy\Events\MigratingDatabase;
|
||||
use Symfony\Component\Console\Output\OutputInterface as OI;
|
||||
|
||||
class Migrate extends MigrateCommand
|
||||
{
|
||||
|
|
@ -52,7 +53,7 @@ class Migrate extends MigrateCommand
|
|||
|
||||
if ($this->getProcesses() > 1) {
|
||||
return $this->runConcurrently($this->getTenantChunks()->map(function ($chunk) {
|
||||
return $this->getTenants($chunk->all());
|
||||
return $this->getTenants($chunk);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -80,9 +81,25 @@ class Migrate extends MigrateCommand
|
|||
$tenant->run(function ($tenant) use (&$success) {
|
||||
event(new MigratingDatabase($tenant));
|
||||
|
||||
// Migrate
|
||||
if (parent::handle() !== 0) {
|
||||
$success = false;
|
||||
$verbosity = (int) $this->output->getVerbosity();
|
||||
|
||||
if ($this->runningConcurrently) {
|
||||
// The output gets messy when multiple processes are writing to the same stdout
|
||||
$this->output->setVerbosity(OI::VERBOSITY_QUIET);
|
||||
}
|
||||
|
||||
try {
|
||||
// Migrate
|
||||
if (parent::handle() !== 0) {
|
||||
$success = false;
|
||||
}
|
||||
} finally {
|
||||
$this->output->setVerbosity($verbosity);
|
||||
}
|
||||
|
||||
if ($this->runningConcurrently) {
|
||||
// todo@cli the Migrating info above always has extra spaces, and the success below does WHEN there is work that got done by the block above. same in Rollback
|
||||
$this->components->success("Migrated tenant {$tenant->getTenantKey()}");
|
||||
}
|
||||
|
||||
event(new DatabaseMigrated($tenant));
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class MigrateFresh extends BaseCommand
|
|||
|
||||
if ($this->getProcesses() > 1) {
|
||||
return $this->runConcurrently($this->getTenantChunks()->map(function ($chunk) {
|
||||
return $this->getTenants($chunk->all());
|
||||
return $this->getTenants($chunk);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +81,8 @@ class MigrateFresh extends BaseCommand
|
|||
}
|
||||
|
||||
/**
|
||||
* Only used when running concurrently.
|
||||
*
|
||||
* @param LazyCollection<covariant int|string, \Stancl\Tenancy\Contracts\Tenant&\Illuminate\Database\Eloquent\Model> $tenants
|
||||
*/
|
||||
protected function migrateFreshTenants(LazyCollection $tenants): bool
|
||||
|
|
@ -89,6 +91,8 @@ class MigrateFresh extends BaseCommand
|
|||
|
||||
foreach ($tenants as $tenant) {
|
||||
try {
|
||||
$this->components->info("Migrating (fresh) tenant {$tenant->getTenantKey()}");
|
||||
|
||||
$tenant->run(function ($tenant) use (&$success) {
|
||||
$this->components->info("Wiping database of tenant {$tenant->getTenantKey()}", OI::VERBOSITY_VERY_VERBOSE);
|
||||
if ($this->wipeDB()) {
|
||||
|
|
@ -105,6 +109,8 @@ class MigrateFresh extends BaseCommand
|
|||
$success = false;
|
||||
$this->components->error("Migrating database of tenant {$tenant->getTenantKey()} failed!");
|
||||
}
|
||||
|
||||
$this->components->success("Migrated (fresh) tenant {$tenant->getTenantKey()}");
|
||||
});
|
||||
} catch (TenantDatabaseDoesNotExistException|QueryException $e) {
|
||||
$this->components->error("Migration failed for tenant {$tenant->getTenantKey()}: {$e->getMessage()}");
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use Stancl\Tenancy\Concerns\ParallelCommand;
|
|||
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||
use Stancl\Tenancy\Events\DatabaseRolledBack;
|
||||
use Stancl\Tenancy\Events\RollingBackDatabase;
|
||||
use Symfony\Component\Console\Output\OutputInterface as OI;
|
||||
|
||||
class Rollback extends RollbackCommand
|
||||
{
|
||||
|
|
@ -42,7 +43,7 @@ class Rollback extends RollbackCommand
|
|||
|
||||
if ($this->getProcesses() > 1) {
|
||||
return $this->runConcurrently($this->getTenantChunks()->map(function ($chunk) {
|
||||
return $this->getTenants($chunk->all());
|
||||
return $this->getTenants($chunk);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -70,14 +71,29 @@ class Rollback extends RollbackCommand
|
|||
|
||||
foreach ($tenants as $tenant) {
|
||||
try {
|
||||
$this->components->info("Tenant {$tenant->getTenantKey()}");
|
||||
$this->components->info("Rolling back tenant {$tenant->getTenantKey()}");
|
||||
|
||||
$tenant->run(function ($tenant) use (&$success) {
|
||||
event(new RollingBackDatabase($tenant));
|
||||
|
||||
// Rollback
|
||||
if (parent::handle() !== 0) {
|
||||
$success = false;
|
||||
$verbosity = (int) $this->output->getVerbosity();
|
||||
|
||||
if ($this->runningConcurrently) {
|
||||
// The output gets messy when multiple processes are writing to the same stdout
|
||||
$this->output->setVerbosity(OI::VERBOSITY_QUIET);
|
||||
}
|
||||
|
||||
try {
|
||||
// Rollback
|
||||
if (parent::handle() !== 0) {
|
||||
$success = false;
|
||||
}
|
||||
} finally {
|
||||
$this->output->setVerbosity($verbosity);
|
||||
}
|
||||
|
||||
if ($this->runningConcurrently) {
|
||||
$this->components->success("Rolled back tenant {$tenant->getTenantKey()}");
|
||||
}
|
||||
|
||||
event(new DatabaseRolledBack($tenant));
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Concerns;
|
||||
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use Exception;
|
||||
use FFI;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
|
@ -12,16 +15,35 @@ use Symfony\Component\Console\Input\InputOption;
|
|||
trait ParallelCommand
|
||||
{
|
||||
public const MAX_PROCESSES = 24;
|
||||
protected bool $runningConcurrently = false;
|
||||
|
||||
abstract protected function childHandle(mixed ...$args): bool;
|
||||
|
||||
public function addProcessesOption(): void
|
||||
{
|
||||
$this->addOption('processes', 'p', InputOption::VALUE_OPTIONAL, 'How many processes to spawn. Maximum value: ' . static::MAX_PROCESSES . ', recommended value: core count', 1);
|
||||
$this->addOption(
|
||||
'processes',
|
||||
'p',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'How many processes to spawn. Maximum value: ' . static::MAX_PROCESSES . ', recommended value: core count (use just -p)',
|
||||
-1,
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
'forceProcesses',
|
||||
'P',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Same as --processes but without a maximum value. Use at your own risk',
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
protected function forkProcess(mixed ...$args): int
|
||||
{
|
||||
if (! app()->runningInConsole()) {
|
||||
throw new Exception('Parallel commands are only available in CLI context.');
|
||||
}
|
||||
|
||||
$pid = pcntl_fork();
|
||||
|
||||
if ($pid === -1) {
|
||||
|
|
@ -37,24 +59,84 @@ trait ParallelCommand
|
|||
}
|
||||
}
|
||||
|
||||
protected function sysctlGetLogicalCoreCount(bool $darwin): int
|
||||
{
|
||||
$ffi = FFI::cdef('int sysctlbyname(const char *name, void *oldp, size_t *oldlenp, void *newp, size_t newlen);');
|
||||
|
||||
$cores = $ffi->new('int');
|
||||
$size = $ffi->new('size_t');
|
||||
$size->cdata = FFI::sizeof($cores);
|
||||
|
||||
// perflevel0 refers to P-cores on M-series, and the entire CPU on Intel Macs
|
||||
if ($darwin && $ffi->sysctlbyname('hw.perflevel0.logicalcpu', FFI::addr($cores), FFI::addr($size), null, 0) === 0) {
|
||||
return $cores->cdata;
|
||||
} else if ($darwin) {
|
||||
// Reset the size in case the pointer got written to (likely shouldn't happen)
|
||||
$size->cdata = FFI::sizeof($cores);
|
||||
}
|
||||
|
||||
// This should return the total number of logical cores on any BSD-based system
|
||||
if ($ffi->sysctlbyname('hw.ncpu', FFI::addr($cores), FFI::addr($size), null, 0) !== 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $cores->cdata;
|
||||
}
|
||||
|
||||
protected function getLogicalCoreCount(): int
|
||||
{
|
||||
// We use the logical core count as it should work best for I/O bound code
|
||||
return match (PHP_OS_FAMILY) {
|
||||
'Windows' => (int) getenv('NUMBER_OF_PROCESSORS'),
|
||||
'Linux' => substr_count(
|
||||
file_get_contents('/proc/cpuinfo') ?: throw new Exception('Could not open /proc/cpuinfo for core count detection, please specify -p manually.'),
|
||||
'processor',
|
||||
),
|
||||
'Darwin', 'BSD' => $this->sysctlGetLogicalCoreCount(PHP_OS_FAMILY === 'Darwin'),
|
||||
default => throw new Exception('Core count detection not implemented for ' . PHP_OS_FAMILY . ', please specify -p manually.'),
|
||||
};
|
||||
}
|
||||
|
||||
protected function getProcesses(): int
|
||||
{
|
||||
$processes = (int) $this->input->getOption('processes');
|
||||
$processes = $this->input->getOption('forceProcesses');
|
||||
$forceProcesses = $processes !== -1;
|
||||
|
||||
if (($processes < 0) || ($processes > static::MAX_PROCESSES)) {
|
||||
$this->components->error('Maximum value for processes is ' . static::MAX_PROCESSES);
|
||||
if ($processes === -1) {
|
||||
$processes = $this->input->getOption('processes');
|
||||
}
|
||||
|
||||
if ($processes === null) {
|
||||
// This is used when the option is set but *without* a value (-p).
|
||||
$processes = $this->getLogicalCoreCount();
|
||||
} else if ((int) $processes === -1) {
|
||||
// Default value we set for the option -- this is used when the option is *not set*.
|
||||
$processes = 1;
|
||||
} else {
|
||||
// Option value set by the user.
|
||||
$processes = (int) $processes;
|
||||
}
|
||||
|
||||
if ($processes < 1) {
|
||||
$this->components->error('Minimum value for processes is 1. Try specifying -p manually.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($processes > static::MAX_PROCESSES && ! $forceProcesses) {
|
||||
$this->components->error('Maximum value for processes is ' . static::MAX_PROCESSES . ' provided value: ' . $processes);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($processes > 1 && ! function_exists('pcntl_fork')) {
|
||||
$this->components->error('The pcntl extension is required for parallel migrations to work.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return $processes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Collection<int, \Stancl\Tenancy\Contracts\Tenant&\Illuminate\Database\Eloquent\Model>>>
|
||||
* @return Collection<int, array<int, \Stancl\Tenancy\Contracts\Tenant&\Illuminate\Database\Eloquent\Model>>>
|
||||
*/
|
||||
protected function getTenantChunks(): Collection
|
||||
{
|
||||
|
|
@ -64,20 +146,26 @@ trait ParallelCommand
|
|||
return $tenants->chunk((int) ceil($tenants->count() / $this->getProcesses()))->map(function ($chunk) {
|
||||
$chunk = array_values($chunk->all());
|
||||
|
||||
/** @var Collection<int, \Stancl\Tenancy\Contracts\Tenant&\Illuminate\Database\Eloquent\Model> $chunk */
|
||||
/** @var array<int, \Stancl\Tenancy\Contracts\Tenant&\Illuminate\Database\Eloquent\Model> $chunk */
|
||||
return $chunk;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|ArrayAccess<int, mixed>|null $args
|
||||
* @param array|(ArrayAccess<int, mixed>&Countable)|null $args
|
||||
*/
|
||||
protected function runConcurrently(array|ArrayAccess|null $args = null): int
|
||||
protected function runConcurrently(array|(ArrayAccess&Countable)|null $args = null): int
|
||||
{
|
||||
$processes = $this->getProcesses();
|
||||
$success = true;
|
||||
$pids = [];
|
||||
|
||||
if ($args !== null && count($args) < $processes) {
|
||||
$processes = count($args);
|
||||
}
|
||||
|
||||
$this->runningConcurrently = true;
|
||||
|
||||
for ($i = 0; $i < $processes; $i++) {
|
||||
$pid = $this->forkProcess($args !== null ? $args[$i] : null);
|
||||
|
||||
|
|
@ -101,7 +189,7 @@ trait ParallelCommand
|
|||
$exitCode = pcntl_wexitstatus($status);
|
||||
|
||||
if ($exitCode === 0) {
|
||||
$this->components->info("Child process [$i] (PID $pid) finished successfully.");
|
||||
$this->components->success("Child process [$i] (PID $pid) finished successfully.");
|
||||
} else {
|
||||
$success = false;
|
||||
$this->components->error("Child process [$i] (PID $pid) completed with failures.");
|
||||
|
|
|
|||
|
|
@ -73,20 +73,15 @@ trait HasPending
|
|||
}
|
||||
|
||||
/** Try to pull a tenant from the pool of pending tenants. */
|
||||
public static function pullPendingFromPool(bool $firstOrCreate = false, array $attributes = []): ?Tenant
|
||||
public static function pullPendingFromPool(bool $firstOrCreate = true, array $attributes = []): ?Tenant
|
||||
{
|
||||
if (! static::onlyPending()->exists()) {
|
||||
if (! $firstOrCreate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
static::createPending($attributes);
|
||||
}
|
||||
|
||||
// A pending tenant is surely available at this point
|
||||
/** @var Model&Tenant $tenant */
|
||||
/** @var (Model&Tenant)|null $tenant */
|
||||
$tenant = static::onlyPending()->first();
|
||||
|
||||
if ($tenant === null) {
|
||||
return $firstOrCreate ? static::create($attributes) : null;
|
||||
}
|
||||
|
||||
event(new PullingPendingTenant($tenant));
|
||||
|
||||
$tenant->update(array_merge($attributes, [
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ trait ManagesPostgresUsers
|
|||
$createUser = ! $this->userExists($username);
|
||||
|
||||
if ($createUser) {
|
||||
$this->database()->statement("CREATE USER \"{$username}\" LOGIN PASSWORD '{$password}'");
|
||||
$this->connection()->statement("CREATE USER \"{$username}\" LOGIN PASSWORD '{$password}'");
|
||||
}
|
||||
|
||||
$this->grantPermissions($databaseConfig);
|
||||
|
|
@ -46,38 +46,38 @@ trait ManagesPostgresUsers
|
|||
$username = $databaseConfig->getUsername();
|
||||
|
||||
// Tenant host connection config
|
||||
$connectionName = $this->database()->getConfig('name');
|
||||
$centralDatabase = $this->database()->getConfig('database');
|
||||
$connectionName = $this->connection()->getConfig('name');
|
||||
$centralDatabase = $this->connection()->getConfig('database');
|
||||
|
||||
// Set the DB/schema name to the tenant DB/schema name
|
||||
config()->set(
|
||||
"database.connections.{$connectionName}",
|
||||
$this->makeConnectionConfig($this->database()->getConfig(), $databaseConfig->getName()),
|
||||
$this->makeConnectionConfig($this->connection()->getConfig(), $databaseConfig->getName()),
|
||||
);
|
||||
|
||||
// Connect to the tenant DB/schema
|
||||
$this->database()->reconnect();
|
||||
$this->connection()->reconnect();
|
||||
|
||||
// Delete all database objects owned by the user (privileges, tables, views, etc.)
|
||||
// Postgres users cannot be deleted unless we delete all objects owned by it first
|
||||
$this->database()->statement("DROP OWNED BY \"{$username}\"");
|
||||
$this->connection()->statement("DROP OWNED BY \"{$username}\"");
|
||||
|
||||
// Delete the user
|
||||
$userDeleted = $this->database()->statement("DROP USER \"{$username}\"");
|
||||
$userDeleted = $this->connection()->statement("DROP USER \"{$username}\"");
|
||||
|
||||
config()->set(
|
||||
"database.connections.{$connectionName}",
|
||||
$this->makeConnectionConfig($this->database()->getConfig(), $centralDatabase),
|
||||
$this->makeConnectionConfig($this->connection()->getConfig(), $centralDatabase),
|
||||
);
|
||||
|
||||
// Reconnect to the central database
|
||||
$this->database()->reconnect();
|
||||
$this->connection()->reconnect();
|
||||
|
||||
return $userDeleted;
|
||||
}
|
||||
|
||||
public function userExists(string $username): bool
|
||||
{
|
||||
return (bool) $this->database()->selectOne("SELECT usename FROM pg_user WHERE usename = '{$username}'");
|
||||
return (bool) $this->connection()->selectOne("SELECT usename FROM pg_user WHERE usename = '{$username}'");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
|||
interface StatefulTenantDatabaseManager extends TenantDatabaseManager
|
||||
{
|
||||
/** Get the DB connection used by the tenant database manager. */
|
||||
public function database(): Connection; // todo@dbRefactor rename to connection()
|
||||
public function connection(): Connection;
|
||||
|
||||
/**
|
||||
* Set the DB connection that should be used by the tenant database manager.
|
||||
|
|
|
|||
|
|
@ -25,21 +25,21 @@ class DatabaseConfig
|
|||
/**
|
||||
* Database username generator (can be set by the developer.).
|
||||
*
|
||||
* @var Closure(Model&Tenant): string
|
||||
* @var Closure(Model&Tenant, self): string
|
||||
*/
|
||||
public static Closure $usernameGenerator;
|
||||
|
||||
/**
|
||||
* Database password generator (can be set by the developer.).
|
||||
*
|
||||
* @var Closure(Model&Tenant): string
|
||||
* @var Closure(Model&Tenant, self): string
|
||||
*/
|
||||
public static Closure $passwordGenerator;
|
||||
|
||||
/**
|
||||
* Database name generator (can be set by the developer.).
|
||||
*
|
||||
* @var Closure(Model&Tenant): string
|
||||
* @var Closure(Model&Tenant, self): string
|
||||
*/
|
||||
public static Closure $databaseNameGenerator;
|
||||
|
||||
|
|
@ -58,8 +58,14 @@ class DatabaseConfig
|
|||
}
|
||||
|
||||
if (! isset(static::$databaseNameGenerator)) {
|
||||
static::$databaseNameGenerator = function (Model&Tenant $tenant) {
|
||||
return config('tenancy.database.prefix') . $tenant->getTenantKey() . config('tenancy.database.suffix');
|
||||
static::$databaseNameGenerator = function (Model&Tenant $tenant, self $self) {
|
||||
$suffix = config('tenancy.database.suffix');
|
||||
|
||||
if (! $suffix && $self->getTemplateConnection()['driver'] === 'sqlite') {
|
||||
$suffix = '.sqlite';
|
||||
}
|
||||
|
||||
return config('tenancy.database.prefix') . $tenant->getTenantKey() . $suffix;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +95,7 @@ class DatabaseConfig
|
|||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant);
|
||||
return $this->tenant->getInternal('db_name') ?? (static::$databaseNameGenerator)($this->tenant, $this);
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
|
|
@ -110,8 +116,8 @@ class DatabaseConfig
|
|||
$this->tenant->setInternal('db_name', $this->getName());
|
||||
|
||||
if ($this->connectionDriverManager($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
||||
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant));
|
||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant, $this));
|
||||
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant, $this));
|
||||
}
|
||||
|
||||
if ($this->tenant->exists) {
|
||||
|
|
@ -192,7 +198,8 @@ class DatabaseConfig
|
|||
DB::purge($this->getTenantHostConnectionName());
|
||||
}
|
||||
|
||||
/** Get the TenantDatabaseManager for this tenant's connection.
|
||||
/**
|
||||
* Get the TenantDatabaseManager for this tenant's connection.
|
||||
*
|
||||
* @throws NoConnectionSetException|DatabaseManagerNotRegisteredException
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use Illuminate\Contracts\Foundation\Application;
|
|||
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
|
||||
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager;
|
||||
|
||||
/**
|
||||
* @internal Class is subject to breaking changes in minor and patch versions.
|
||||
|
|
@ -71,7 +72,9 @@ class DatabaseManager
|
|||
$manager = $tenant->database()->manager();
|
||||
|
||||
if ($manager->databaseExists($database = $tenant->database()->getName())) {
|
||||
throw new Exceptions\TenantDatabaseAlreadyExistsException($database);
|
||||
if (! $manager instanceof SQLiteDatabaseManager || ! SQLiteDatabaseManager::isInMemory($database)) {
|
||||
throw new Exceptions\TenantDatabaseAlreadyExistsException($database);
|
||||
}
|
||||
}
|
||||
|
||||
if ($manager instanceof Contracts\ManagesDatabaseUsers) {
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ class MicrosoftSQLDatabaseManager extends TenantDatabaseManager
|
|||
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
$database = $tenant->database()->getName();
|
||||
$charset = $this->database()->getConfig('charset');
|
||||
$collation = $this->database()->getConfig('collation'); // todo check why these are not used
|
||||
$charset = $this->connection()->getConfig('charset');
|
||||
$collation = $this->connection()->getConfig('collation'); // todo check why these are not used
|
||||
|
||||
return $this->database()->statement("CREATE DATABASE [{$database}]");
|
||||
return $this->connection()->statement("CREATE DATABASE [{$database}]");
|
||||
}
|
||||
|
||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
return $this->database()->statement("DROP DATABASE [{$tenant->database()->getName()}]");
|
||||
return $this->connection()->statement("DROP DATABASE [{$tenant->database()->getName()}]");
|
||||
}
|
||||
|
||||
public function databaseExists(string $name): bool
|
||||
{
|
||||
return (bool) $this->database()->select("SELECT name FROM master.sys.databases WHERE name = '$name'");
|
||||
return (bool) $this->connection()->select("SELECT name FROM master.sys.databases WHERE name = '$name'");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ class MySQLDatabaseManager extends TenantDatabaseManager
|
|||
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
$database = $tenant->database()->getName();
|
||||
$charset = $this->database()->getConfig('charset');
|
||||
$collation = $this->database()->getConfig('collation');
|
||||
$charset = $this->connection()->getConfig('charset');
|
||||
$collation = $this->connection()->getConfig('collation');
|
||||
|
||||
return $this->database()->statement("CREATE DATABASE `{$database}` CHARACTER SET `$charset` COLLATE `$collation`");
|
||||
return $this->connection()->statement("CREATE DATABASE `{$database}` CHARACTER SET `$charset` COLLATE `$collation`");
|
||||
}
|
||||
|
||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
return $this->database()->statement("DROP DATABASE `{$tenant->database()->getName()}`");
|
||||
return $this->connection()->statement("DROP DATABASE `{$tenant->database()->getName()}`");
|
||||
}
|
||||
|
||||
public function databaseExists(string $name): bool
|
||||
{
|
||||
return (bool) $this->database()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'");
|
||||
return (bool) $this->connection()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,24 +25,24 @@ class PermissionControlledMicrosoftSQLServerDatabaseManager extends MicrosoftSQL
|
|||
$password = $databaseConfig->getPassword();
|
||||
|
||||
// Create login
|
||||
$this->database()->statement("CREATE LOGIN [$username] WITH PASSWORD = '$password'");
|
||||
$this->connection()->statement("CREATE LOGIN [$username] WITH PASSWORD = '$password'");
|
||||
|
||||
// Create user in the database
|
||||
// Grant the user permissions specified in the $grants array
|
||||
// The 'CONNECT' permission is granted automatically
|
||||
$grants = implode(', ', static::$grants);
|
||||
|
||||
return $this->database()->statement("USE [$database]; CREATE USER [$username] FOR LOGIN [$username]; GRANT $grants TO [$username]");
|
||||
return $this->connection()->statement("USE [$database]; CREATE USER [$username] FOR LOGIN [$username]; GRANT $grants TO [$username]");
|
||||
}
|
||||
|
||||
public function deleteUser(DatabaseConfig $databaseConfig): bool
|
||||
{
|
||||
return $this->database()->statement("DROP LOGIN [{$databaseConfig->getUsername()}]");
|
||||
return $this->connection()->statement("DROP LOGIN [{$databaseConfig->getUsername()}]");
|
||||
}
|
||||
|
||||
public function userExists(string $username): bool
|
||||
{
|
||||
return (bool) $this->database()->select("SELECT sp.name as username FROM sys.server_principals sp WHERE sp.name = '{$username}'");
|
||||
return (bool) $this->connection()->select("SELECT sp.name as username FROM sys.server_principals sp WHERE sp.name = '{$username}'");
|
||||
}
|
||||
|
||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
||||
|
|
@ -58,7 +58,7 @@ class PermissionControlledMicrosoftSQLServerDatabaseManager extends MicrosoftSQL
|
|||
// Set the database to SINGLE_USER mode to ensure that
|
||||
// No other connections are using the database while we're trying to delete it
|
||||
// Rollback all active transactions
|
||||
$this->database()->statement("ALTER DATABASE [{$tenant->database()->getName()}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;");
|
||||
$this->connection()->statement("ALTER DATABASE [{$tenant->database()->getName()}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;");
|
||||
|
||||
return parent::deleteDatabase($tenant);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
|
|||
$hostname = $databaseConfig->connection()['host'];
|
||||
$password = $databaseConfig->getPassword();
|
||||
|
||||
$this->database()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'");
|
||||
$this->connection()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'");
|
||||
|
||||
$grants = implode(', ', static::$grants);
|
||||
|
||||
|
|
@ -36,24 +36,24 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
|
|||
$grantQuery = "GRANT $grants ON `$database`.* TO `$username`@`%` IDENTIFIED BY '$password'";
|
||||
}
|
||||
|
||||
return $this->database()->statement($grantQuery);
|
||||
return $this->connection()->statement($grantQuery);
|
||||
}
|
||||
|
||||
protected function isVersion8(): bool
|
||||
{
|
||||
$versionSelect = (string) $this->database()->raw('select version()')->getValue($this->database()->getQueryGrammar());
|
||||
$version = $this->database()->select($versionSelect)[0]->{'version()'};
|
||||
$versionSelect = (string) $this->connection()->raw('select version()')->getValue($this->connection()->getQueryGrammar());
|
||||
$version = $this->connection()->select($versionSelect)[0]->{'version()'};
|
||||
|
||||
return version_compare($version, '8.0.0') >= 0;
|
||||
}
|
||||
|
||||
public function deleteUser(DatabaseConfig $databaseConfig): bool
|
||||
{
|
||||
return $this->database()->statement("DROP USER IF EXISTS '{$databaseConfig->getUsername()}'");
|
||||
return $this->connection()->statement("DROP USER IF EXISTS '{$databaseConfig->getUsername()}'");
|
||||
}
|
||||
|
||||
public function userExists(string $username): bool
|
||||
{
|
||||
return (bool) $this->database()->select("SELECT count(*) FROM mysql.user WHERE user = '$username'")[0]->{'count(*)'};
|
||||
return (bool) $this->connection()->select("SELECT count(*) FROM mysql.user WHERE user = '$username'")[0]->{'count(*)'};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,26 +21,26 @@ class PermissionControlledPostgreSQLDatabaseManager extends PostgreSQLDatabaseMa
|
|||
$schema = $databaseConfig->connection()['search_path'];
|
||||
|
||||
// Host config
|
||||
$connectionName = $this->database()->getConfig('name');
|
||||
$centralDatabase = $this->database()->getConfig('database');
|
||||
$connectionName = $this->connection()->getConfig('name');
|
||||
$centralDatabase = $this->connection()->getConfig('database');
|
||||
|
||||
$this->database()->statement("GRANT CONNECT ON DATABASE \"{$database}\" TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT CONNECT ON DATABASE \"{$database}\" TO \"{$username}\"");
|
||||
|
||||
// Connect to tenant database
|
||||
config(["database.connections.{$connectionName}.database" => $database]);
|
||||
|
||||
$this->database()->reconnect();
|
||||
$this->connection()->reconnect();
|
||||
|
||||
// Grant permissions to create and use tables in the configured schema ("public" by default) to the user
|
||||
$this->database()->statement("GRANT USAGE, CREATE ON SCHEMA {$schema} TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT USAGE, CREATE ON SCHEMA {$schema} TO \"{$username}\"");
|
||||
|
||||
// Grant permissions to use sequences in the current schema to the user
|
||||
$this->database()->statement("GRANT USAGE ON ALL SEQUENCES IN SCHEMA {$schema} TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT USAGE ON ALL SEQUENCES IN SCHEMA {$schema} TO \"{$username}\"");
|
||||
|
||||
// Reconnect to central database
|
||||
config(["database.connections.{$connectionName}.database" => $centralDatabase]);
|
||||
|
||||
$this->database()->reconnect();
|
||||
$this->connection()->reconnect();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ class PermissionControlledPostgreSQLSchemaManager extends PostgreSQLSchemaManage
|
|||
// Central database name
|
||||
$database = DB::connection(config('tenancy.database.central_connection'))->getDatabaseName();
|
||||
|
||||
$this->database()->statement("GRANT CONNECT ON DATABASE {$database} TO \"{$username}\"");
|
||||
$this->database()->statement("GRANT USAGE, CREATE ON SCHEMA \"{$schema}\" TO \"{$username}\"");
|
||||
$this->database()->statement("GRANT USAGE ON ALL SEQUENCES IN SCHEMA \"{$schema}\" TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT CONNECT ON DATABASE {$database} TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT USAGE, CREATE ON SCHEMA \"{$schema}\" TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT USAGE ON ALL SEQUENCES IN SCHEMA \"{$schema}\" TO \"{$username}\"");
|
||||
|
||||
$tables = $this->database()->select("SELECT table_name FROM information_schema.tables WHERE table_schema = '{$schema}'");
|
||||
$tables = $this->connection()->select("SELECT table_name FROM information_schema.tables WHERE table_schema = '{$schema}'");
|
||||
|
||||
// Grant permissions to any existing tables. This is used with RLS
|
||||
// todo@samuel refactor this along with the todo in TenantDatabaseManager
|
||||
|
|
@ -36,7 +36,7 @@ class PermissionControlledPostgreSQLSchemaManager extends PostgreSQLSchemaManage
|
|||
$tableName = $table->table_name;
|
||||
|
||||
/** @var string $primaryKey */
|
||||
$primaryKey = $this->database()->selectOne(<<<SQL
|
||||
$primaryKey = $this->connection()->selectOne(<<<SQL
|
||||
SELECT column_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_name = '{$tableName}'
|
||||
|
|
@ -44,11 +44,11 @@ class PermissionControlledPostgreSQLSchemaManager extends PostgreSQLSchemaManage
|
|||
SQL)->column_name;
|
||||
|
||||
// Grant all permissions for all existing tables
|
||||
$this->database()->statement("GRANT ALL ON \"{$schema}\".\"{$tableName}\" TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT ALL ON \"{$schema}\".\"{$tableName}\" TO \"{$username}\"");
|
||||
|
||||
// Grant permission to reference the primary key for the table
|
||||
// The previous query doesn't grant the references privilege, so it has to be granted here
|
||||
$this->database()->statement("GRANT REFERENCES (\"{$primaryKey}\") ON \"{$schema}\".\"{$tableName}\" TO \"{$username}\"");
|
||||
$this->connection()->statement("GRANT REFERENCES (\"{$primaryKey}\") ON \"{$schema}\".\"{$tableName}\" TO \"{$username}\"");
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@ class PostgreSQLDatabaseManager extends TenantDatabaseManager
|
|||
{
|
||||
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
return $this->database()->statement("CREATE DATABASE \"{$tenant->database()->getName()}\" WITH TEMPLATE=template0");
|
||||
return $this->connection()->statement("CREATE DATABASE \"{$tenant->database()->getName()}\" WITH TEMPLATE=template0");
|
||||
}
|
||||
|
||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
return $this->database()->statement("DROP DATABASE \"{$tenant->database()->getName()}\"");
|
||||
return $this->connection()->statement("DROP DATABASE \"{$tenant->database()->getName()}\"");
|
||||
}
|
||||
|
||||
public function databaseExists(string $name): bool
|
||||
{
|
||||
return (bool) $this->database()->selectOne("SELECT datname FROM pg_database WHERE datname = '$name'");
|
||||
return (bool) $this->connection()->selectOne("SELECT datname FROM pg_database WHERE datname = '$name'");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@ class PostgreSQLSchemaManager extends TenantDatabaseManager
|
|||
{
|
||||
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
return $this->database()->statement("CREATE SCHEMA \"{$tenant->database()->getName()}\"");
|
||||
return $this->connection()->statement("CREATE SCHEMA \"{$tenant->database()->getName()}\"");
|
||||
}
|
||||
|
||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
return $this->database()->statement("DROP SCHEMA \"{$tenant->database()->getName()}\" CASCADE");
|
||||
return $this->connection()->statement("DROP SCHEMA \"{$tenant->database()->getName()}\" CASCADE");
|
||||
}
|
||||
|
||||
public function databaseExists(string $name): bool
|
||||
{
|
||||
return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'");
|
||||
return (bool) $this->connection()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'");
|
||||
}
|
||||
|
||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Database\TenantDatabaseManagers;
|
||||
|
||||
use AssertionError;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDO;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Throwable;
|
||||
|
|
@ -15,10 +19,92 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
|||
*/
|
||||
public static string|null $path = null;
|
||||
|
||||
/**
|
||||
* Should the WAL journal mode be used for newly created databases.
|
||||
*
|
||||
* @see https://www.sqlite.org/pragma.html#pragma_journal_mode
|
||||
*/
|
||||
public static bool $WAL = true;
|
||||
|
||||
/*
|
||||
* If this isn't null, a connection to the tenant DB will be created
|
||||
* and passed to the provided closure, for the purpose of keeping the
|
||||
* connection alive for the desired lifetime. This means it's the
|
||||
* closure's job to store the connection in a place that lives as
|
||||
* long as the connection should live.
|
||||
*
|
||||
* The closure is called in makeConnectionConfig() -- a method normally
|
||||
* called shortly before a connection is established.
|
||||
*
|
||||
* NOTE: The closure is called EVERY time makeConnectionConfig()
|
||||
* is called, therefore it's up to the closure to discard
|
||||
* the connection if a connection to the same database is already persisted.
|
||||
*
|
||||
* The closure also receives the DSN used to create the PDO connection,
|
||||
* since the PDO connection driver makes it a bit hard to recover DB names
|
||||
* from PDO instances. That should make it easier to match these with
|
||||
* tenant instances passed to $closeInMemoryConnectionUsing closures,
|
||||
* if you're setting that property as well.
|
||||
*
|
||||
* @property Closure(PDO, string)|null
|
||||
*/
|
||||
public static Closure|null $persistInMemoryConnectionUsing = null;
|
||||
|
||||
/*
|
||||
* The opposite of $persistInMemoryConnectionUsing. This closure
|
||||
* is called when the tenant is deleted, to clear the database
|
||||
* in case a tenant with the same ID should be created within
|
||||
* the lifetime of the $persistInMemoryConnectionUsing logic.
|
||||
*
|
||||
* NOTE: The parameter provided to the closure is the Tenant
|
||||
* instance, not a PDO connection.
|
||||
*
|
||||
* @property Closure(Tenant)|null
|
||||
*/
|
||||
public static Closure|null $closeInMemoryConnectionUsing = null;
|
||||
|
||||
public function createDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
/** @var TenantWithDatabase&Model $tenant */
|
||||
$name = $tenant->database()->getName();
|
||||
|
||||
if ($this->isInMemory($name)) {
|
||||
// If :memory: is used, we update the tenant with a *named* in-memory SQLite connection.
|
||||
//
|
||||
// This makes use of the package feasible with in-memory SQLite. Pure :memory: isn't
|
||||
// sufficient since the tenant creation process involves constant creation and destruction
|
||||
// of the tenant connection, always clearing the memory (like migrations). Additionally,
|
||||
// tenancy()->central() calls would close the database since at the moment we close the
|
||||
// tenant connection (to prevent accidental references to it in the central context) when
|
||||
// tenancy is ended.
|
||||
//
|
||||
// Note that named in-memory databases DO NOT have process lifetime. You need an open
|
||||
// PDO connection to keep the memory from being cleaned up. It's up to the user how they
|
||||
// handle this, common solutions may involve storing the connection in the service container
|
||||
// or creating a closure holding a reference to it and passing that to register_shutdown_function().
|
||||
|
||||
$name = '_tenancy_inmemory_' . $tenant->getTenantKey();
|
||||
$tenant->update(['tenancy_db_name' => "file:$name?mode=memory&cache=shared"]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return (bool) file_put_contents($this->getPath($tenant->database()->getName()), '');
|
||||
if (file_put_contents($path = $this->getPath($name), '') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static::$WAL) {
|
||||
$pdo = new PDO('sqlite:' . $path);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// @phpstan-ignore-next-line method.nonObject
|
||||
assert($pdo->query('pragma journal_mode = wal')->fetch(PDO::FETCH_ASSOC)['journal_mode'] === 'wal', 'Unable to set journal mode to wal.');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (AssertionError $e) {
|
||||
throw $e;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -26,8 +112,18 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
|||
|
||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||
{
|
||||
$name = $tenant->database()->getName();
|
||||
|
||||
if ($this->isInMemory($name)) {
|
||||
if (static::$closeInMemoryConnectionUsing) {
|
||||
(static::$closeInMemoryConnectionUsing)($tenant);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return unlink($this->getPath($tenant->database()->getName()));
|
||||
return unlink($this->getPath($name));
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -35,12 +131,21 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
|||
|
||||
public function databaseExists(string $name): bool
|
||||
{
|
||||
return file_exists($this->getPath($name));
|
||||
return $this->isInMemory($name) || file_exists($this->getPath($name));
|
||||
}
|
||||
|
||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
||||
{
|
||||
$baseConfig['database'] = database_path($databaseName);
|
||||
if ($this->isInMemory($databaseName)) {
|
||||
$baseConfig['database'] = $databaseName;
|
||||
|
||||
if (static::$persistInMemoryConnectionUsing !== null) {
|
||||
$dsn = "sqlite:$databaseName";
|
||||
(static::$persistInMemoryConnectionUsing)(new PDO($dsn), $dsn);
|
||||
}
|
||||
} else {
|
||||
$baseConfig['database'] = database_path($databaseName);
|
||||
}
|
||||
|
||||
return $baseConfig;
|
||||
}
|
||||
|
|
@ -58,4 +163,9 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
|||
|
||||
return database_path($name);
|
||||
}
|
||||
|
||||
public static function isInMemory(string $name): bool
|
||||
{
|
||||
return $name === ':memory:' || str_contains($name, '_tenancy_inmemory_');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ abstract class TenantDatabaseManager implements StatefulTenantDatabaseManager
|
|||
/** The database connection to the server. */
|
||||
protected string $connection;
|
||||
|
||||
public function database(): Connection
|
||||
public function connection(): Connection
|
||||
{
|
||||
if (! isset($this->connection)) {
|
||||
throw new NoConnectionSetException(static::class);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,14 @@ class InitializeTenancyByDomain extends IdentificationMiddleware
|
|||
*/
|
||||
public function requestHasTenant(Request $request): bool
|
||||
{
|
||||
return ! in_array($this->getDomain($request), config('tenancy.identification.central_domains'));
|
||||
$domain = $this->getDomain($request);
|
||||
|
||||
// Mainly used with origin identification if the header isn't specified and e.g. universal routes are used
|
||||
if (! $domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! in_array($domain, config('tenancy.identification.central_domains'));
|
||||
}
|
||||
|
||||
public function getDomain(Request $request): string
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ use Closure;
|
|||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
||||
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||
|
||||
class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
|
||||
{
|
||||
|
|
@ -23,34 +24,46 @@ class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
|
|||
}
|
||||
|
||||
$domain = $this->getDomain($request);
|
||||
$subdomain = null;
|
||||
|
||||
if ($this->isSubdomain($domain)) {
|
||||
$domain = $this->makeSubdomain($domain);
|
||||
if (DomainTenantResolver::isSubdomain($domain)) {
|
||||
$subdomain = $this->makeSubdomain($domain);
|
||||
|
||||
if ($domain instanceof Exception) {
|
||||
if ($subdomain instanceof Exception) {
|
||||
$onFail = static::$onFail ?? function ($e) {
|
||||
throw $e;
|
||||
};
|
||||
|
||||
return $onFail($domain, $request, $next);
|
||||
}
|
||||
|
||||
// If a Response instance was returned, we return it immediately.
|
||||
// todo@samuel when does this execute?
|
||||
if ($domain instanceof Response) {
|
||||
return $domain;
|
||||
return $onFail($subdomain, $request, $next);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->initializeTenancy(
|
||||
$request,
|
||||
$next,
|
||||
$domain
|
||||
);
|
||||
}
|
||||
try {
|
||||
$this->tenancy->initialize(
|
||||
$this->resolver->resolve($subdomain ?? $domain)
|
||||
);
|
||||
} catch (TenantCouldNotBeIdentifiedException $e) {
|
||||
if ($subdomain) {
|
||||
try {
|
||||
$this->tenancy->initialize(
|
||||
$this->resolver->resolve($domain)
|
||||
);
|
||||
} catch (TenantCouldNotBeIdentifiedException $e) {
|
||||
$onFail = static::$onFail ?? function ($e) {
|
||||
throw $e;
|
||||
};
|
||||
|
||||
protected function isSubdomain(string $hostname): bool
|
||||
{
|
||||
return Str::endsWith($hostname, config('tenancy.identification.central_domains'));
|
||||
return $onFail($e, $request, $next);
|
||||
}
|
||||
} else {
|
||||
$onFail = static::$onFail ?? function ($e) {
|
||||
throw $e;
|
||||
};
|
||||
|
||||
return $onFail($e, $request, $next);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ use Closure;
|
|||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
||||
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||
|
||||
class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
||||
{
|
||||
|
|
@ -57,20 +57,16 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
|||
);
|
||||
}
|
||||
|
||||
/** @return string|Response|Exception|mixed */
|
||||
/** @return string|Exception */
|
||||
protected function makeSubdomain(string $hostname)
|
||||
{
|
||||
$parts = explode('.', $hostname);
|
||||
|
||||
$isLocalhost = count($parts) === 1;
|
||||
$isIpAddress = count(array_filter($parts, 'is_numeric')) === count($parts);
|
||||
|
||||
// If we're on localhost or an IP address, then we're not visiting a subdomain.
|
||||
$isACentralDomain = in_array($hostname, config('tenancy.identification.central_domains'), true);
|
||||
$notADomain = $isLocalhost || $isIpAddress;
|
||||
$thirdPartyDomain = ! Str::endsWith($hostname, config('tenancy.identification.central_domains'));
|
||||
$thirdPartyDomain = ! DomainTenantResolver::isSubdomain($hostname);
|
||||
|
||||
if ($isACentralDomain || $notADomain || $thirdPartyDomain) {
|
||||
if ($isACentralDomain || $isIpAddress || $thirdPartyDomain) {
|
||||
return new NotASubdomainException($hostname);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class CacheManager extends BaseCacheManager
|
|||
throw new \Exception("Method tags() takes exactly 1 argument. $count passed.");
|
||||
}
|
||||
|
||||
$names = $parameters[0];
|
||||
$names = array_values($parameters)[0];
|
||||
$names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/9.x/cache#removing-tagged-cache-items
|
||||
|
||||
return $this->store()->tags(array_merge($tags, $names));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Overrides;
|
||||
|
||||
use BackedEnum;
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
use Illuminate\Support\Arr;
|
||||
use InvalidArgumentException;
|
||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||
|
||||
/**
|
||||
|
|
@ -51,7 +53,11 @@ class TenancyUrlGenerator extends UrlGenerator
|
|||
*/
|
||||
public function route($name, $parameters = [], $absolute = true)
|
||||
{
|
||||
[$name, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters));
|
||||
if ($name instanceof BackedEnum && ! is_string($name = $name->value)) { // @phpstan-ignore function.impossibleType
|
||||
throw new InvalidArgumentException('Attribute [name] expects a string backed enum.');
|
||||
}
|
||||
|
||||
[$name, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); // @phpstan-ignore argument.type
|
||||
|
||||
$url = parent::route($name, $parameters, $absolute);
|
||||
|
||||
|
|
@ -79,7 +85,11 @@ class TenancyUrlGenerator extends UrlGenerator
|
|||
*/
|
||||
public function temporarySignedRoute($name, $expiration, $parameters = [], $absolute = true)
|
||||
{
|
||||
[$name, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters));
|
||||
if ($name instanceof BackedEnum && ! is_string($name = $name->value)) { // @phpstan-ignore function.impossibleType
|
||||
throw new InvalidArgumentException('Attribute [name] expects a string backed enum.');
|
||||
}
|
||||
|
||||
[$name, $parameters] = $this->prepareRouteInputs($name, Arr::wrap($parameters)); // @phpstan-ignore argument.type
|
||||
|
||||
return parent::temporarySignedRoute($name, $expiration, $parameters, $absolute);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use Stancl\Tenancy\Contracts\SingleDomainTenant;
|
|||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DomainTenantResolver extends Contracts\CachedTenantResolver
|
||||
{
|
||||
|
|
@ -55,6 +56,11 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
|||
return $tenant;
|
||||
}
|
||||
|
||||
public static function isSubdomain(string $domain): bool
|
||||
{
|
||||
return Str::endsWith($domain, config('tenancy.identification.central_domains'));
|
||||
}
|
||||
|
||||
public function resolved(Tenant $tenant, mixed ...$args): void
|
||||
{
|
||||
$this->setCurrentDomain($tenant, $args[0]);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ class ModelNotSyncMasterException extends Exception
|
|||
{
|
||||
public function __construct(string $class)
|
||||
{
|
||||
parent::__construct("Model of $class class is not a SyncMaster model. Make sure you're using the central model to make changes to synced resources when you're in the central context");
|
||||
parent::__construct("Model of $class class is not a SyncMaster model. Make sure you're using the central model to make changes to synced resources when you're in the central context.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ if (! function_exists('tenant_channel')) {
|
|||
function tenant_channel(string $channelName, Closure $callback, array $options = []): void
|
||||
{
|
||||
// Register '{tenant}.channelName'
|
||||
Broadcast::channel('{tenant}.' . $channelName, fn ($user, $tenantKey, ...$args) => $callback($user, ...$args), $options);
|
||||
Broadcast::channel('{tenant}.' . $channelName, $callback, $options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,17 +121,6 @@ if (! function_exists('global_channel')) {
|
|||
{
|
||||
// Register 'global__channelName'
|
||||
// Global channels are available in both the central and tenant contexts
|
||||
Broadcast::channel('global__' . $channelName, fn ($user, ...$args) => $callback($user, ...$args), $options);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('universal_channel')) {
|
||||
function universal_channel(string $channelName, Closure $callback, array $options = []): void
|
||||
{
|
||||
// Register 'channelName'
|
||||
Broadcast::channel($channelName, $callback, $options);
|
||||
|
||||
// Register '{tenant}.channelName'
|
||||
tenant_channel($channelName, $callback, $options);
|
||||
Broadcast::channel('global__' . $channelName, $callback, $options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue