1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-08-06 08:54:03 +00:00

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.
This commit is contained in:
lukinovec 2026-07-12 11:49:34 +02:00
parent 128a1ad036
commit 9e6e0a988b
3 changed files with 44 additions and 93 deletions

View file

@ -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();

View file

@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Overrides;
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
use Illuminate\Broadcasting\BroadcastManager;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
/**
* BroadcastManager override that makes the newly resolved (tenant) broadcasters
* inherit the channels of the original (central) broadcaster.
*
* BroadcastingConfigBootstrapper binds a new instance of this manager on each tenancy
* initialization, so the broadcasters get resolved using the tenant's broadcasting config
* and stay cached (like in the parent manager) for the duration of the tenant's context.
*
* @see Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper
*/
class TenancyBroadcastManager extends BroadcastManager
{
/**
* Resolve the broadcaster and pass it the channels of the currently bound broadcaster
* (the central one, when the default driver is resolved during bootstrap).
*/
protected function resolve($name)
{
$newBroadcaster = parent::resolve($name);
/** @var Broadcaster|null $originalBroadcaster */
$originalBroadcaster = $this->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));
}
}
}

View file

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