From 435d8528a75acdbdc2994962bc47e90f4288aed1 Mon Sep 17 00:00:00 2001 From: Stefan Ninic Date: Sat, 25 Dec 2021 22:10:34 +0100 Subject: [PATCH 01/30] 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 02/30] 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 03/30] 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 04/30] 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 05/30] 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 06/30] 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, From f08e33afd80d14d99eb0595261cb100f981d4b52 Mon Sep 17 00:00:00 2001 From: Jori Stein <44996807+stein-j@users.noreply.github.com> Date: Thu, 6 Jan 2022 21:35:56 +0100 Subject: [PATCH 07/30] Remove redondant initialization (#775) --- src/Commands/Run.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 4216d1c6..aa518d7a 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -34,7 +34,6 @@ class Run extends Command { tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { $this->line("Tenant: {$tenant->getTenantKey()}"); - tenancy()->initialize($tenant); $callback = function ($prefix = '') { return function ($arguments, $argument) use ($prefix) { From 9c79267e2444b5df4c32123f62387b71953f0a26 Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Mon, 14 Feb 2022 14:31:01 +0100 Subject: [PATCH 08/30] Fix .env loading in development (#799) * Upgrade vlucas/phpdotenv to ^5.0 `Dotenv::create($paths)` was the syntax for releases before v4 * Remove vlucas/phpdotenv dependency and make it work with newer versions. --- composer.json | 1 - tests/TestCase.php | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b10f2d16..bf66e1f2 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,6 @@ "stancl/virtualcolumn": "^1.0" }, "require-dev": { - "vlucas/phpdotenv": "^3.3|^4.0|^5.0", "laravel/framework": "^6.0|^7.0|^8.0", "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0", "league/flysystem-aws-s3-v3": "~1.0", diff --git a/tests/TestCase.php b/tests/TestCase.php index bbc42489..d3e42ea1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -48,7 +48,11 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase protected function getEnvironmentSetUp($app) { if (file_exists(__DIR__ . '/../.env')) { - \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); + if (method_exists(\Dotenv\Dotenv::class, 'createImmutable')) { + \Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->load(); + } else { + \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); + } } $app['config']->set([ From 27f916c3239fb4dbad44f1cfa74fbde4bd9d656d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:12:38 +0100 Subject: [PATCH 09/30] end tenancy in queue if the next job is not tenant aware --- src/Bootstrappers/QueueTenancyBootstrapper.php | 10 ++++++++-- tests/Etc/tmp/queuetest.json | 1 - tests/QueueTest.php | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) delete mode 100644 tests/Etc/tmp/queuetest.json diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index d2ce52d0..2e9aa051 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -75,14 +75,20 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper protected static function initializeTenancyForQueue($tenantId) { - // The job is not tenant-aware if (! $tenantId) { + // The job is not tenant-aware + if (tenancy()->initialized) { + // Tenancy was initialized, so we revert back to the central context + tenancy()->end(); + } + return; } if (tenancy()->initialized) { + // Tenancy is already initialized if (tenant()->getTenantKey() === $tenantId) { - // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) + // It's initialized for the same tenant (e.g. dispatchNow was used, or the previous job also ran for this tenant) return; } } diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json deleted file mode 100644 index dc158c6b..00000000 --- a/tests/Etc/tmp/queuetest.json +++ /dev/null @@ -1 +0,0 @@ -{"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 158ad56b..afe64fea 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -55,6 +55,11 @@ class QueueTest extends TestCase $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush(); } + public function tearDown(): void + { + $this->valuestore->flush(); + } + protected function withFailedJobs() { Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { From 368d3cc99f7916af5e8290cddabd4968eeec73ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:21:27 +0100 Subject: [PATCH 10/30] add forceRefresh option to QueueTenancyBootstrapper --- .../QueueTenancyBootstrapper.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 2e9aa051..6a88f701 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -25,6 +25,14 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper /** @var QueueManager */ protected $queue; + /** + * Don't persist the same tenant across multiple jobs even if they have the same tenant ID. + * + * This is useful when you're changing the tenant's state (e.g. properties in the `data` column) and want the next job to initialize tenancy again + * with the new data. Features like the Tenant Config are only executed when tenancy is initialized, so the re-initialization is needed in some cases. + */ + public static bool $forceRefresh = false; + /** * The normal constructor is only executed after tenancy is bootstrapped. * However, we're registering a hook to initialize tenancy. Therefore, @@ -85,6 +93,17 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper return; } + if (static::$forceRefresh) { + // Re-initialize tenancy between all jobs + if (tenancy()->initialized) { + tenancy()->end(); + } + + tenancy()->initialize(tenancy()->find($tenantId)); + + return; + } + if (tenancy()->initialized) { // Tenancy is already initialized if (tenant()->getTenantKey() === $tenantId) { From 8e9485f9b1f51d066a791d5cf94834738383ab76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:31:31 +0100 Subject: [PATCH 11/30] add empty queuetest.json --- tests/Etc/tmp/queuetest.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/Etc/tmp/queuetest.json diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json new file mode 100644 index 00000000..e69de29b From 5249ec7c82013a5122f076d9fac6ed2a04b8f532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:31:45 +0100 Subject: [PATCH 12/30] ignore changes to queuetest.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1d03dbec..b3223156 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ psysh phpunit_var_*.xml coverage/ clover.xml +tests/Etc/tmp/queuetest.json From 5b9b3845261fcd5f24181f2a92911db62b26364f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:33:59 +0100 Subject: [PATCH 13/30] Remove codecov --- .github/workflows/ci.yml | 4 ---- README.md | 1 - 2 files changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efb8ad02..f7b64d2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,3 @@ jobs: run: docker-compose exec -T test composer require --no-interaction "laravel/framework:${{ matrix.laravel }}" - name: Run tests run: ./test - - name: Send code coverage to codecov - env: - CODECOV_TOKEN: 24382d15-84e7-4a55-bea4-c4df96a24a9b - run: bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 46f1b097..f4d28288 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Laravel 6.x/7.x/8.x Latest Stable Version GitHub Actions CI status - codecov Donate

From b4a4eab949481d2f96b0b2c1c21aa8db92a2147e Mon Sep 17 00:00:00 2001 From: masiorama Date: Tue, 22 Feb 2022 16:26:07 +0100 Subject: [PATCH 14/30] Add drop of db views on migrate fresh command (#812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Optionally handle drop of table views on MigrateFresh @stancl I managed to make the modification discussed here #811 Afaik (and I can understand) this is the easiest way to handle it, but I'm open to discuss. * Remove redundant store variable * code style Co-authored-by: Samuel Štancl --- src/Commands/MigrateFresh.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index f50e2f5f..4d003db0 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\HasATenantsOption; +use Symfony\Component\Console\Input\InputOption; final class MigrateFresh extends Command { @@ -22,6 +23,8 @@ final class MigrateFresh extends Command public function __construct() { parent::__construct(); + + $this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null); $this->setName('tenants:migrate-fresh'); } @@ -37,6 +40,7 @@ final class MigrateFresh extends Command $this->info('Dropping tables.'); $this->call('db:wipe', array_filter([ '--database' => 'tenant', + '--drop-views' => $this->option('drop-views'), '--force' => true, ])); From 79e3d53b06f33aa6e30ee4454177e1d798918704 Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Tue, 8 Mar 2022 01:50:25 +0100 Subject: [PATCH 15/30] [3.x] Compatibility with Laravel 9 (#802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test on Laravel 9 * Don't extend final Kernel class * Make FilesystemTenancyBootstrapper compatible with Flysystem v3 Co-authored-by: George * Update tenant maintenance mode te be in line with Laravel * Exclude PHP 7.4 <> L9 combination from testing * add root_override-related assertions * getPrefix -> getPathPrefix * handle / inconsistency in s3 prefix * Refactor Storage facade changes Co-authored-by: George Co-authored-by: Samuel Štancl --- .github/workflows/ci.yml | 5 +- composer.json | 8 +-- .../FilesystemTenancyBootstrapper.php | 31 ++++----- .../CheckTenantForMaintenanceMode.php | 9 ++- tests/BootstrapperTest.php | 69 +++++++++++-------- tests/Etc/ConsoleKernel.php | 2 +- tests/MaintenanceModeTest.php | 4 +- tests/TestCase.php | 1 + 8 files changed, 74 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7b64d2d..1303061a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,10 @@ jobs: strategy: matrix: php: ["7.4", "8.0"] - laravel: ["^6.0", "^8.0"] + laravel: ["^6.0", "^8.0", "^9.0"] + exclude: + - laravel: "^9.0" + php: "7.4" steps: - uses: actions/checkout@v2 diff --git a/composer.json b/composer.json index bf66e1f2..88bfea29 100644 --- a/composer.json +++ b/composer.json @@ -11,16 +11,16 @@ ], "require": { "ext-json": "*", - "illuminate/support": "^6.0|^7.0|^8.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0", "facade/ignition-contracts": "^1.0", "ramsey/uuid": "^3.7|^4.0", "stancl/jobpipeline": "^1.0", "stancl/virtualcolumn": "^1.0" }, "require-dev": { - "laravel/framework": "^6.0|^7.0|^8.0", - "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0", - "league/flysystem-aws-s3-v3": "~1.0", + "laravel/framework": "^6.0|^7.0|^8.0|^9.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", + "league/flysystem-aws-s3-v3": "^1.0|^3.0", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5" }, diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index d5ae2d50..418be93f 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -54,20 +54,20 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper } // Storage facade - foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { - /** @var FilesystemAdapter $filesystemDisk */ - $filesystemDisk = Storage::disk($disk); - $this->originalPaths['disks'][$disk] = $filesystemDisk->getAdapter()->getPathPrefix(); + Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']); - if ($root = str_replace( + foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { + $originalRoot = $this->app['config']["filesystems.disks.{$disk}.root"]; + $this->originalPaths['disks'][$disk] = $originalRoot; + + $finalPrefix = str_replace( '%storage_path%', storage_path(), - $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '' - )) { - $filesystemDisk->getAdapter()->setPathPrefix($finalPrefix = $root); - } else { - $root = $this->app['config']["filesystems.disks.{$disk}.root"]; - $filesystemDisk->getAdapter()->setPathPrefix($finalPrefix = $root . "/{$suffix}"); + $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '', + ); + + if (! $finalPrefix) { + $finalPrefix = $originalRoot . '/'. $suffix; } $this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix; @@ -84,14 +84,9 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper $this->app['url']->setAssetRoot($this->app['config']['app.asset_url']); // Storage facade + Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']); foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { - /** @var FilesystemAdapter $filesystemDisk */ - $filesystemDisk = Storage::disk($disk); - - $root = $this->originalPaths['disks'][$disk]; - - $filesystemDisk->getAdapter()->setPathPrefix($root); - $this->app['config']["filesystems.disks.{$disk}.root"] = $root; + $this->app['config']["filesystems.disks.{$disk}.root"] = $this->originalPaths['disks'][$disk]; } } } diff --git a/src/Middleware/CheckTenantForMaintenanceMode.php b/src/Middleware/CheckTenantForMaintenanceMode.php index 5554663f..8e29a31e 100644 --- a/src/Middleware/CheckTenantForMaintenanceMode.php +++ b/src/Middleware/CheckTenantForMaintenanceMode.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Middleware; use Closure; -use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException; +use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; use Symfony\Component\HttpFoundation\IpUtils; @@ -29,7 +29,12 @@ class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode return $next($request); } - throw new MaintenanceModeException($data['time'], $data['retry'], $data['message']); + throw new HttpException( + 503, + 'Service Unavailable', + null, + isset($data['retry']) ? ['Retry-After' => $data['retry']] : [] + ); } return $next($request); diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 1b0c880d..29aa7dc9 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -4,23 +4,27 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; -use Illuminate\Support\Facades\Cache; +use Illuminate\Filesystem\FilesystemAdapter; +use ReflectionObject; +use ReflectionProperty; +use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Events\TenancyEnded; -use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; class BootstrapperTest extends TestCase { @@ -165,6 +169,7 @@ class BootstrapperTest extends TestCase $tenant2 = Tenant::create(); tenancy()->initialize($tenant1); + Storage::disk('public')->put('foo', 'bar'); $this->assertSame('bar', Storage::disk('public')->get('foo')); @@ -184,30 +189,38 @@ class BootstrapperTest extends TestCase $this->assertFalse(Storage::disk('public')->exists('foo')); $this->assertFalse(Storage::disk('public')->exists('abc')); + $expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base + + // Check that disk prefixes respect the root_override logic + $this->assertSame($expected_storage_path . '/app/', $this->getDiskPrefix('local')); + $this->assertSame($expected_storage_path . '/app/public/', $this->getDiskPrefix('public')); + $this->assertSame('tenant' . tenant('id') . '/', ltrim($this->getDiskPrefix('s3'), '/')); + // Check suffixing logic $new_storage_path = storage_path(); - $this->assertEquals($old_storage_path . '/' . config('tenancy.filesystem.suffix_base') . tenant('id'), $new_storage_path); + $this->assertEquals($expected_storage_path, $new_storage_path); + } - foreach (config('tenancy.filesystem.disks') as $disk) { - $suffix = config('tenancy.filesystem.suffix_base') . tenant('id'); + protected function getDiskPrefix(string $disk): string + { + /** @var FilesystemAdapter $disk */ + $disk = Storage::disk($disk); + $adapter = $disk->getAdapter(); - /** @var FilesystemAdapter $filesystemDisk */ - $filesystemDisk = Storage::disk($disk); - - $current_path_prefix = $filesystemDisk->getAdapter()->getPathPrefix(); - - if ($override = config("tenancy.filesystem.root_override.{$disk}")) { - $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override); - } else { - if ($base = $old_storage_facade_roots[$disk]) { - $correct_path_prefix = $base . "/$suffix/"; - } else { - $correct_path_prefix = "$suffix/"; - } - } - - $this->assertSame($correct_path_prefix, $current_path_prefix); + if (! Str::startsWith(app()->version(), '9.')) { + return $adapter->getPathPrefix(); } + + $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); + $prefixer->setAccessible(true); + + // reflection -> instance + $prefixer = $prefixer->getValue($adapter); + + $prefix = (new ReflectionProperty($prefixer, 'prefix')); + $prefix->setAccessible(true); + + return $prefix->getValue($prefixer); } // for queues see QueueTest diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/ConsoleKernel.php index 1bc66365..9d37d3c6 100644 --- a/tests/Etc/ConsoleKernel.php +++ b/tests/Etc/ConsoleKernel.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests\Etc; -use Orchestra\Testbench\Console\Kernel; +use Orchestra\Testbench\Foundation\Console\Kernel; class ConsoleKernel extends Kernel { diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index a8ecb064..4a8d8d0c 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -4,12 +4,14 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException; use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Tests\Etc\Tenant; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; class MaintenanceModeTest extends TestCase { @@ -32,7 +34,7 @@ class MaintenanceModeTest extends TestCase $tenant->putDownForMaintenance(); - $this->expectException(MaintenanceModeException::class); + $this->expectException(HttpException::class); $this->withoutExceptionHandling() ->get('http://acme.localhost/foo'); } diff --git a/tests/TestCase.php b/tests/TestCase.php index d3e42ea1..cea669a1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -87,6 +87,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'public', 's3', ], + 'filesystems.disks.s3.bucket' => 'foo', 'tenancy.redis.tenancy' => env('TENANCY_TEST_REDIS_TENANCY', true), 'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'), 'tenancy.redis.prefixed_connections' => ['default'], From eb1a2ebe32a3dd9e5941c81602d55aab2268db06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 17 Mar 2022 12:24:57 +0100 Subject: [PATCH 16/30] Use 7.2 instead of 7.4 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1303061a..3d1698b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: strategy: matrix: - php: ["7.4", "8.0"] + php: ["7.2", "8.0"] laravel: ["^6.0", "^8.0", "^9.0"] exclude: - laravel: "^9.0" - php: "7.4" + php: "7.2" steps: - uses: actions/checkout@v2 From fa2a61fcd74126587312567d5e07e273c8ea43b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 17 Mar 2022 12:30:14 +0100 Subject: [PATCH 17/30] Use PHP 7.3 instead of 7.2 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d1698b4..cc8ad985 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: strategy: matrix: - php: ["7.2", "8.0"] + php: ["7.3", "8.0"] laravel: ["^6.0", "^8.0", "^9.0"] exclude: - laravel: "^9.0" - php: "7.2" + php: "7.3" steps: - uses: actions/checkout@v2 From 49ebb75f007d649465fc3f847ddc76f6396b0337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 17 Mar 2022 12:46:49 +0100 Subject: [PATCH 18/30] Fixes #827 --- src/Bootstrappers/QueueTenancyBootstrapper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 6a88f701..666e29ed 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -30,8 +30,10 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper * * This is useful when you're changing the tenant's state (e.g. properties in the `data` column) and want the next job to initialize tenancy again * with the new data. Features like the Tenant Config are only executed when tenancy is initialized, so the re-initialization is needed in some cases. + * + * @var bool */ - public static bool $forceRefresh = false; + public static $forceRefresh = false; /** * The normal constructor is only executed after tenancy is bootstrapped. From 5026f54a6d4482226951d3a8196218ea41434db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:48:55 +0100 Subject: [PATCH 19/30] fix path prefixing --- CONTRIBUTING.md | 13 +++++++++++++ Dockerfile | 2 +- docker-compose.override.yml | 5 +++++ src/Bootstrappers/FilesystemTenancyBootstrapper.php | 4 +++- tests/BootstrapperTest.php | 2 +- tests/Etc/tmp/queuetest.json | 0 6 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 docker-compose.override.yml delete mode 100644 tests/Etc/tmp/queuetest.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7dce1b82..a5a6ec3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,3 +9,16 @@ StyleCI will flag code style violations in your pull requests. Run `docker-compose up -d` to start the containers. Then run `./test` to run the tests. When you're done testing, run `docker-compose down` to shut down the containers. + +### Docker on M1 + +You can add: +```yaml +services: + mysql: + platform: linux/amd64 + mysql2: + platform: linux/amd64 +``` + +to `docker-compose.override.yml` to make `docker-compose up-d` work on M1. diff --git a/Dockerfile b/Dockerfile index 06d97aea..36f52d6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ # && if [ "${PHP_VERSION}" = "7.4" ]; then docker-php-ext-configure gd --with-freetype --with-jpeg; else docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; fi \ && docker-php-ext-install -j$(nproc) gd pdo pdo_mysql pdo_pgsql pdo_sqlite pgsql zip gmp bcmath pcntl ldap sysvmsg exif \ # install the redis php extension - && pecl install redis-5.3.2 \ + && pecl install redis-5.3.7 \ && docker-php-ext-enable redis \ # install the pcov extention && pecl install pcov \ diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..29e9fb37 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,5 @@ +services: + mysql: + platform: linux/amd64 + mysql2: + platform: linux/amd64 diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index 418be93f..dcd7299e 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -67,7 +67,9 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper ); if (! $finalPrefix) { - $finalPrefix = $originalRoot . '/'. $suffix; + $finalPrefix = $originalRoot + ? $originalRoot . '/'. $suffix + : $suffix; } $this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix; diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 29aa7dc9..588fadd8 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -194,7 +194,7 @@ class BootstrapperTest extends TestCase // Check that disk prefixes respect the root_override logic $this->assertSame($expected_storage_path . '/app/', $this->getDiskPrefix('local')); $this->assertSame($expected_storage_path . '/app/public/', $this->getDiskPrefix('public')); - $this->assertSame('tenant' . tenant('id') . '/', ltrim($this->getDiskPrefix('s3'), '/')); + $this->assertSame('tenant' . tenant('id') . '/', $this->getDiskPrefix('s3'), '/'); // Check suffixing logic $new_storage_path = storage_path(); diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json deleted file mode 100644 index e69de29b..00000000 From 600bb823de185ef02c3ff5aeba0c75f41409bafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:49:25 +0100 Subject: [PATCH 20/30] avoid double // in prefix --- src/Bootstrappers/FilesystemTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index dcd7299e..da1e5e2a 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -68,7 +68,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper if (! $finalPrefix) { $finalPrefix = $originalRoot - ? $originalRoot . '/'. $suffix + ? rtrim($originalRoot, '/') . '/'. $suffix : $suffix; } From e1ae6f4380bf2062adfb44bc1acb83e603b95ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:57:15 +0100 Subject: [PATCH 21/30] re-add queuetest.json --- .gitignore | 1 - tests/Etc/tmp/queuetest.json | 0 2 files changed, 1 deletion(-) create mode 100644 tests/Etc/tmp/queuetest.json diff --git a/.gitignore b/.gitignore index b3223156..1d03dbec 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ psysh phpunit_var_*.xml coverage/ clover.xml -tests/Etc/tmp/queuetest.json diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json new file mode 100644 index 00000000..e69de29b From 4e717236f9c1aa5d4e4aa588bace783d4d7bcde2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:57:32 +0100 Subject: [PATCH 22/30] revert gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1d03dbec..b3223156 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ psysh phpunit_var_*.xml coverage/ clover.xml +tests/Etc/tmp/queuetest.json From 4f196097979653fa7b7522ac339116d88b5baae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:58:47 +0100 Subject: [PATCH 23/30] remove docker-compose.override.yml --- .gitignore | 1 + docker-compose.override.yml | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 docker-compose.override.yml diff --git a/.gitignore b/.gitignore index b3223156..f470ba75 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ phpunit_var_*.xml coverage/ clover.xml tests/Etc/tmp/queuetest.json +docker-compose.override.yml diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index 29e9fb37..00000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - mysql: - platform: linux/amd64 - mysql2: - platform: linux/amd64 From 349125c02ebe71216bfbbb4ea0c3b955e03ba474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Mar 2022 18:00:55 +0200 Subject: [PATCH 24/30] Merge hotfix branch (#834) * try specifying the signature in __construct * constructor doesn't work since Reflection is used, try specifying getDefaultName() instead * Fixed: make migration commands compatible * Fix failing tests * Fix username generation * Re-create tmp dir as well if needed * wip --- src/Commands/Migrate.php | 20 ++++++---------- src/Commands/MigrateFresh.php | 2 +- src/Commands/Rollback.php | 11 ++++++--- src/Concerns/ExtendsLaravelCommand.php | 23 +++++++++++++++++++ src/Database/DatabaseManager.php | 10 +++++++- src/Jobs/CreateDatabase.php | 2 +- ...rmissionControlledMySQLDatabaseManager.php | 5 ---- tests/DatabaseUsersTest.php | 9 ++++++-- tests/Etc/tmp/queuetest.json | 0 tests/QueueTest.php | 20 +++++++++++++++- 10 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 src/Concerns/ExtendsLaravelCommand.php delete mode 100644 tests/Etc/tmp/queuetest.json diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index bf92dfcd..c67d3598 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -8,32 +8,26 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Console\Migrations\MigrateCommand; use Illuminate\Database\Migrations\Migrator; use Stancl\Tenancy\Concerns\DealsWithMigrations; +use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Events\DatabaseMigrated; use Stancl\Tenancy\Events\MigratingDatabase; class Migrate extends MigrateCommand { - use HasATenantsOption, DealsWithMigrations; + use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand; - /** - * The console command description. - * - * @var string - */ protected $description = 'Run migrations for tenant(s)'; - /** - * Create a new command instance. - * - * @param Migrator $migrator - * @param Dispatcher $dispatcher - */ + protected static function getTenantCommandName(): string + { + return 'tenants:migrate'; + } + public function __construct(Migrator $migrator, Dispatcher $dispatcher) { parent::__construct($migrator, $dispatcher); - $this->setName('tenants:migrate'); $this->specifyParameters(); } diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 4d003db0..283d70b0 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -23,7 +23,7 @@ final class MigrateFresh extends Command public function __construct() { parent::__construct(); - + $this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null); $this->setName('tenants:migrate-fresh'); diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index 081872c8..e60d974b 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -7,13 +7,19 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Migrations\Migrator; use Stancl\Tenancy\Concerns\DealsWithMigrations; +use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Events\DatabaseRolledBack; use Stancl\Tenancy\Events\RollingBackDatabase; class Rollback extends RollbackCommand { - use HasATenantsOption, DealsWithMigrations; + use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand; + + protected static function getTenantCommandName(): string + { + return 'tenants:rollback'; + } /** * The console command description. @@ -31,8 +37,7 @@ class Rollback extends RollbackCommand { parent::__construct($migrator); - $this->setName('tenants:rollback'); - $this->specifyParameters(); + $this->specifyTenantSignature(); } /** diff --git a/src/Concerns/ExtendsLaravelCommand.php b/src/Concerns/ExtendsLaravelCommand.php new file mode 100644 index 00000000..bdafc8f7 --- /dev/null +++ b/src/Concerns/ExtendsLaravelCommand.php @@ -0,0 +1,23 @@ +specifyParameters(); + } + + public function getName(): ?string + { + return static::getTenantCommandName(); + } + + public static function getDefaultName(): ?string + { + return static::getTenantCommandName(); + } + + abstract protected static function getTenantCommandName(): string; +} diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index e85fd659..6242ffa9 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -7,10 +7,12 @@ namespace Stancl\Tenancy\Database; use Illuminate\Config\Repository; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; +use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; +use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; /** * @internal Class is subject to breaking changes in minor and patch versions. @@ -90,8 +92,14 @@ class DatabaseManager */ public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void { - if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) { + $manager = $tenant->database()->manager(); + + if ($manager->databaseExists($database = $tenant->database()->getName())) { throw new TenantDatabaseAlreadyExistsException($database); } + + if ($manager instanceof ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) { + throw new TenantDatabaseUserAlreadyExistsException($username); + } } } diff --git a/src/Jobs/CreateDatabase.php b/src/Jobs/CreateDatabase.php index 3a74534d..3cb2a6b4 100644 --- a/src/Jobs/CreateDatabase.php +++ b/src/Jobs/CreateDatabase.php @@ -36,8 +36,8 @@ class CreateDatabase implements ShouldQueue return false; } - $databaseManager->ensureTenantCanBeCreated($this->tenant); $this->tenant->database()->makeCredentials(); + $databaseManager->ensureTenantCanBeCreated($this->tenant); $this->tenant->database()->manager()->createDatabase($this->tenant); event(new DatabaseCreated($this->tenant)); diff --git a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php index f8bedc97..918601a8 100644 --- a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php @@ -7,7 +7,6 @@ namespace Stancl\Tenancy\TenantDatabaseManagers; use Stancl\Tenancy\Concerns\CreatesDatabaseUsers; use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\DatabaseConfig; -use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager implements ManagesDatabaseUsers { @@ -26,10 +25,6 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl $hostname = $databaseConfig->connection()['host']; $password = $databaseConfig->getPassword(); - if ($this->userExists($username)) { - throw new TenantDatabaseUserAlreadyExistsException($username); - } - $this->database()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'"); $grants = implode(', ', static::$grants); diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php index 0b095024..344239d1 100644 --- a/tests/DatabaseUsersTest.php +++ b/tests/DatabaseUsersTest.php @@ -10,6 +10,7 @@ use Illuminate\Support\Str; use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; +use Stancl\Tenancy\Events\DatabaseCreated; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; @@ -67,14 +68,18 @@ class DatabaseUsersTest extends TestCase $this->assertTrue($manager->databaseExists($tenant->database()->getName())); $this->expectException(TenantDatabaseUserAlreadyExistsException::class); + Event::fake([DatabaseCreated::class]); + $tenant2 = Tenant::create([ 'tenancy_db_username' => $username, ]); /** @var ManagesDatabaseUsers $manager */ - $manager = $tenant2->database()->manager(); + $manager2 = $tenant2->database()->manager(); + // database was not created because of DB transaction - $this->assertFalse($manager->databaseExists($tenant2->database()->getName())); + $this->assertFalse($manager2->databaseExists($tenant2->database()->getName())); + Event::assertNotDispatched(DatabaseCreated::class); } /** @test */ diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/QueueTest.php b/tests/QueueTest.php index afe64fea..a3df9cd7 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Closure; use Exception; use Illuminate\Support\Str; use Illuminate\Bus\Queueable; @@ -24,6 +25,7 @@ use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use PDO; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; @@ -52,7 +54,7 @@ class QueueTest extends TestCase Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); - $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush(); + $this->createValueStore(); } public function tearDown(): void @@ -60,6 +62,22 @@ class QueueTest extends TestCase $this->valuestore->flush(); } + protected function createValueStore(): void + { + $valueStorePath = __DIR__ . '/Etc/tmp/queuetest.json'; + + if (! file_exists($valueStorePath)) { + // The directory sometimes goes missing as well when the file is deleted in git + if (! is_dir(__DIR__ . '/Etc/tmp')) { + mkdir(__DIR__ . '/Etc/tmp'); + } + + file_put_contents($valueStorePath, ''); + } + + $this->valuestore = Valuestore::make($valueStorePath)->flush(); + } + protected function withFailedJobs() { Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { From f065ea60b0e56c6a22cd75c476ca448649f8ffe2 Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Fri, 1 Apr 2022 22:53:09 +0200 Subject: [PATCH 25/30] Update QueueTenancyBootstrapper.php (#836) --- src/Bootstrappers/QueueTenancyBootstrapper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 666e29ed..790e1344 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -63,8 +63,8 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); }); - if (Str::startsWith(app()->version(), '8')) { - // JobRetryRequested only exists since Laravel 8 + if (version_compare(app()->version(), '8.64', '>=')) { + // JobRetryRequested only exists since Laravel 8.64 $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { $previousTenant = tenant(); From 40bf576e0087f3350fa5f91c2c73962a7dcdff50 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 8 Apr 2022 02:13:29 +0100 Subject: [PATCH 26/30] [3.x] Update PostgreSQLSchemaManager to set correct config key value (#840) * Update PostgreSQLSchemaManager to set correct config key value * Update to use version_compare * Update TenantDatabaseManagerTest * Improve TenantDatabaseManagerTest * Update TenantDatabaseManager --- src/TenantDatabaseManagers/PostgreSQLSchemaManager.php | 6 +++++- tests/TenantDatabaseManagerTest.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php index 9d815b25..55f049d0 100644 --- a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -46,7 +46,11 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager public function makeConnectionConfig(array $baseConfig, string $databaseName): array { - $baseConfig['schema'] = $databaseName; + if (version_compare(app()->version(), '9.0', '>=')) { + $baseConfig['search_path'] = $databaseName; + } else { + $baseConfig['schema'] = $databaseName; + } return $baseConfig; } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index f64770b1..3d45d96f 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -194,7 +194,11 @@ class TenantDatabaseManagerTest extends TestCase ]); tenancy()->initialize($tenant); - $this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema')); + $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? + config('database.connections.' . config('database.default') . '.search_path') : + config('database.connections.' . config('database.default') . '.schema'); + + $this->assertSame($tenant->database()->getName(), $schemaConfig); $this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database'])); } From 0569bf5a3495a194079540f8c7096ab6aac68117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sun, 1 May 2022 12:56:25 +0200 Subject: [PATCH 27/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4d28288..95fb7c60 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Laravel 6.x/7.x/8.x + Laravel 9.x Latest Stable Version GitHub Actions CI status Donate From a1c34421488b8eb4c5776cbf457f0719c79d742e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sun, 15 May 2022 13:32:09 +0200 Subject: [PATCH 28/30] Resolve #854 --- src/Database/Concerns/BelongsToTenant.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index 5410758d..fc899411 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -16,7 +16,7 @@ trait BelongsToTenant public function tenant() { - return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn); + return $this->belongsTo(config('tenancy.tenant_model'), static::$tenantIdColumn); } public static function bootBelongsToTenant() @@ -24,9 +24,9 @@ trait BelongsToTenant static::addGlobalScope(new TenantScope); static::creating(function ($model) { - if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { + if (! $model->getAttribute(static::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { if (tenancy()->initialized) { - $model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey()); + $model->setAttribute(static::$tenantIdColumn, tenant()->getTenantKey()); $model->setRelation('tenant', tenant()); } } From 4d95e88e272d5c3f4beebd10a8e549d17259a079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sun, 15 May 2022 13:45:54 +0200 Subject: [PATCH 29/30] Revert "Resolve #854" This reverts commit a1c34421488b8eb4c5776cbf457f0719c79d742e. --- src/Database/Concerns/BelongsToTenant.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index fc899411..5410758d 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -16,7 +16,7 @@ trait BelongsToTenant public function tenant() { - return $this->belongsTo(config('tenancy.tenant_model'), static::$tenantIdColumn); + return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn); } public static function bootBelongsToTenant() @@ -24,9 +24,9 @@ trait BelongsToTenant static::addGlobalScope(new TenantScope); static::creating(function ($model) { - if (! $model->getAttribute(static::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { + if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { if (tenancy()->initialized) { - $model->setAttribute(static::$tenantIdColumn, tenant()->getTenantKey()); + $model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey()); $model->setRelation('tenant', tenant()); } } From 51228defc68a6362e31f486a04e3117105abf3e0 Mon Sep 17 00:00:00 2001 From: Vincent GS Date: Thu, 26 May 2022 04:51:27 -0500 Subject: [PATCH 30/30] [3.x][Filesystem] Provide an additional argument for tenant name path (#817) * Let the user pass the tenant suffix by %tenant% In this PR we let the user pass an additional parameter using `%tenant%` so the user can additionally pass the folder corresponding to each tenant. This is my proposal, because if I try to use %storage_path% within Linux, I get the full path to the project when I use Google Cloud Storage * Missing missing updates Moving from $subject to $root when %storage_path% has been replaced --- src/Bootstrappers/FilesystemTenancyBootstrapper.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index da1e5e2a..346892b3 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Facades\Storage; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; @@ -61,8 +60,8 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper $this->originalPaths['disks'][$disk] = $originalRoot; $finalPrefix = str_replace( - '%storage_path%', - storage_path(), + ['%storage_path%', '%tenant%'], + [storage_path(), $tenant->getTenantKey()], $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '', );