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() {