From d9f3525700699d50202e0ef3d1f6739b36aab50a Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 25 Aug 2025 15:47:16 +0200 Subject: [PATCH 1/3] Add --force option to tenants:migrate-fresh (#1391) --- src/Commands/MigrateFresh.php | 8 +++++++- tests/CommandsTest.php | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 4e89cefd..d4733552 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; +use Illuminate\Console\ConfirmableTrait; use Illuminate\Database\Console\Migrations\BaseCommand; use Illuminate\Database\QueryException; use Illuminate\Support\LazyCollection; @@ -17,7 +18,7 @@ use Symfony\Component\Console\Output\OutputInterface as OI; class MigrateFresh extends BaseCommand { - use HasTenantOptions, DealsWithMigrations, ParallelCommand; + use HasTenantOptions, DealsWithMigrations, ParallelCommand, ConfirmableTrait; protected $description = 'Drop all tables and re-run all migrations for tenant(s)'; @@ -27,6 +28,7 @@ class MigrateFresh extends BaseCommand $this->addOption('drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null); $this->addOption('step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually.'); + $this->addOption('force', null, InputOption::VALUE_NONE, 'Force the command to run when in production.', null); $this->addProcessesOption(); $this->setName('tenants:migrate-fresh'); @@ -34,6 +36,10 @@ class MigrateFresh extends BaseCommand public function handle(): int { + if (! $this->confirmToProceed()) { + return 1; + } + $success = true; if ($this->getProcesses() > 1) { diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 7ebb07a8..16aecd92 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -311,6 +311,21 @@ test('migrate fresh command works', function () { expect(DB::table('users')->exists())->toBeFalse(); }); +test('migrate fresh command respects force option in production', function () { + // Set environment to production + app()->detectEnvironment(fn() => 'production'); + + Tenant::create(); + + // Without --force in production, command should prompt for confirmation + pest()->artisan('tenants:migrate-fresh') + ->expectsConfirmation('Are you sure you want to run this command?'); + + // With --force, command should succeed without prompting + pest()->artisan('tenants:migrate-fresh', ['--force' => true]) + ->assertSuccessful(); +}); + test('run command with array of tenants works', function () { $tenantId1 = Tenant::create()->getTenantKey(); $tenantId2 = Tenant::create()->getTenantKey(); From 3b42c9e20ca8a502d20e627e2ecb8978732a6076 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 25 Aug 2025 15:57:15 +0200 Subject: [PATCH 2/3] [4.x] Use --database in tenants:migrate as the template connection (#1386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make the `--database` option passed to `tenants:migrate` use the passed connection as the tenant connection template * Reset template connection regardless of process count --------- Co-authored-by: Samuel Štancl --- src/Commands/Migrate.php | 21 +++++++++++---- tests/CommandsTest.php | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 5348b509..6ecd6e14 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -51,13 +51,24 @@ class Migrate extends MigrateCommand return 1; } - if ($this->getProcesses() > 1) { - return $this->runConcurrently($this->getTenantChunks()->map(function ($chunk) { - return $this->getTenants($chunk); - })); + $originalTemplateConnection = config('tenancy.database.template_tenant_connection'); + + if ($database = $this->input->getOption('database')) { + config(['tenancy.database.template_tenant_connection' => $database]); } - return $this->migrateTenants($this->getTenants()) ? 0 : 1; + if ($this->getProcesses() > 1) { + $code = $this->runConcurrently($this->getTenantChunks()->map(function ($chunk) { + return $this->getTenants($chunk); + })); + } else { + $code = $this->migrateTenants($this->getTenants()) ? 0 : 1; + } + + // Reset the template tenant connection to the original one + config(['tenancy.database.template_tenant_connection' => $originalTemplateConnection]); + + return $code; } protected function childHandle(mixed ...$args): bool diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 16aecd92..a5b3b856 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -27,6 +27,7 @@ use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException; use function Stancl\Tenancy\Tests\pest; +use Stancl\Tenancy\Events\MigratingDatabase; beforeEach(function () { if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) { @@ -95,6 +96,60 @@ test('migrate command works with tenants option', function () { expect(Schema::hasTable('users'))->toBeTrue(); }); +test('migrate command uses the passed database option as the template tenant connection', function () { + $originalTemplateConnection = config('tenancy.database.template_tenant_connection'); + + // Add a custom connection that will be used as the template for the tenant connection + // Identical to the default (mysql), just with different charset and collation + config(['database.connections.custom_connection' => [ + "driver" => "mysql", + "url" => "", + "host" => "mysql", + "port" => "3306", + "database" => "main", + "username" => "root", + "password" => "password", + "unix_socket" => "", + "charset" => "latin1", // Different from the default (utf8mb4) + "collation" => "latin1_swedish_ci", // Different from the default (utf8mb4_unicode_ci) + "prefix" => "", + "prefix_indexes" => true, + "strict" => true, + "engine" => null, + "options" => [] + ]]); + + $templateConnectionDuringMigration = null; + $tenantConnectionDuringMigration = null; + + Event::listen(MigratingDatabase::class, function() use (&$templateConnectionDuringMigration, &$tenantConnectionDuringMigration) { + $templateConnectionDuringMigration = config('tenancy.database.template_tenant_connection'); + $tenantConnectionDuringMigration = DB::connection('tenant')->getConfig(); + }); + + // The original tenant template connection config remains default + expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection); + + Tenant::create(); + + // The original template connection is used when the --database option is not passed + pest()->artisan('tenants:migrate'); + expect($templateConnectionDuringMigration)->toBe($originalTemplateConnection); + + Tenant::create(); + + // The migrate command temporarily uses the connection passed in the --database option + pest()->artisan('tenants:migrate', ['--database' => 'custom_connection']); + expect($templateConnectionDuringMigration)->toBe('custom_connection'); + + // The tenant connection during migration actually used custom_connection's config + expect($tenantConnectionDuringMigration['charset'])->toBe('latin1'); + expect($tenantConnectionDuringMigration['collation'])->toBe('latin1_swedish_ci'); + + // The tenant template connection config is restored to the original after migrating + expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection); +}); + test('migrate command only throws exceptions if skip-failing is not passed', function() { Tenant::create(); From 99d854ed8e87affed8f527791ab923d1e8e31f9a Mon Sep 17 00:00:00 2001 From: Farishrf Date: Mon, 25 Aug 2025 18:27:59 +0300 Subject: [PATCH 3/3] [4.x] Fix ViteBundler not affecting Vite static calls (#1389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix ViteBundler not affecting Vite static calls Replace custom Vite class override with Vite::createAssetPathsUsing() to ensure ViteBundler works for both container and static usage when asset_helper_override is enabled. Fixes #1388 * Remove redundant logic from tests * Simplify test further * Re-add file creation logic --------- Co-authored-by: Samuel Štancl --- src/Features/ViteBundler.php | 6 ++-- src/Overrides/Vite.php | 22 -------------- tests/Features/ViteBundlerTest.php | 48 +++++++++++++++++++----------- 3 files changed, 35 insertions(+), 41 deletions(-) delete mode 100644 src/Overrides/Vite.php diff --git a/src/Features/ViteBundler.php b/src/Features/ViteBundler.php index 96f379b7..987187c7 100644 --- a/src/Features/ViteBundler.php +++ b/src/Features/ViteBundler.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Features; use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\Vite; use Stancl\Tenancy\Contracts\Feature; -use Stancl\Tenancy\Overrides\Vite; use Stancl\Tenancy\Tenancy; class ViteBundler implements Feature @@ -21,6 +21,8 @@ class ViteBundler implements Feature public function bootstrap(Tenancy $tenancy): void { - $this->app->singleton(\Illuminate\Foundation\Vite::class, Vite::class); + Vite::createAssetPathsUsing(function ($path, $secure = null) { + return global_asset($path); + }); } } diff --git a/src/Overrides/Vite.php b/src/Overrides/Vite.php deleted file mode 100644 index 66bc9268..00000000 --- a/src/Overrides/Vite.php +++ /dev/null @@ -1,22 +0,0 @@ -toBeInstanceOf(Vite::class); - expect($vite)->not()->toBeInstanceOf(StanclVite::class); +use function Stancl\Tenancy\Tests\withBootstrapping; +beforeEach(function () { config([ - 'tenancy.features' => [ViteBundler::class], + 'tenancy.filesystem.asset_helper_override' => true, + 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], ]); - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - app()->forgetInstance(Vite::class); - - $vite = app(Vite::class); - - expect($vite)->toBeInstanceOf(StanclVite::class); + File::ensureDirectoryExists(dirname($manifestPath = public_path('build/manifest.json'))); + File::put($manifestPath, json_encode([ + 'foo' => [ + 'file' => 'assets/foo-AbC123.js', + 'src' => 'js/foo.js', + ], + ])); +}); + +test('vite bundler ensures vite assets use global_asset when asset_helper_override is enabled', function () { + config(['tenancy.features' => [ViteBundler::class]]); + + withBootstrapping(); + + tenancy()->initialize(Tenant::create()); + + // Not what we want + expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo'])); + + $viteAssetUrl = app(Vite::class)->asset('foo'); + $expectedGlobalUrl = global_asset('build/assets/foo-AbC123.js'); + + expect($viteAssetUrl)->toBe($expectedGlobalUrl); + expect($viteAssetUrl)->toBe('http://localhost/build/assets/foo-AbC123.js'); });