From e02bd3927a700195631d66c09c4999f04f2d3b79 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samuel=20=C5=A0tancl?=
Date: Sun, 8 Mar 2020 14:57:18 +0100
Subject: [PATCH 1/7] Single-line docblock
---
src/TenantManager.php | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
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 */
From d0023c482a25c16604d8d5da8d14be8fd8a3a644 Mon Sep 17 00:00:00 2001
From: Noor Adiana
Date: Wed, 11 Mar 2020 02:15:07 +0700
Subject: [PATCH 2/7] Add support for postgres schema (#237)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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
---
assets/config.php | 2 +
src/DatabaseManager.php | 26 +++-
.../PostgreSQLSchemaManager.php | 47 ++++++
tests/DatabaseSchemaManagerTest.php | 143 ++++++++++++++++++
tests/TenantDatabaseManagerTest.php | 2 +
tests/TestCase.php | 3 +-
6 files changed, 221 insertions(+), 2 deletions(-)
create mode 100644 src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
create mode 100644 tests/DatabaseSchemaManagerTest.php
diff --git a/assets/config.php b/assets/config.php
index d15fb08d..9155dae4 100644
--- a/assets/config.php
+++ b/assets/config.php
@@ -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.
'prefix' => 'tenant',
'suffix' => '',
+ 'separate_by' => 'database', // database or schema (only supported by pgsql)
],
'redis' => [
'prefix_base' => 'tenant',
@@ -61,6 +62,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/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/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/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();
From d0bd8f2ad88d154fe448f5ed93c18ab8749e272f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samuel=20=C5=A0tancl?=
Date: Tue, 10 Mar 2020 21:33:25 +0100
Subject: [PATCH 3/7] Force migrate when running MigrateFresh
---
src/Commands/MigrateFresh.php | 1 +
1 file changed, 1 insertion(+)
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,
]);
});
});
From c3bd02bc128a46773fe463cd211d075bc39aa9d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samuel=20=C5=A0tancl?=
Date: Tue, 10 Mar 2020 23:03:22 +0100
Subject: [PATCH 4/7] Create DONATIONS.md
---
DONATIONS.md | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 DONATIONS.md
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.
From 142912edc5f4099c58a6b0bedacf1428f5e52ee4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samuel=20=C5=A0tancl?=
Date: Tue, 10 Mar 2020 23:04:15 +0100
Subject: [PATCH 5/7] Update donation link
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index da359983..5f934854 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
-
+
From c7c6a7fec85a594bf3ee337def09b19acba3caed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samuel=20=C5=A0tancl?=
Date: Sat, 14 Mar 2020 19:52:35 +0100
Subject: [PATCH 6/7] [2.3.0] Cached tenant lookup (#316)
* Begin work on cached lookup
* Apply fixes from StyleCI
* wip
* wip cache invalidation
* Apply fixes from StyleCI
* Finish cache invalidation
* Apply fixes from StyleCI
* Remove config from TestCase
* Enable cache in the single test file
* Separate data & domains logic
* Apply fixes from StyleCI
* wip
* Apply fixes from StyleCI
---
assets/config.php | 2 +
.../Database/CachedTenantResolver.php | 68 ++++++++
.../Database/DatabaseStorageDriver.php | 85 +++++++--
.../Database/DomainRepository.php | 3 +-
test | 8 +-
tests/CachedResolverTest.php | 164 ++++++++++++++++++
6 files changed, 313 insertions(+), 17 deletions(-)
create mode 100644 src/StorageDrivers/Database/CachedTenantResolver.php
create mode 100644 tests/CachedResolverTest.php
diff --git a/assets/config.php b/assets/config.php
index 9155dae4..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,
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/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));
+ }
+}
From 13422fb0901c0982b921ea7ec6e0381b284bdea7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samuel=20=C5=A0tancl?=
Date: Sun, 15 Mar 2020 15:45:21 +0100
Subject: [PATCH 7/7] Use phpredis 4.3.0 (#319)
---
Dockerfile | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index a6512668..285a645f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,14 +16,16 @@ RUN apt-get update \
&& php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \
&& mkdir /run/php
-RUN apt-get install -y php7.2-redis
-
RUN apt-get install -y python3
RUN apt-get install -y php7.2-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 '' > /etc/php/7.2/cli/conf.d/20-xdebug.ini
-# RUN echo 'zend_extension=/usr/lib/php/20170718/xdebug.so' >> /etc/php/7.2/cli/php.ini
RUN echo 'zend_extension=/usr/lib/php/20170718/xdebug.so' > /etc/php/7.2/cli/conf.d/20-xdebug.ini
RUN apt-get -y autoremove \