diff --git a/composer.json b/composer.json index 9af33b15..3a988ada 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,6 @@ "Stancl\\Tenancy\\TenancyServiceProvider" ], "aliases": { - "Tenant": "Stancl\\Tenancy\\Facades\\Tenant", "Tenancy": "Stancl\\Tenancy\\Facades\\Tenancy", "GlobalCache": "Stancl\\Tenancy\\Facades\\GlobalCache" } diff --git a/src/Database/Models/Concerns/GeneratesIds.php b/src/Database/Models/Concerns/GeneratesIds.php index 9ec84430..0c0a7500 100644 --- a/src/Database/Models/Concerns/GeneratesIds.php +++ b/src/Database/Models/Concerns/GeneratesIds.php @@ -2,13 +2,15 @@ namespace Stancl\Tenancy\Database\Models\Concerns; +use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; + trait GeneratesIds { public static function bootGeneratesIds() { static::creating(function (self $model) { - if (! $model->id && config('tenancy.id_generator')) { - $model->id = app(config('tenancy.id_generator'))->generate($model); + if (! $model->id && app()->bound(UniqueIdentifierGenerator::class)) { + $model->id = app(UniqueIdentifierGenerator::class)->generate($model); } }); } diff --git a/src/Facades/Tenant.php b/src/Facades/Tenant.php deleted file mode 100644 index 6cfd05c8..00000000 --- a/src/Facades/Tenant.php +++ /dev/null @@ -1,21 +0,0 @@ -model()->find($id); + } } diff --git a/src/TenancyBootstrappers/QueueTenancyBootstrapper.php b/src/TenancyBootstrappers/QueueTenancyBootstrapper.php index 7f8ee49b..96d9c5a3 100644 --- a/src/TenancyBootstrappers/QueueTenancyBootstrapper.php +++ b/src/TenancyBootstrappers/QueueTenancyBootstrapper.php @@ -5,28 +5,63 @@ declare(strict_types=1); namespace Stancl\Tenancy\TenancyBootstrappers; use Illuminate\Config\Repository; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\QueueManager; use Illuminate\Support\Testing\Fakes\QueueFake; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -// todo rewrite this class QueueTenancyBootstrapper implements TenancyBootstrapper { - /** @var bool Has tenancy been started. */ - public $started = false; + public $tenancyInitialized = false; /** @var Repository */ protected $config; - public function __construct(Repository $config, QueueManager $queue) + /** @var QueueManager */ + protected $queue; + + /** @var Dispatcher */ + protected $event; + + public function __construct(Repository $config, QueueManager $queue, Dispatcher $event) { $this->config = $config; + $this->queue = $queue; + $this->event = $event; + $this->setUpJobListener(); + $this->setUpPayloadGenerator(); + } + + protected function setUpJobListener() + { + $this->event->listen(JobProcessing::class, function ($event) { + $tenantId = $event->job->payload()['tenant_id'] ?? null; + + // The job is not tenant-aware + if (!$tenantId) { + return; + } + + // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) + if (tenancy()->initialized && tenant('id') === $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)); + }); + } + + protected function setUpPayloadGenerator() + { $bootstrapper = &$this; - if (! $queue instanceof QueueFake) { - $queue->createPayloadUsing(function ($connection) use (&$bootstrapper) { + if (! $this->queue instanceof QueueFake) { + $this->queue->createPayloadUsing(function ($connection) use (&$bootstrapper) { return $bootstrapper->getPayload($connection); }); } @@ -34,17 +69,17 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper public function start(Tenant $tenant) { - $this->started = true; + $this->tenancyInitialized = true; } public function end() { - $this->started = false; + $this->tenancyInitialized = false; } public function getPayload(string $connection) { - if (! $this->started) { + if (! $this->tenancyInitialized) { return []; } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 1310222f..0eb2627f 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -5,10 +5,8 @@ declare(strict_types=1); namespace Stancl\Tenancy; use Illuminate\Cache\CacheManager; -use Illuminate\Contracts\Http\Kernel; -use Illuminate\Support\Facades\Route; +use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\ServiceProvider; -use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver; use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; @@ -23,10 +21,7 @@ class TenancyServiceProvider extends ServiceProvider { $this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy'); - $this->app->bind(Contracts\StorageDriver::class, function ($app) { - return $app->make(DatabaseStorageDriver::class); - }); - $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.unique_id_generator']); + $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.id_generator']); $this->app->singleton(DatabaseManager::class); $this->app->singleton(Tenancy::class); $this->app->bind(Tenant::class, function ($app) { @@ -80,22 +75,6 @@ class TenancyServiceProvider extends ServiceProvider __DIR__ . '/../assets/migrations/' => database_path('migrations'), ], 'migrations'); - foreach ($this->app['config']['tenancy.global_middleware'] as $middleware) { - $this->app->make(Kernel::class)->prependMiddleware($middleware); - } - - /* - * Since tenancy is initialized in the global middleware stack, this - * middleware group acts mostly as a 'flag' for the PreventAccess - * middleware to decide whether the request should be aborted. - */ - Route::middlewareGroup('tenancy', [ - /* Prevent access from tenant domains to central routes and vice versa. */ - Middleware\PreventAccessFromTenantDomains::class, - ]); - - Route::middlewareGroup('universal', []); - $this->loadRoutesFrom(__DIR__ . '/routes.php'); $this->app->singleton('globalUrl', function ($app) { @@ -108,24 +87,5 @@ class TenancyServiceProvider extends ServiceProvider return $instance; }); - - // Queue tenancy - $this->app['events']->listen(\Illuminate\Queue\Events\JobProcessing::class, function ($event) { - $tenantId = $event->job->payload()['tenant_id'] ?? null; - - // The job is not tenant-aware - if (! $tenantId) { - return; - } - - // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) - if (tenancy()->initialized && tenant('id') === $tenantId) { - return; - } - - // Tenancy was either not initialized, or initialized for a different tenant. - // Therefore, we initialize it for the correct tenant. - tenancy()->initById($tenantId); - }); } } diff --git a/tests/Etc/tmp/jobpipelinetest.json b/tests/Etc/tmp/jobpipelinetest.json new file mode 100644 index 00000000..9f5dd4e3 --- /dev/null +++ b/tests/Etc/tmp/jobpipelinetest.json @@ -0,0 +1 @@ +{"foo":"bar"} \ No newline at end of file diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json new file mode 100644 index 00000000..00cf7c37 --- /dev/null +++ b/tests/Etc/tmp/queuetest.json @@ -0,0 +1 @@ +{"tenant_id":"The current tenant id is: acme"} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index f3079db2..01eb58f6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,15 +4,12 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Illuminate\Support\Facades\Redis; use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\TestResponse; -use Stancl\Tenancy\Tenant; abstract class TestCase extends \Orchestra\Testbench\TestCase { - public $autoCreateTenant = false; - public $autoInitTenancy = false; - /** * Setup the test environment. * @@ -22,7 +19,8 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase { parent::setUp(); - // Redis::connection('cache')->flushdb(); + Redis::connection('default')->flushdb(); + Redis::connection('cache')->flushdb(); file_put_contents(database_path('central.sqlite'), ''); $this->artisan('migrate:fresh', [ @@ -31,14 +29,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '--realpath' => true, ]); - if ($this->autoCreateTenant) { - $this->createTenant(); - } - - if ($this->autoInitTenancy) { - $this->initTenancy(); - } - TestResponse::macro('assertContent', function ($content) { /** @var TestResponse $this */ @@ -48,16 +38,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase }); } - public function createTenant($domains = ['test.localhost']) - { - Tenant::new()->withDomains($domains)->save(); - } - - public function initTenancy($domain = 'test.localhost') - { - return tenancy()->init($domain); - } - /** * Define environment setup. * @@ -75,25 +55,11 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), 'database.redis.default.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), 'database.redis.options.prefix' => 'foo', - 'database.redis.tenancy' => [ - 'host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), - 'password' => env('TENANCY_TEST_REDIS_PASSWORD', null), - 'port' => env('TENANCY_TEST_REDIS_PORT', 6379), - // Use the #14 Redis database unless specified otherwise. - // Make sure you don't store anything in this db! - 'database' => env('TENANCY_TEST_REDIS_DB', 14), - 'prefix' => 'abc', // unrelated to tenancy, but this doesn't seem to have an effect? try to replicate in a fresh laravel installation - ], 'database.connections.central' => [ 'driver' => 'sqlite', 'database' => database_path('central.sqlite'), // 'database' => ':memory:', ], - 'tenancy.database' => [ - 'template_connection' => 'central', - 'prefix' => 'tenant', - 'suffix' => '.sqlite', - ], 'database.connections.sqlite.database' => ':memory:', 'database.connections.mysql.host' => env('TENANCY_TEST_MYSQL_HOST', '127.0.0.1'), 'database.connections.pgsql.host' => env('TENANCY_TEST_PGSQL_HOST', '127.0.0.1'), @@ -132,7 +98,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase protected function getPackageAliases($app) { return [ - 'Tenant' => \Stancl\Tenancy\Facades\Tenant::class, 'Tenancy' => \Stancl\Tenancy\Facades\Tenancy::class, 'GlobalCache' => \Stancl\Tenancy\Facades\GlobalCache::class, ]; @@ -165,11 +130,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', (int) (ceil($length / strlen($x))))), 1, $length); } - public function isContainerized() - { - return env('CONTINUOUS_INTEGRATION') || env('DOCKER'); - } - public function assertArrayIsSubset($subset, $array, string $message = ''): void { parent::assertTrue(array_intersect($subset, $array) == $subset, $message); diff --git a/tests/v3/BootstrapperTest.php b/tests/v3/BootstrapperTest.php index c07bf0e9..71bdf997 100644 --- a/tests/v3/BootstrapperTest.php +++ b/tests/v3/BootstrapperTest.php @@ -178,9 +178,5 @@ class BootstrapperTest extends TestCase $this->assertFalse(Storage::disk('public')->exists('abc')); } - /** @test */ - public function queue_data_is_separated() - { - // todo - } + // for queues see QueueTest } diff --git a/tests/v3/JobPipelineTest.php b/tests/v3/JobPipelineTest.php index 79f51ea9..23eca4e0 100644 --- a/tests/v3/JobPipelineTest.php +++ b/tests/v3/JobPipelineTest.php @@ -23,7 +23,6 @@ class JobPipelineTest extends TestCase config(['queue.default' => 'redis']); - file_put_contents(__DIR__ . '/../Etc/tmp/jobpipelinetest.json', '{}'); $this->valuestore = Valuestore::make(__DIR__ . '/../Etc/tmp/jobpipelinetest.json')->flush(); } @@ -64,6 +63,22 @@ class JobPipelineTest extends TestCase }); } + /** @test */ + public function job_pipelines_run_when_queued() + { + Event::listen(TenantCreated::class, JobPipeline::make([ + FooJob::class, + ])->send(function () { + return $this->valuestore; + })->shouldBeQueued(true)->toListener()); + + $this->assertFalse($this->valuestore->has('foo')); + Tenant::create(); + $this->artisan('queue:work --once'); + + $this->assertSame('bar', $this->valuestore->get('foo')); + } + /** @test */ public function job_pipeline_executes_jobs_and_passes_the_object_sequentially() { @@ -159,6 +174,7 @@ class JobWithMultipleArguments public function handle() { + // we dont queue this job so no need to use valuestore here app()->instance('test_args', [$this->first, $this->second]); } } diff --git a/tests/v3/QueueTest.php b/tests/v3/QueueTest.php new file mode 100644 index 00000000..2fdb4da9 --- /dev/null +++ b/tests/v3/QueueTest.php @@ -0,0 +1,136 @@ + [ + QueueTenancyBootstrapper::class, + ], + 'queue.default' => 'redis', + ]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + + $this->valuestore = Valuestore::make(__DIR__ . '/../Etc/tmp/queuetest.json')->flush(); + } + + /** @test */ + public function tenant_id_is_passed_to_tenant_queues() + { + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + Event::fake(); + + dispatch(new TestJob($this->valuestore)); + + Event::assertDispatched(JobProcessing::class, function ($event) { + return $event->job->payload()['tenant_id'] === tenant('id'); + }); + } + + /** @test */ + public function tenant_id_is_not_passed_to_central_queues() + { + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + Event::fake(); + + config(['queue.connections.central' => [ + 'driver' => 'sync', + 'central' => true, + ]]); + + dispatch(new TestJob($this->valuestore))->onConnection('central'); + + Event::assertDispatched(JobProcessing::class, function ($event) { + return ! isset($event->job->payload()['tenant_id']); + }); + } + + /** @test */ + public function tenancy_is_initialized_inside_queues() + { + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant); + + dispatch(new TestJob($this->valuestore)); + + $this->assertFalse($this->valuestore->has('tenant_id')); + $this->artisan('queue:work --once'); + + $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); + } + + /** @test */ + public function the_tenant_used_by_the_job_doesnt_change_when_the_current_tenant_changes() + { + $tenant1 = Tenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant1); + + dispatch(new TestJob($this->valuestore)); + + $tenant2 = Tenant::create([ + 'id' => 'foobar', + ]); + + tenancy()->initialize($tenant2); + + $this->assertFalse($this->valuestore->has('tenant_id')); + $this->artisan('queue:work --once'); + + $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); + } +} + +class TestJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + /** @var Valuestore */ + protected $valuestore; + + public function __construct(Valuestore $valuestore) + { + $this->valuestore = $valuestore; + } + + public function handle() + { + $this->valuestore->put('tenant_id', "The current tenant id is: " . tenant('id')); + } +} diff --git a/tests/v3/TenantModelTest.php b/tests/v3/TenantModelTest.php index 2aff19a7..bf67f851 100644 --- a/tests/v3/TenantModelTest.php +++ b/tests/v3/TenantModelTest.php @@ -130,6 +130,8 @@ class TenantModelTest extends TestCase $this->assertTrue(tenant() instanceof AnotherTenant); } + + // todo test that tenant can be created even in another DB context - that the central trait works } class MyTenant extends Tenant