diff --git a/DONATIONS.md b/DONATIONS.md new file mode 100644 index 00000000..0b0cb90f --- /dev/null +++ b/DONATIONS.md @@ -0,0 +1,27 @@ +# Donations + +Any donations will be greatly appreciated and help ensure that the package is developed and maintained in the future. + +If you're a company and this package is helping you make money, please consider donating. + +### PayPal + +PayPal is the preferable donation method as it comes with the lowest fees. + +You can donate here: [https://paypal.me/samuelstancl](https://paypal.me/samuelstancl) + +### Other methods + +If you can't use PayPal, you may use my Gumroad link. This comes with higher fees but any donations will be greatly appreciated nonetheless. + +You can donate here: [https://gumroad.com/l/tenancy](https://gumroad.com/l/tenancy) + +### Legal + +If you're a business making a donation, you may want an invoice. + +Contact me on [samuel.stancl@gmail.com](mailto:samuel.stancl@gmail.com) and let me know what you need to have on the invoice and I will make it happen. + +### Thank you! + +Again, any donations are greatly appreciated. Thanks to everyone who has donated, you're helping keep this package maintained. diff --git a/Dockerfile b/Dockerfile index 2de25cb0..99d5d4f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,13 +15,19 @@ RUN apt-get install -y curl zip unzip git sqlite3 \ php7.4-imap php7.4-mysql php7.4-mbstring \ php7.4-xml php7.4-zip php7.4-bcmath php7.4-soap \ php7.4-intl php7.4-readline php7.4-xdebug \ - php7.4-redis php-msgpack php-igbinary \ + php-msgpack php-igbinary \ && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ && mkdir /run/php RUN apt-get install -y python3 RUN apt-get install -y php7.4-dev php-pear + +RUN pecl install redis-4.3.0 +RUN echo "extension=redis.so" > /etc/php/7.2/mods-available/redis.ini +RUN ln -sf /etc/php/7.2/mods-available/redis.ini /etc/php/7.2/fpm/conf.d/20-redis.ini +RUN ln -sf /etc/php/7.2/mods-available/redis.ini /etc/php/7.2/cli/conf.d/20-redis.ini + RUN pecl install xdebug RUN echo 'zend_extension=/usr/lib/php/20190902/xdebug.so' > /etc/php/7.4/cli/conf.d/20-xdebug.ini diff --git a/README.md b/README.md index da359983..5f934854 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Latest Stable Version Travis CI build codecov - Donate + Donate

stancl/tenancy

diff --git a/assets/config.php b/assets/config.php index d15fb08d..96cf05db 100644 --- a/assets/config.php +++ b/assets/config.php @@ -16,6 +16,8 @@ return [ 'tenants' => 'tenants', 'domains' => 'domains', ], + 'cache_store' => false, // What store should be used to cache tenant resolution. Set to false to disable cache, null to use default store, or a string with a specific cache store name. + 'cache_ttl' => 3600, // seconds ], 'redis' => [ 'driver' => Stancl\Tenancy\StorageDrivers\RedisStorageDriver::class, @@ -30,6 +32,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. 'prefix' => 'tenant', 'suffix' => '', + 'separate_by' => 'database', // database or schema (only supported by pgsql) ], 'redis' => [ 'prefix_base' => 'tenant', @@ -61,6 +64,7 @@ return [ 'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class, 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class, 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, + // 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database ], 'database_manager_connections' => [ // Connections used by TenantDatabaseManagers. This tells, for example, the diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 34493c3f..bef3a09a 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -46,6 +46,7 @@ final class MigrateFresh extends Command $this->info('Migrating.'); $this->callSilent('tenants:migrate', [ '--tenants' => [$tenant->id], + '--force' => true, ]); }); }); diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 9890e588..0004ea69 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; use Illuminate\Foundation\Application; use Stancl\Tenancy\Contracts\Future\CanSetConnection; +use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; @@ -101,7 +102,9 @@ class DatabaseManager // Change database name. $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 * @return void * @throws TenantCannotBeCreatedException + * @throws DatabaseManagerNotRegisteredException + * @throws TenantDatabaseAlreadyExistsException */ public function ensureTenantCanBeCreated(Tenant $tenant): void { @@ -161,6 +166,7 @@ class DatabaseManager * @param Tenant $tenant * @param ShouldQueue[]|callable[] $afterCreating * @return void + * @throws DatabaseManagerNotRegisteredException */ public function createDatabase(Tenant $tenant, array $afterCreating = []) { @@ -202,6 +208,7 @@ class DatabaseManager * * @param Tenant $tenant * @return void + * @throws DatabaseManagerNotRegisteredException */ public function deleteDatabase(Tenant $tenant) { @@ -224,6 +231,7 @@ class DatabaseManager * * @param Tenant $tenant * @return TenantDatabaseManager + * @throws DatabaseManagerNotRegisteredException */ public function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager { @@ -243,4 +251,20 @@ class 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'; + } } diff --git a/src/StorageDrivers/Database/CachedTenantResolver.php b/src/StorageDrivers/Database/CachedTenantResolver.php new file mode 100644 index 00000000..9a4a345c --- /dev/null +++ b/src/StorageDrivers/Database/CachedTenantResolver.php @@ -0,0 +1,68 @@ +cache = $cacheManager->store($config->get('tenancy.storage_drivers.db.cache_store')); + $this->config = $config; + } + + protected function ttl(): int + { + return $this->config->get('tenancy.storage_drivers.db.cache_ttl'); + } + + public function getTenantIdByDomain(string $domain, Closure $query): string + { + return $this->cache->remember('_tenancy_domain_to_id:' . $domain, $this->ttl(), $query); + } + + public function getDataById(string $id, Closure $dataQuery): ?array + { + return $this->cache->remember('_tenancy_id_to_data:' . $id, $this->ttl(), $dataQuery); + } + + public function getDomainsById(string $id, Closure $domainsQuery): ?array + { + return $this->cache->remember('_tenancy_id_to_domains:' . $id, $this->ttl(), $domainsQuery); + } + + public function invalidateTenant(string $id): void + { + $this->invalidateTenantData($id); + $this->invalidateTenantDomains($id); + } + + public function invalidateTenantData(string $id): void + { + $this->cache->forget('_tenancy_id_to_data:' . $id); + } + + public function invalidateTenantDomains(string $id): void + { + $this->cache->forget('_tenancy_id_to_domains:' . $id); + } + + public function invalidateDomainToIdMapping(array $domains): void + { + foreach ($domains as $domain) { + $this->cache->forget('_tenancy_domain_to_id:' . $domain); + } + } +} diff --git a/src/StorageDrivers/Database/DatabaseStorageDriver.php b/src/StorageDrivers/Database/DatabaseStorageDriver.php index 383cb890..42ce00b4 100644 --- a/src/StorageDrivers/Database/DatabaseStorageDriver.php +++ b/src/StorageDrivers/Database/DatabaseStorageDriver.php @@ -32,12 +32,16 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAn /** @var DomainRepository */ protected $domains; + /** @var CachedTenantResolver */ + protected $cache; + /** @var Tenant The default tenant. */ protected $tenant; - public function __construct(Application $app, ConfigRepository $config) + public function __construct(Application $app, ConfigRepository $config, CachedTenantResolver $cache) { $this->app = $app; + $this->cache = $cache; $this->centralDatabase = $this->getCentralConnection(); $this->tenants = new TenantRepository($config); $this->domains = new DomainRepository($config); @@ -60,7 +64,16 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAn public function findByDomain(string $domain): Tenant { - $id = $this->domains->getTenantIdByDomain($domain); + $query = function () use ($domain) { + return $this->domains->getTenantIdByDomain($domain); + }; + + if ($this->usesCache()) { + $id = $this->cache->getTenantIdByDomain($domain, $query); + } else { + $id = $query(); + } + if (! $id) { throw new TenantCouldNotBeIdentifiedException($domain); } @@ -70,14 +83,29 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAn public function findById(string $id): Tenant { - $tenant = $this->tenants->find($id); + $dataQuery = function () use ($id) { + $data = $this->tenants->find($id); - if (! $tenant) { + return $data ? $this->tenants->decodeData($data) : null; + }; + $domainsQuery = function () use ($id) { + return $this->domains->getTenantDomains($id); + }; + + if ($this->usesCache()) { + $data = $this->cache->getDataById($id, $dataQuery); + $domains = $this->cache->getDomainsById($id, $domainsQuery); + } else { + $data = $dataQuery(); + $domains = $domainsQuery(); + } + + if (! $data) { throw new TenantDoesNotExistException($id); } - return Tenant::fromStorage($this->tenants->decodeData($tenant)) - ->withDomains($this->domains->getTenantDomains($id)); + return Tenant::fromStorage($data) + ->withDomains($domains); } /** @@ -128,19 +156,33 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAn public function updateTenant(Tenant $tenant): void { - $this->centralDatabase->transaction(function () use ($tenant) { + $originalDomains = $this->domains->getTenantDomains($tenant); + + $this->centralDatabase->transaction(function () use ($tenant, $originalDomains) { $this->tenants->updateTenant($tenant); - $this->domains->updateTenantDomains($tenant); + $this->domains->updateTenantDomains($tenant, $originalDomains); }); + + if ($this->usesCache()) { + $this->cache->invalidateTenant($tenant->id); + $this->cache->invalidateDomainToIdMapping($originalDomains); + } } public function deleteTenant(Tenant $tenant): void { + $originalDomains = $this->domains->getTenantDomains($tenant); + $this->centralDatabase->transaction(function () use ($tenant) { $this->tenants->where('id', $tenant->id)->delete(); $this->domains->where('tenant_id', $tenant->id)->delete(); }); + + if ($this->usesCache()) { + $this->cache->invalidateTenant($tenant->id); + $this->cache->invalidateDomainToIdMapping($originalDomains); + } } /** @@ -179,16 +221,37 @@ class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAn public function put(string $key, $value, Tenant $tenant = null): void { - $this->tenants->put($key, $value, $tenant ?? $this->currentTenant()); + $tenant = $tenant ?? $this->currentTenant(); + $this->tenants->put($key, $value, $tenant); + + if ($this->usesCache()) { + $this->cache->invalidateTenantData($tenant->id); + } } public function putMany(array $kvPairs, Tenant $tenant = null): void { - $this->tenants->putMany($kvPairs, $tenant ?? $this->currentTenant()); + $tenant = $tenant ?? $this->currentTenant(); + $this->tenants->putMany($kvPairs, $tenant); + + if ($this->usesCache()) { + $this->cache->invalidateTenantData($tenant->id); + } } public function deleteMany(array $keys, Tenant $tenant = null): void { - $this->tenants->deleteMany($keys, $tenant ?? $this->currentTenant()); + $tenant = $tenant ?? $this->currentTenant(); + $this->tenants->deleteMany($keys, $tenant); + + if ($this->usesCache()) { + $this->cache->invalidateTenantData($tenant->id); + } + } + + public function usesCache(): bool + { + // null is also truthy here + return $this->app['config']['tenancy.storage_drivers.db.cache_store'] !== false; } } diff --git a/src/StorageDrivers/Database/DomainRepository.php b/src/StorageDrivers/Database/DomainRepository.php index e8ac2f13..4e21b9ad 100644 --- a/src/StorageDrivers/Database/DomainRepository.php +++ b/src/StorageDrivers/Database/DomainRepository.php @@ -33,9 +33,8 @@ class DomainRepository extends Repository }, $tenant->domains)); } - public function updateTenantDomains(Tenant $tenant) + public function updateTenantDomains(Tenant $tenant, array $originalDomains) { - $originalDomains = $this->getTenantDomains($tenant); $deletedDomains = array_diff($originalDomains, $tenant->domains); $newDomains = array_diff($tenant->domains, $originalDomains); diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php new file mode 100644 index 00000000..a93ed901 --- /dev/null +++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -0,0 +1,47 @@ +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'"); + } +} diff --git a/src/TenantManager.php b/src/TenantManager.php index d0d8b428..e25b5711 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -24,11 +24,7 @@ class TenantManager { use ForwardsCalls; - /** - * The current tenant. - * - * @var Tenant - */ + /** @var Tenant The current tenant. */ protected $tenant; /** @var Application */ diff --git a/test b/test index 3f8244b3..1d492f02 100755 --- a/test +++ b/test @@ -1,7 +1,7 @@ #!/bin/bash set -e -printf "Variant 1\n\n" -docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/2.cov "$@" -printf "Variant 2\n\n" -docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/1.cov "$@" +printf "Variant 1 (DB)\n\n" +docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/1.cov "$@" +printf "Variant 2 (Redis)\n\n" +docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/2.cov "$@" diff --git a/tests/CachedResolverTest.php b/tests/CachedResolverTest.php new file mode 100644 index 00000000..903539fd --- /dev/null +++ b/tests/CachedResolverTest.php @@ -0,0 +1,164 @@ +markTestSkipped('This test is only relevant for the DB storage driver.'); + } + + config(['tenancy.storage_drivers.db.cache_store' => null]); // default driver + } + + /** @test */ + public function a_query_is_not_made_for_tenant_id_once_domain_is_cached() + { + $tenant = Tenant::new() + ->withData(['foo' => 'bar']) + ->withDomains(['foo.localhost']) + ->save(); + + // query is made + $queried = tenancy()->findByDomain('foo.localhost'); + $this->assertEquals($tenant->data, $queried->data); + $this->assertSame($tenant->domains, $queried->domains); + + // cache is set + $this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost')); + $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id)); + $this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); + + // query is not made + DatabaseStorageDriver::getCentralConnection()->enableQueryLog(); + $cached = tenancy()->findByDomain('foo.localhost'); + $this->assertEquals($tenant->data, $cached->data); + $this->assertSame($tenant->domains, $cached->domains); + $this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog()); + } + + /** @test */ + public function a_query_is_not_made_for_tenant_once_id_is_cached() + { + $tenant = Tenant::new() + ->withData(['foo' => 'bar']) + ->withDomains(['foo.localhost']) + ->save(); + + // query is made + $queried = tenancy()->find($tenant->id); + $this->assertEquals($tenant->data, $queried->data); + $this->assertSame($tenant->domains, $queried->domains); + + // cache is set + $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id)); + $this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); + + // query is not made + DatabaseStorageDriver::getCentralConnection()->enableQueryLog(); + $cached = tenancy()->find($tenant->id); + $this->assertEquals($tenant->data, $cached->data); + $this->assertSame($tenant->domains, $cached->domains); + $this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog()); + } + + /** @test */ + public function modifying_tenant_domains_invalidates_the_cached_domain_to_id_mapping() + { + $tenant = Tenant::new() + ->withDomains(['foo.localhost', 'bar.localhost']) + ->save(); + + // queried + $this->assertSame($tenant->id, tenancy()->findByDomain('foo.localhost')->id); + $this->assertSame($tenant->id, tenancy()->findByDomain('bar.localhost')->id); + + // assert cache set + $this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost')); + $this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:bar.localhost')); + + $tenant + ->removeDomains(['foo.localhost', 'bar.localhost']) + ->addDomains(['xyz.localhost']) + ->save(); + + // assert neither domain is cached + $this->assertSame(null, Cache::get('_tenancy_domain_to_id:foo.localhost')); + $this->assertSame(null, Cache::get('_tenancy_domain_to_id:bar.localhost')); + $this->assertSame(null, Cache::get('_tenancy_domain_to_id:xyz.localhost')); + } + + /** @test */ + public function modifying_tenants_data_invalidates_tenant_data_cache() + { + $tenant = Tenant::new()->withData(['foo' => 'bar'])->save(); + + // cache record is set + $this->assertSame('bar', tenancy()->find($tenant->id)->get('foo')); + $this->assertSame('bar', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']); + + // cache record is invalidated + $tenant->set('foo', 'xyz'); + $this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id)); + + // cache record is set + $this->assertSame('xyz', tenancy()->find($tenant->id)->get('foo')); + $this->assertSame('xyz', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']); + + // cache record is invalidated + $tenant->foo = 'abc'; + $tenant->save(); + $this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id)); + } + + /** @test */ + public function modifying_tenants_domains_invalidates_tenant_domain_cache() + { + $tenant = Tenant::new() + ->withData(['foo' => 'bar']) + ->withDomains(['foo.localhost']) + ->save(); + + // cache record is set + $this->assertSame(['foo.localhost'], tenancy()->find($tenant->id)->domains); + $this->assertSame(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id)); + + // cache record is invalidated + $tenant->addDomains(['bar.localhost'])->save(); + $this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); + + $this->assertEquals(['foo.localhost', 'bar.localhost'], tenancy()->find($tenant->id)->domains); + } + + /** @test */ + public function deleting_a_tenant_invalidates_all_caches() + { + $tenant = Tenant::new() + ->withData(['foo' => 'bar']) + ->withDomains(['foo.localhost']) + ->save(); + + tenancy()->findByDomain('foo.localhost'); + $this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost')); + $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id)); + $this->assertEquals(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id)); + + $tenant->delete(); + $this->assertEquals(null, Cache::get('_tenancy_domain_to_id:foo.localhost')); + $this->assertEquals(null, Cache::get('_tenancy_id_to_data:' . $tenant->id)); + $this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); + } +} diff --git a/tests/DatabaseSchemaManagerTest.php b/tests/DatabaseSchemaManagerTest.php new file mode 100644 index 00000000..5f9589e0 --- /dev/null +++ b/tests/DatabaseSchemaManagerTest.php @@ -0,0 +1,143 @@ +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); + } +} diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index fc3c34f4..89c0bbd7 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter; use Stancl\Tenancy\Tenant; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager; +use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager; use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager; class TenantDatabaseManagerTest extends TestCase @@ -78,6 +79,7 @@ class TenantDatabaseManagerTest extends TestCase ['mysql', MySQLDatabaseManager::class], ['sqlite', SQLiteDatabaseManager::class], ['pgsql', PostgreSQLDatabaseManager::class], + ['pgsql', PostgreSQLSchemaManager::class], ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 336bff07..257964d1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,11 +24,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase Redis::connection('tenancy')->flushdb(); Redis::connection('cache')->flushdb(); + $originalConnection = config('database.default'); $this->loadMigrationsFrom([ '--path' => realpath(__DIR__ . '/../assets/migrations'), '--database' => 'central', ]); - config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom + config(['database.default' => $originalConnection]); // fix issue caused by loadMigrationsFrom if ($this->autoCreateTenant) { $this->createTenant();