diff --git a/assets/config.php b/assets/config.php index 2a54e0b9..b8a759c9 100644 --- a/assets/config.php +++ b/assets/config.php @@ -47,6 +47,11 @@ return [ */ 'template_tenant_connection' => null, + /** + * Tenant host connection name used to create a temporary connection for creating, deleting database + */ + 'tenant_host_connection_name' => 'tenant_host_connection', + /** * Tenant database names are created like this: * prefix + tenant_id + suffix. diff --git a/src/Database/DatabaseConfig.php b/src/Database/DatabaseConfig.php index a4c79582..b41dd75a 100644 --- a/src/Database/DatabaseConfig.php +++ b/src/Database/DatabaseConfig.php @@ -100,6 +100,11 @@ class DatabaseConfig ?? config('tenancy.database.central_connection'); } + public function getTenantHostConnectionName(): ?string + { + return config('tenancy.database.tenant_host_connection_name'); + } + /** * Tenant's own database connection config. */ @@ -114,6 +119,27 @@ class DatabaseConfig ); } + /** + * Tenant's host database connection config. + */ + public function hostConnection(): array + { + $config = $this->tenantConfig(); + $template = $this->getTemplateConnectionName(); + $templateConnection = config("database.connections.{$template}"); + + if ($this->manager() instanceof Contracts\ManagesDatabaseUsers) { + unset($config['username']); + unset($config['password']); + } + + if (empty($config)) { + $config = $templateConnection; + } + + return array_replace($templateConnection, $config); + } + /** * Additional config for the database connection, specific to this tenant. */ @@ -158,4 +184,25 @@ class DatabaseConfig return $databaseManager; } + + /** Get the TenantDatabaseManager for this tenant's connection. */ + public function hostManager(): Contracts\TenantDatabaseManager + { + $tenantHostConnection = $this->getTenantHostConnectionName(); + config(["database.connections.{$tenantHostConnection}" => $this->hostConnection()]); + + $driver = config("database.connections.{$tenantHostConnection}.driver"); + $databaseManagers = config('tenancy.database.managers'); + + if (! array_key_exists($driver, $databaseManagers)) { + throw new Exceptions\DatabaseManagerNotRegisteredException($driver); + } + + /** @var Contracts\TenantDatabaseManager $databaseManager */ + $databaseManager = app($databaseManagers[$driver]); + + $databaseManager->setConnection($tenantHostConnection); + + return $databaseManager; + } } diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index a92ccb7b..657f5756 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -88,7 +88,7 @@ class DatabaseManager */ public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void { - $manager = $tenant->database()->manager(); + $manager = $tenant->database()->hostManager(); if ($manager->databaseExists($database = $tenant->database()->getName())) { throw new Exceptions\TenantDatabaseAlreadyExistsException($database); diff --git a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php index b7dd15fa..9b6f55c3 100644 --- a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php @@ -20,6 +20,10 @@ abstract class TenantDatabaseManager implements Contract // todo better naming? throw new NoConnectionSetException(static::class); } + if (config("database.connections.{$this->getTenantHostConnectionName()}")) { + // DB::purge($this->getTenantHostConnectionName()); + } + return DB::connection($this->connection); } @@ -34,4 +38,9 @@ abstract class TenantDatabaseManager implements Contract // todo better naming? return $baseConfig; } + + public function getTenantHostConnectionName(): ?string + { + return config('tenancy.database.tenant_host_connection_name'); + } } diff --git a/src/Jobs/CreateDatabase.php b/src/Jobs/CreateDatabase.php index f143f399..f536d0f7 100644 --- a/src/Jobs/CreateDatabase.php +++ b/src/Jobs/CreateDatabase.php @@ -35,7 +35,7 @@ class CreateDatabase implements ShouldQueue $this->tenant->database()->makeCredentials(); $databaseManager->ensureTenantCanBeCreated($this->tenant); - $this->tenant->database()->manager()->createDatabase($this->tenant); + $this->tenant->database()->hostManager()->createDatabase($this->tenant); event(new DatabaseCreated($this->tenant)); } diff --git a/src/Jobs/DeleteDatabase.php b/src/Jobs/DeleteDatabase.php index 71358f74..d6acd99e 100644 --- a/src/Jobs/DeleteDatabase.php +++ b/src/Jobs/DeleteDatabase.php @@ -27,7 +27,7 @@ class DeleteDatabase implements ShouldQueue { event(new DeletingDatabase($this->tenant)); - $this->tenant->database()->manager()->deleteDatabase($this->tenant); + $this->tenant->database()->hostManager()->deleteDatabase($this->tenant); event(new DatabaseDeleted($this->tenant)); } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index ab25310c..fce66a20 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -100,20 +100,22 @@ test('the tenant connection is fully removed', function () { $tenant = Tenant::create(); - expect(array_keys(app('db')->getConnections()))->toBe(['central']); + // Connections array can contain other connections built runtime like 'tenant_host_connection' + // So check if tenant connection does not exist in connections + expect(array_keys(app('db')->getConnections()))->not()->toContain('tenant'); pest()->assertArrayNotHasKey('tenant', config('database.connections')); tenancy()->initialize($tenant); createUsersTable(); - expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant']); + expect(array_keys(app('db')->getConnections()))->toContain('central', 'tenant'); pest()->assertArrayHasKey('tenant', config('database.connections')); tenancy()->end(); - expect(array_keys(app('db')->getConnections()))->toBe(['central']); - expect(config('database.connections.tenant'))->toBeNull(); + expect(array_keys(app('db')->getConnections()))->not()->toContain('tenant') + ->and(config('database.connections.tenant'))->toBeNull(); }); test('db name is prefixed with db path when sqlite is used', function () { @@ -181,7 +183,7 @@ test('a tenants database cannot be created when the database already exists', fu ]); }); -test('tenant database can be created on a foreign server', function () { +test('tenant database can be created and deleted on a foreign server', function () { config([ 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, 'database.connections.mysql2' => [ @@ -222,6 +224,136 @@ test('tenant database can be created on a foreign server', function () { $manager->setConnection('mysql2'); expect($manager->databaseExists($name))->toBeTrue(); + + $manager->deleteDatabase($tenant); + expect($manager->databaseExists($name))->toBeFalse(); +}); + +test('tenant database can be created on template tenant connection', function () { + config([ + 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class, + 'tenancy.database.template_tenant_connection' => 'mysql2', + 'database.connections.mysql2' => [ + 'driver' => 'mysql', + 'host' => 'mysql2', + 'port' => 3306, + 'database' => 'main', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $name = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + ]); + + /** @var MySQLDatabaseManager $manager */ + $manager = $tenant->database()->manager(); + + // central was default connection so we are making sure + // that database did not create on central + $manager->setConnection('central'); + expect($manager->databaseExists($name))->toBeFalse(); + + $manager->setConnection('mysql2'); + expect($manager->databaseExists($name))->toBeTrue(); +}); + +test('tenant database can be created on a foreign server by using the host from tenant config', function () { + config([ + 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class, + 'tenancy.database.template_tenant_connection' => 'mysql', + 'database.connections.mysql2' => [ + 'driver' => 'mysql', + 'host' => 'mysql2', + 'port' => 3306, + 'database' => 'main', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $name = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_host' => 'mysql2', + ]); + + /** @var MySQLDatabaseManager $manager */ + $manager = $tenant->database()->manager(); + + $manager->setConnection('mysql2'); + expect($manager->databaseExists($name))->toBeTrue(); +}); + +test('tenant database can be created on a foreign server by using the username and password from tenant config', function () { + config([ + 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class, + 'tenancy.database.template_tenant_connection' => 'mysql', + 'database.connections.mysql' => [ + 'driver' => 'mysql', + 'host' => 'mysql', + 'port' => 3306, + 'database' => 'main', + 'username' => '', // provide using tenant config + 'password' => '', // provide using tenant config + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $name = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_username' => 'root', + 'tenancy_db_password' => 'password', + ]); + + /** @var MySQLDatabaseManager $manager */ + $manager = $tenant->database()->hostManager(); + + expect($manager->databaseExists($name))->toBeTrue(); }); test('path used by sqlite manager can be customized', function () {