diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efb8ad02..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 @@ -26,7 +29,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/.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 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

diff --git a/composer.json b/composer.json index b10f2d16..88bfea29 100644 --- a/composer.json +++ b/composer.json @@ -11,17 +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": { - "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", + "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/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index d2ce52d0..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, @@ -75,14 +83,31 @@ 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 (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) { - // 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/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, ])); 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) { diff --git a/src/Middleware/CheckTenantForMaintenanceMode.php b/src/Middleware/CheckTenantForMaintenanceMode.php index cf5ac64b..46bb4460 100644 --- a/src/Middleware/CheckTenantForMaintenanceMode.php +++ b/src/Middleware/CheckTenantForMaintenanceMode.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Middleware; use Closure; +use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; use Symfony\Component\HttpKernel\Exception\HttpException; 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/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json index dc158c6b..e69de29b 100644 --- a/tests/Etc/tmp/queuetest.json +++ b/tests/Etc/tmp/queuetest.json @@ -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/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index 089ece8f..6b1454d8 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -5,11 +5,13 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; use Illuminate\Support\Facades\Artisan; +use Symfony\Component\HttpKernel\Exception\HttpException; 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,8 +34,9 @@ class MaintenanceModeTest extends TestCase $tenant->putDownForMaintenance(); - $this->get('http://acme.localhost/foo') - ->assertStatus(503); + $this->expectException(HttpException::class); + $this->withoutExceptionHandling() + ->get('http://acme.localhost/foo'); tenancy()->end(); @@ -65,8 +68,9 @@ class MaintenanceModeTest extends TestCase Artisan::call('tenancy:down'); - $this->get('http://acme.localhost/foo') - ->assertStatus(503); + $this->expectException(HttpException::class); + $this->withoutExceptionHandling() + ->get('http://acme.localhost/foo'); tenancy()->end(); 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) { diff --git a/tests/TestCase.php b/tests/TestCase.php index bbc42489..cea669a1 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([ @@ -83,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'],