Skip to content

Commit 30e9ad0

Browse files
authored
Merge pull request #8543 from ProcessMaker/feature/FOUR-24651_C
Multitenancy - Refactor and fix for config caching
2 parents e5bb977 + 3fc107c commit 30e9ad0

18 files changed

Lines changed: 407 additions & 349 deletions

ProcessMaker/Application.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,25 @@
55
use Igaster\LaravelTheme\Facades\Theme;
66
use Illuminate\Filesystem\Filesystem;
77
use Illuminate\Foundation\Application as IlluminateApplication;
8+
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
9+
use Illuminate\Foundation\Bootstrap\RegisterProviders;
810
use Illuminate\Foundation\PackageManifest;
11+
use Illuminate\Support\Env;
12+
use Illuminate\Support\Facades\App;
913
use Illuminate\Support\Facades\Auth;
14+
use Illuminate\Support\Facades\Config;
15+
use ProcessMaker\Multitenancy\Tenant;
16+
use ProcessMaker\Multitenancy\TenantBootstrapper;
1017

1118
/**
1219
* Class Application.
1320
*/
1421
class Application extends IlluminateApplication
1522
{
23+
public $overrideTenantId = null;
24+
25+
public $skipCacheEvents = false;
26+
1627
/**
1728
* Sets the timezone for the application and for php with the specified timezone.
1829
*
@@ -90,4 +101,15 @@ public function registerConfiguredProviders()
90101

91102
parent::registerConfiguredProviders();
92103
}
104+
105+
public function bootstrapWith(array $bootstrappers)
106+
{
107+
// Insert TenantBootstrapper after LoadEnvironmentVariables
108+
if ($bootstrappers[0] !== LoadEnvironmentVariables::class) {
109+
throw new \Exception('LoadEnvironmentVariables is not the first bootstrapper. Did a laravel upgrade change this?');
110+
}
111+
array_splice($bootstrappers, 1, 0, [TenantBootstrapper::class]);
112+
113+
return parent::bootstrapWith($bootstrappers);
114+
}
93115
}

ProcessMaker/Console/Commands/ProcessMakerLicenseRemove.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ class ProcessMakerLicenseRemove extends Command
3434
*/
3535
public function handle()
3636
{
37-
if (Storage::disk('root')->exists('license.json')) {
37+
if (Storage::disk('local')->exists('license.json')) {
3838
if ($this->option('force') || $this->confirm('Are you sure you want to remove the license.json file?')) {
39-
Storage::disk('root')->delete('license.json');
39+
Storage::disk('local')->delete('license.json');
4040
$this->info('license.json removed successfully!');
4141

4242
$this->info('Calling package:discover to update the package cache with enabled packages');

ProcessMaker/Console/Commands/ProcessMakerLicenseUpdate.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function handle()
4040
return 1;
4141
}
4242

43-
Storage::disk('root')->put('license.json', $content);
43+
Storage::disk('local')->put('license.json', $content);
4444

4545
$this->info('Calling package:discover to update the package cache with enabled packages');
4646
Artisan::call('package:discover');

ProcessMaker/Console/Commands/TenantsVerify.php

Lines changed: 61 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
namespace ProcessMaker\Console\Commands;
44

55
use Illuminate\Console\Command;
6+
use Illuminate\Contracts\Encryption\DecryptException;
67
use Illuminate\Support\Facades\Config;
8+
use Illuminate\Support\Facades\Crypt;
9+
use Illuminate\Support\Facades\File;
710
use Illuminate\Support\Facades\Log;
11+
use ProcessMaker\Models\EnvironmentVariable;
12+
use ProcessMaker\Models\User;
813
use Spatie\Multitenancy\Models\Tenant;
914

1015
class TenantsVerify extends Command
@@ -14,7 +19,7 @@ class TenantsVerify extends Command
1419
*
1520
* @var string
1621
*/
17-
protected $signature = 'tenants:verify {--verify-against= : The tenant ID to verify against}';
22+
protected $signature = 'tenants:verify';
1823

1924
/**
2025
* The console command description.
@@ -23,17 +28,6 @@ class TenantsVerify extends Command
2328
*/
2429
protected $description = 'Verify tenant configuration and storage paths';
2530

26-
/**
27-
* Strip protocol from URL
28-
*
29-
* @param string $url
30-
* @return string
31-
*/
32-
private function stripProtocol(string $url): string
33-
{
34-
return preg_replace('#^https?://#', '', $url);
35-
}
36-
3731
/**
3832
* Execute the console command.
3933
*
@@ -46,85 +40,72 @@ public function handle()
4640
$currentTenant = app('currentTenant');
4741
}
4842

49-
$verifyAgainstId = $this->option('verify-against');
50-
51-
if (!$currentTenant) {
52-
$this->error('No current tenant found');
43+
if (config('app.multitenancy') && !$currentTenant) {
44+
$this->error('Multitenancy enabled but no current tenant found.');
5345

5446
return;
5547
}
5648

57-
$this->info('Current Tenant ID: ' . $currentTenant->id);
58-
$this->line('----------------------------------------');
49+
$this->info('Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE'));
5950

60-
// Expected paths and configurations
61-
$expectedStoragePath = base_path('storage/tenant_' . $currentTenant->id);
62-
$actualConfigs = [
63-
'filesystems.disks.local.root' => storage_path('app'),
64-
'cache.prefix' => config('cache.prefix'),
65-
'app.url' => config('app.url'),
66-
'script-runner-microservice.callback' => config('script-runner-microservice.callback'),
51+
$paths = [
52+
['Storage Path', storage_path()],
53+
['Config Cache Path', app()->getCachedConfigPath()],
54+
['Lang Path', lang_path()],
6755
];
6856

69-
// Display current values
70-
$this->info('Current Storage Path: ' . storage_path());
71-
$this->line('----------------------------------------');
72-
73-
$this->info('Current Configuration Values:');
74-
foreach ($actualConfigs as $key => $expectedValue) {
75-
$currentValue = config($key);
76-
$this->line("{$key}: {$currentValue}");
77-
}
78-
79-
// If verify-against is specified, perform verification
80-
if ($verifyAgainstId) {
81-
$this->line('----------------------------------------');
82-
$this->info("Verifying against tenant ID: {$verifyAgainstId}");
57+
// Display paths in a nice table
58+
$this->table(['Path', 'Value'], $paths);
59+
60+
$configs = [
61+
'app.key',
62+
'app.url',
63+
'app.instance',
64+
'cache.prefix',
65+
'database.redis.options.prefix',
66+
'cache.stores.cache_settings.prefix',
67+
'script-runner-microservice.callback',
68+
'database.connections.processmaker.database',
69+
'logging.channels.daily.path',
70+
'filesystems.disks.public.root',
71+
'filesystems.disks.local.root',
72+
'filesystems.disks.lang.root',
73+
];
8374

84-
$expectedStoragePath = base_path('storage/tenant_' . $verifyAgainstId);
85-
$expectedConfigs = [
86-
'filesystems.disks.local.root' => $expectedStoragePath . '/app',
87-
'cache.prefix' => 'tenant_id_' . $verifyAgainstId,
88-
'app.url' => config('app.url'),
75+
$configs = array_map(function ($config) {
76+
return [
77+
$config,
78+
config($config),
8979
];
90-
91-
$hasMismatch = false;
92-
93-
// Verify storage path
94-
if (storage_path() !== $expectedStoragePath) {
95-
$this->error('Storage path mismatch!');
96-
$this->line("Expected: {$expectedStoragePath}");
97-
$this->line('Current: ' . storage_path());
98-
$hasMismatch = true;
99-
}
100-
101-
// Verify tenant URL if tenant exists
102-
$verifyTenant = Tenant::find($verifyAgainstId);
103-
if ($verifyTenant && $verifyTenant->domain !== $this->stripProtocol(config('app.url'))) {
104-
$this->error('Tenant URL mismatch!');
105-
$this->line("Expected: {$verifyTenant->domain}");
106-
$this->line('Current: ' . config('app.url'));
107-
$hasMismatch = true;
108-
}
109-
110-
// Verify config values
111-
foreach ($expectedConfigs as $key => $expectedValue) {
112-
$currentValue = config($key);
113-
if ($currentValue !== $expectedValue) {
114-
$this->error("Config mismatch for {$key}!");
115-
$this->line("Expected: {$expectedValue}");
116-
$this->line("Current: {$currentValue}");
117-
$hasMismatch = true;
118-
}
80+
}, $configs);
81+
82+
// Display configs in a nice table
83+
$this->table(['Config', 'Value'], $configs);
84+
85+
$env = EnvironmentVariable::first();
86+
if (!$env) {
87+
$decrypted = 'No environment variables found to test decryption';
88+
} else {
89+
$encryptedValue = $env->getAttributes()['value'];
90+
try {
91+
Crypt::decryptString($encryptedValue);
92+
$decrypted = 'OK';
93+
} catch (DecryptException $e) {
94+
$decrypted = 'FAILED! ' . $e->getMessage();
11995
}
120-
121-
if (!$hasMismatch) {
122-
$this->info('All configurations match as expected!');
123-
}
124-
125-
return $hasMismatch ? Command::FAILURE : Command::SUCCESS;
12696
}
12797

128-
return Command::SUCCESS;
98+
$other = [
99+
['Landlord Config Cache Path', base_path('bootstrap/cache/config.php')],
100+
['Landlord Config Is Cached', File::exists(base_path('bootstrap/cache/config.php')) ? 'Yes' : 'No'],
101+
['Tenant Config Cache Path', app()->getCachedConfigPath()],
102+
['Tenant Config Is Cached', File::exists(app()->getCachedConfigPath()) ? 'Yes' : 'No'],
103+
['First username (database check)', User::first()?->username ?? 'No users found'],
104+
['Decrypted check', substr($decrypted, 0, 50)],
105+
['Original App URL (landlord)', $currentTenant?->getOriginalValue('APP_URL') ?? config('app.url')],
106+
];
107+
108+
// Display other in a nice table
109+
$this->table(['Other', 'Value'], $other);
129110
}
130111
}

ProcessMaker/Exception/Handler.php

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ class Handler extends ExceptionHandler
4343
*/
4444
public function report(Throwable $exception)
4545
{
46+
if (!App::getFacadeRoot()) {
47+
error_log(get_class($exception) . ': ' . $exception->getMessage());
48+
49+
return;
50+
}
4651
if (App::environment() == 'testing' && env('TESTING_VERBOSE')) {
4752
// If we're verbose, we should print ALL Exceptions to the screen
4853
echo $exception->getMessage() . "\n";
@@ -146,18 +151,4 @@ protected function convertExceptionToArray(Throwable $e)
146151
'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
147152
];
148153
}
149-
150-
/**
151-
* Errors in the console must have an exit status > 0 for CI to see it as an error.
152-
* This prevents the symfony console from handling the error and returning an
153-
* exit status of 0, which it does by default surprisingly.
154-
*
155-
* @param \Symfony\Component\Console\Output\OutputInterface $output
156-
* @param Throwable $e
157-
* @return void
158-
*/
159-
public function renderForConsole($output, Throwable $e)
160-
{
161-
throw $e;
162-
}
163154
}

ProcessMaker/Jobs/RefreshArtisanCaches.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33
namespace ProcessMaker\Jobs;
44

55
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldBeUnique;
67
use Illuminate\Contracts\Queue\ShouldQueue;
78
use Illuminate\Foundation\Bus\Dispatchable;
89
use Illuminate\Queue\InteractsWithQueue;
910
use Illuminate\Queue\Middleware\WithoutOverlapping;
1011
use Illuminate\Support\Facades\Artisan;
1112

12-
class RefreshArtisanCaches implements ShouldQueue
13+
class RefreshArtisanCaches implements ShouldQueue, ShouldBeUnique
1314
{
1415
use Dispatchable, InteractsWithQueue, Queueable;
1516

16-
public $tries = 1;
17+
public $tries = 2; // One extra try to handle the debounce release
18+
19+
public $queuedAt;
1720

1821
/**
1922
* Create a new job instance.
@@ -22,7 +25,7 @@ class RefreshArtisanCaches implements ShouldQueue
2225
*/
2326
public function __construct()
2427
{
25-
//
28+
$this->queuedAt = time();
2629
}
2730

2831
/**
@@ -32,7 +35,9 @@ public function __construct()
3235
*/
3336
public function middleware(): array
3437
{
35-
return [(new WithoutOverlapping('refresh_artisan_caches'))->dontRelease()];
38+
return [
39+
(new WithoutOverlapping('refresh_artisan_caches'))->dontRelease(),
40+
];
3641
}
3742

3843
/**
@@ -49,14 +54,26 @@ public function handle()
4954
return;
5055
}
5156

57+
// Wait 3 seconds before running the job - debounce
58+
if ($this->queuedAt && $this->queuedAt >= time() - 3) {
59+
$this->release(3);
60+
61+
return;
62+
}
63+
5264
$options = [
5365
'--no-interaction' => true,
5466
'--quiet' => true,
5567
];
5668

5769
if (app()->configurationIsCached()) {
5870
Artisan::call('config:cache', $options);
71+
} else {
72+
Artisan::call('queue:restart', $options);
73+
74+
// We call this manually here since this job is dispatched
75+
// automatically when the config *is* cached
76+
RestartMessageConsumers::dispatchSync();
5977
}
60-
Artisan::call('queue:restart', $options);
6178
}
6279
}

ProcessMaker/LicensedPackageManifest.php

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Illuminate\Support\Facades\Cache;
99
use Illuminate\Support\Facades\Log;
1010
use Illuminate\Support\Facades\Storage;
11+
use ProcessMaker\Providers\ProcessMakerServiceProvider;
12+
use Spatie\Multitenancy\MultitenancyServiceProvider;
1113
use Throwable;
1214

1315
class LicensedPackageManifest extends PackageManifest
@@ -20,20 +22,6 @@ class LicensedPackageManifest extends PackageManifest
2022

2123
const LAST_PACKAGE_DISCOVERY = 0;
2224

23-
/**
24-
* Consider this the beginning of licenesing refactor for multitenancy.
25-
*
26-
* For now, this will just move the Spatie MultitenancyServiceProvider to the beginning of the service providers.
27-
*/
28-
protected function getManifest()
29-
{
30-
$manifest = parent::getManifest();
31-
$multitenancyKey = 'spatie/laravel-multitenancy';
32-
33-
// Make sure the MultitenancyServiceProvider is at the beginning of the manifest
34-
return [$multitenancyKey => $manifest[$multitenancyKey]] + $manifest;
35-
}
36-
3725
protected function packagesToIgnore()
3826
{
3927
$packagesToIgnore = $this->loadPackagesToIgnore()->all();
@@ -66,7 +54,7 @@ private function parseLicense()
6654
if (!$this->hasLicenseFile()) {
6755
return null;
6856
}
69-
$license = Storage::disk('root')->get('license.json');
57+
$license = Storage::disk('local')->get('license.json');
7058

7159
return json_decode($license, true);
7260
}
@@ -87,7 +75,7 @@ private function licensedPackages()
8775

8876
private function hasLicenseFile()
8977
{
90-
return Storage::disk('root')->exists('license.json');
78+
return Storage::disk('local')->exists('license.json');
9179
}
9280

9381
private function setExpireCache()

0 commit comments

Comments
 (0)