['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])] */ 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'); $channels = $this->getChannels(); $this->configureChannels($channels, $tenant); $this->forgetChannels($channels); } public function revert(): void { $this->config->set('logging.channels', $this->defaultConfig); $this->forgetChannels($this->getChannels()); } /** * 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 $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. * * 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) { 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) $this->config->set("logging.channels.{$channel}.path", storage_path('logs/laravel.log')); } } } 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) { $tenantAttribute = $tenant->getAttribute($tenantAttributeName); if ($tenantAttribute !== null) { $this->config->set("logging.channels.{$channel}.{$configKey}", $tenantAttribute); } } } elseif ($override instanceof Closure) { $channelConfigKey = "logging.channels.{$channel}"; $this->config->set($channelConfigKey, $override($tenant, $this->config->get($channelConfigKey))); } } /** * Forget all passed channels so they can be re-resolved * with updated config on the next logging attempt. */ protected function forgetChannels(array $channels): void { foreach ($channels as $channel) { $this->logManager->forgetChannel($channel); } } }