From 01a06c92102ba2c5fccb91740eb8dba1855c9224 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 28 Jul 2025 12:11:09 +0200 Subject: [PATCH 01/56] Add LogTenancyBootstrapper --- src/Bootstrappers/LogTenancyBootstrapper.php | 105 +++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/Bootstrappers/LogTenancyBootstrapper.php diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php new file mode 100644 index 000000000..6484cb03c --- /dev/null +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -0,0 +1,105 @@ + ['url' => 'webhookUrl'] + // or 'slack' => function ($config, $tenant) { ... } + public static array $channelOverrides = []; + + public function __construct( + protected Config $config, + protected LogManager $logManager, + ) {} + + public function bootstrap(Tenant $tenant): void + { + $this->defaultConfig = $this->config->get('logging.channels'); + $this->channels = $this->getChannels(); + + $this->configureChannels($this->channels, $tenant); + $this->forgetChannels(); + } + + public function revert(): void + { + $this->config->set('logging.channels', $this->defaultConfig); + + $this->forgetChannels(); + + $this->channels = []; + } + + protected function getChannels(): array { + $channels = [$this->config->get('logging.default')]; + + // If the default channel is stack, also get all the channels it contains + if ($channels[0] === 'stack') { + $channels = array_merge($channels, $this->config->get('logging.channels.stack.channels')); + } + + return $channels; + } + + protected function configureChannels(array $channels, Tenant $tenant): void { + foreach ($channels as $channel) { + if (in_array($channel, array_keys(static::$channelOverrides))) { + // Override specified channel's config as specified in the $channelOverrides property + // Takes precedence over the storage path channels handling + // The override is an array, use tenant property for overriding the channel config (the default approach) + if (is_array(static::$channelOverrides[$channel])) { + foreach (static::$channelOverrides[$channel] as $channelConfigKey => $tenantProperty) { + // E.g. set 'slack' channel's 'url' to $tenant->webhookUrl + $this->config->set("logging.channels.{$channel}.{$channelConfigKey}", $tenant->$tenantProperty); + } + } + + // If the override is a closure, call it with the config and tenant + // This allows for more custom configurations + if (static::$channelOverrides[$channel] instanceof Closure) { + static::$channelOverrides[$channel]($this->config, $tenant); + } + } else if (in_array($channel, static::$storagePathChannels)) { + // Default handling for storage path channels ('single', 'daily') + // Can be overriden by the $channelOverrides property + // Set the log path to storage_path('logs/laravel.log') for the tenant + // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" + $this->config->set("logging.channels.{$channel}.path", storage_path("logs/laravel.log")); + } + } + } + + protected function forgetChannels(): void { + // Forget the channels so that they can be re-resolved with the new config on the next log attempt + foreach ($this->channels as $channel) { + $this->logManager->forgetChannel($channel); + } + } +} From 96a05cdce8f05cf053dd9dfc0861de52112838ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 10:11:35 +0000 Subject: [PATCH 02/56] Fix code style (php-cs-fixer) --- src/Bootstrappers/LogTenancyBootstrapper.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 6484cb03c..63ba2ee50 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -5,10 +5,10 @@ namespace Stancl\Tenancy\Bootstrappers; use Closure; +use Illuminate\Contracts\Config\Repository as Config; use Illuminate\Log\LogManager; -use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\TenancyBootstrapper; -use Illuminate\Contracts\Config\Repository as Config; +use Stancl\Tenancy\Contracts\Tenant; /** * This bootstrapper allows modifying the logs so that they're tenant-specific. @@ -57,7 +57,8 @@ public function revert(): void $this->channels = []; } - protected function getChannels(): array { + protected function getChannels(): array + { $channels = [$this->config->get('logging.default')]; // If the default channel is stack, also get all the channels it contains @@ -68,7 +69,8 @@ protected function getChannels(): array { return $channels; } - protected function configureChannels(array $channels, Tenant $tenant): void { + protected function configureChannels(array $channels, Tenant $tenant): void + { foreach ($channels as $channel) { if (in_array($channel, array_keys(static::$channelOverrides))) { // Override specified channel's config as specified in the $channelOverrides property @@ -86,17 +88,18 @@ protected function configureChannels(array $channels, Tenant $tenant): void { if (static::$channelOverrides[$channel] instanceof Closure) { static::$channelOverrides[$channel]($this->config, $tenant); } - } else if (in_array($channel, static::$storagePathChannels)) { + } elseif (in_array($channel, static::$storagePathChannels)) { // Default handling for storage path channels ('single', 'daily') // Can be overriden by the $channelOverrides property // Set the log path to storage_path('logs/laravel.log') for the tenant // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" - $this->config->set("logging.channels.{$channel}.path", storage_path("logs/laravel.log")); + $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); } } } - protected function forgetChannels(): void { + protected function forgetChannels(): void + { // Forget the channels so that they can be re-resolved with the new config on the next log attempt foreach ($this->channels as $channel) { $this->logManager->forgetChannel($channel); From 50853a3c4509e405d3f967488dde5b789164629f Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 10:59:56 +0200 Subject: [PATCH 03/56] Test LogTenancyBootstrapper logic (low-level tests) --- .../LogTenancyBootstrapperTest.php | 227 ++++++++++++++++++ tests/TestCase.php | 2 + 2 files changed, 229 insertions(+) create mode 100644 tests/Bootstrappers/LogTenancyBootstrapperTest.php diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php new file mode 100644 index 000000000..cd74d1aef --- /dev/null +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -0,0 +1,227 @@ + [ + // FilesystemTenancyBootstrapper needed for storage path channels (added in tests that check the storage path channel logic) + LogTenancyBootstrapper::class, + ], + ]); + + // Reset static properties + LogTenancyBootstrapper::$channelOverrides = []; + LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); +}); + +afterEach(function () { + LogTenancyBootstrapper::$channelOverrides = []; + LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; +}); + +test('storage path channels get tenant-specific paths', function () { + // Note that for LogTenancyBootstrapper to change the paths correctly, + // the bootstrapper MUST run after FilesystemTenancyBootstrapper. + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + LogTenancyBootstrapper::class, + ], + ]); + + $tenant = Tenant::create(); + + // Storage path channels are 'single' and 'daily' by default. + // This can be customized via LogTenancyBootstrapper::$storagePathChannels. + foreach (LogTenancyBootstrapper::$storagePathChannels as $channel) { + config(['logging.default' => $channel]); + + $originalPath = config("logging.channels.{$channel}.path"); + + tenancy()->initialize($tenant); + + // Path should now point to the log in the tenant's storage directory + $tenantLogPath = "storage/tenant{$tenant->id}/logs/laravel.log"; + expect(config("logging.channels.{$channel}.path")) + ->not()->toBe($originalPath) + ->toEndWith($tenantLogPath); + + tenancy()->end(); + + // Path should be reverted + expect(config("logging.channels.{$channel}.path"))->toBe($originalPath); + } +}); + +test('all channels included in the log stack get processed', function () { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + LogTenancyBootstrapper::class, + ], + 'logging.default' => 'stack', + 'logging.channels.stack' => [ + 'driver' => 'stack', + 'channels' => ['single', 'daily'], + ], + ]); + + $originalSinglePath = config('logging.channels.single.path'); + $originalDailyPath = config('logging.channels.daily.path'); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + // Both channels in the stack should be updated + expect(config('logging.channels.single.path'))->not()->toBe($originalSinglePath); + expect(config('logging.channels.daily.path'))->not()->toBe($originalDailyPath); + + tenancy()->end(); + + expect(config('logging.channels.single.path'))->toBe($originalSinglePath); + expect(config('logging.channels.daily.path'))->toBe($originalDailyPath); +}); + +test('channel overrides work correctly with both arrays and closures', function () { + config([ + 'logging.default' => 'slack', + 'logging.channels.slack' => [ + 'driver' => 'slack', + 'url' => $originalSlackUrl = 'https://default-webhook.example.com', + 'username' => 'Default', + ], + ]); + + $tenant = Tenant::create(['id' => 'tenant1', 'webhookUrl' => 'https://tenant-webhook.example.com']); + + // Specify channel override for 'slack' channel using an array + LogTenancyBootstrapper::$channelOverrides = [ + 'slack' => [ + 'url' => 'webhookUrl', // $tenant->webhookUrl will be used + ], + ]; + + tenancy()->initialize($tenant); + + expect(config('logging.channels.slack.url'))->toBe($tenant->webhookUrl); + expect(config('logging.channels.slack.username'))->toBe('Default'); // Default username -- remains default unless specified + + tenancy()->end(); + + // After tenancy ends, the original config should be restored + expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); + + // Now, use closure to set the slack username to $tenant->id (tenant1) + LogTenancyBootstrapper::$channelOverrides['slack'] = function ($config, $tenant) { + $config->set('logging.channels.slack.username', $tenant->id); + }; + + tenancy()->initialize($tenant); + + expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); // Unchanged + expect(config('logging.channels.slack.username'))->toBe($tenant->id); + + tenancy()->end(); + + // Config reverted back to original + expect(config('logging.channels.slack.username'))->toBe('Default'); +}); + +test('channel overrides take precedence over the default storage path channel updating logic', function () { + config(['logging.default' => 'single']); + + $tenant = Tenant::create(['id' => 'tenant1']); + + LogTenancyBootstrapper::$channelOverrides = [ + 'single' => function ($config, $tenant) { + $config->set('logging.channels.single.path', storage_path("logs/override-{$tenant->id}.log")); + }, + ]; + + tenancy()->initialize($tenant); + + // Should use override, not the default storage path updating behavior + expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); +}); + +test('multiple channel overrides work together', function () { + config([ + 'logging.default' => 'stack', + 'logging.channels.stack' => [ + 'driver' => 'stack', + 'channels' => ['slack', 'single'], + ], + ]); + + $originalSinglePath = config('logging.channels.single.path'); + $originalSlackUrl = config('logging.channels.slack.url'); + + $tenant = Tenant::create(['id' => 'tenant1', 'slackUrl' => 'https://tenant-slack.example.com']); + + LogTenancyBootstrapper::$channelOverrides = [ + 'slack' => ['url' => 'slackUrl'], + 'single' => function ($config, $tenant) { + $config->set('logging.channels.single.path', storage_path("logs/override-{$tenant->id}.log")); + }, + ]; + + tenancy()->initialize($tenant); + + expect(config('logging.channels.slack.url'))->toBe('https://tenant-slack.example.com'); + expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); + + tenancy()->end(); + + expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); + expect(config('logging.channels.single.path'))->toBe($originalSinglePath); +}); + +test('channels are forgotten and re-resolved during bootstrap and revert', function () { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + LogTenancyBootstrapper::class, + ], + 'logging.default' => 'single' + ]); + + $logManager = app('log'); + $originalChannel = $logManager->channel('single'); + $originalSinglePath = config('logging.channels.single.path'); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + // After bootstrap, the channel should be a new instance with the updated config + $tenantChannel = $logManager->channel('single'); + $tenantSingleChannelPath = $tenantChannel->getLogger()->getHandlers()[0]->getUrl(); + + expect($tenantChannel)->not()->toBe($originalChannel); + expect($tenantSingleChannelPath) + ->not()->toBe($originalSinglePath) + ->toEndWith("storage/tenant{$tenant->id}/logs/laravel.log"); + + tenancy()->end(); + + // After revert, the channel should get re-resolved with the original config + $currentChannel = $logManager->channel('single'); + $currentChannelPath = $currentChannel->getLogger()->getHandlers()[0]->getUrl(); + + expect($currentChannel)->not()->toBe($tenantChannel); + expect($currentChannelPath)->toBe($originalSinglePath); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 47af9e7db..0999c117a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,6 +24,7 @@ use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use function Stancl\Tenancy\Tests\pest; +use Stancl\Tenancy\Bootstrappers\LogTenancyBootstrapper; abstract class TestCase extends \Orchestra\Testbench\TestCase { @@ -187,6 +188,7 @@ protected function getEnvironmentSetUp($app) $app->singleton(RootUrlBootstrapper::class); $app->singleton(UrlGeneratorBootstrapper::class); $app->singleton(FilesystemTenancyBootstrapper::class); + $app->singleton(LogTenancyBootstrapper::class); } protected function getPackageProviders($app) From b80d7b3996459eda5655bf27ff1d744b89752092 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 11:44:32 +0200 Subject: [PATCH 04/56] Test real usage with storage path-based channels --- .../LogTenancyBootstrapperTest.php | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index cd74d1aef..70f775f2b 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -225,3 +225,125 @@ expect($currentChannel)->not()->toBe($tenantChannel); expect($currentChannelPath)->toBe($originalSinglePath); }); + +// Test real usage +test('logs are written to tenant-specific files and do not leak between contexts', function () { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + LogTenancyBootstrapper::class, + ], + 'logging.default' => 'single', + ]); + + $centralLogPath = storage_path('logs/laravel.log'); + + logger('central'); + + expect(file_get_contents($centralLogPath))->toContain('central'); + + [$tenant1, $tenant2] = [Tenant::create(['id' => 'tenant1']), Tenant::create(['id' => 'tenant2'])]; + + tenancy()->runForMultiple([$tenant1, $tenant2], function (Tenant $tenant) use ($centralLogPath) { + logger($tenant->id); + + $tenantLogPath = storage_path('logs/laravel.log'); + + // The log gets saved to the tenant's storage directory (default behavior) + expect($tenantLogPath) + ->not()->toBe($centralLogPath) + ->toEndWith("storage/tenant{$tenant->id}/logs/laravel.log"); + + expect(file_get_contents($tenantLogPath)) + ->toContain($tenant->id) + ->not()->toContain('central'); + }); + + // Tenant log messages didn't leak into central log + expect(file_get_contents($centralLogPath)) + ->toContain('central') + ->not()->toContain('tenant1') + ->not()->toContain('tenant2'); + + // Tenant log messages didn't leak to logs of other tenants + tenancy()->initialize($tenant1); + + expect(file_get_contents(storage_path('logs/laravel.log'))) + ->toContain('tenant1') + ->not()->toContain('central') + ->not()->toContain('tenant2'); + + tenancy()->initialize($tenant2); + + expect(file_get_contents(storage_path('logs/laravel.log'))) + ->toContain('tenant2') + ->not()->toContain('central') + ->not()->toContain('tenant1'); + + // Overriding the channels also works + // Channel overrides also override the default behavior for the storage path-based channels + $tenant = Tenant::create(['id' => 'override-tenant']); + + LogTenancyBootstrapper::$channelOverrides = [ + 'single' => function ($config, $tenant) { + // The tenant log path will be set to storage/tenantoverride-tenant/logs/custom-override-tenant.log + $config->set('logging.channels.single.path', storage_path("logs/custom-{$tenant->id}.log")); + }, + ]; + + // Tenant context log (should use custom path due to override) + tenancy()->initialize($tenant); + + logger('tenant-override'); + + expect(file_get_contents(storage_path('logs/custom-override-tenant.log')))->toContain('tenant-override'); +}); + +test('stack logs are written to all configured channels with tenant-specific paths', function () { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + LogTenancyBootstrapper::class, + ], + 'logging.default' => 'stack', + 'logging.channels.stack' => [ + 'driver' => 'stack', + 'channels' => ['single', 'daily'], + ], + ]); + + $tenant = Tenant::create(['id' => 'stack-tenant']); + $today = now()->format('Y-m-d'); + + // Central context stack log + logger('central'); + $centralSingleLogPath = storage_path('logs/laravel.log'); + $centralDailyLogPath = storage_path("logs/laravel-{$today}.log"); + + expect(file_get_contents($centralSingleLogPath))->toContain('central'); + expect(file_get_contents($centralDailyLogPath))->toContain('central'); + + // Tenant context stack log + tenancy()->initialize($tenant); + logger('tenant'); + $tenantSingleLogPath = storage_path('logs/laravel.log'); + $tenantDailyLogPath = storage_path("logs/laravel-{$today}.log"); + + expect(file_get_contents($tenantSingleLogPath))->toContain('tenant'); + expect(file_get_contents($tenantDailyLogPath))->toContain('tenant'); + + // Verify tenant logs don't contain central messages + expect(file_get_contents($tenantSingleLogPath))->not()->toContain('central'); + expect(file_get_contents($tenantDailyLogPath))->not()->toContain('central'); + + tenancy()->end(); + + // Verify central logs still only contain the central messages + expect(file_get_contents($centralSingleLogPath)) + ->toContain('central') + ->not()->toContain('tenant'); + + expect(file_get_contents($centralDailyLogPath)) + ->toContain('central') + ->not()->toContain('tenant'); +}); From a13110c88002ca9bbbc4a843199b972e663e1e37 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 12:19:04 +0200 Subject: [PATCH 05/56] Test real usage with slack channel (the bootstrapper updates the webhook used by the slack channel correctly) --- .../LogTenancyBootstrapperTest.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 70f775f2b..c5613b9e0 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -347,3 +347,54 @@ ->toContain('central') ->not()->toContain('tenant'); }); + +test('slack channel uses correct webhook urls', function () { + config([ + 'logging.default' => 'slack', + 'logging.channels.slack.url' => 'central-webhook', + 'logging.channels.slack.level' => 'debug', // Set level to debug to keep the tests simple, since the default level here is 'critical' + ]); + + $tenant1 = Tenant::create(['id' => 'tenant1', 'slackUrl' => 'tenant1-webhook']); + $tenant2 = Tenant::create(['id' => 'tenant2', 'slackUrl' => 'tenant2-webhook']); + + LogTenancyBootstrapper::$channelOverrides = [ + 'slack' => ['url' => 'slackUrl'], + ]; + + // Test central context - should attempt to use central webhook + try { + logger('central'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('central-webhook'); + } + + // Test tenant 1 context - should attempt to use tenant1 webhook + tenancy()->initialize($tenant1); + + try { + logger('tenant1'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('tenant1-webhook'); + } + + tenancy()->end(); + + // Test tenant 2 context - should attempt to use tenant2 webhook + tenancy()->initialize($tenant2); + + try { + logger('tenant2'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('tenant2-webhook'); + } + + tenancy()->end(); + + // Back to central - should use central webhook again + try { + logger('central'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('central-webhook'); + } +}); From 718afd306933243b4b7ce34adf6c9ee85ad21c3e Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 12:24:06 +0200 Subject: [PATCH 06/56] Simplify the slack channel usage test --- .../LogTenancyBootstrapperTest.php | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index c5613b9e0..743d7124b 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -363,35 +363,24 @@ ]; // Test central context - should attempt to use central webhook + // Because the Slack channel uses cURL to send messages, we cannot use Http::fake() here. + // Instead, we catch the exception and check the error message which contains the actual webhook URL. try { logger('central'); } catch (Exception $e) { expect($e->getMessage())->toContain('central-webhook'); } - // Test tenant 1 context - should attempt to use tenant1 webhook - tenancy()->initialize($tenant1); - - try { - logger('tenant1'); - } catch (Exception $e) { - expect($e->getMessage())->toContain('tenant1-webhook'); - } - - tenancy()->end(); - - // Test tenant 2 context - should attempt to use tenant2 webhook - tenancy()->initialize($tenant2); - - try { - logger('tenant2'); - } catch (Exception $e) { - expect($e->getMessage())->toContain('tenant2-webhook'); - } - - tenancy()->end(); + // Slack channel should attempt to use the tenant-specific webhooks + tenancy()->runForMultiple([$tenant1, $tenant2], function (Tenant $tenant) { + try { + logger($tenant->id); + } catch (Exception $e) { + expect($e->getMessage())->toContain($tenant->slackUrl); + } + }); - // Back to central - should use central webhook again + // Central context, central webhook should be used again try { logger('central'); } catch (Exception $e) { From a806df063d3b8d8e65b2c4b5014b41cc8b8dc6b0 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 12:25:02 +0200 Subject: [PATCH 07/56] Stop using real domains in the tests --- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 743d7124b..93709b53b 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -101,12 +101,12 @@ 'logging.default' => 'slack', 'logging.channels.slack' => [ 'driver' => 'slack', - 'url' => $originalSlackUrl = 'https://default-webhook.example.com', + 'url' => $originalSlackUrl = 'default-webhook', 'username' => 'Default', ], ]); - $tenant = Tenant::create(['id' => 'tenant1', 'webhookUrl' => 'https://tenant-webhook.example.com']); + $tenant = Tenant::create(['id' => 'tenant1', 'webhookUrl' => 'tenant-webhook']); // Specify channel override for 'slack' channel using an array LogTenancyBootstrapper::$channelOverrides = [ @@ -170,7 +170,7 @@ $originalSinglePath = config('logging.channels.single.path'); $originalSlackUrl = config('logging.channels.slack.url'); - $tenant = Tenant::create(['id' => 'tenant1', 'slackUrl' => 'https://tenant-slack.example.com']); + $tenant = Tenant::create(['id' => 'tenant1', 'slackUrl' => 'tenant-slack']); LogTenancyBootstrapper::$channelOverrides = [ 'slack' => ['url' => 'slackUrl'], @@ -181,7 +181,7 @@ tenancy()->initialize($tenant); - expect(config('logging.channels.slack.url'))->toBe('https://tenant-slack.example.com'); + expect(config('logging.channels.slack.url'))->toBe('tenant-slack'); expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); tenancy()->end(); From ec4752881ce993b5fe080839648092e260b89c99 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 12:48:11 +0200 Subject: [PATCH 08/56] Refactor bootstrapper, make comments more concise --- src/Bootstrappers/LogTenancyBootstrapper.php | 71 +++++++++++--------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 63ba2ee50..1493a6109 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -11,27 +11,33 @@ use Stancl\Tenancy\Contracts\Tenant; /** - * This bootstrapper allows modifying the logs so that they're tenant-specific. + * Bootstrapper for tenant-specific logging configuration. * - * If the used channel is 'single' or 'daily', it will set the log path to - * storage_path('logs/laravel.log') for the tenant (so the tenant log will be located at storage/tenantX/logs/laravel.log). - * For this to work correctly, the bootstrapper needs to run after FilesystemTenancyBootstrapper. + * Automatically configures storage path channels (single, daily) to use tenant storage directories. + * Supports custom channel overrides via the $channelOverrides property. * - * Channels that don't use the storage path (e.g. 'slack') will be modified as specified in the $channelOverrides property. - * - * You can also completely override configuration of specific channels by specifying a closure in the $channelOverrides property. + * Note: Must run after FilesystemTenancyBootstrapper for storage path channels to work correctly. */ class LogTenancyBootstrapper implements TenancyBootstrapper { protected array $defaultConfig = []; - // The channels that were modified (set during bootstrap so that they can be reverted later) + /** Channels that were modified during bootstrap (for reverting later) */ protected array $channels = []; + /** + * Log channels that use storage paths for storing the logs. + * Requires FilesystemTenancyBootstrapper to run first. + */ public static array $storagePathChannels = ['single', 'daily']; - // E.g. 'slack' => ['url' => 'webhookUrl'] - // or 'slack' => function ($config, $tenant) { ... } + /** + * Custom channel configuration overrides. + * + * Examples: + * - Array mapping: ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url + * - Closure: ['slack' => fn ($config, $tenant) => $config->set('logging.channels.slack.url', $tenant->slackUrl)] + */ public static array $channelOverrides = []; public function __construct( @@ -57,6 +63,7 @@ public function revert(): void $this->channels = []; } + /** Get all channels that need to be configured, including channels in the log stack. */ protected function getChannels(): array { $channels = [$this->config->get('logging.default')]; @@ -69,38 +76,42 @@ protected function getChannels(): array return $channels; } + /** Configure channels for the tenant context. */ protected function configureChannels(array $channels, Tenant $tenant): void { foreach ($channels as $channel) { - if (in_array($channel, array_keys(static::$channelOverrides))) { - // Override specified channel's config as specified in the $channelOverrides property - // Takes precedence over the storage path channels handling - // The override is an array, use tenant property for overriding the channel config (the default approach) - if (is_array(static::$channelOverrides[$channel])) { - foreach (static::$channelOverrides[$channel] as $channelConfigKey => $tenantProperty) { - // E.g. set 'slack' channel's 'url' to $tenant->webhookUrl - $this->config->set("logging.channels.{$channel}.{$channelConfigKey}", $tenant->$tenantProperty); - } - } - - // If the override is a closure, call it with the config and tenant - // This allows for more custom configurations - if (static::$channelOverrides[$channel] instanceof Closure) { - static::$channelOverrides[$channel]($this->config, $tenant); - } + if (isset(static::$channelOverrides[$channel])) { + $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); } elseif (in_array($channel, static::$storagePathChannels)) { - // Default handling for storage path channels ('single', 'daily') - // Can be overriden by the $channelOverrides property - // Set the log path to storage_path('logs/laravel.log') for the tenant + // Set storage path channels to use tenant-specific directory (default behavior) // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); } } } + /** + * Apply channel override configuration. + */ + protected function overrideChannelConfig(string $channel, array|Closure $override, Tenant $tenant): void + { + if (is_array($override)) { + // Map tenant properties to channel config keys + foreach ($override as $configKey => $tenantProperty) { + $this->config->set("logging.channels.{$channel}.{$configKey}", $tenant->$tenantProperty); + } + } elseif ($override instanceof Closure) { + // Execute custom configuration closure + $override($this->config, $tenant); + } + } + + /** + * Forget channels so they can be re-resolved + * with updated configuration on the next log attempt. + */ protected function forgetChannels(): void { - // Forget the channels so that they can be re-resolved with the new config on the next log attempt foreach ($this->channels as $channel) { $this->logManager->forgetChannel($channel); } From 8cd35d39857b8d84ef275f565ecb12cf0bd12129 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 12:50:18 +0200 Subject: [PATCH 09/56] Add @see to bootstrapper docblock --- src/Bootstrappers/LogTenancyBootstrapper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 1493a6109..9c51c6027 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -17,6 +17,8 @@ * Supports custom channel overrides via the $channelOverrides property. * * Note: Must run after FilesystemTenancyBootstrapper for storage path channels to work correctly. + * + * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper */ class LogTenancyBootstrapper implements TenancyBootstrapper { From 62a0e395c37fd21048230f67162b5de4ce61e6e1 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 13:13:31 +0200 Subject: [PATCH 10/56] Delete redundant test, test the same logic in the one larger test --- .../LogTenancyBootstrapperTest.php | 71 +++++++------------ 1 file changed, 24 insertions(+), 47 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 93709b53b..906a81ef6 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -98,46 +98,55 @@ test('channel overrides work correctly with both arrays and closures', function () { config([ - 'logging.default' => 'slack', + 'logging.default' => 'stack', + 'logging.channels.stack.channels' => ['slack', 'single'], 'logging.channels.slack' => [ - 'driver' => 'slack', 'url' => $originalSlackUrl = 'default-webhook', 'username' => 'Default', ], ]); + $originalSinglePath = config('logging.channels.single.path'); + $tenant = Tenant::create(['id' => 'tenant1', 'webhookUrl' => 'tenant-webhook']); - // Specify channel override for 'slack' channel using an array + // Test both array mapping and closure-based overrides LogTenancyBootstrapper::$channelOverrides = [ - 'slack' => [ - 'url' => 'webhookUrl', // $tenant->webhookUrl will be used - ], + 'slack' => ['url' => 'webhookUrl'], // slack.url will be mapped to $tenant->webhookUrl + 'single' => function ($config, $tenant) { + $config->set('logging.channels.single.path', storage_path("logs/override-{$tenant->id}.log")); + }, ]; tenancy()->initialize($tenant); + // Array mapping overrides work expect(config('logging.channels.slack.url'))->toBe($tenant->webhookUrl); - expect(config('logging.channels.slack.username'))->toBe('Default'); // Default username -- remains default unless specified + expect(config('logging.channels.slack.username'))->toBe('Default'); // Default username, remains default unless overridden + + // Closure overrides work + expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); tenancy()->end(); // After tenancy ends, the original config should be restored expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); + expect(config('logging.channels.single.path'))->toBe($originalSinglePath); - // Now, use closure to set the slack username to $tenant->id (tenant1) - LogTenancyBootstrapper::$channelOverrides['slack'] = function ($config, $tenant) { - $config->set('logging.channels.slack.username', $tenant->id); - }; + // Test that we can also change array mappings to different properties + $tenant->update(['slackUrl' => 'tenant-slack']); - tenancy()->initialize($tenant); + LogTenancyBootstrapper::$channelOverrides = [ + 'slack' => ['url' => 'slackUrl'], + ]; - expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); // Unchanged - expect(config('logging.channels.slack.username'))->toBe($tenant->id); + tenancy()->initialize($tenant); + expect(config('logging.channels.slack.url'))->toBe($tenant->slackUrl); + expect(config('logging.channels.slack.username'))->toBe('Default'); // Still remains default since we only override url tenancy()->end(); - // Config reverted back to original + expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); expect(config('logging.channels.slack.username'))->toBe('Default'); }); @@ -158,38 +167,6 @@ expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); }); -test('multiple channel overrides work together', function () { - config([ - 'logging.default' => 'stack', - 'logging.channels.stack' => [ - 'driver' => 'stack', - 'channels' => ['slack', 'single'], - ], - ]); - - $originalSinglePath = config('logging.channels.single.path'); - $originalSlackUrl = config('logging.channels.slack.url'); - - $tenant = Tenant::create(['id' => 'tenant1', 'slackUrl' => 'tenant-slack']); - - LogTenancyBootstrapper::$channelOverrides = [ - 'slack' => ['url' => 'slackUrl'], - 'single' => function ($config, $tenant) { - $config->set('logging.channels.single.path', storage_path("logs/override-{$tenant->id}.log")); - }, - ]; - - tenancy()->initialize($tenant); - - expect(config('logging.channels.slack.url'))->toBe('tenant-slack'); - expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); - - tenancy()->end(); - - expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); - expect(config('logging.channels.single.path'))->toBe($originalSinglePath); -}); - test('channels are forgotten and re-resolved during bootstrap and revert', function () { config([ 'tenancy.bootstrappers' => [ From 582243c53f79bd776837306a540a9440e22a7bcb Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Jul 2025 13:22:02 +0200 Subject: [PATCH 11/56] Clarify test name --- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 906a81ef6..e5282b9aa 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -32,7 +32,7 @@ LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; }); -test('storage path channels get tenant-specific paths', function () { +test('storage path channels get tenant-specific paths by default', function () { // Note that for LogTenancyBootstrapper to change the paths correctly, // the bootstrapper MUST run after FilesystemTenancyBootstrapper. config([ From bd44036a9fc00244859f2e58371dd3d933f40a4b Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 31 Jul 2025 15:15:33 +0200 Subject: [PATCH 12/56] By default, only override the config if the override tenant property is set (otherwise, just skip the override and keep the default config value) --- src/Bootstrappers/LogTenancyBootstrapper.php | 4 +++- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 9c51c6027..31f75667d 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -100,7 +100,9 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid if (is_array($override)) { // Map tenant properties to channel config keys foreach ($override as $configKey => $tenantProperty) { - $this->config->set("logging.channels.{$channel}.{$configKey}", $tenant->$tenantProperty); + if ($tenant->$tenantProperty) { + $this->config->set("logging.channels.{$channel}.{$configKey}", $tenant->$tenantProperty); + } } } elseif ($override instanceof Closure) { // Execute custom configuration closure diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index e5282b9aa..1efef35dc 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -150,6 +150,20 @@ expect(config('logging.channels.slack.username'))->toBe('Default'); }); +test('channel config keys remains unchanged if the specified tenant override property is not set', function() { + config(['logging.default' => 'slack']); + config(['logging.channels.slack.username' => 'Default username']); + + LogTenancyBootstrapper::$channelOverrides = [ + 'slack' => ['username' => 'nonExistentProperty'], // $tenant->nonExistentProperty + ]; + + tenancy()->initialize(Tenant::create()); + + // The username should remain unchanged since the tenant property is not set + expect(config('logging.channels.slack.username'))->toBe('Default username'); +}); + test('channel overrides take precedence over the default storage path channel updating logic', function () { config(['logging.default' => 'single']); From 63bf4bf80ef6377b191ca9020bdd4cb2571fe6ce Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 31 Jul 2025 15:30:14 +0200 Subject: [PATCH 13/56] Clarify bootstrapper comments --- src/Bootstrappers/LogTenancyBootstrapper.php | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 31f75667d..6d2ec164d 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -11,12 +11,14 @@ use Stancl\Tenancy\Contracts\Tenant; /** - * Bootstrapper for tenant-specific logging configuration. + * This bootstrapper makes it possible to configure tenant-specific logging. * - * Automatically configures storage path channels (single, daily) to use tenant storage directories. - * Supports custom channel overrides via the $channelOverrides property. + * By default, the storage path channels ('single' and 'daily' by default, feel free to configure that using the $storagePathChannels property) + * are configured to use tenant storage directories. For this to work correctly, + * this bootstrapper must run *after* FilesystemTenancyBootstrapper. + * + * The bootstrapper also supports custom channel overrides via the $channelOverrides property (see the ). * - * Note: Must run after FilesystemTenancyBootstrapper for storage path channels to work correctly. * * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper */ @@ -28,8 +30,8 @@ class LogTenancyBootstrapper implements TenancyBootstrapper protected array $channels = []; /** - * Log channels that use storage paths for storing the logs. - * Requires FilesystemTenancyBootstrapper to run first. + * Log channels that use the storage_path() helper for storing the logs. + * Requires FilesystemTenancyBootstrapper to run before this bootstrapper. */ public static array $storagePathChannels = ['single', 'daily']; @@ -37,7 +39,7 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * Custom channel configuration overrides. * * Examples: - * - Array mapping: ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url + * - Array mapping: ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is set, otherwise, the override is ignored) * - Closure: ['slack' => fn ($config, $tenant) => $config->set('logging.channels.slack.url', $tenant->slackUrl)] */ public static array $channelOverrides = []; @@ -65,9 +67,10 @@ public function revert(): void $this->channels = []; } - /** Get all channels that need to be configured, including channels in the log stack. */ + /** Channels to configure (including the channels in the log stack). */ protected function getChannels(): array { + // Get the currently used (default) logging channel $channels = [$this->config->get('logging.default')]; // If the default channel is stack, also get all the channels it contains @@ -92,20 +95,18 @@ protected function configureChannels(array $channels, Tenant $tenant): void } } - /** - * Apply channel override configuration. - */ protected function overrideChannelConfig(string $channel, array|Closure $override, Tenant $tenant): void { if (is_array($override)) { - // Map tenant properties to channel config keys + // Map tenant properties to channel config keys. + // If the tenant property is not set, + // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantProperty) { if ($tenant->$tenantProperty) { $this->config->set("logging.channels.{$channel}.{$configKey}", $tenant->$tenantProperty); } } } elseif ($override instanceof Closure) { - // Execute custom configuration closure $override($this->config, $tenant); } } From 81daa9d0544ad10294970d8ea086584bda898a24 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 31 Jul 2025 16:16:32 +0200 Subject: [PATCH 14/56] Simplify test --- .../LogTenancyBootstrapperTest.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 1efef35dc..bebbb8412 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -132,22 +132,7 @@ // After tenancy ends, the original config should be restored expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); expect(config('logging.channels.single.path'))->toBe($originalSinglePath); - - // Test that we can also change array mappings to different properties - $tenant->update(['slackUrl' => 'tenant-slack']); - - LogTenancyBootstrapper::$channelOverrides = [ - 'slack' => ['url' => 'slackUrl'], - ]; - - tenancy()->initialize($tenant); - expect(config('logging.channels.slack.url'))->toBe($tenant->slackUrl); - expect(config('logging.channels.slack.username'))->toBe('Default'); // Still remains default since we only override url - - tenancy()->end(); - - expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); - expect(config('logging.channels.slack.username'))->toBe('Default'); + expect(config('logging.channels.slack.username'))->toBe('Default'); // Not changed at all }); test('channel config keys remains unchanged if the specified tenant override property is not set', function() { From 42c837d96735dae5cb484d7e7a1089f7c8b53adb Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 31 Jul 2025 16:25:40 +0200 Subject: [PATCH 15/56] Refactor bootstrapper, provide more info in comments --- src/Bootstrappers/LogTenancyBootstrapper.php | 67 ++++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 6d2ec164d..05619bb01 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -13,12 +13,12 @@ /** * This bootstrapper makes it possible to configure tenant-specific logging. * - * By default, the storage path channels ('single' and 'daily' by default, feel free to configure that using the $storagePathChannels property) - * are configured to use tenant storage directories. For this to work correctly, - * this bootstrapper must run *after* FilesystemTenancyBootstrapper. - * - * The bootstrapper also supports custom channel overrides via the $channelOverrides property (see the ). + * By default, the storage path channels ('single' and 'daily' by default, + * but feel free to customize that using the $storagePathChannels property) + * are configured to use tenant storage directories. + * For this to work correctly, this bootstrapper must run *after* FilesystemTenancyBootstrapper. * + * The bootstrapper also supports custom channel overrides via the $channelOverrides property (see the property's docblock). * * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper */ @@ -26,9 +26,6 @@ class LogTenancyBootstrapper implements TenancyBootstrapper { protected array $defaultConfig = []; - /** Channels that were modified during bootstrap (for reverting later) */ - protected array $channels = []; - /** * Log channels that use the storage_path() helper for storing the logs. * Requires FilesystemTenancyBootstrapper to run before this bootstrapper. @@ -39,7 +36,7 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * Custom channel configuration overrides. * * Examples: - * - Array mapping: ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is set, otherwise, the override is ignored) + * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is set, otherwise, the override is ignored) * - Closure: ['slack' => fn ($config, $tenant) => $config->set('logging.channels.slack.url', $tenant->slackUrl)] */ public static array $channelOverrides = []; @@ -52,36 +49,50 @@ public function __construct( public function bootstrap(Tenant $tenant): void { $this->defaultConfig = $this->config->get('logging.channels'); - $this->channels = $this->getChannels(); + $channels = $this->getChannels(); - $this->configureChannels($this->channels, $tenant); - $this->forgetChannels(); + $this->configureChannels($channels, $tenant); + $this->forgetChannels($channels); } public function revert(): void { $this->config->set('logging.channels', $this->defaultConfig); - $this->forgetChannels(); - - $this->channels = []; + $this->forgetChannels($this->getChannels()); } - /** Channels to configure (including the channels in the log stack). */ + /** + * Channels to configure and re-resolve afterwards (including the channels in the log stack). + */ protected function getChannels(): array { // Get the currently used (default) logging channel - $channels = [$this->config->get('logging.default')]; - - // If the default channel is stack, also get all the channels it contains - if ($channels[0] === 'stack') { - $channels = array_merge($channels, $this->config->get('logging.channels.stack.channels')); - } + $defaultChannel = $this->config->get('logging.default'); + $channelIsStack = $this->config->get("logging.channels.{$defaultChannel}.driver") === 'stack'; + + // If the default channel is stack, also get all the channels it contains. + // The stack channel also has to be included in the list of channels + // since the channel will be resolved and saved in the log manager, + // and its config could accidentally be used instead of the underlying channels. + // + // For example, when you use 'stack' with the 'slack' channel and you want to configure the webhook URL, + // both the 'stack' and the 'slack' must be re-resolved after updating the config for the channels to use the correct webhook URLs. + // If only one of the mentioned channels would be re-resolved, the other's webhook URL would be used for logging. + $channels = $channelIsStack + ? [$defaultChannel, ...$this->config->get("logging.channels.{$defaultChannel}.channels")] + : [$defaultChannel]; return $channels; } - /** Configure channels for the tenant context. */ + /** + * Configure channels for the tenant context. + * + * Only the channels that are in the $storagePathChannels array + * or have custom overrides in the $channelOverrides property + * will be configured. + */ protected function configureChannels(array $channels, Tenant $tenant): void { foreach ($channels as $channel) { @@ -99,7 +110,7 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid { if (is_array($override)) { // Map tenant properties to channel config keys. - // If the tenant property is not set, + // If the tenant property is not set (= is null), // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantProperty) { if ($tenant->$tenantProperty) { @@ -112,12 +123,12 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid } /** - * Forget channels so they can be re-resolved - * with updated configuration on the next log attempt. + * Forget all passed channels so they can be re-resolved + * with updated config on the next logging attempt. */ - protected function forgetChannels(): void + protected function forgetChannels(array $channels): void { - foreach ($this->channels as $channel) { + foreach ($channels as $channel) { $this->logManager->forgetChannel($channel); } } From 7bdbe9d880d3cad8faea01ef0346d0410aaf13f2 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 31 Jul 2025 16:52:06 +0200 Subject: [PATCH 16/56] Improve checking if tenant attribute is set --- src/Bootstrappers/LogTenancyBootstrapper.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 05619bb01..6089500ef 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -112,9 +112,11 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid // Map tenant properties to channel config keys. // If the tenant property is not set (= is null), // the override is ignored and the channel config key's value remains unchanged. - foreach ($override as $configKey => $tenantProperty) { - if ($tenant->$tenantProperty) { - $this->config->set("logging.channels.{$channel}.{$configKey}", $tenant->$tenantProperty); + foreach ($override as $configKey => $tenantAttributeName) { + $tenantAttribute = $tenant->getAttribute($tenantAttributeName); + + if ($tenantAttribute !== null) { + $this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute); } } } elseif ($override instanceof Closure) { From c180c2c54e1e6cac61ffcf6c4e7f7f778950bbc0 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 31 Jul 2025 16:54:08 +0200 Subject: [PATCH 17/56] Use more accurate terminology --- src/Bootstrappers/LogTenancyBootstrapper.php | 6 +++--- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 6089500ef..9bc1899ea 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -36,7 +36,7 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * Custom channel configuration overrides. * * Examples: - * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is set, otherwise, the override is ignored) + * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) * - Closure: ['slack' => fn ($config, $tenant) => $config->set('logging.channels.slack.url', $tenant->slackUrl)] */ public static array $channelOverrides = []; @@ -109,8 +109,8 @@ protected function configureChannels(array $channels, Tenant $tenant): void protected function overrideChannelConfig(string $channel, array|Closure $override, Tenant $tenant): void { if (is_array($override)) { - // Map tenant properties to channel config keys. - // If the tenant property is not set (= is null), + // Map tenant attributes to channel config keys. + // If the tenant attribute is null, // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantAttributeName) { $tenantAttribute = $tenant->getAttribute($tenantAttributeName); diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index bebbb8412..609418920 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -135,17 +135,17 @@ expect(config('logging.channels.slack.username'))->toBe('Default'); // Not changed at all }); -test('channel config keys remains unchanged if the specified tenant override property is not set', function() { +test('channel config keys remains unchanged if the specified tenant override attribute is null', function() { config(['logging.default' => 'slack']); config(['logging.channels.slack.username' => 'Default username']); LogTenancyBootstrapper::$channelOverrides = [ - 'slack' => ['username' => 'nonExistentProperty'], // $tenant->nonExistentProperty + 'slack' => ['username' => 'nonExistentAttribute'], // $tenant->nonExistentAttribute ]; tenancy()->initialize(Tenant::create()); - // The username should remain unchanged since the tenant property is not set + // The username should remain unchanged since the tenant attribute is null expect(config('logging.channels.slack.username'))->toBe('Default username'); }); From f878aaf4e4f3abd1f072139220f79402d31dde56 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 29 Oct 2025 10:21:08 +0100 Subject: [PATCH 18/56] Improve closure overrides --- src/Bootstrappers/LogTenancyBootstrapper.php | 4 +++- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 9bc1899ea..6785a80a9 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -120,7 +120,9 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid } } } elseif ($override instanceof Closure) { - $override($this->config, $tenant); + $channelConfigKey = "logging.channels.{$channel}"; + + $this->config->set($channelConfigKey, $override($this->config->get($channelConfigKey), $tenant)); } } diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 609418920..55c68dfc9 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -113,8 +113,8 @@ // Test both array mapping and closure-based overrides LogTenancyBootstrapper::$channelOverrides = [ 'slack' => ['url' => 'webhookUrl'], // slack.url will be mapped to $tenant->webhookUrl - 'single' => function ($config, $tenant) { - $config->set('logging.channels.single.path', storage_path("logs/override-{$tenant->id}.log")); + 'single' => function (array $channel, Tenant $tenant) { + return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]); }, ]; @@ -155,8 +155,8 @@ $tenant = Tenant::create(['id' => 'tenant1']); LogTenancyBootstrapper::$channelOverrides = [ - 'single' => function ($config, $tenant) { - $config->set('logging.channels.single.path', storage_path("logs/override-{$tenant->id}.log")); + 'single' => function (array $channel, Tenant $tenant) { + return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]); }, ]; @@ -261,9 +261,9 @@ $tenant = Tenant::create(['id' => 'override-tenant']); LogTenancyBootstrapper::$channelOverrides = [ - 'single' => function ($config, $tenant) { + 'single' => function (array $channel, Tenant $tenant) { // The tenant log path will be set to storage/tenantoverride-tenant/logs/custom-override-tenant.log - $config->set('logging.channels.single.path', storage_path("logs/custom-{$tenant->id}.log")); + return array_merge($channel, ['path' => storage_path("logs/custom-{$tenant->id}.log")]); }, ]; From b36f3ce4ee46836b47e29bc6190b2e6ad9f65c24 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 29 Oct 2025 13:36:55 +0100 Subject: [PATCH 19/56] Fix typo --- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 55c68dfc9..86b112f1f 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -135,7 +135,7 @@ expect(config('logging.channels.slack.username'))->toBe('Default'); // Not changed at all }); -test('channel config keys remains unchanged if the specified tenant override attribute is null', function() { +test('channel config keys remain unchanged if the specified tenant override attribute is null', function() { config(['logging.default' => 'slack']); config(['logging.channels.slack.username' => 'Default username']); From 108e0d13637d5fee3e8b80b660da566f961a2b23 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 29 Oct 2025 14:12:53 +0100 Subject: [PATCH 20/56] Swap closure param order, add/update comments --- src/Bootstrappers/LogTenancyBootstrapper.php | 6 +++--- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 6785a80a9..0355dd7ed 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -37,7 +37,7 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * * Examples: * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) - * - Closure: ['slack' => fn ($config, $tenant) => $config->set('logging.channels.slack.url', $tenant->slackUrl)] + * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] */ public static array $channelOverrides = []; @@ -100,7 +100,7 @@ protected function configureChannels(array $channels, Tenant $tenant): void $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); } elseif (in_array($channel, static::$storagePathChannels)) { // Set storage path channels to use tenant-specific directory (default behavior) - // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" + // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" (assuming FilesystemTenancyBootstrapper is used before this bootstrapper) $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); } } @@ -122,7 +122,7 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid } elseif ($override instanceof Closure) { $channelConfigKey = "logging.channels.{$channel}"; - $this->config->set($channelConfigKey, $override($this->config->get($channelConfigKey), $tenant)); + $this->config->set($channelConfigKey, $override($tenant, $this->config->get($channelConfigKey))); } } diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 86b112f1f..eb43e605e 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -14,7 +14,7 @@ beforeEach(function () { config([ 'tenancy.bootstrappers' => [ - // FilesystemTenancyBootstrapper needed for storage path channels (added in tests that check the storage path channel logic) + // FilesystemTenancyBootstrapper needed for LogTenancyBootstrapper to work with storage path channels BY DEFAULT (note that this can be completely overridden) LogTenancyBootstrapper::class, ], ]); @@ -33,7 +33,7 @@ }); test('storage path channels get tenant-specific paths by default', function () { - // Note that for LogTenancyBootstrapper to change the paths correctly, + // Note that for LogTenancyBootstrapper to change the paths correctly by default, // the bootstrapper MUST run after FilesystemTenancyBootstrapper. config([ 'tenancy.bootstrappers' => [ @@ -113,7 +113,7 @@ // Test both array mapping and closure-based overrides LogTenancyBootstrapper::$channelOverrides = [ 'slack' => ['url' => 'webhookUrl'], // slack.url will be mapped to $tenant->webhookUrl - 'single' => function (array $channel, Tenant $tenant) { + 'single' => function (Tenant $tenant, array $channel) { return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]); }, ]; @@ -155,7 +155,7 @@ $tenant = Tenant::create(['id' => 'tenant1']); LogTenancyBootstrapper::$channelOverrides = [ - 'single' => function (array $channel, Tenant $tenant) { + 'single' => function (Tenant $tenant, array $channel) { return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]); }, ]; @@ -261,7 +261,7 @@ $tenant = Tenant::create(['id' => 'override-tenant']); LogTenancyBootstrapper::$channelOverrides = [ - 'single' => function (array $channel, Tenant $tenant) { + 'single' => function (Tenant $tenant, array $channel) { // The tenant log path will be set to storage/tenantoverride-tenant/logs/custom-override-tenant.log return array_merge($channel, ['path' => storage_path("logs/custom-{$tenant->id}.log")]); }, From e133c87c666b3a0d58321ec4dcd116d4f6b8f4dd Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 29 Oct 2025 14:15:06 +0100 Subject: [PATCH 21/56] Make test priovide sufficient context for understanding the default behavior, improve test by making assertions more specific --- .../LogTenancyBootstrapperTest.php | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index eb43e605e..e1519cb18 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -66,7 +66,7 @@ } }); -test('all channels included in the log stack get processed', function () { +test('all channels included in the log stack get processed correctly', function () { config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, @@ -79,16 +79,29 @@ ], ]); + $centralStoragePath = storage_path(); + $centralLogPath = $centralStoragePath . '/logs/laravel.log'; $originalSinglePath = config('logging.channels.single.path'); $originalDailyPath = config('logging.channels.daily.path'); + // By default, both paths are the same in the config. + // Note that in actual usage, the daily log file name is parsed differently from the path in the config, + // but the paths *in the config* are the same. + expect($centralLogPath) + ->toBe($centralStoragePath . '/logs/laravel.log') + ->toBe($originalSinglePath) + ->toBe($originalDailyPath); + $tenant = Tenant::create(); tenancy()->initialize($tenant); - // Both channels in the stack should be updated - expect(config('logging.channels.single.path'))->not()->toBe($originalSinglePath); - expect(config('logging.channels.daily.path'))->not()->toBe($originalDailyPath); + // Both channels in the stack are updated correctly + expect("{$centralStoragePath}/tenant{$tenant->id}/logs/laravel.log") + ->not()->toBe($originalSinglePath) + ->not()->toBe($originalDailyPath) + ->toBe(config('logging.channels.single.path')) + ->toBe(config('logging.channels.daily.path')); tenancy()->end(); From 58a2447adc3d8c192e141a55d40f0fd729ba5b00 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 29 Oct 2025 14:47:07 +0100 Subject: [PATCH 22/56] Use more direct assertions in the tests that assert the actual behavior, keep simpler/less direct assertions in tests that don't require direct assertions, add comments to clarify the behavior --- .../Bootstrappers/LogTenancyBootstrapperTest.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index e1519cb18..5900beb23 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -42,6 +42,7 @@ ], ]); + $centralStoragePath = storage_path(); $tenant = Tenant::create(); // Storage path channels are 'single' and 'daily' by default. @@ -54,10 +55,10 @@ tenancy()->initialize($tenant); // Path should now point to the log in the tenant's storage directory - $tenantLogPath = "storage/tenant{$tenant->id}/logs/laravel.log"; + $tenantLogPath = "{$centralStoragePath}/tenant{$tenant->id}/logs/laravel.log"; expect(config("logging.channels.{$channel}.path")) ->not()->toBe($originalPath) - ->toEndWith($tenantLogPath); + ->toBe($tenantLogPath); tenancy()->end(); @@ -119,15 +120,16 @@ ], ]); + $centralStoragePath = storage_path(); $originalSinglePath = config('logging.channels.single.path'); - $tenant = Tenant::create(['id' => 'tenant1', 'webhookUrl' => 'tenant-webhook']); + $tenant = Tenant::create(['webhookUrl' => 'tenant-webhook']); // Test both array mapping and closure-based overrides LogTenancyBootstrapper::$channelOverrides = [ 'slack' => ['url' => 'webhookUrl'], // slack.url will be mapped to $tenant->webhookUrl - 'single' => function (Tenant $tenant, array $channel) { - return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]); + 'single' => function (Tenant $tenant, array $channel) use ($centralStoragePath) { + return array_merge($channel, ['path' => $centralStoragePath . "/logs/override-{$tenant->id}.log"]); }, ]; @@ -138,7 +140,7 @@ expect(config('logging.channels.slack.username'))->toBe('Default'); // Default username, remains default unless overridden // Closure overrides work - expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); + expect(config('logging.channels.single.path'))->toBe("{$centralStoragePath}/logs/override-{$tenant->id}.log"); tenancy()->end(); @@ -307,6 +309,8 @@ // Central context stack log logger('central'); $centralSingleLogPath = storage_path('logs/laravel.log'); + + // The single and daily channels have the same path in the config, but the daily driver parses the file name so that the date is included in the file name $centralDailyLogPath = storage_path("logs/laravel-{$today}.log"); expect(file_get_contents($centralSingleLogPath))->toContain('central'); From ae39e4dfd49bfbb209faccd8802d1ad3e8441420 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 29 Oct 2025 14:56:37 +0100 Subject: [PATCH 23/56] Clarify behavior in log bootstrapper comments --- src/Bootstrappers/LogTenancyBootstrapper.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 0355dd7ed..aec260840 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -17,6 +17,7 @@ * but feel free to customize that using the $storagePathChannels property) * are configured to use tenant storage directories. * For this to work correctly, this bootstrapper must run *after* FilesystemTenancyBootstrapper. + * FilesystemTenancyBootstrapper alters how storage_path() works in the tenant context. * * The bootstrapper also supports custom channel overrides via the $channelOverrides property (see the property's docblock). * @@ -27,8 +28,8 @@ class LogTenancyBootstrapper implements TenancyBootstrapper protected array $defaultConfig = []; /** - * Log channels that use the storage_path() helper for storing the logs. - * Requires FilesystemTenancyBootstrapper to run before this bootstrapper. + * Log channels that use the storage_path() helper for storing the logs. Requires FilesystemTenancyBootstrapper to run before this bootstrapper. + * Or you can bypass this default behavior by using overrides, since they take precedence over the default behavior. */ public static array $storagePathChannels = ['single', 'daily']; From aedb33bb3a23c442a1296749d8e5a46cd507ac04 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 12:45:53 +0200 Subject: [PATCH 24/56] Clean up log files in before/afterEach --- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 5900beb23..45ef9e798 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -19,6 +19,12 @@ ], ]); + @unlink(storage_path('logs/laravel.log')); + @unlink(storage_path('logs/laravel-' . now()->format('Y-m-d') . '.log')); + foreach (glob(storage_path('tenant*/logs/*.log')) ?: [] as $path) { + @unlink($path); + } + // Reset static properties LogTenancyBootstrapper::$channelOverrides = []; LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; @@ -30,6 +36,12 @@ afterEach(function () { LogTenancyBootstrapper::$channelOverrides = []; LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; + + @unlink(storage_path('logs/laravel.log')); + @unlink(storage_path('logs/laravel-' . now()->format('Y-m-d') . '.log')); + foreach (glob(storage_path('tenant*/logs/*.log')) ?: [] as $path) { + @unlink($path); + } }); test('storage path channels get tenant-specific paths by default', function () { From c68b91cd43ca4675628cabd03985ef5d07881155 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 13:54:20 +0200 Subject: [PATCH 25/56] Make tests not depend on setting the default logging channel --- .../LogTenancyBootstrapperTest.php | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 45ef9e798..469fed6f1 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\LogTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; +use Illuminate\Support\Facades\Log; beforeEach(function () { config([ @@ -60,8 +61,6 @@ // Storage path channels are 'single' and 'daily' by default. // This can be customized via LogTenancyBootstrapper::$storagePathChannels. foreach (LogTenancyBootstrapper::$storagePathChannels as $channel) { - config(['logging.default' => $channel]); - $originalPath = config("logging.channels.{$channel}.path"); tenancy()->initialize($tenant); @@ -85,7 +84,6 @@ FilesystemTenancyBootstrapper::class, LogTenancyBootstrapper::class, ], - 'logging.default' => 'stack', 'logging.channels.stack' => [ 'driver' => 'stack', 'channels' => ['single', 'daily'], @@ -124,7 +122,6 @@ test('channel overrides work correctly with both arrays and closures', function () { config([ - 'logging.default' => 'stack', 'logging.channels.stack.channels' => ['slack', 'single'], 'logging.channels.slack' => [ 'url' => $originalSlackUrl = 'default-webhook', @@ -163,7 +160,6 @@ }); test('channel config keys remain unchanged if the specified tenant override attribute is null', function() { - config(['logging.default' => 'slack']); config(['logging.channels.slack.username' => 'Default username']); LogTenancyBootstrapper::$channelOverrides = [ @@ -177,8 +173,6 @@ }); test('channel overrides take precedence over the default storage path channel updating logic', function () { - config(['logging.default' => 'single']); - $tenant = Tenant::create(['id' => 'tenant1']); LogTenancyBootstrapper::$channelOverrides = [ @@ -199,7 +193,6 @@ FilesystemTenancyBootstrapper::class, LogTenancyBootstrapper::class, ], - 'logging.default' => 'single' ]); $logManager = app('log'); @@ -236,19 +229,18 @@ FilesystemTenancyBootstrapper::class, LogTenancyBootstrapper::class, ], - 'logging.default' => 'single', ]); $centralLogPath = storage_path('logs/laravel.log'); - logger('central'); + Log::channel('single')->info('central'); expect(file_get_contents($centralLogPath))->toContain('central'); [$tenant1, $tenant2] = [Tenant::create(['id' => 'tenant1']), Tenant::create(['id' => 'tenant2'])]; tenancy()->runForMultiple([$tenant1, $tenant2], function (Tenant $tenant) use ($centralLogPath) { - logger($tenant->id); + Log::channel('single')->info($tenant->id); $tenantLogPath = storage_path('logs/laravel.log'); @@ -297,7 +289,7 @@ // Tenant context log (should use custom path due to override) tenancy()->initialize($tenant); - logger('tenant-override'); + Log::channel('single')->info('tenant-override'); expect(file_get_contents(storage_path('logs/custom-override-tenant.log')))->toContain('tenant-override'); }); @@ -308,7 +300,6 @@ FilesystemTenancyBootstrapper::class, LogTenancyBootstrapper::class, ], - 'logging.default' => 'stack', 'logging.channels.stack' => [ 'driver' => 'stack', 'channels' => ['single', 'daily'], @@ -319,7 +310,7 @@ $today = now()->format('Y-m-d'); // Central context stack log - logger('central'); + Log::channel('stack')->info('central'); $centralSingleLogPath = storage_path('logs/laravel.log'); // The single and daily channels have the same path in the config, but the daily driver parses the file name so that the date is included in the file name @@ -330,7 +321,7 @@ // Tenant context stack log tenancy()->initialize($tenant); - logger('tenant'); + Log::channel('stack')->info('tenant'); $tenantSingleLogPath = storage_path('logs/laravel.log'); $tenantDailyLogPath = storage_path("logs/laravel-{$today}.log"); @@ -355,7 +346,6 @@ test('slack channel uses correct webhook urls', function () { config([ - 'logging.default' => 'slack', 'logging.channels.slack.url' => 'central-webhook', 'logging.channels.slack.level' => 'debug', // Set level to debug to keep the tests simple, since the default level here is 'critical' ]); @@ -371,7 +361,7 @@ // Because the Slack channel uses cURL to send messages, we cannot use Http::fake() here. // Instead, we catch the exception and check the error message which contains the actual webhook URL. try { - logger('central'); + Log::channel('slack')->info('central'); } catch (Exception $e) { expect($e->getMessage())->toContain('central-webhook'); } @@ -379,7 +369,7 @@ // Slack channel should attempt to use the tenant-specific webhooks tenancy()->runForMultiple([$tenant1, $tenant2], function (Tenant $tenant) { try { - logger($tenant->id); + Log::channel('slack')->info($tenant->id); } catch (Exception $e) { expect($e->getMessage())->toContain($tenant->slackUrl); } @@ -387,7 +377,7 @@ // Central context, central webhook should be used again try { - logger('central'); + Log::channel('slack')->info('central'); } catch (Exception $e) { expect($e->getMessage())->toContain('central-webhook'); } From 89b0d1cb4bf58ab706cc57a14e67ab59338c436d Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 13:55:59 +0200 Subject: [PATCH 26/56] Include all storage path channels and overrides in `getChannels()` Instead of handling just the default channel, make LogTenancyBootstrapper handle all the channels that should be affected (= $storagePathChannels, channels from $channelOverrides and the default channel in case 'stack' is the default) --- src/Bootstrappers/LogTenancyBootstrapper.php | 48 +++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index aec260840..64d0cb951 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -30,12 +30,16 @@ class LogTenancyBootstrapper implements TenancyBootstrapper /** * Log channels that use the storage_path() helper for storing the logs. Requires FilesystemTenancyBootstrapper to run before this bootstrapper. * Or you can bypass this default behavior by using overrides, since they take precedence over the default behavior. + * + * All channels included here will be configured to use tenant-specific storage paths. */ public static array $storagePathChannels = ['single', 'daily']; /** * Custom channel configuration overrides. * + * All channels included here will be configured using the provided override. + * * Examples: * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] @@ -64,27 +68,37 @@ public function revert(): void } /** - * Channels to configure and re-resolve afterwards (including the channels in the log stack). + * Channels to configure and forget so they can be re-resolved afterwards. + * + * Includes: + * - the default channel + * - all channels in the $storagePathChannels array + * - all channels that have custom overrides in the $channelOverrides property */ protected function getChannels(): array { - // Get the currently used (default) logging channel + /** + * Include the default channel in the list of channels to configure/re-resolve. + * + * Including the default channel is harmless (if it's not overridden or not in $storagePathChannels, + * it'll just be forgotten and re-resolved on the next use), and for the case where 'stack' is the default, + * this is necessary since the 'stack' channel will be resolved and saved in the log manager, + * and its stale config could accidentally be used instead of the stack member channels. + * + * For example, when you use 'stack' with the 'slack' channel and you want to configure the webhook URL, + * both 'stack' and 'slack' must be re-resolved after updating the config for the channels to use the correct webhook URLs. + * If only one of the mentioned channels would be re-resolved, the other's (stale) webhook URL could be used for logging. + */ $defaultChannel = $this->config->get('logging.default'); - $channelIsStack = $this->config->get("logging.channels.{$defaultChannel}.driver") === 'stack'; - - // If the default channel is stack, also get all the channels it contains. - // The stack channel also has to be included in the list of channels - // since the channel will be resolved and saved in the log manager, - // and its config could accidentally be used instead of the underlying channels. - // - // For example, when you use 'stack' with the 'slack' channel and you want to configure the webhook URL, - // both the 'stack' and the 'slack' must be re-resolved after updating the config for the channels to use the correct webhook URLs. - // If only one of the mentioned channels would be re-resolved, the other's webhook URL would be used for logging. - $channels = $channelIsStack - ? [$defaultChannel, ...$this->config->get("logging.channels.{$defaultChannel}.channels")] - : [$defaultChannel]; - - return $channels; + + return array_filter( + array_unique([ + $defaultChannel, + ...static::$storagePathChannels, + ...array_keys(static::$channelOverrides), + ]), + fn (string $channel): bool => $this->config->has("logging.channels.{$channel}") + ); } /** From 9660faf2f93501ed905a8f26d272b3f02d336e1c Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 14:10:05 +0200 Subject: [PATCH 27/56] Ensure Slack throws cURL exception in test --- .../LogTenancyBootstrapperTest.php | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 469fed6f1..e83b2710c 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -350,6 +350,22 @@ 'logging.channels.slack.level' => 'debug', // Set level to debug to keep the tests simple, since the default level here is 'critical' ]); + $assertWebhook = function (string $expectedWebhook, string $message): void { + $thrown = false; + + // Because the Slack channel uses cURL to send messages, we cannot use Http::fake() here. + // Instead, we catch the exception and check the error message which contains the actual webhook URL + // (logging always throws "Curl error (code 6): Could not resolve host: {webhook_url}"). + try { + Log::channel('slack')->info($message); + } catch (Exception $e) { + $thrown = true; + expect($e->getMessage())->toContain($expectedWebhook); + } + + expect($thrown)->toBeTrue(); + }; + $tenant1 = Tenant::create(['id' => 'tenant1', 'slackUrl' => 'tenant1-webhook']); $tenant2 = Tenant::create(['id' => 'tenant2', 'slackUrl' => 'tenant2-webhook']); @@ -358,27 +374,13 @@ ]; // Test central context - should attempt to use central webhook - // Because the Slack channel uses cURL to send messages, we cannot use Http::fake() here. - // Instead, we catch the exception and check the error message which contains the actual webhook URL. - try { - Log::channel('slack')->info('central'); - } catch (Exception $e) { - expect($e->getMessage())->toContain('central-webhook'); - } + $assertWebhook('central-webhook', 'central'); // Slack channel should attempt to use the tenant-specific webhooks - tenancy()->runForMultiple([$tenant1, $tenant2], function (Tenant $tenant) { - try { - Log::channel('slack')->info($tenant->id); - } catch (Exception $e) { - expect($e->getMessage())->toContain($tenant->slackUrl); - } + tenancy()->runForMultiple([$tenant1, $tenant2], function (Tenant $tenant) use ($assertWebhook) { + $assertWebhook($tenant->slackUrl, $tenant->id); }); // Central context, central webhook should be used again - try { - Log::channel('slack')->info('central'); - } catch (Exception $e) { - expect($e->getMessage())->toContain('central-webhook'); - } + $assertWebhook('central-webhook', 'central'); }); From 221a9950c2f1023e36e3c9f5a1b13f223bc70999 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 14:15:27 +0200 Subject: [PATCH 28/56] Support channel overrides using dot notation --- src/Bootstrappers/LogTenancyBootstrapper.php | 3 ++- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 64d0cb951..9c8812af1 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -9,6 +9,7 @@ use Illuminate\Log\LogManager; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use Illuminate\Support\Arr; /** * This bootstrapper makes it possible to configure tenant-specific logging. @@ -128,7 +129,7 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid // If the tenant attribute is null, // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantAttributeName) { - $tenantAttribute = $tenant->getAttribute($tenantAttributeName); + $tenantAttribute = Arr::get($tenant, $tenantAttributeName); if ($tenantAttribute !== null) { $this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute); diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index e83b2710c..c1c2854b7 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -366,11 +366,12 @@ expect($thrown)->toBeTrue(); }; - $tenant1 = Tenant::create(['id' => 'tenant1', 'slackUrl' => 'tenant1-webhook']); - $tenant2 = Tenant::create(['id' => 'tenant2', 'slackUrl' => 'tenant2-webhook']); + $tenant1 = Tenant::create(['id' => 'tenant1', 'logging' => ['slackUrl' => 'tenant1-webhook']]); + $tenant2 = Tenant::create(['id' => 'tenant2', 'logging' => ['slackUrl' => 'tenant2-webhook']]); + // Attribute mapping using nested attributes (dot notation) works LogTenancyBootstrapper::$channelOverrides = [ - 'slack' => ['url' => 'slackUrl'], + 'slack' => ['url' => 'logging.slackUrl'], ]; // Test central context - should attempt to use central webhook @@ -378,7 +379,7 @@ // Slack channel should attempt to use the tenant-specific webhooks tenancy()->runForMultiple([$tenant1, $tenant2], function (Tenant $tenant) use ($assertWebhook) { - $assertWebhook($tenant->slackUrl, $tenant->id); + $assertWebhook($tenant->logging['slackUrl'], $tenant->id); }); // Central context, central webhook should be used again From f705f5849f64069afadf800a7a5d55dce305bdff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 13 Apr 2026 12:15:54 +0000 Subject: [PATCH 29/56] Fix code style (php-cs-fixer) --- src/Bootstrappers/LogTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 9c8812af1..42ac2fa63 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -7,9 +7,9 @@ use Closure; use Illuminate\Contracts\Config\Repository as Config; use Illuminate\Log\LogManager; +use Illuminate\Support\Arr; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -use Illuminate\Support\Arr; /** * This bootstrapper makes it possible to configure tenant-specific logging. From 697ba6592b13bff9962d1100e1ba71f5e2aa79c9 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 14:23:39 +0200 Subject: [PATCH 30/56] Correct log file cleanup --- .../LogTenancyBootstrapperTest.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index c1c2854b7..dd530dfc3 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -20,9 +20,12 @@ ], ]); - @unlink(storage_path('logs/laravel.log')); - @unlink(storage_path('logs/laravel-' . now()->format('Y-m-d') . '.log')); - foreach (glob(storage_path('tenant*/logs/*.log')) ?: [] as $path) { + $logFiles = array_merge( + glob(storage_path('logs/*.log')) ?: [], + glob(storage_path('tenant*/logs/*.log')) ?: [] + ); + + foreach ($logFiles as $path) { @unlink($path); } @@ -38,9 +41,12 @@ LogTenancyBootstrapper::$channelOverrides = []; LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; - @unlink(storage_path('logs/laravel.log')); - @unlink(storage_path('logs/laravel-' . now()->format('Y-m-d') . '.log')); - foreach (glob(storage_path('tenant*/logs/*.log')) ?: [] as $path) { + $logFiles = array_merge( + glob(storage_path('logs/*.log')) ?: [], + glob(storage_path('tenant*/logs/*.log')) ?: [] + ); + + foreach ($logFiles as $path) { @unlink($path); } }); From b74416721a98faecb3794870d41cac2f0bb8a981 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 14:26:41 +0200 Subject: [PATCH 31/56] Fix PHPStan error --- src/Bootstrappers/LogTenancyBootstrapper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 42ac2fa63..c09437729 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -10,6 +10,7 @@ use Illuminate\Support\Arr; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use Illuminate\Database\Eloquent\Model; /** * This bootstrapper makes it possible to configure tenant-specific logging. @@ -129,6 +130,7 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid // If the tenant attribute is null, // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantAttributeName) { + /** @var Tenant&Model $tenant */ $tenantAttribute = Arr::get($tenant, $tenantAttributeName); if ($tenantAttribute !== null) { From cdea112ad5b320d590dba1d5b9bd4854a13247f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 13 Apr 2026 12:26:58 +0000 Subject: [PATCH 32/56] Fix code style (php-cs-fixer) --- src/Bootstrappers/LogTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index c09437729..5631e5bc3 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -6,11 +6,11 @@ use Closure; use Illuminate\Contracts\Config\Repository as Config; +use Illuminate\Database\Eloquent\Model; use Illuminate\Log\LogManager; use Illuminate\Support\Arr; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -use Illuminate\Database\Eloquent\Model; /** * This bootstrapper makes it possible to configure tenant-specific logging. From 8fda84fceea020e3cc2cfb7485a4dca3da771794 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 14:39:42 +0200 Subject: [PATCH 33/56] Throw exception if override closure doesn't return array --- src/Bootstrappers/LogTenancyBootstrapper.php | 12 ++++++++++-- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 5631e5bc3..ea1f33932 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -44,7 +44,9 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * * Examples: * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) - * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] + * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] (the closure should return the whole channel's config) + * + * In both cases, the override should be an array. */ public static array $channelOverrides = []; @@ -140,7 +142,13 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid } elseif ($override instanceof Closure) { $channelConfigKey = "logging.channels.{$channel}"; - $this->config->set($channelConfigKey, $override($tenant, $this->config->get($channelConfigKey))); + $result = $override($tenant, $this->config->get($channelConfigKey)); + + if (! is_array($result)) { + throw new \InvalidArgumentException("Channel override closure for '{$channel}' must return an array."); + } + + $this->config->set($channelConfigKey, $result); } } diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index dd530dfc3..d738d1d5d 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -140,6 +140,11 @@ $tenant = Tenant::create(['webhookUrl' => 'tenant-webhook']); + // Channel override closures must return an array, otherwise an exception is thrown + LogTenancyBootstrapper::$channelOverrides['slack'] = fn (Tenant $tenant, array $channel) => 'invalid override'; + + expect(fn() => tenancy()->initialize($tenant))->toThrow(InvalidArgumentException::class); + // Test both array mapping and closure-based overrides LogTenancyBootstrapper::$channelOverrides = [ 'slack' => ['url' => 'webhookUrl'], // slack.url will be mapped to $tenant->webhookUrl @@ -148,7 +153,8 @@ }, ]; - tenancy()->initialize($tenant); + // Reinitialize tenancy to apply the new overrides + tenancy()->reinitialize(); // Array mapping overrides work expect(config('logging.channels.slack.url'))->toBe($tenant->webhookUrl); From 34115e84c716f72cc3b7f863062ef23d5939c7eb Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 13 Apr 2026 15:23:04 +0200 Subject: [PATCH 34/56] Rollback config if bootstrap fails --- src/Bootstrappers/LogTenancyBootstrapper.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index ea1f33932..64ee4a684 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -60,8 +60,15 @@ public function bootstrap(Tenant $tenant): void $this->defaultConfig = $this->config->get('logging.channels'); $channels = $this->getChannels(); - $this->configureChannels($channels, $tenant); - $this->forgetChannels($channels); + try { + $this->configureChannels($channels, $tenant); + $this->forgetChannels($channels); + } catch (\Throwable $exception) { + $this->config->set('logging.channels', $this->defaultConfig); + $this->forgetChannels($channels); + + throw $exception; + } } public function revert(): void From 1ae418c8b3fbe2dbfbfad0dfc2371e8ba0887d6f Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 14 Apr 2026 09:31:32 +0200 Subject: [PATCH 35/56] Store configured channels in a property, forget only the stored channels --- src/Bootstrappers/LogTenancyBootstrapper.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 64ee4a684..9504b1ba3 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -29,6 +29,8 @@ class LogTenancyBootstrapper implements TenancyBootstrapper { protected array $defaultConfig = []; + protected array $configuredChannels = []; + /** * Log channels that use the storage_path() helper for storing the logs. Requires FilesystemTenancyBootstrapper to run before this bootstrapper. * Or you can bypass this default behavior by using overrides, since they take precedence over the default behavior. @@ -58,14 +60,15 @@ public function __construct( public function bootstrap(Tenant $tenant): void { $this->defaultConfig = $this->config->get('logging.channels'); - $channels = $this->getChannels(); + $this->configuredChannels = $this->getChannels(); try { - $this->configureChannels($channels, $tenant); - $this->forgetChannels($channels); + $this->configureChannels($this->configuredChannels, $tenant); + $this->forgetChannels($this->configuredChannels); } catch (\Throwable $exception) { + // Revert to default config if anything goes wrong during channel configuration $this->config->set('logging.channels', $this->defaultConfig); - $this->forgetChannels($channels); + $this->forgetChannels($this->configuredChannels); throw $exception; } @@ -75,7 +78,7 @@ public function revert(): void { $this->config->set('logging.channels', $this->defaultConfig); - $this->forgetChannels($this->getChannels()); + $this->forgetChannels($this->configuredChannels); } /** From 95fd0462e657da689fe07dec4e7d7bed2631ce16 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 14 Apr 2026 09:53:25 +0200 Subject: [PATCH 36/56] Import InvalidArgumentException --- src/Bootstrappers/LogTenancyBootstrapper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 9504b1ba3..f5ac2255b 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -11,6 +11,7 @@ use Illuminate\Support\Arr; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use InvalidArgumentException; /** * This bootstrapper makes it possible to configure tenant-specific logging. @@ -155,7 +156,7 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid $result = $override($tenant, $this->config->get($channelConfigKey)); if (! is_array($result)) { - throw new \InvalidArgumentException("Channel override closure for '{$channel}' must return an array."); + throw new InvalidArgumentException("Channel override closure for '{$channel}' must return an array."); } $this->config->set($channelConfigKey, $result); From 06472d5cae56a9b6226c71a0d765b363742c7117 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Apr 2026 07:53:45 +0000 Subject: [PATCH 37/56] Fix code style (php-cs-fixer) --- src/Bootstrappers/LogTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index f5ac2255b..31235df12 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -9,9 +9,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Log\LogManager; use Illuminate\Support\Arr; +use InvalidArgumentException; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -use InvalidArgumentException; /** * This bootstrapper makes it possible to configure tenant-specific logging. From 9ea3813d28028afcf71eb1162f9b6a4e4053ca52 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 14 Apr 2026 10:07:16 +0200 Subject: [PATCH 38/56] Improve $channelOverrides docblock --- src/Bootstrappers/LogTenancyBootstrapper.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 31235df12..f0794a76b 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -46,10 +46,12 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * All channels included here will be configured using the provided override. * * Examples: - * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) - * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] (the closure should return the whole channel's config) + * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] + * - this maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) + * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] + * - this merges ['url' => $tenant->slackUrl] into the channel's config. * - * In both cases, the override should be an array. + * So the channel overrides can be arrays and closures that return arrays. */ public static array $channelOverrides = []; From 23ae15a8f145d3bbf414b9caf111fd434d3c0719 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 14 Apr 2026 10:46:15 +0200 Subject: [PATCH 39/56] Preserve filename from central log path in tenant context --- src/Bootstrappers/LogTenancyBootstrapper.php | 4 +- .../LogTenancyBootstrapperTest.php | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index f0794a76b..0007ce61c 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -133,7 +133,9 @@ protected function configureChannels(array $channels, Tenant $tenant): void } elseif (in_array($channel, static::$storagePathChannels)) { // Set storage path channels to use tenant-specific directory (default behavior) // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" (assuming FilesystemTenancyBootstrapper is used before this bootstrapper) - $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); + $path = $this->config->get("logging.channels.{$channel}.path"); + + $this->config->set("logging.channels.{$channel}.path", storage_path('logs/' . ($path ? basename($path) : 'laravel.log'))); } } } diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index d738d1d5d..4b62005e9 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -397,3 +397,44 @@ // Central context, central webhook should be used again $assertWebhook('central-webhook', 'central'); }); + +test('tenant logs inherit the filename from the central log path config', function () { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + LogTenancyBootstrapper::class, + ], + 'logging.channels.stack' => [ + 'driver' => 'stack', + 'channels' => ['single', 'daily'], + ], + 'logging.channels.single.path' => storage_path('logs/custom-name.log'), + 'logging.channels.daily.path' => storage_path('logs/custom-name.log'), + ]); + + $tenant = Tenant::create(); + $today = now()->format('Y-m-d'); + + // Central log is located at storage/logs/custom-name.log + Log::channel('stack')->info('central'); + + expect(file_get_contents(storage_path('logs/custom-name.log')))->toContain('central'); + expect(file_get_contents(storage_path("logs/custom-name-{$today}.log")))->toContain('central'); + + tenancy()->initialize($tenant); + + // Tenant log is located at storage/tenantX/logs/custom-name.log + Log::channel('stack')->info($tenant->id); + + // The filename from the central config is preserved in tenant context + expect(config('logging.channels.single.path'))->toEndWith('custom-name.log'); + expect(config('logging.channels.daily.path'))->toEndWith('custom-name.log'); + + expect(file_get_contents(storage_path('logs/custom-name.log'))) + ->toContain($tenant->id) + ->not()->toContain('central'); + + expect(file_get_contents(storage_path("logs/custom-name-{$today}.log"))) + ->toContain($tenant->id) + ->not()->toContain('central'); +}); From b234308e26eea61d379c94891194b3dd25ab5b3f Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 14 Apr 2026 11:17:31 +0200 Subject: [PATCH 40/56] Add comment about log path customization --- src/Bootstrappers/LogTenancyBootstrapper.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 0007ce61c..eb1dc55bb 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -131,10 +131,13 @@ protected function configureChannels(array $channels, Tenant $tenant): void if (isset(static::$channelOverrides[$channel])) { $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); } elseif (in_array($channel, static::$storagePathChannels)) { - // Set storage path channels to use tenant-specific directory (default behavior) - // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" (assuming FilesystemTenancyBootstrapper is used before this bootstrapper) + // Set storage path channels to use tenant-specific directory (default behavior). + // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" + // (assuming FilesystemTenancyBootstrapper is used before this bootstrapper). $path = $this->config->get("logging.channels.{$channel}.path"); + // The tenant log will inherit the central log filename from the central channel path config. + // For better customization, e.g. using custom paths for tenant logs, look into channel overrides. $this->config->set("logging.channels.{$channel}.path", storage_path('logs/' . ($path ? basename($path) : 'laravel.log'))); } } From 42d60e90851b260a551233e6e4c22cce49ff204e Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 14 Apr 2026 14:06:51 +0200 Subject: [PATCH 41/56] Make tenant log channels inherit paths from central config, improve comments --- src/Bootstrappers/LogTenancyBootstrapper.php | 37 +++++++++++++------ .../LogTenancyBootstrapperTest.php | 19 +++++----- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index eb1dc55bb..bb3b4ba0b 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -9,18 +9,23 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Log\LogManager; use Illuminate\Support\Arr; +use Exception; use InvalidArgumentException; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use Illuminate\Support\Str; /** * This bootstrapper makes it possible to configure tenant-specific logging. * - * By default, the storage path channels ('single' and 'daily' by default, - * but feel free to customize that using the $storagePathChannels property) - * are configured to use tenant storage directories. - * For this to work correctly, this bootstrapper must run *after* FilesystemTenancyBootstrapper. - * FilesystemTenancyBootstrapper alters how storage_path() works in the tenant context. + * By default, all the storage path channels are configured to use tenant + * storage directories (see the $storagePathChannels property). + * + * For this to work correctly: + * - this bootstrapper must run *after* FilesystemTenancyBootstrapper, + * since FilesystemTenancyBootstrapper alters how storage_path() works in the tenant context + * - storage path suffixing has to be enabled (= config('tenancy.filesystem.suffix_storage_path') + * has to be true), since the storage path suffix is what separates logs by tenant * * The bootstrapper also supports custom channel overrides via the $channelOverrides property (see the property's docblock). * @@ -33,10 +38,16 @@ class LogTenancyBootstrapper implements TenancyBootstrapper protected array $configuredChannels = []; /** - * Log channels that use the storage_path() helper for storing the logs. Requires FilesystemTenancyBootstrapper to run before this bootstrapper. - * Or you can bypass this default behavior by using overrides, since they take precedence over the default behavior. + * Logging channels that use the storage_path() helper for storing the logs. + * Or you can bypass this default behavior by using overrides, since they take + * precedence over the default behavior. * * All channels included here will be configured to use tenant-specific storage paths. + * + * Requires FilesystemTenancyBootstrapper to run before this bootstrapper, + * and storage path suffixing to be enabled. + * + * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper */ public static array $storagePathChannels = ['single', 'daily']; @@ -44,6 +55,8 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * Custom channel configuration overrides. * * All channels included here will be configured using the provided override. + * The overrides take precedence over the default storage path channels + * behavior. * * Examples: * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] @@ -134,11 +147,13 @@ protected function configureChannels(array $channels, Tenant $tenant): void // Set storage path channels to use tenant-specific directory (default behavior). // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" // (assuming FilesystemTenancyBootstrapper is used before this bootstrapper). - $path = $this->config->get("logging.channels.{$channel}.path"); + $originalChannelPath = $this->config->get("logging.channels.{$channel}.path"); + $centralStoragePath = Str::before(storage_path(), $this->config->get('tenancy.filesystem.suffix_base') . $tenant->getTenantKey()); - // The tenant log will inherit the central log filename from the central channel path config. - // For better customization, e.g. using custom paths for tenant logs, look into channel overrides. - $this->config->set("logging.channels.{$channel}.path", storage_path('logs/' . ($path ? basename($path) : 'laravel.log'))); + // The tenant log will inherit the segment that follows the storage path from the central channel path config. + // For example, if a channel's path is configured to storage_path('custom/logs/path.log') (storage/custom/logs/path.log), + // the 'custom/logs/path.log' segment will be passed to storage_path() in the tenant context (storage/tenantfoo/custom/logs/path.log). + $this->config->set("logging.channels.{$channel}.path", storage_path(Str::after($originalChannelPath, $centralStoragePath))); } } } diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 4b62005e9..d087c1e84 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -398,7 +398,7 @@ $assertWebhook('central-webhook', 'central'); }); -test('tenant logs inherit the filename from the central log path config', function () { +test('tenant logs inherit the path from the central log path config', function () { config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, @@ -408,18 +408,17 @@ 'driver' => 'stack', 'channels' => ['single', 'daily'], ], - 'logging.channels.single.path' => storage_path('logs/custom-name.log'), - 'logging.channels.daily.path' => storage_path('logs/custom-name.log'), + 'logging.channels.single.path' => storage_path('logs/single/custom-name.log'), + 'logging.channels.daily.path' => storage_path('logs/daily/custom-name.log'), ]); $tenant = Tenant::create(); $today = now()->format('Y-m-d'); - // Central log is located at storage/logs/custom-name.log Log::channel('stack')->info('central'); - expect(file_get_contents(storage_path('logs/custom-name.log')))->toContain('central'); - expect(file_get_contents(storage_path("logs/custom-name-{$today}.log")))->toContain('central'); + expect(file_get_contents(storage_path('logs/single/custom-name.log')))->toContain('central'); + expect(file_get_contents(storage_path("logs/daily/custom-name-{$today}.log")))->toContain('central'); tenancy()->initialize($tenant); @@ -427,14 +426,14 @@ Log::channel('stack')->info($tenant->id); // The filename from the central config is preserved in tenant context - expect(config('logging.channels.single.path'))->toEndWith('custom-name.log'); - expect(config('logging.channels.daily.path'))->toEndWith('custom-name.log'); + expect(config('logging.channels.single.path'))->toEndWith('logs/single/custom-name.log'); + expect(config('logging.channels.daily.path'))->toEndWith('logs/daily/custom-name.log'); - expect(file_get_contents(storage_path('logs/custom-name.log'))) + expect(file_get_contents(storage_path('logs/single/custom-name.log'))) ->toContain($tenant->id) ->not()->toContain('central'); - expect(file_get_contents(storage_path("logs/custom-name-{$today}.log"))) + expect(file_get_contents(storage_path("logs/daily/custom-name-{$today}.log"))) ->toContain($tenant->id) ->not()->toContain('central'); }); From c2a80c248f19b022364c2e3634928a3fb0055d87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Apr 2026 12:07:13 +0000 Subject: [PATCH 42/56] Fix code style (php-cs-fixer) --- src/Bootstrappers/LogTenancyBootstrapper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index bb3b4ba0b..bd23c79ec 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -9,11 +9,10 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Log\LogManager; use Illuminate\Support\Arr; -use Exception; +use Illuminate\Support\Str; use InvalidArgumentException; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -use Illuminate\Support\Str; /** * This bootstrapper makes it possible to configure tenant-specific logging. From 2f60e7672e5538f9fb0ed8a66a4bba13731aa11c Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 14 Apr 2026 14:40:00 +0200 Subject: [PATCH 43/56] Clean up nested log files created by tests --- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index d087c1e84..72664080a 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -22,7 +22,9 @@ $logFiles = array_merge( glob(storage_path('logs/*.log')) ?: [], - glob(storage_path('tenant*/logs/*.log')) ?: [] + glob(storage_path('logs/*/*.log')) ?: [], + glob(storage_path('tenant*/logs/*.log')) ?: [], + glob(storage_path('tenant*/logs/*/*.log')) ?: [] ); foreach ($logFiles as $path) { @@ -43,7 +45,9 @@ $logFiles = array_merge( glob(storage_path('logs/*.log')) ?: [], - glob(storage_path('tenant*/logs/*.log')) ?: [] + glob(storage_path('logs/*/*.log')) ?: [], + glob(storage_path('tenant*/logs/*.log')) ?: [], + glob(storage_path('tenant*/logs/*/*.log')) ?: [] ); foreach ($logFiles as $path) { From c5683d8e00d127ced386301f6cf11d3e39f448be Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 21 Apr 2026 12:02:52 +0200 Subject: [PATCH 44/56] Extract cleanup in test file --- .../LogTenancyBootstrapperTest.php | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 72664080a..65d64180c 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -12,13 +12,9 @@ use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Illuminate\Support\Facades\Log; -beforeEach(function () { - config([ - 'tenancy.bootstrappers' => [ - // FilesystemTenancyBootstrapper needed for LogTenancyBootstrapper to work with storage path channels BY DEFAULT (note that this can be completely overridden) - LogTenancyBootstrapper::class, - ], - ]); +$cleanup = function () { + LogTenancyBootstrapper::$channelOverrides = []; + LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; $logFiles = array_merge( glob(storage_path('logs/*.log')) ?: [], @@ -30,30 +26,23 @@ foreach ($logFiles as $path) { @unlink($path); } +}; - // Reset static properties - LogTenancyBootstrapper::$channelOverrides = []; - LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; +beforeEach(function () use ($cleanup) { + config([ + 'tenancy.bootstrappers' => [ + // FilesystemTenancyBootstrapper needed for LogTenancyBootstrapper to work with storage path channels BY DEFAULT + LogTenancyBootstrapper::class, + ], + ]); + + $cleanup(); Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); }); -afterEach(function () { - LogTenancyBootstrapper::$channelOverrides = []; - LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; - - $logFiles = array_merge( - glob(storage_path('logs/*.log')) ?: [], - glob(storage_path('logs/*/*.log')) ?: [], - glob(storage_path('tenant*/logs/*.log')) ?: [], - glob(storage_path('tenant*/logs/*/*.log')) ?: [] - ); - - foreach ($logFiles as $path) { - @unlink($path); - } -}); +afterEach($cleanup); test('storage path channels get tenant-specific paths by default', function () { // Note that for LogTenancyBootstrapper to change the paths correctly by default, From 8276f3b0083d59c0a31c41cb17c2261196ad66be Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 21 Apr 2026 12:03:11 +0200 Subject: [PATCH 45/56] Improve comments in tests --- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 65d64180c..e90ad0606 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -96,7 +96,8 @@ // By default, both paths are the same in the config. // Note that in actual usage, the daily log file name is parsed differently from the path in the config, - // but the paths *in the config* are the same. + // e.g. if daily channel has 'path' => storage_path('logs/laravel.log') in config, the log will be + // located at storage_path('logs/laravel-2026-01-01.log'). But the paths *in the config* are the same. expect($centralLogPath) ->toBe($centralStoragePath . '/logs/laravel.log') ->toBe($originalSinglePath) @@ -161,7 +162,7 @@ // After tenancy ends, the original config should be restored expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); expect(config('logging.channels.single.path'))->toBe($originalSinglePath); - expect(config('logging.channels.slack.username'))->toBe('Default'); // Not changed at all + expect(config('logging.channels.slack.username'))->toBe('Default'); // Unchanged }); test('channel config keys remain unchanged if the specified tenant override attribute is null', function() { From 6e474aca801d41e26c5056961f990722a2ad58f8 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 21 Apr 2026 12:25:44 +0200 Subject: [PATCH 46/56] Improve comments, test reverting on failure during configuration --- src/Bootstrappers/LogTenancyBootstrapper.php | 64 +++++++++++-------- .../LogTenancyBootstrapperTest.php | 29 +++++++++ 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index bd23c79ec..9f74e835c 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -15,18 +15,18 @@ use Stancl\Tenancy\Contracts\Tenant; /** - * This bootstrapper makes it possible to configure tenant-specific logging. + * Enable tenant-specific logging. * - * By default, all the storage path channels are configured to use tenant - * storage directories (see the $storagePathChannels property). + * All the storage path channels are configured to use tenant + * directories by default (see the $storagePathChannels property). * * For this to work correctly: * - this bootstrapper must run *after* FilesystemTenancyBootstrapper, - * since FilesystemTenancyBootstrapper alters how storage_path() works in the tenant context + * since FilesystemTenancyBootstrapper makes storage_path() return the tenant-specific storage path * - storage path suffixing has to be enabled (= config('tenancy.filesystem.suffix_storage_path') - * has to be true), since the storage path suffix is what separates logs by tenant + * has to be true), since the storage path suffix is what separates logs by tenant * - * The bootstrapper also supports custom channel overrides via the $channelOverrides property (see the property's docblock). + * Also supports custom channel overrides (see the $channelOverrides property). * * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper */ @@ -37,11 +37,13 @@ class LogTenancyBootstrapper implements TenancyBootstrapper protected array $configuredChannels = []; /** - * Logging channels that use the storage_path() helper for storing the logs. - * Or you can bypass this default behavior by using overrides, since they take - * precedence over the default behavior. + * Logging channels whose paths use storage_path() by default in the logging config. * - * All channels included here will be configured to use tenant-specific storage paths. + * All channels included here will be configured to use tenant-specific storage paths + * generated using storage_path() in the tenant context. + * + * This is the default behavior. The $channelOverrides property can be used to override + * this behavior (the overrides take precedence over $storagePathChannels). * * Requires FilesystemTenancyBootstrapper to run before this bootstrapper, * and storage path suffixing to be enabled. @@ -54,8 +56,10 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * Custom channel configuration overrides. * * All channels included here will be configured using the provided override. - * The overrides take precedence over the default storage path channels - * behavior. + * The overrides take precedence over the default ($storagePathChannels) behavior. + * + * You can either map tenant attributes to channel config keys using an array, + * or provide a closure that returns the full channel config array. * * Examples: * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] @@ -81,9 +85,11 @@ public function bootstrap(Tenant $tenant): void $this->configureChannels($this->configuredChannels, $tenant); $this->forgetChannels($this->configuredChannels); } catch (\Throwable $exception) { - // Revert to default config if anything goes wrong during channel configuration - $this->config->set('logging.channels', $this->defaultConfig); - $this->forgetChannels($this->configuredChannels); + // If an exception is thrown while updating the logging config, the logging config + // could be left in a corrupt (tenant) state, so we revert to the original config + // to e.g. avoid logging the failure in the tenant log (which would happen if + // the channel wasn't resolved before). + $this->revert(); throw $exception; } @@ -97,10 +103,11 @@ public function revert(): void } /** - * Channels to configure and forget so they can be re-resolved afterwards. + * Channels to configure and forget from the log manager so they can be + * re-resolved with the new, tenant-specific config on the next use. * * Includes: - * - the default channel + * - the default channel (primarily because it can be 'stack') * - all channels in the $storagePathChannels array * - all channels that have custom overrides in the $channelOverrides property */ @@ -110,13 +117,13 @@ protected function getChannels(): array * Include the default channel in the list of channels to configure/re-resolve. * * Including the default channel is harmless (if it's not overridden or not in $storagePathChannels, - * it'll just be forgotten and re-resolved on the next use), and for the case where 'stack' is the default, - * this is necessary since the 'stack' channel will be resolved and saved in the log manager, - * and its stale config could accidentally be used instead of the stack member channels. + * it'll just be forgotten and re-resolved on the next use with the original config), and for the + * case where 'stack' is the default, this is necessary since the 'stack' channel will be resolved + * and saved in the log manager, and its stale config could accidentally be used instead of the stack member channels. * - * For example, when you use 'stack' with the 'slack' channel and you want to configure the webhook URL, - * both 'stack' and 'slack' must be re-resolved after updating the config for the channels to use the correct webhook URLs. - * If only one of the mentioned channels would be re-resolved, the other's (stale) webhook URL could be used for logging. + * For example, when you use 'stack' with the 'slack' channel, + * if only 'slack' is forgotten, 'stack' would still use the stale cached 'slack' driver, + * and if only 'stack' is forgotten, the 'slack' channel's config would remain unchanged (central). */ $defaultChannel = $this->config->get('logging.default'); @@ -135,7 +142,7 @@ protected function getChannels(): array * * Only the channels that are in the $storagePathChannels array * or have custom overrides in the $channelOverrides property - * will be configured. + * will be configured (overrides take precedence over storage path channels). */ protected function configureChannels(array $channels, Tenant $tenant): void { @@ -144,8 +151,8 @@ protected function configureChannels(array $channels, Tenant $tenant): void $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); } elseif (in_array($channel, static::$storagePathChannels)) { // Set storage path channels to use tenant-specific directory (default behavior). - // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log" - // (assuming FilesystemTenancyBootstrapper is used before this bootstrapper). + // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log", + // assuming FilesystemTenancyBootstrapper is used before this bootstrapper. $originalChannelPath = $this->config->get("logging.channels.{$channel}.path"); $centralStoragePath = Str::before(storage_path(), $this->config->get('tenancy.filesystem.suffix_base') . $tenant->getTenantKey()); @@ -185,8 +192,9 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid } /** - * Forget all passed channels so they can be re-resolved - * with updated config on the next logging attempt. + * Forget all passed channels from the log manager so that + * they can be re-resolved with the updated (tenant-specific) + * config on the next logging attempt. */ protected function forgetChannels(array $channels): void { diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index e90ad0606..6c73fdbb1 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -431,3 +431,32 @@ ->toContain($tenant->id) ->not()->toContain('central'); }); + +test('logging config is reverted to the original state if configuration fails', function() { + config([ + 'logging.channels.slack.url' => $originalSlackUrl = 'default-webhook', + 'logging.channels.single.path' => $originalSinglePath = storage_path('logs/default-single-path.log'), + ]); + + $tenant = Tenant::create(['loggingPath' => storage_path('logs/tenant-single-path.log')]); + + // Valid override first, the config will be updated properly, + // then an invalid override that will cause the configuration to fail and throw an exception. + LogTenancyBootstrapper::$channelOverrides = [ + 'single' => ['path' => 'loggingPath'], // Valid override + 'slack' => fn () => 'invalid override', + ]; + + expect(fn() => tenancy()->initialize($tenant))->toThrow(InvalidArgumentException::class); + + // Single channel config reverted to original state after the exception was thrown + expect(config('logging.channels.single.path'))->toBe($originalSinglePath); + + // Exception thrown before slack config got changed + expect(config('logging.channels.slack.url'))->toBe($originalSlackUrl); + + // The single channel uses the original path for logging + Log::channel('single')->info('bootstrap failed'); + expect(file_exists($originalSinglePath))->toBeTrue(); + expect(file_get_contents($originalSinglePath))->toContain('bootstrap failed'); +}); From b51d5caaf59bfac1fa403127d3e798627bfa39ce Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Wed, 10 Jun 2026 17:23:28 -0700 Subject: [PATCH 47/56] clarify language in docblocks too many mentions of "default" behavior when there are just different approaches for different channels --- src/Bootstrappers/LogTenancyBootstrapper.php | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 9f74e835c..a05cfdb58 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -5,10 +5,9 @@ namespace Stancl\Tenancy\Bootstrappers; use Closure; -use Illuminate\Contracts\Config\Repository as Config; +use Illuminate\Contracts\Config\Repository; use Illuminate\Database\Eloquent\Model; use Illuminate\Log\LogManager; -use Illuminate\Support\Arr; use Illuminate\Support\Str; use InvalidArgumentException; use Stancl\Tenancy\Contracts\TenancyBootstrapper; @@ -22,11 +21,11 @@ * * For this to work correctly: * - this bootstrapper must run *after* FilesystemTenancyBootstrapper, - * since FilesystemTenancyBootstrapper makes storage_path() return the tenant-specific storage path + * since FilesystemTenancyBootstrapper adjusts storage_path() for the tenant * - storage path suffixing has to be enabled (= config('tenancy.filesystem.suffix_storage_path') - * has to be true), since the storage path suffix is what separates logs by tenant + * must be true), since the storage path suffix is what separates filesystem-based logs * - * Also supports custom channel overrides (see the $channelOverrides property). + * For logging channels that are not filesystem-based, see the $channelOverrides logic. * * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper */ @@ -56,14 +55,15 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * Custom channel configuration overrides. * * All channels included here will be configured using the provided override. - * The overrides take precedence over the default ($storagePathChannels) behavior. + * The overrides take precedence over the $storagePathChannels behavior + * when both approaches are used for the same channel. * * You can either map tenant attributes to channel config keys using an array, * or provide a closure that returns the full channel config array. * * Examples: - * - Array mapping (the default approach): ['slack' => ['url' => 'webhookUrl']] - * - this maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null, otherwise, the override is ignored) + * - Array mapping: ['slack' => ['url' => 'webhookUrl']] + * - this maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null; otherwise the override is ignored) * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] * - this merges ['url' => $tenant->slackUrl] into the channel's config. * @@ -72,7 +72,7 @@ class LogTenancyBootstrapper implements TenancyBootstrapper public static array $channelOverrides = []; public function __construct( - protected Config $config, + protected Repository $config, protected LogManager $logManager, ) {} @@ -86,11 +86,11 @@ public function bootstrap(Tenant $tenant): void $this->forgetChannels($this->configuredChannels); } catch (\Throwable $exception) { // If an exception is thrown while updating the logging config, the logging config - // could be left in a corrupt (tenant) state, so we revert to the original config - // to e.g. avoid logging the failure in the tenant log (which would happen if - // the channel wasn't resolved before). + // could be left in a corrupt state, so we revert to the original config to + // to avoid logging the exception in a tenant channel or a broken channel. $this->revert(); + // We re-throw the exception after having reverted the logging config to central. throw $exception; } } @@ -116,7 +116,7 @@ protected function getChannels(): array /** * Include the default channel in the list of channels to configure/re-resolve. * - * Including the default channel is harmless (if it's not overridden or not in $storagePathChannels, + * Including the default channel is harmless (if it's not overridden and not in $storagePathChannels, * it'll just be forgotten and re-resolved on the next use with the original config), and for the * case where 'stack' is the default, this is necessary since the 'stack' channel will be resolved * and saved in the log manager, and its stale config could accidentally be used instead of the stack member channels. @@ -150,15 +150,14 @@ protected function configureChannels(array $channels, Tenant $tenant): void if (isset(static::$channelOverrides[$channel])) { $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); } elseif (in_array($channel, static::$storagePathChannels)) { - // Set storage path channels to use tenant-specific directory (default behavior). - // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log", - // assuming FilesystemTenancyBootstrapper is used before this bootstrapper. + // Set storage path channels to use a tenant-specific directory (default behavior). + // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log". $originalChannelPath = $this->config->get("logging.channels.{$channel}.path"); $centralStoragePath = Str::before(storage_path(), $this->config->get('tenancy.filesystem.suffix_base') . $tenant->getTenantKey()); // The tenant log will inherit the segment that follows the storage path from the central channel path config. - // For example, if a channel's path is configured to storage_path('custom/logs/path.log') (storage/custom/logs/path.log), - // the 'custom/logs/path.log' segment will be passed to storage_path() in the tenant context (storage/tenantfoo/custom/logs/path.log). + // For example, if a channel's path is configured to storage_path('logs/foo.log') (storage/logs/foo.log), + // the 'logs/foo.log' segment will be passed to storage_path() in the tenant context (storage/tenant123/logs/foo.log). $this->config->set("logging.channels.{$channel}.path", storage_path(Str::after($originalChannelPath, $centralStoragePath))); } } @@ -172,7 +171,8 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantAttributeName) { /** @var Tenant&Model $tenant */ - $tenantAttribute = Arr::get($tenant, $tenantAttributeName); + + $tenantAttribute = $tenant->getAttribute($tenantAttributeName); if ($tenantAttribute !== null) { $this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute); From 8107ed4eccf7537d2fa592d40d6e8e016e5d31f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Jun 2026 00:25:07 +0000 Subject: [PATCH 48/56] Fix code style (php-cs-fixer) --- src/Bootstrappers/LogTenancyBootstrapper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index a05cfdb58..4cf8429b1 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -171,7 +171,6 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantAttributeName) { /** @var Tenant&Model $tenant */ - $tenantAttribute = $tenant->getAttribute($tenantAttributeName); if ($tenantAttribute !== null) { From dc331bfd9b2c12b52c132cb52ee562c7f7b53cba Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 29 Jun 2026 15:37:14 +0200 Subject: [PATCH 49/56] Use `data_get()` instead of `getAttribute()` in `overrideChannelConfig` The bootstrapper should be able to map nested tenant attributes to config, and `getAttribute()` doesn't support dot notation, so e.g. `$tenant->getAttribute('logging.slackUrl')` would return `null` instead of the actual attribute, so the test failed. `data_get()` supports dot notation, so we'll use that. --- src/Bootstrappers/LogTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 4cf8429b1..5e14f8297 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -171,7 +171,7 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantAttributeName) { /** @var Tenant&Model $tenant */ - $tenantAttribute = $tenant->getAttribute($tenantAttributeName); + $tenantAttribute = data_get($tenant, $tenantAttributeName); if ($tenantAttribute !== null) { $this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute); From eaedd4a2e515f3b707e19fdd71805b604d38837d Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 29 Jun 2026 16:25:33 +0200 Subject: [PATCH 50/56] Clarify docblocks --- src/Bootstrappers/LogTenancyBootstrapper.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 5e14f8297..35e1ea12d 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -16,8 +16,10 @@ /** * Enable tenant-specific logging. * - * All the storage path channels are configured to use tenant - * directories by default (see the $storagePathChannels property). + * Channels included in the $storagePathChannels property will be configured + * to write logs into the tenant's storage directory. The list includes + * Laravel's 'single' and 'daily' channels by default. To customize it, + * see the property's docblock. * * For this to work correctly: * - this bootstrapper must run *after* FilesystemTenancyBootstrapper, @@ -36,13 +38,11 @@ class LogTenancyBootstrapper implements TenancyBootstrapper protected array $configuredChannels = []; /** - * Logging channels whose paths use storage_path() by default in the logging config. + * Logging channels whose path is built using storage_path() (e.g. Laravel's single and daily). * - * All channels included here will be configured to use tenant-specific storage paths - * generated using storage_path() in the tenant context. - * - * This is the default behavior. The $channelOverrides property can be used to override - * this behavior (the overrides take precedence over $storagePathChannels). + * Channels included here will be configured to use tenant-specific storage paths + * generated using storage_path() in the tenant context. Overrides in the $channelOverrides + * property take precedence over $storagePathChannels when a channel is included in both. * * Requires FilesystemTenancyBootstrapper to run before this bootstrapper, * and storage path suffixing to be enabled. @@ -54,7 +54,7 @@ class LogTenancyBootstrapper implements TenancyBootstrapper /** * Custom channel configuration overrides. * - * All channels included here will be configured using the provided override. + * Channels included here will be configured using the provided override. * The overrides take precedence over the $storagePathChannels behavior * when both approaches are used for the same channel. * @@ -150,7 +150,7 @@ protected function configureChannels(array $channels, Tenant $tenant): void if (isset(static::$channelOverrides[$channel])) { $this->overrideChannelConfig($channel, static::$channelOverrides[$channel], $tenant); } elseif (in_array($channel, static::$storagePathChannels)) { - // Set storage path channels to use a tenant-specific directory (default behavior). + // Set storage path channels to use a tenant-specific directory. // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log". $originalChannelPath = $this->config->get("logging.channels.{$channel}.path"); $centralStoragePath = Str::before(storage_path(), $this->config->get('tenancy.filesystem.suffix_base') . $tenant->getTenantKey()); From 68e6fd6c02397e5a26c1c522efb6b7f595141d52 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 29 Jun 2026 17:51:35 +0200 Subject: [PATCH 51/56] Re-resolve all stack channels that include any of the configured channels Reverting the LogTenancyBootstrapper changes will make the new "stack channels that include any configured channel are re-resolved" test fali --- src/Bootstrappers/LogTenancyBootstrapper.php | 43 +++++++++++-------- .../LogTenancyBootstrapperTest.php | 39 +++++++++++++++++ 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 35e1ea12d..8894b44b6 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -107,32 +107,37 @@ public function revert(): void * re-resolved with the new, tenant-specific config on the next use. * * Includes: - * - the default channel (primarily because it can be 'stack') * - all channels in the $storagePathChannels array * - all channels that have custom overrides in the $channelOverrides property + * - any 'stack' channel that includes one of the above as a member + * + * Stack channels are included because once a stack has been used, it keeps logging + * to wherever its members pointed at that moment. So a stack used in the central + * context would keep writing to the central logs, even after tenancy is initialized + * and its member channels are configured for the tenant. + * Forgetting the stack forces it to be re-resolved with its members' updated (tenant) + * config. + * + * Only stacks one level deep are handled. */ protected function getChannels(): array { - /** - * Include the default channel in the list of channels to configure/re-resolve. - * - * Including the default channel is harmless (if it's not overridden and not in $storagePathChannels, - * it'll just be forgotten and re-resolved on the next use with the original config), and for the - * case where 'stack' is the default, this is necessary since the 'stack' channel will be resolved - * and saved in the log manager, and its stale config could accidentally be used instead of the stack member channels. - * - * For example, when you use 'stack' with the 'slack' channel, - * if only 'slack' is forgotten, 'stack' would still use the stale cached 'slack' driver, - * and if only 'stack' is forgotten, the 'slack' channel's config would remain unchanged (central). - */ - $defaultChannel = $this->config->get('logging.default'); + $configuredChannels = array_unique([ + ...static::$storagePathChannels, + ...array_keys(static::$channelOverrides), + ]); + + $stackChannels = []; + + foreach ($this->config->get('logging.channels') as $channel => $config) { + // Include stack channels that have at least one configured channel as a member + if (($config['driver'] ?? null) === 'stack' && array_intersect($config['channels'] ?? [], $configuredChannels)) { + $stackChannels[] = $channel; + } + } return array_filter( - array_unique([ - $defaultChannel, - ...static::$storagePathChannels, - ...array_keys(static::$channelOverrides), - ]), + array_unique([...$configuredChannels, ...$stackChannels]), fn (string $channel): bool => $this->config->has("logging.channels.{$channel}") ); } diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 6c73fdbb1..91c76d973 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -350,6 +350,45 @@ ->not()->toContain('tenant'); }); +test('stack channels that include any configured channel are re-resolved', function () { + config([ + 'tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + LogTenancyBootstrapper::class, + ], + 'logging.channels.custom_stack' => [ + 'driver' => 'stack', + 'channels' => ['single'], + ], + ]); + + $tenant = Tenant::create(['id' => 'stack-tenant']); + $centralLogPath = storage_path('logs/laravel.log'); + + // Resolve the stack channel in the central context first + // (this caches the stack with its members still pointing at the central logs). + Log::channel('custom_stack')->info('central'); + expect(file_get_contents($centralLogPath))->toContain('central'); + + tenancy()->initialize($tenant); + + Log::channel('custom_stack')->info('tenant log message'); + + // The stack channel should have been re-resolved with the + // updated (tenant) config for its member channels, + // so 'tenant log message' should be logged to the tenant log, + // not the central log. + expect(file_get_contents($centralLogPath)) + ->toContain('central') + ->not()->toContain('tenant log message'); + + $tenantLogPath = storage_path('logs/laravel.log'); + + expect(file_exists($tenantLogPath))->toBeTrue(); + expect(file_get_contents($tenantLogPath)) + ->toContain('tenant log message'); +}); + test('slack channel uses correct webhook urls', function () { config([ 'logging.channels.slack.url' => 'central-webhook', From a6ce0e2b882fc14c11122b7f11e2e51b854a3249 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Mon, 29 Jun 2026 18:01:14 +0200 Subject: [PATCH 52/56] Test that ending tenancy forgets the stack channels properly --- .../LogTenancyBootstrapperTest.php | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index 91c76d973..c91e617fb 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -367,8 +367,8 @@ // Resolve the stack channel in the central context first // (this caches the stack with its members still pointing at the central logs). - Log::channel('custom_stack')->info('central'); - expect(file_get_contents($centralLogPath))->toContain('central'); + Log::channel('custom_stack')->info('central log message'); + expect(file_get_contents($centralLogPath))->toContain('central log message'); tenancy()->initialize($tenant); @@ -379,14 +379,26 @@ // so 'tenant log message' should be logged to the tenant log, // not the central log. expect(file_get_contents($centralLogPath)) - ->toContain('central') + ->toContain('central log message') ->not()->toContain('tenant log message'); $tenantLogPath = storage_path('logs/laravel.log'); - expect(file_exists($tenantLogPath))->toBeTrue(); expect(file_get_contents($tenantLogPath)) ->toContain('tenant log message'); + + tenancy()->end(); + + Log::channel('custom_stack')->info('central after revert'); + + expect(file_get_contents($centralLogPath)) + ->toContain('central log message') + ->toContain('central after revert') + ->not()->toContain('tenant log message'); + + expect(file_get_contents($tenantLogPath)) + ->toContain('tenant log message') + ->not()->toContain('central after revert'); }); test('slack channel uses correct webhook urls', function () { From e114e1b9fe4f8e3ca58d0bbe587333d1c59a412b Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Tue, 30 Jun 2026 20:33:17 -0700 Subject: [PATCH 53/56] update comments, add getBoundCentralStoragePath() we use FilesystemTenancyBootstrapper::getBoundCentralStoragePath() in LogTenancyBootstrapper to simplify the logic for extracting a path inside the central storage directory we could use this method in more places, such as DeleteTenantStorage: $centralStoragePath = tenancy()->central(fn () => storage_path()); $tenantStoragePath = tenancy()->run($this->tenant, fn () => storage_path()); however for consistency (we're getting both the central and tenant paths there) we're keeping it simple as it is now using the newly added method would also introduce more coupling to FilesystemTenancyBootstrapper - its specific implementation. this isn't a big issue in LogTenancyBootstrapper which already has several dependencies on the FS bootstrapper, and we do use app(static::class) in the added method so extending the bootstrapper should work --- .../FilesystemTenancyBootstrapper.php | 6 +++ src/Bootstrappers/LogTenancyBootstrapper.php | 38 +++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index 560916006..0a864fbd5 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -255,4 +255,10 @@ public function scopeSessions(string|false $suffix): void $sessionManager->getDrivers()['file']->setHandler($handler); } } + + /** Get the central storage path from the bound singleton instance of this class. */ + public static function getBoundCentralStoragePath(): string + { + return app(static::class)->originalStoragePath; + } } diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogTenancyBootstrapper.php index 8894b44b6..d90b95cdb 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogTenancyBootstrapper.php @@ -21,7 +21,7 @@ * Laravel's 'single' and 'daily' channels by default. To customize it, * see the property's docblock. * - * For this to work correctly: + * For the storage path channels to be scoped correctly: * - this bootstrapper must run *after* FilesystemTenancyBootstrapper, * since FilesystemTenancyBootstrapper adjusts storage_path() for the tenant * - storage path suffixing has to be enabled (= config('tenancy.filesystem.suffix_storage_path') @@ -38,10 +38,10 @@ class LogTenancyBootstrapper implements TenancyBootstrapper protected array $configuredChannels = []; /** - * Logging channels whose path is built using storage_path() (e.g. Laravel's single and daily). + * Logging channels whose path is built using storage_path() (e.g. Laravel's 'single' and 'daily'). * * Channels included here will be configured to use tenant-specific storage paths - * generated using storage_path() in the tenant context. Overrides in the $channelOverrides + * created using storage_path() in the tenant context. Overrides in the $channelOverrides * property take precedence over $storagePathChannels when a channel is included in both. * * Requires FilesystemTenancyBootstrapper to run before this bootstrapper, @@ -63,9 +63,10 @@ class LogTenancyBootstrapper implements TenancyBootstrapper * * Examples: * - Array mapping: ['slack' => ['url' => 'webhookUrl']] - * - this maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is not null; otherwise the override is ignored) + * - this maps $tenant->webhookUrl to slack.url (if $tenant->webhookUrl is null, the override is ignored) * - Closure: ['slack' => fn (Tenant $tenant, array $channel) => array_merge($channel, ['url' => $tenant->slackUrl])] - * - this merges ['url' => $tenant->slackUrl] into the channel's config. + * - this manually merges ['url' => $tenant->slackUrl] into the channel's config + * - null is not ignored, the closure controls the override fully * * So the channel overrides can be arrays and closures that return arrays. */ @@ -112,13 +113,13 @@ public function revert(): void * - any 'stack' channel that includes one of the above as a member * * Stack channels are included because once a stack has been used, it keeps logging - * to wherever its members pointed at that moment. So a stack used in the central + * to wherever its members pointed to at that moment. So a stack used in the central * context would keep writing to the central logs, even after tenancy is initialized * and its member channels are configured for the tenant. * Forgetting the stack forces it to be re-resolved with its members' updated (tenant) * config. * - * Only stacks one level deep are handled. + * Importantly, stacks are only inspected one level deep - they are not traversed recursively. */ protected function getChannels(): array { @@ -145,9 +146,7 @@ protected function getChannels(): array /** * Configure channels for the tenant context. * - * Only the channels that are in the $storagePathChannels array - * or have custom overrides in the $channelOverrides property - * will be configured (overrides take precedence over storage path channels). + * This handles both $storagePathChannels and $channelOverrides. */ protected function configureChannels(array $channels, Tenant $tenant): void { @@ -158,7 +157,7 @@ protected function configureChannels(array $channels, Tenant $tenant): void // Set storage path channels to use a tenant-specific directory. // The tenant log will be located at e.g. "storage/tenant{$tenantKey}/logs/laravel.log". $originalChannelPath = $this->config->get("logging.channels.{$channel}.path"); - $centralStoragePath = Str::before(storage_path(), $this->config->get('tenancy.filesystem.suffix_base') . $tenant->getTenantKey()); + $centralStoragePath = FilesystemTenancyBootstrapper::getBoundCentralStoragePath(); // The tenant log will inherit the segment that follows the storage path from the central channel path config. // For example, if a channel's path is configured to storage_path('logs/foo.log') (storage/logs/foo.log), @@ -168,16 +167,24 @@ protected function configureChannels(array $channels, Tenant $tenant): void } } + /** + * Update channel configurations per $channelOverrides. + * + * For overrides set in array format, update individual keys of the channel. + * - This ignores cases where the value of the respective tenant attribute is null. + * For overrides set as closures, replace the entire channel with the returned config override. + * - This does not ignore cases where parts of the config may be null - the closure fully controls the override. + */ protected function overrideChannelConfig(string $channel, array|Closure $override, Tenant $tenant): void { if (is_array($override)) { // Map tenant attributes to channel config keys. - // If the tenant attribute is null, - // the override is ignored and the channel config key's value remains unchanged. foreach ($override as $configKey => $tenantAttributeName) { /** @var Tenant&Model $tenant */ $tenantAttribute = data_get($tenant, $tenantAttributeName); + // If the tenant attribute is null, the override is ignored + // and the channel config key's value remains unchanged. if ($tenantAttribute !== null) { $this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute); } @@ -196,9 +203,8 @@ protected function overrideChannelConfig(string $channel, array|Closure $overrid } /** - * Forget all passed channels from the log manager so that - * they can be re-resolved with the updated (tenant-specific) - * config on the next logging attempt. + * Forget all passed channels from the log manager so that they can be + * re-resolved with the updated config on the next logging attempt. */ protected function forgetChannels(array $channels): void { From f2b7c7d5edaea358e4ea5bab60d3d571a2a11594 Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Fri, 3 Jul 2026 20:18:40 -0700 Subject: [PATCH 54/56] minor test improvements the main change here is adding similar assertions related to *re-resolving* channels to the 'stack channels that include any configured channel are re-resolved' test, similar to the 'channels are forgotten and re-resolved during bootstrap and revert' test --- .../LogTenancyBootstrapperTest.php | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index c91e617fb..e4fcc0899 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -12,7 +12,7 @@ use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Illuminate\Support\Facades\Log; -$cleanup = function () { +afterEach($cleanup = function () { LogTenancyBootstrapper::$channelOverrides = []; LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; @@ -26,7 +26,7 @@ foreach ($logFiles as $path) { @unlink($path); } -}; +}); beforeEach(function () use ($cleanup) { config([ @@ -42,8 +42,6 @@ Event::listen(TenancyEnded::class, RevertToCentralContext::class); }); -afterEach($cleanup); - test('storage path channels get tenant-specific paths by default', function () { // Note that for LogTenancyBootstrapper to change the paths correctly by default, // the bootstrapper MUST run after FilesystemTenancyBootstrapper. @@ -77,7 +75,7 @@ } }); -test('all channels included in the log stack get processed correctly', function () { +test('all channels included in a stack get processed correctly', function () { config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, @@ -181,6 +179,8 @@ test('channel overrides take precedence over the default storage path channel updating logic', function () { $tenant = Tenant::create(['id' => 'tenant1']); + LogTenancyBootstrapper::$storagePathChannels = ['single']; + LogTenancyBootstrapper::$channelOverrides = [ 'single' => function (Tenant $tenant, array $channel) { return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]); @@ -189,7 +189,7 @@ tenancy()->initialize($tenant); - // Should use override, not the default storage path updating behavior + // Should use channel override, not the storage path updating behavior expect(config('logging.channels.single.path'))->toEndWith('storage/logs/override-tenant1.log'); }); @@ -365,17 +365,23 @@ $tenant = Tenant::create(['id' => 'stack-tenant']); $centralLogPath = storage_path('logs/laravel.log'); + $logManager = app('log'); + // Resolve the stack channel in the central context first // (this caches the stack with its members still pointing at the central logs). - Log::channel('custom_stack')->info('central log message'); + $originalStackChannel = $logManager->channel('custom_stack'); + $originalStackChannel->info('central log message'); expect(file_get_contents($centralLogPath))->toContain('central log message'); tenancy()->initialize($tenant); - Log::channel('custom_stack')->info('tenant log message'); - // The stack channel should have been re-resolved with the - // updated (tenant) config for its member channels, + // updated (tenant) config for its member channels + $tenantStackChannel = $logManager->channel('custom_stack'); + expect($tenantStackChannel)->not()->toBe($originalStackChannel); + + $tenantStackChannel->info('tenant log message'); + // so 'tenant log message' should be logged to the tenant log, // not the central log. expect(file_get_contents($centralLogPath)) @@ -389,7 +395,11 @@ tenancy()->end(); - Log::channel('custom_stack')->info('central after revert'); + // After revert, the stack channel should get re-resolved again with the original config + $currentStackChannel = $logManager->channel('custom_stack'); + expect($currentStackChannel)->not()->toBe($tenantStackChannel); + + $currentStackChannel->info('central after revert'); expect(file_get_contents($centralLogPath)) ->toContain('central log message') From cef7f11e209cb8ac2ad2131d34f3532c76c2ce6e Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Tue, 7 Jul 2026 18:34:17 -0700 Subject: [PATCH 55/56] tests: remove comment --- tests/Bootstrappers/LogTenancyBootstrapperTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogTenancyBootstrapperTest.php index e4fcc0899..36e9a4de4 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogTenancyBootstrapperTest.php @@ -31,7 +31,6 @@ beforeEach(function () use ($cleanup) { config([ 'tenancy.bootstrappers' => [ - // FilesystemTenancyBootstrapper needed for LogTenancyBootstrapper to work with storage path channels BY DEFAULT LogTenancyBootstrapper::class, ], ]); From 24029f71503899bb98bc5e914ee2ce4f88eae3a9 Mon Sep 17 00:00:00 2001 From: Samuel Stancl Date: Tue, 7 Jul 2026 18:40:23 -0700 Subject: [PATCH 56/56] rename to LogChannelBootstrapper, add to config --- assets/config.php | 1 + ...trapper.php => LogChannelBootstrapper.php} | 4 +- ...est.php => LogChannelBootstrapperTest.php} | 44 +++++++++---------- tests/TestCase.php | 4 +- 4 files changed, 27 insertions(+), 26 deletions(-) rename src/Bootstrappers/{LogTenancyBootstrapper.php => LogChannelBootstrapper.php} (98%) rename tests/Bootstrappers/{LogTenancyBootstrapperTest.php => LogChannelBootstrapperTest.php} (94%) diff --git a/assets/config.php b/assets/config.php index 2a3a07e2c..b20010919 100644 --- a/assets/config.php +++ b/assets/config.php @@ -185,6 +185,7 @@ // Bootstrappers\RootUrlBootstrapper::class, // Bootstrappers\UrlGeneratorBootstrapper::class, // Bootstrappers\MailConfigBootstrapper::class, + // Bootstrappers\LogChannelBootstrapper::class, // Bootstrappers\BroadcastingConfigBootstrapper::class, // Bootstrappers\BroadcastChannelPrefixBootstrapper::class, diff --git a/src/Bootstrappers/LogTenancyBootstrapper.php b/src/Bootstrappers/LogChannelBootstrapper.php similarity index 98% rename from src/Bootstrappers/LogTenancyBootstrapper.php rename to src/Bootstrappers/LogChannelBootstrapper.php index d90b95cdb..293028e30 100644 --- a/src/Bootstrappers/LogTenancyBootstrapper.php +++ b/src/Bootstrappers/LogChannelBootstrapper.php @@ -14,7 +14,7 @@ use Stancl\Tenancy\Contracts\Tenant; /** - * Enable tenant-specific logging. + * Use tenant-specific logging channels. * * Channels included in the $storagePathChannels property will be configured * to write logs into the tenant's storage directory. The list includes @@ -31,7 +31,7 @@ * * @see Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper */ -class LogTenancyBootstrapper implements TenancyBootstrapper +class LogChannelBootstrapper implements TenancyBootstrapper { protected array $defaultConfig = []; diff --git a/tests/Bootstrappers/LogTenancyBootstrapperTest.php b/tests/Bootstrappers/LogChannelBootstrapperTest.php similarity index 94% rename from tests/Bootstrappers/LogTenancyBootstrapperTest.php rename to tests/Bootstrappers/LogChannelBootstrapperTest.php index 36e9a4de4..c37e7b54c 100644 --- a/tests/Bootstrappers/LogTenancyBootstrapperTest.php +++ b/tests/Bootstrappers/LogChannelBootstrapperTest.php @@ -8,13 +8,13 @@ use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Bootstrappers\LogTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\LogChannelBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Illuminate\Support\Facades\Log; afterEach($cleanup = function () { - LogTenancyBootstrapper::$channelOverrides = []; - LogTenancyBootstrapper::$storagePathChannels = ['single', 'daily']; + LogChannelBootstrapper::$channelOverrides = []; + LogChannelBootstrapper::$storagePathChannels = ['single', 'daily']; $logFiles = array_merge( glob(storage_path('logs/*.log')) ?: [], @@ -31,7 +31,7 @@ beforeEach(function () use ($cleanup) { config([ 'tenancy.bootstrappers' => [ - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], ]); @@ -42,12 +42,12 @@ }); test('storage path channels get tenant-specific paths by default', function () { - // Note that for LogTenancyBootstrapper to change the paths correctly by default, + // Note that for LogChannelBootstrapper to change the paths correctly by default, // the bootstrapper MUST run after FilesystemTenancyBootstrapper. config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], ]); @@ -55,8 +55,8 @@ $tenant = Tenant::create(); // Storage path channels are 'single' and 'daily' by default. - // This can be customized via LogTenancyBootstrapper::$storagePathChannels. - foreach (LogTenancyBootstrapper::$storagePathChannels as $channel) { + // This can be customized via LogChannelBootstrapper::$storagePathChannels. + foreach (LogChannelBootstrapper::$storagePathChannels as $channel) { $originalPath = config("logging.channels.{$channel}.path"); tenancy()->initialize($tenant); @@ -78,7 +78,7 @@ config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], 'logging.channels.stack' => [ 'driver' => 'stack', @@ -132,12 +132,12 @@ $tenant = Tenant::create(['webhookUrl' => 'tenant-webhook']); // Channel override closures must return an array, otherwise an exception is thrown - LogTenancyBootstrapper::$channelOverrides['slack'] = fn (Tenant $tenant, array $channel) => 'invalid override'; + LogChannelBootstrapper::$channelOverrides['slack'] = fn (Tenant $tenant, array $channel) => 'invalid override'; expect(fn() => tenancy()->initialize($tenant))->toThrow(InvalidArgumentException::class); // Test both array mapping and closure-based overrides - LogTenancyBootstrapper::$channelOverrides = [ + LogChannelBootstrapper::$channelOverrides = [ 'slack' => ['url' => 'webhookUrl'], // slack.url will be mapped to $tenant->webhookUrl 'single' => function (Tenant $tenant, array $channel) use ($centralStoragePath) { return array_merge($channel, ['path' => $centralStoragePath . "/logs/override-{$tenant->id}.log"]); @@ -165,7 +165,7 @@ test('channel config keys remain unchanged if the specified tenant override attribute is null', function() { config(['logging.channels.slack.username' => 'Default username']); - LogTenancyBootstrapper::$channelOverrides = [ + LogChannelBootstrapper::$channelOverrides = [ 'slack' => ['username' => 'nonExistentAttribute'], // $tenant->nonExistentAttribute ]; @@ -178,9 +178,9 @@ test('channel overrides take precedence over the default storage path channel updating logic', function () { $tenant = Tenant::create(['id' => 'tenant1']); - LogTenancyBootstrapper::$storagePathChannels = ['single']; + LogChannelBootstrapper::$storagePathChannels = ['single']; - LogTenancyBootstrapper::$channelOverrides = [ + LogChannelBootstrapper::$channelOverrides = [ 'single' => function (Tenant $tenant, array $channel) { return array_merge($channel, ['path' => storage_path("logs/override-{$tenant->id}.log")]); }, @@ -196,7 +196,7 @@ config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], ]); @@ -232,7 +232,7 @@ config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], ]); @@ -284,7 +284,7 @@ // Channel overrides also override the default behavior for the storage path-based channels $tenant = Tenant::create(['id' => 'override-tenant']); - LogTenancyBootstrapper::$channelOverrides = [ + LogChannelBootstrapper::$channelOverrides = [ 'single' => function (Tenant $tenant, array $channel) { // The tenant log path will be set to storage/tenantoverride-tenant/logs/custom-override-tenant.log return array_merge($channel, ['path' => storage_path("logs/custom-{$tenant->id}.log")]); @@ -303,7 +303,7 @@ config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], 'logging.channels.stack' => [ 'driver' => 'stack', @@ -353,7 +353,7 @@ config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], 'logging.channels.custom_stack' => [ 'driver' => 'stack', @@ -436,7 +436,7 @@ $tenant2 = Tenant::create(['id' => 'tenant2', 'logging' => ['slackUrl' => 'tenant2-webhook']]); // Attribute mapping using nested attributes (dot notation) works - LogTenancyBootstrapper::$channelOverrides = [ + LogChannelBootstrapper::$channelOverrides = [ 'slack' => ['url' => 'logging.slackUrl'], ]; @@ -456,7 +456,7 @@ config([ 'tenancy.bootstrappers' => [ FilesystemTenancyBootstrapper::class, - LogTenancyBootstrapper::class, + LogChannelBootstrapper::class, ], 'logging.channels.stack' => [ 'driver' => 'stack', @@ -502,7 +502,7 @@ // Valid override first, the config will be updated properly, // then an invalid override that will cause the configuration to fail and throw an exception. - LogTenancyBootstrapper::$channelOverrides = [ + LogChannelBootstrapper::$channelOverrides = [ 'single' => ['path' => 'loggingPath'], // Valid override 'slack' => fn () => 'invalid override', ]; diff --git a/tests/TestCase.php b/tests/TestCase.php index b9f363345..e59d491a0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,7 +23,7 @@ use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\LogTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\LogChannelBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper; use Stancl\Tenancy\Bootstrappers\TenantConfigBootstrapper; @@ -193,7 +193,7 @@ protected function getEnvironmentSetUp($app) $app->singleton(RootUrlBootstrapper::class); $app->singleton(UrlGeneratorBootstrapper::class); $app->singleton(FilesystemTenancyBootstrapper::class); - $app->singleton(LogTenancyBootstrapper::class); + $app->singleton(LogChannelBootstrapper::class); $app->singleton(TenantConfigBootstrapper::class); }