From 9e6e0a988bbd1988254a78a5257ddc6280ff9737 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Sun, 12 Jul 2026 11:49:34 +0200 Subject: [PATCH] Remove TenancyBroadcastManager, pass the channel auth closures directly in the Broadcaster extend() closure Channel auth closures (registered using Broadcast::channel() e.g. in routes/channels.php) are only ever read from the default broadcaster -- both Broadcast::channel() and Broadcast::auth() go through the default broadcaster. TenancyBroadcastManager passed the closures to every broadcaster it resolved, which is pointless -- even in central context, Laravel doesn't do anything like that). Instead, bind a fresh BroadcastManager and pass the central channel closures to its default broadcaster while extending Broadcaster. So now, tenant context behaves exactly like central context, just with broadcasters resolved using the tenant credentials. Also update comments. --- .../BroadcastingConfigBootstrapper.php | 63 +++++++++++-------- src/Overrides/TenancyBroadcastManager.php | 59 ----------------- .../BroadcastingConfigBootstrapperTest.php | 15 ++--- 3 files changed, 44 insertions(+), 93 deletions(-) delete mode 100644 src/Overrides/TenancyBroadcastManager.php diff --git a/src/Bootstrappers/BroadcastingConfigBootstrapper.php b/src/Bootstrappers/BroadcastingConfigBootstrapper.php index 193cff74..387fefc4 100644 --- a/src/Bootstrappers/BroadcastingConfigBootstrapper.php +++ b/src/Bootstrappers/BroadcastingConfigBootstrapper.php @@ -4,20 +4,18 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; +use Illuminate\Broadcasting\Broadcasters\Broadcaster; use Illuminate\Broadcasting\BroadcastManager; use Illuminate\Config\Repository; -use Illuminate\Contracts\Broadcasting\Broadcaster; +use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Broadcast; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -use Stancl\Tenancy\Overrides\TenancyBroadcastManager; /** * Maps tenant credentials to the broadcasting config and rebinds BroadcastManager * and Broadcaster so that broadcasters get resolved using the tenant credentials. - * - * @see TenancyBroadcastManager */ class BroadcastingConfigBootstrapper implements TenancyBootstrapper { @@ -37,7 +35,7 @@ class BroadcastingConfigBootstrapper implements TenancyBootstrapper protected array $originalConfig = []; protected BroadcastManager|null $originalBroadcastManager = null; - protected Broadcaster|null $originalBroadcaster = null; + protected BroadcasterContract|null $originalBroadcaster = null; public static array $mapPresets = [ 'pusher' => [ @@ -69,40 +67,55 @@ class BroadcastingConfigBootstrapper implements TenancyBootstrapper public function bootstrap(Tenant $tenant): void { $this->originalBroadcastManager = $this->app->make(BroadcastManager::class); - $this->originalBroadcaster = $this->app->make(Broadcaster::class); + $this->originalBroadcaster = $this->app->make(BroadcasterContract::class); $this->setConfig($tenant); - // Make BroadcastManager resolve to a fresh TenancyBroadcastManager. The new manager: - // - has no cached broadcasters, so its broadcasters get resolved using the updated (tenant) - // broadcasting config and stay cached for the duration of the tenant's context - // - makes the tenant broadcasters inherit the channels of the original (central) broadcaster - // (since newly resolved broadcasters don't receive any channels by default, broadcasting on - // channels registered in central context, e.g. in routes/channels.php, would otherwise not - // work with the tenant broadcasters) - $this->app->extend(BroadcastManager::class, function (BroadcastManager $broadcastManager) { - $originalCustomCreators = invade($broadcastManager)->customCreators; - $tenantBroadcastManager = new TenancyBroadcastManager($this->app); + // Make BroadcastManager resolve to a fresh manager with no cached broadcasters, + // so that its broadcasters get resolved using the updated (tenant) broadcasting + // config and stay cached for the duration of the tenant's context. + $this->app->extend(BroadcastManager::class, function (BroadcastManager $centralManager) { + $tenantManager = new BroadcastManager($this->app); - // Make TenancyBroadcastManager inherit the custom driver creators registered in the central context + // Pass the custom driver creators registered in the central context to the new manager // so that custom drivers work in tenant context without having to re-register the creators manually. - foreach ($originalCustomCreators as $driver => $closure) { - $tenantBroadcastManager->extend($driver, $closure); + foreach (invade($centralManager)->customCreators as $driver => $creator) { + $tenantManager->extend($driver, $creator); } - return $tenantBroadcastManager; + return $tenantManager; }); // Swap the currently bound Broadcaster singleton (resolved earlier with the central credentials) // for the tenant BroadcastManager's default broadcaster, so that anything resolving the Broadcaster // contract gets the same tenant broadcaster that the manager uses, instead of the stale central one. - $this->app->extend(Broadcaster::class, function () { - return $this->app->make(BroadcastManager::class)->connection(); + $this->app->extend(BroadcasterContract::class, function (BroadcasterContract $centralBroadcaster) { + $tenantBroadcaster = $this->app->make(BroadcastManager::class)->connection(); + + // The newly resolved broadcaster doesn't have any channel auth closures registered, so the + // closures registered in central context (e.g. in routes/channels.php) have to be passed to it + // manually, otherwise, Broadcast::auth() would throw a 403 for those channels. + // Since Laravel only ever uses the default broadcaster's channel auth closures for broadcasting auth, + // we only have to pass the channel closures to the default broadcaster. + // + // The $channels proeprty and the channel() method aren't part of the Broadcaster contract -- they come + // from the abstract Broadcaster class, so the closures can only be copied between broadcasters extending it + // (which all of Laravel's default broadcasters, e.g. PusherBroadcaster, do). + if ($centralBroadcaster instanceof Broadcaster && $tenantBroadcaster instanceof Broadcaster) { + // invade() because channels can't be retrieved through any of the broadcaster's public methods + $centralBroadcaster = invade($centralBroadcaster); + + foreach ($centralBroadcaster->channels as $channel => $callback) { + $tenantBroadcaster->channel($channel, $callback, $centralBroadcaster->retrieveChannelOptions($channel)); + } + } + + return $tenantBroadcaster; }); // Extending the binding doesn't update the Broadcast facade's cached instance, - // so clear it to make the facade re-resolve to TenancyBroadcastManager instead of the central - // BroadcastManager — e.g. in the Broadcast::auth() call in BroadcastController (/broadcasting/auth). + // so clear it to make the facade re-resolve to the tenant BroadcastManager instead of the central + // one — e.g. in the Broadcast::auth() call in BroadcastController (/broadcasting/auth). Broadcast::clearResolvedInstance(); } @@ -110,7 +123,7 @@ class BroadcastingConfigBootstrapper implements TenancyBootstrapper { // Revert the bound BroadcastManager and Broadcaster singletons back to their original state $this->app->singleton(BroadcastManager::class, fn () => $this->originalBroadcastManager); - $this->app->singleton(Broadcaster::class, fn () => $this->originalBroadcaster); + $this->app->singleton(BroadcasterContract::class, fn () => $this->originalBroadcaster); // Clear the resolved Broadcast facade instance so that it gets re-resolved as the central BroadcastManager Broadcast::clearResolvedInstance(); diff --git a/src/Overrides/TenancyBroadcastManager.php b/src/Overrides/TenancyBroadcastManager.php deleted file mode 100644 index 97383cf0..00000000 --- a/src/Overrides/TenancyBroadcastManager.php +++ /dev/null @@ -1,59 +0,0 @@ -app->make(BroadcasterContract::class); - - // Broadcasters only have to implement the Illuminate\Contracts\Broadcasting\Broadcaster contract - // which doesn't require the channels property, so we only pass the channels to - // Illuminate\Broadcasting\Broadcasters\Broadcaster instances (= all the default broadcasters, e.g. PusherBroadcaster). - if ($originalBroadcaster instanceof Broadcaster && $newBroadcaster instanceof Broadcaster) { - $this->passChannelsFromOriginalBroadcaster($originalBroadcaster, $newBroadcaster); - } - - return $newBroadcaster; - } - - /** - * The newly resolved broadcasters don't automatically receive the channels registered - * in central context (e.g. Broadcast::channel() in routes/channels.php), so the channels - * have to be obtained from the original (central) broadcaster and manually passed to the new broadcasters - * (broadcasting using a broadcaster with no channels results in a 403 error on Broadcast::auth()). - */ - protected function passChannelsFromOriginalBroadcaster(Broadcaster $originalBroadcaster, Broadcaster $newBroadcaster): void - { - // invade() because channels can't be retrieved through any of the broadcaster's public methods - $originalBroadcaster = invade($originalBroadcaster); - - foreach ($originalBroadcaster->channels as $channel => $callback) { - $newBroadcaster->channel($channel, $callback, $originalBroadcaster->retrieveChannelOptions($channel)); - } - } -} diff --git a/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php b/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php index 418d9f6d..d2065627 100644 --- a/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php +++ b/tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php @@ -8,7 +8,6 @@ use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Tests\Etc\TestingBroadcaster; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Overrides\TenancyBroadcastManager; use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper; use Illuminate\Support\Facades\Broadcast; use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract; @@ -45,22 +44,20 @@ beforeEach(function () use ($cleanup) { afterEach($cleanup); -test('BroadcastingConfigBootstrapper binds TenancyBroadcastManager to BroadcastManager and reverts the binding when tenancy is ended', function() { +test('BroadcastingConfigBootstrapper binds a fresh BroadcastManager and reverts the binding when tenancy is ended', function() { config(['tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class]]); - expect(app(BroadcastManager::class)) - ->toBeInstanceOf(BroadcastManager::class) - ->not()->toBeInstanceOf(TenancyBroadcastManager::class); + $centralManager = app(BroadcastManager::class); tenancy()->initialize(Tenant::create()); - expect(app(BroadcastManager::class))->toBeInstanceOf(TenancyBroadcastManager::class); + expect(app(BroadcastManager::class)) + ->toBeInstanceOf(BroadcastManager::class) + ->not()->toBe($centralManager); tenancy()->end(); - expect(app(BroadcastManager::class)) - ->toBeInstanceOf(BroadcastManager::class) - ->not()->toBeInstanceOf(TenancyBroadcastManager::class); + expect(app(BroadcastManager::class))->toBe($centralManager); }); test('ending tenancy reverts the bound broadcaster to the original instance', function() {