From b58b068434718c00679ee0dffad371b13fc33e13 Mon Sep 17 00:00:00 2001 From: Jasper Zonneveld Date: Wed, 3 Nov 2021 10:21:51 +0100 Subject: [PATCH 01/10] Add missing import for Domain model (#745) --- src/TenancyServiceProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index cf326792..4faaccf3 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy; use Illuminate\Cache\CacheManager; use Illuminate\Support\ServiceProvider; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; +use Stancl\Tenancy\Contracts\Domain; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Resolvers\DomainTenantResolver; From f12c826df52c91fa5a024e8b1982b1056baed728 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Thu, 18 Nov 2021 01:45:44 +0500 Subject: [PATCH 02/10] Use GitHub forms for issues template. (#755) * Create bug-report_new.md * wip * Delete bug-report.md * Update bug-report.yml --- .github/ISSUE_TEMPLATE/bug-report.md | 21 ------------ .github/ISSUE_TEMPLATE/bug-report.yml | 48 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 21 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 9f3d2e65..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: "\U0001F41B Bug Report" -about: Report unexpected behavior with stancl/tenancy. -title: '' -labels: bug -assignees: stancl - ---- - -#### Describe the bug - - -#### Steps to reproduce - - -#### Expected behavior -A clear and concise description of what you expected to happen. - -#### Your setup - - Laravel version: [e.g. 8.2.0] - - stancl/tenancy version: [e.g. 3.1.0] diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..75e345b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,48 @@ +name: 🐛 Bug Report +description: Report unexpected behavior with stancl/tenancy. +labels: ["bug"] +assignees: + - stancl +body: + - type: markdown + attributes: + value: | + Before opening a bug report, please search for the behaviour in the existing issues. + --- + Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information. + - type: textarea + id: bug-description + attributes: + label: Bug description + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Step-by-step guide for reproducing the bug in a fresh Laravel application. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: input + id: laravel-version + attributes: + label: Laravel version + placeholder: "e.g. 8.2.0" + validations: + required: true + - type: input + id: tenancy-version + attributes: + label: stancl/tenancy version + placeholder: "e.g. 3.1.0" + validations: + required: true From 2726f07bcaecca6351ad2716a04df591042f4972 Mon Sep 17 00:00:00 2001 From: Frederic Habich <53627251+CodeAdminDe@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:24:07 +0100 Subject: [PATCH 03/10] fixed typo (#766) fixed typo within description 'searhced' => 'searched' --- src/Features/UniversalRoutes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/UniversalRoutes.php b/src/Features/UniversalRoutes.php index 970dcd7b..6b729962 100644 --- a/src/Features/UniversalRoutes.php +++ b/src/Features/UniversalRoutes.php @@ -40,7 +40,7 @@ class UniversalRoutes implements Feature } // Loop one level deep and check if the route's middleware - // groups have the searhced middleware group inside them + // groups have the searched middleware group inside them $middlewareGroups = Router::getMiddlewareGroups(); foreach ($route->gatherMiddleware() as $inner) { if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) { From 08bfd6f9bb04e5364f6d9855e216c9c1bfcf54de Mon Sep 17 00:00:00 2001 From: sort72 <47725212+sort72@users.noreply.github.com> Date: Sat, 25 Dec 2021 09:24:34 -0500 Subject: [PATCH 04/10] Use tenant key on console commands instead of id (#768) --- src/Commands/Migrate.php | 2 +- src/Commands/Rollback.php | 2 +- src/Commands/Run.php | 2 +- src/Commands/Seed.php | 2 +- src/Commands/TenantList.php | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 4bf8408c..bf92dfcd 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -55,7 +55,7 @@ class Migrate extends MigrateCommand } tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); event(new MigratingDatabase($tenant)); diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index ec9cc461..081872c8 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -53,7 +53,7 @@ class Rollback extends RollbackCommand } tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); event(new RollingBackDatabase($tenant)); diff --git a/src/Commands/Run.php b/src/Commands/Run.php index c2770825..4216d1c6 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -33,7 +33,7 @@ class Run extends Command public function handle() { tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); tenancy()->initialize($tenant); $callback = function ($prefix = '') { diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 43038107..dc97ae71 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -51,7 +51,7 @@ class Seed extends SeedCommand } tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); event(new SeedingDatabase($tenant)); diff --git a/src/Commands/TenantList.php b/src/Commands/TenantList.php index 493b5a93..d01afcb9 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -36,9 +36,9 @@ class TenantList extends Command ->cursor() ->each(function (Tenant $tenant) { if ($tenant->domains) { - $this->line("[Tenant] id: {$tenant['id']} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); + $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); } else { - $this->line("[Tenant] id: {$tenant['id']}"); + $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); } }); } From 435d8528a75acdbdc2994962bc47e90f4288aed1 Mon Sep 17 00:00:00 2001 From: Stefan Ninic Date: Sat, 25 Dec 2021 22:10:34 +0100 Subject: [PATCH 05/10] Fixed array to string conversion (#718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed array to string conversion Previous code would give this warning before actually showing exception message `PHP Warning: Array to string conversion in .../vendor/stancl/tenancy/src/CacheManager.php on line 24` * Update variable & syntax Co-authored-by: Samuel Štancl --- src/CacheManager.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CacheManager.php b/src/CacheManager.php index f7190842..88428353 100644 --- a/src/CacheManager.php +++ b/src/CacheManager.php @@ -20,8 +20,10 @@ class CacheManager extends BaseCacheManager $tags = [config('tenancy.cache.tag_base') . tenant()->getTenantKey()]; if ($method === 'tags') { - if (count($parameters) !== 1) { - throw new \Exception("Method tags() takes exactly 1 argument. {count($parameters)} passed."); + $count = count($parameters); + + if ($count !== 1) { + throw new \Exception("Method tags() takes exactly 1 argument. $count passed."); } $names = $parameters[0]; From 73a4a3018cadca2ba0fb5f2130fca1718a2b3670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:10:03 +0100 Subject: [PATCH 06/10] Improve queue tenancy --- docker-compose.yml | 2 +- .../DatabaseTenancyBootstrapper.php | 1 + .../QueueTenancyBootstrapper.php | 80 +++++++-- src/Database/DatabaseManager.php | 23 ++- .../TenantCouldNotBeIdentifiedById.php | 2 +- src/Tenancy.php | 4 +- tests/QueueTest.php | 152 ++++++++++++++++-- tests/TenantDatabaseManagerTest.php | 51 ++++++ 8 files changed, 279 insertions(+), 36 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 30d87dfd..e8e8d418 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: build: context: . args: - PHP_VERSION: ${PHP_VERSION} + PHP_VERSION: ${PHP_VERSION:-8.1} depends_on: mysql: condition: service_healthy diff --git a/src/Bootstrappers/DatabaseTenancyBootstrapper.php b/src/Bootstrappers/DatabaseTenancyBootstrapper.php index a107fc0d..59ee0aec 100644 --- a/src/Bootstrappers/DatabaseTenancyBootstrapper.php +++ b/src/Bootstrappers/DatabaseTenancyBootstrapper.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Bootstrappers; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException; diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 6fefaad2..5706963e 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -7,7 +7,10 @@ namespace Stancl\Tenancy\Bootstrappers; use Illuminate\Config\Repository; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Queue\Events\JobFailed; +use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Queue\Events\JobRetryRequested; use Illuminate\Queue\QueueManager; use Illuminate\Support\Testing\Fakes\QueueFake; use Stancl\Tenancy\Contracts\TenancyBootstrapper; @@ -28,7 +31,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper */ public static function __constructStatic(Application $app) { - static::setUpJobListener($app->make(Dispatcher::class)); + static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests()); } public function __construct(Repository $config, QueueManager $queue) @@ -39,25 +42,70 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper $this->setUpPayloadGenerator(); } - protected static function setUpJobListener($dispatcher) + protected static function setUpJobListener($dispatcher, $runningTests) { - $dispatcher->listen(JobProcessing::class, function ($event) { - $tenantId = $event->job->payload()['tenant_id'] ?? null; + $previousTenant = null; - // The job is not tenant-aware - if (! $tenantId) { - return; - } + $dispatcher->listen(JobProcessing::class, function ($event) use (&$previousTenant) { + $previousTenant = tenant(); - // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) - if (tenancy()->initialized && tenant()->getTenantKey() === $tenantId) { - return; - } - - // Tenancy was either not initialized, or initialized for a different tenant. - // Therefore, we initialize it for the correct tenant. - tenancy()->initialize(tenancy()->find($tenantId)); + static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); }); + + $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { + $previousTenant = tenant(); + + static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); + }); + + // If we're running tests, we make sure to clean up after any artisan('queue:work') calls + $revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) { + if ($runningTests) { + static::revertToPreviousState($event, $previousTenant); + } + }; + + $dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds + $dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails + } + + protected static function initializeTenancyForQueue($tenantId) + { + // The job is not tenant-aware + if (! $tenantId) { + return; + } + + if (tenancy()->initialized) { + if (tenant()->getTenantKey() === $tenantId) { + // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) + return; + } + } + + // Tenancy was either not initialized, or initialized for a different tenant. + // Therefore, we initialize it for the correct tenant. + tenancy()->initialize(tenancy()->find($tenantId)); + } + + protected static function revertToPreviousState($event, &$previousTenant) + { + $tenantId = $event->job->payload()['tenant_id'] ?? null; + + // The job was not tenant-aware + if (! $tenantId) { + return; + } + + // Revert back to the previous tenant + if (tenant() && $previousTenant && $previousTenant->isNot(tenant())) { + tenancy()->initialize($previousTenant); + } + + // End tenancy + if (tenant() && (! $previousTenant)) { + tenancy()->end(); + } } protected function setUpPayloadGenerator() diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index dd30f443..e85fd659 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -38,7 +38,7 @@ class DatabaseManager */ public function connectToTenant(TenantWithDatabase $tenant) { - $this->database->purge('tenant'); + $this->purgeTenantConnection(); $this->createTenantConnection($tenant); $this->setDefaultConnection('tenant'); } @@ -48,10 +48,7 @@ class DatabaseManager */ public function reconnectToCentral() { - if (tenancy()->initialized) { - $this->database->purge('tenant'); - } - + $this->purgeTenantConnection(); $this->setDefaultConnection($this->config->get('tenancy.database.central_connection')); } @@ -60,7 +57,7 @@ class DatabaseManager */ public function setDefaultConnection(string $connection) { - $this->app['config']['database.default'] = $connection; + $this->config['database.default'] = $connection; $this->database->setDefaultConnection($connection); } @@ -69,7 +66,19 @@ class DatabaseManager */ public function createTenantConnection(TenantWithDatabase $tenant) { - $this->app['config']['database.connections.tenant'] = $tenant->database()->connection(); + $this->config['database.connections.tenant'] = $tenant->database()->connection(); + } + + /** + * Purge the tenant database connection. + */ + public function purgeTenantConnection() + { + if (array_key_exists('tenant', $this->database->getConnections())) { + $this->database->purge('tenant'); + } + + unset($this->config['database.connections.tenant']); } /** diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedById.php b/src/Exceptions/TenantCouldNotBeIdentifiedById.php index 8fa103ea..5c2e562c 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedById.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedById.php @@ -9,7 +9,7 @@ use Facade\IgnitionContracts\ProvidesSolution; use Facade\IgnitionContracts\Solution; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; -// todo: in v3 this should be suffixed with Exception +// todo: in v4 this should be suffixed with Exception class TenantCouldNotBeIdentifiedById extends TenantCouldNotBeIdentifiedException implements ProvidesSolution { public function __construct($tenant_id) diff --git a/src/Tenancy.php b/src/Tenancy.php index 864c00f0..30f138e3 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -66,10 +66,10 @@ class Tenancy return; } - $this->initialized = false; - event(new Events\TenancyEnded($this)); + $this->initialized = false; + $this->tenant = null; } diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 41d71320..75c727ce 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -4,18 +4,30 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; use Spatie\Valuestore\Valuestore; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; +use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Tests\Etc\User; class QueueTest extends TestCase { @@ -31,15 +43,49 @@ class QueueTest extends TestCase config([ 'tenancy.bootstrappers' => [ QueueTenancyBootstrapper::class, + DatabaseTenancyBootstrapper::class, ], 'queue.default' => 'redis', ]); Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush(); } + protected function withFailedJobs() + { + Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + protected function withUsers() + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + protected function withTenantDatabases() + { + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + } + /** @test */ public function tenant_id_is_passed_to_tenant_queues() { @@ -49,7 +95,7 @@ class QueueTest extends TestCase tenancy()->initialize($tenant); - Event::fake([JobProcessing::class]); + Event::fake([JobProcessing::class, JobProcessed::class]); dispatch(new TestJob($this->valuestore)); @@ -79,21 +125,91 @@ class QueueTest extends TestCase }); } - /** @test */ - public function tenancy_is_initialized_inside_queues() + /** + * @test + * + * @testWith [true] + * [false] + */ + public function tenancy_is_initialized_inside_queues(bool $shouldEndTenancy) { - $tenant = Tenant::create([ - 'id' => 'acme', - ]); + $this->withTenantDatabases(); + $this->withFailedJobs(); + + $tenant = Tenant::create(); tenancy()->initialize($tenant); - dispatch(new TestJob($this->valuestore)); + $this->withUsers(); + + $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + + $this->valuestore->put('userName', 'Bar'); + + dispatch(new TestJob($this->valuestore, $user)); $this->assertFalse($this->valuestore->has('tenant_id')); + + if ($shouldEndTenancy) { + tenancy()->end(); + } + $this->artisan('queue:work --once'); - $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); + $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count()); + + $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id')); + + $tenant->run(function () use ($user) { + $this->assertSame('Bar', $user->fresh()->name); + }); + } + + /** + * @test + * + * @testWith [true] + * [false] + */ + public function tenancy_is_initialized_when_retrying_jobs(bool $shouldEndTenancy) + { + $this->withFailedJobs(); + $this->withTenantDatabases(); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + $this->withUsers(); + + $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + + $this->valuestore->put('userName', 'Bar'); + $this->valuestore->put('shouldFail', true); + + dispatch(new TestJob($this->valuestore, $user)); + + $this->assertFalse($this->valuestore->has('tenant_id')); + + if ($shouldEndTenancy) { + tenancy()->end(); + } + + $this->artisan('queue:work --once'); + + $this->assertSame(1, DB::connection('central')->table('failed_jobs')->count()); + $this->assertNull($this->valuestore->get('tenant_id')); // job failed + + $this->artisan('queue:retry all'); + $this->artisan('queue:work --once'); + + $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count()); + + $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id')); // job succeeded + + $tenant->run(function () use ($user) { + $this->assertSame('Bar', $user->fresh()->name); + }); } /** @test */ @@ -127,13 +243,31 @@ class TestJob implements ShouldQueue /** @var Valuestore */ protected $valuestore; - public function __construct(Valuestore $valuestore) + /** @var User|null */ + protected $user; + + public function __construct(Valuestore $valuestore, User $user = null) { $this->valuestore = $valuestore; + $this->user = $user; } public function handle() { + if ($this->valuestore->get('shouldFail')) { + $this->valuestore->put('shouldFail', false); + + throw new Exception('failing'); + } + + if ($this->user) { + assert($this->user->getConnectionName() === 'tenant'); + } + $this->valuestore->put('tenant_id', 'The current tenant id is: ' . tenant('id')); + + if ($userName = $this->valuestore->get('userName')) { + $this->user->update(['name' => $userName]); + } } } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index ead2bba8..f64770b1 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -4,17 +4,21 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use PDO; use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Database\DatabaseManager; +use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager; @@ -102,6 +106,52 @@ class TenantDatabaseManagerTest extends TestCase ]; } + /** @test */ + public function the_tenant_connection_is_fully_removed() + { + config([ + 'tenancy.boostrappers' => [ + DatabaseTenancyBootstrapper::class, + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + $tenant = Tenant::create(); + + $this->assertSame(['central'], array_keys(app('db')->getConnections())); + $this->assertArrayNotHasKey('tenant', config('database.connections')); + + tenancy()->initialize($tenant); + + $this->createUsersTable(); + + $this->assertSame(['central', 'tenant'], array_keys(app('db')->getConnections())); + $this->assertArrayHasKey('tenant', config('database.connections')); + + tenancy()->end(); + + $this->assertSame(['central'], array_keys(app('db')->getConnections())); + $this->assertNull(config('database.connections.tenant')); + } + + protected function createUsersTable() + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + /** @test */ public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() { @@ -217,5 +267,6 @@ class TenantDatabaseManagerTest extends TestCase /** @test */ public function path_used_by_sqlite_manager_can_be_customized() { + $this->markTestIncomplete(); } } From 49ef28da059bb0423f9ccd8f9f88b43cd58cfb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:19:53 +0100 Subject: [PATCH 07/10] 6.x support --- tests/QueueTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 75c727ce..9758c9c8 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -57,7 +57,7 @@ class QueueTest extends TestCase protected function withFailedJobs() { Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { - $table->id(); + $table->increments('id'); $table->string('uuid')->unique(); $table->text('connection'); $table->text('queue'); From a83568ded280ebca364ad898f28db2ec047f87a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:28:37 +0100 Subject: [PATCH 08/10] Only use JobRetryRequested in Laravel 8 --- .../QueueTenancyBootstrapper.php | 20 ++++++---- tests/Etc/tmp/queuetest.json | 2 +- tests/QueueTest.php | 37 +++++++++++-------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 5706963e..c94d6749 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -4,17 +4,18 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; +use Illuminate\Support\Str; use Illuminate\Config\Repository; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Contracts\Foundation\Application; +use Illuminate\Queue\QueueManager; +use Stancl\Tenancy\Contracts\Tenant; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\Events\JobRetryRequested; -use Illuminate\Queue\QueueManager; use Illuminate\Support\Testing\Fakes\QueueFake; +use Illuminate\Contracts\Foundation\Application; use Stancl\Tenancy\Contracts\TenancyBootstrapper; -use Stancl\Tenancy\Contracts\Tenant; class QueueTenancyBootstrapper implements TenancyBootstrapper { @@ -52,11 +53,14 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); }); - $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { - $previousTenant = tenant(); + if (Str::startsWith(app()->version(), '8')) { + // queue:retry tenancy is only supported in Laravel 8 + $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { + $previousTenant = tenant(); - static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); - }); + static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); + }); + } // If we're running tests, we make sure to clean up after any artisan('queue:work') calls $revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) { diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json index 00cf7c37..dc158c6b 100644 --- a/tests/Etc/tmp/queuetest.json +++ b/tests/Etc/tmp/queuetest.json @@ -1 +1 @@ -{"tenant_id":"The current tenant id is: acme"} \ No newline at end of file +{"userName":"Bar","shouldFail":false,"tenant_id":"The current tenant id is: a7f73c10-9879-40ae-b7b0-1ded7c1f7b1b"} \ No newline at end of file diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 9758c9c8..158ad56b 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -5,29 +5,30 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; use Exception; +use Illuminate\Support\Str; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use Spatie\Valuestore\Valuestore; +use Illuminate\Support\Facades\DB; +use Stancl\Tenancy\Tests\Etc\User; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Support\Facades\Event; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Schema; +use Stancl\Tenancy\Events\TenancyEnded; +use Stancl\Tenancy\Jobs\CreateDatabase; +use Illuminate\Queue\InteractsWithQueue; +use Stancl\Tenancy\Events\TenantCreated; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Schema; -use Spatie\Valuestore\Valuestore; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; -use Stancl\Tenancy\Events\TenancyEnded; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Events\TenantCreated; -use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Tests\Etc\Tenant; -use Stancl\Tenancy\Tests\Etc\User; +use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; class QueueTest extends TestCase { @@ -173,6 +174,10 @@ class QueueTest extends TestCase */ public function tenancy_is_initialized_when_retrying_jobs(bool $shouldEndTenancy) { + if (! Str::startsWith(app()->version(), '8')) { + $this->markTestSkipped('queue:retry tenancy is only supported in Laravel 8'); + } + $this->withFailedJobs(); $this->withTenantDatabases(); From e442bdb64419038758d4fd6e10946735050b5a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:29:05 +0100 Subject: [PATCH 09/10] Only use JobRetryRequested in Laravel 8 --- src/Bootstrappers/QueueTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index c94d6749..d2ce52d0 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -54,7 +54,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper }); if (Str::startsWith(app()->version(), '8')) { - // queue:retry tenancy is only supported in Laravel 8 + // JobRetryRequested only exists since Laravel 8 $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { $previousTenant = tenant(); From 96d9ad13d821b1619062e930c2bce54642b1ffd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 6 Jan 2022 16:57:01 +0100 Subject: [PATCH 10/10] Add a note about 'tenant' connection being reserved (fixes #774) --- assets/config.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/config.php b/assets/config.php index 029591ad..85592d14 100644 --- a/assets/config.php +++ b/assets/config.php @@ -42,7 +42,8 @@ return [ 'central_connection' => env('DB_CONNECTION', 'central'), /** - * Connection used as a "template" for the tenant database connection. + * Connection used as a "template" for the dynamically created tenant database connection. + * Note: don't name your template connection tenant. That name is reserved by package. */ 'template_tenant_connection' => null,