mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-14 02:54:03 +00:00
Add support for postgres schema (#237)
* Add support for postgres schema * wip * Apply fixes from StyleCI * revert to db as default for pgsql * Move separate_by to database * Fixing testing * Fixing style * Reverted change * Store string instead of Connection instance * Remove use statement * Add use statement for DB facade * mysql -> pgsql Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
parent
e02bd3927a
commit
d0023c482a
6 changed files with 221 additions and 2 deletions
|
|
@ -30,6 +30,7 @@ return [
|
||||||
'based_on' => null, // The connection that will be used as a base for the dynamically created tenant connection. Set to null to use the default connection.
|
'based_on' => null, // The connection that will be used as a base for the dynamically created tenant connection. Set to null to use the default connection.
|
||||||
'prefix' => 'tenant',
|
'prefix' => 'tenant',
|
||||||
'suffix' => '',
|
'suffix' => '',
|
||||||
|
'separate_by' => 'database', // database or schema (only supported by pgsql)
|
||||||
],
|
],
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'prefix_base' => 'tenant',
|
'prefix_base' => 'tenant',
|
||||||
|
|
@ -61,6 +62,7 @@ return [
|
||||||
'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
|
'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
|
||||||
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
|
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
|
||||||
'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
|
'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
|
||||||
|
// 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database
|
||||||
],
|
],
|
||||||
'database_manager_connections' => [
|
'database_manager_connections' => [
|
||||||
// Connections used by TenantDatabaseManagers. This tells, for example, the
|
// Connections used by TenantDatabaseManagers. This tells, for example, the
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
|
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Stancl\Tenancy\Contracts\Future\CanSetConnection;
|
use Stancl\Tenancy\Contracts\Future\CanSetConnection;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
|
||||||
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
||||||
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
|
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException;
|
use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException;
|
||||||
|
|
@ -101,7 +102,9 @@ class DatabaseManager
|
||||||
|
|
||||||
// Change database name.
|
// Change database name.
|
||||||
$databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName;
|
$databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName;
|
||||||
$this->app['config']["database.connections.$connectionName.database"] = $databaseName;
|
$separateBy = $this->separateBy($connectionName);
|
||||||
|
|
||||||
|
$this->app['config']["database.connections.$connectionName.$separateBy"] = $databaseName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -147,6 +150,8 @@ class DatabaseManager
|
||||||
* @param Tenant $tenant
|
* @param Tenant $tenant
|
||||||
* @return void
|
* @return void
|
||||||
* @throws TenantCannotBeCreatedException
|
* @throws TenantCannotBeCreatedException
|
||||||
|
* @throws DatabaseManagerNotRegisteredException
|
||||||
|
* @throws TenantDatabaseAlreadyExistsException
|
||||||
*/
|
*/
|
||||||
public function ensureTenantCanBeCreated(Tenant $tenant): void
|
public function ensureTenantCanBeCreated(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
|
|
@ -161,6 +166,7 @@ class DatabaseManager
|
||||||
* @param Tenant $tenant
|
* @param Tenant $tenant
|
||||||
* @param ShouldQueue[]|callable[] $afterCreating
|
* @param ShouldQueue[]|callable[] $afterCreating
|
||||||
* @return void
|
* @return void
|
||||||
|
* @throws DatabaseManagerNotRegisteredException
|
||||||
*/
|
*/
|
||||||
public function createDatabase(Tenant $tenant, array $afterCreating = [])
|
public function createDatabase(Tenant $tenant, array $afterCreating = [])
|
||||||
{
|
{
|
||||||
|
|
@ -202,6 +208,7 @@ class DatabaseManager
|
||||||
*
|
*
|
||||||
* @param Tenant $tenant
|
* @param Tenant $tenant
|
||||||
* @return void
|
* @return void
|
||||||
|
* @throws DatabaseManagerNotRegisteredException
|
||||||
*/
|
*/
|
||||||
public function deleteDatabase(Tenant $tenant)
|
public function deleteDatabase(Tenant $tenant)
|
||||||
{
|
{
|
||||||
|
|
@ -224,6 +231,7 @@ class DatabaseManager
|
||||||
*
|
*
|
||||||
* @param Tenant $tenant
|
* @param Tenant $tenant
|
||||||
* @return TenantDatabaseManager
|
* @return TenantDatabaseManager
|
||||||
|
* @throws DatabaseManagerNotRegisteredException
|
||||||
*/
|
*/
|
||||||
public function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
|
public function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
|
||||||
{
|
{
|
||||||
|
|
@ -243,4 +251,20 @@ class DatabaseManager
|
||||||
|
|
||||||
return $databaseManager;
|
return $databaseManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What key on the connection config should be used to separate tenants.
|
||||||
|
*
|
||||||
|
* @param string $connectionName
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function separateBy(string $connectionName): string
|
||||||
|
{
|
||||||
|
if ($this->getDriver($this->getBaseConnection($connectionName)) === 'pgsql'
|
||||||
|
&& $this->app['config']['tenancy.database.separate_by'] === 'schema') {
|
||||||
|
return 'schema';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'database';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
Normal file
47
src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\TenantDatabaseManagers;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Config\Repository;
|
||||||
|
use Illuminate\Database\Connection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Stancl\Tenancy\Contracts\Future\CanSetConnection;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
||||||
|
|
||||||
|
class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
protected $connection;
|
||||||
|
|
||||||
|
public function __construct(Repository $config)
|
||||||
|
{
|
||||||
|
$this->connection = $config->get('tenancy.database_manager_connections.pgsql');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function database(): Connection
|
||||||
|
{
|
||||||
|
return DB::connection($this->connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConnection(string $connection): void
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDatabase(string $name): bool
|
||||||
|
{
|
||||||
|
return $this->database()->statement("CREATE SCHEMA \"$name\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteDatabase(string $name): bool
|
||||||
|
{
|
||||||
|
return $this->database()->statement("DROP SCHEMA \"$name\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function databaseExists(string $name): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'");
|
||||||
|
}
|
||||||
|
}
|
||||||
143
tests/DatabaseSchemaManagerTest.php
Normal file
143
tests/DatabaseSchemaManagerTest.php
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Tests;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Stancl\Tenancy\Tenant;
|
||||||
|
|
||||||
|
class DatabaseSchemaManagerTest extends TestCase
|
||||||
|
{
|
||||||
|
public $autoInitTenancy = false;
|
||||||
|
|
||||||
|
protected function getEnvironmentSetUp($app)
|
||||||
|
{
|
||||||
|
parent::getEnvironmentSetUp($app);
|
||||||
|
|
||||||
|
$app['config']->set([
|
||||||
|
'database.default' => 'pgsql',
|
||||||
|
'database.connections.pgsql.database' => 'main',
|
||||||
|
'database.connections.pgsql.schema' => 'public',
|
||||||
|
'tenancy.database.based_on' => null,
|
||||||
|
'tenancy.database.suffix' => '',
|
||||||
|
'tenancy.database.separate_by' => 'schema',
|
||||||
|
'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function reconnect_method_works()
|
||||||
|
{
|
||||||
|
$old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
|
||||||
|
|
||||||
|
tenancy()->init('test.localhost');
|
||||||
|
|
||||||
|
app(\Stancl\Tenancy\DatabaseManager::class)->reconnect();
|
||||||
|
|
||||||
|
$new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
|
||||||
|
|
||||||
|
$this->assertSame($old_connection_name, $new_connection_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function the_default_db_is_used_when_based_on_is_null()
|
||||||
|
{
|
||||||
|
config(['database.default' => 'pgsql']);
|
||||||
|
|
||||||
|
$this->assertSame('pgsql', config('database.default'));
|
||||||
|
config([
|
||||||
|
'database.connections.pgsql.foo' => 'bar',
|
||||||
|
'tenancy.database.based_on' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenancy()->init('test.localhost');
|
||||||
|
|
||||||
|
$this->assertSame('tenant', config('database.default'));
|
||||||
|
$this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function make_sure_using_schema_connection()
|
||||||
|
{
|
||||||
|
$tenant = tenancy()->create(['schema.localhost']);
|
||||||
|
tenancy()->init('schema.localhost');
|
||||||
|
|
||||||
|
$this->assertSame($tenant->getDatabaseName(), config('database.connections.' . config('database.default') . '.schema'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function databases_are_separated_using_schema_and_not_database()
|
||||||
|
{
|
||||||
|
tenancy()->create('foo.localhost');
|
||||||
|
tenancy()->init('foo.localhost');
|
||||||
|
$this->assertSame('tenant', config('database.default'));
|
||||||
|
$this->assertSame('main', config('database.connections.tenant.database'));
|
||||||
|
|
||||||
|
$schema1 = config('database.connections.' . config('database.default') . '.schema');
|
||||||
|
$database1 = config('database.connections.' . config('database.default') . '.database');
|
||||||
|
|
||||||
|
tenancy()->create('bar.localhost');
|
||||||
|
tenancy()->init('bar.localhost');
|
||||||
|
$this->assertSame('tenant', config('database.default'));
|
||||||
|
$this->assertSame('main', config('database.connections.tenant.database'));
|
||||||
|
|
||||||
|
$schema2 = config('database.connections.' . config('database.default') . '.schema');
|
||||||
|
$database2 = config('database.connections.' . config('database.default') . '.database');
|
||||||
|
|
||||||
|
$this->assertSame($database1, $database2);
|
||||||
|
$this->assertNotSame($schema1, $schema2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function schemas_are_separated()
|
||||||
|
{
|
||||||
|
// copied from DataSeparationTest
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create('tenant1.localhost');
|
||||||
|
$tenant2 = Tenant::create('tenant2.localhost');
|
||||||
|
\Artisan::call('tenants:migrate', [
|
||||||
|
'--tenants' => [$tenant1['id'], $tenant2['id']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenancy()->init('tenant1.localhost');
|
||||||
|
User::create([
|
||||||
|
'name' => 'foo',
|
||||||
|
'email' => 'foo@bar.com',
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
]);
|
||||||
|
$this->assertSame('foo', User::first()->name);
|
||||||
|
|
||||||
|
tenancy()->init('tenant2.localhost');
|
||||||
|
$this->assertSame(null, User::first());
|
||||||
|
|
||||||
|
User::create([
|
||||||
|
'name' => 'xyz',
|
||||||
|
'email' => 'xyz@bar.com',
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('xyz', User::first()->name);
|
||||||
|
$this->assertSame('xyz@bar.com', User::first()->email);
|
||||||
|
|
||||||
|
tenancy()->init('tenant1.localhost');
|
||||||
|
$this->assertSame('foo', User::first()->name);
|
||||||
|
$this->assertSame('foo@bar.com', User::first()->email);
|
||||||
|
|
||||||
|
$tenant3 = Tenant::create('tenant3.localhost');
|
||||||
|
\Artisan::call('tenants:migrate', [
|
||||||
|
'--tenants' => [$tenant1['id'], $tenant3['id']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenancy()->init('tenant3.localhost');
|
||||||
|
$this->assertSame(null, User::first());
|
||||||
|
|
||||||
|
tenancy()->init('tenant1.localhost');
|
||||||
|
\DB::table('users')->where('id', 1)->update(['name' => 'xxx']);
|
||||||
|
$this->assertSame('xxx', User::first()->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter;
|
||||||
use Stancl\Tenancy\Tenant;
|
use Stancl\Tenancy\Tenant;
|
||||||
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
|
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
|
||||||
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager;
|
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager;
|
||||||
|
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager;
|
||||||
use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;
|
use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;
|
||||||
|
|
||||||
class TenantDatabaseManagerTest extends TestCase
|
class TenantDatabaseManagerTest extends TestCase
|
||||||
|
|
@ -78,6 +79,7 @@ class TenantDatabaseManagerTest extends TestCase
|
||||||
['mysql', MySQLDatabaseManager::class],
|
['mysql', MySQLDatabaseManager::class],
|
||||||
['sqlite', SQLiteDatabaseManager::class],
|
['sqlite', SQLiteDatabaseManager::class],
|
||||||
['pgsql', PostgreSQLDatabaseManager::class],
|
['pgsql', PostgreSQLDatabaseManager::class],
|
||||||
|
['pgsql', PostgreSQLSchemaManager::class],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
Redis::connection('tenancy')->flushdb();
|
Redis::connection('tenancy')->flushdb();
|
||||||
Redis::connection('cache')->flushdb();
|
Redis::connection('cache')->flushdb();
|
||||||
|
|
||||||
|
$originalConnection = config('database.default');
|
||||||
$this->loadMigrationsFrom([
|
$this->loadMigrationsFrom([
|
||||||
'--path' => realpath(__DIR__ . '/../assets/migrations'),
|
'--path' => realpath(__DIR__ . '/../assets/migrations'),
|
||||||
'--database' => 'central',
|
'--database' => 'central',
|
||||||
]);
|
]);
|
||||||
config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
|
config(['database.default' => $originalConnection]); // fix issue caused by loadMigrationsFrom
|
||||||
|
|
||||||
if ($this->autoCreateTenant) {
|
if ($this->autoCreateTenant) {
|
||||||
$this->createTenant();
|
$this->createTenant();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue