Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
7d749eb
BroadcastingConfigBootstrapper: test mapping credentials
lukinovec Mar 31, 2026
2ecc94d
BroadcastingConfigBootstrapper: test persistence of custom driver cre…
lukinovec Mar 31, 2026
fafd082
Fix typo in test
lukinovec Mar 31, 2026
c653c51
BroadcastingConfigBootstrapper: make tenant manager inherit central m…
lukinovec Mar 31, 2026
b1e91f1
BroadcastingConfigBootstrapper: make `Broadcaster::class` resolve to …
lukinovec Mar 31, 2026
65beecf
BroadcastingConfigBootstrapper: clear the `Broadcast` facade's resolv…
lukinovec Mar 31, 2026
0b860ea
Fix code style (php-cs-fixer)
github-actions[bot] Mar 31, 2026
c4f4451
Add assertions for the config of the bound manager's driver
lukinovec Mar 31, 2026
d939866
Fix custom creator assertions
lukinovec Mar 31, 2026
28b6119
TenancyBroadcastManager: update docblocks
lukinovec Apr 1, 2026
0fbe1bc
TenancyBroadcastManager: delete `Broadcaster` singleton binding
lukinovec Apr 2, 2026
b6c035c
Improve comments
lukinovec Apr 2, 2026
bbe2ff0
BroadcastingTest: update channel inheritance test
lukinovec Apr 2, 2026
b2add06
Delete BroadcastingTest
lukinovec Apr 2, 2026
9e9bedc
Fix code style (php-cs-fixer)
github-actions[bot] Apr 2, 2026
4b1cc9c
Improve comments
lukinovec Apr 2, 2026
fc45e09
Update BroadcastingConfigBootstrapperTest
lukinovec Apr 3, 2026
6b99921
BroadcastingConfigBootstrapper: correct `$credentialsMap` array_merge…
lukinovec Apr 3, 2026
29dd23d
BroadcastingConfigBootstrapperTest: add 'reverb' driver to datasets
lukinovec Apr 3, 2026
4937a74
BroadcastingConfigBootstrapper and TenancyBroadcastManager: comments
lukinovec Apr 3, 2026
c831393
Update comment
lukinovec Apr 3, 2026
ef476c5
Polish comments
lukinovec Apr 3, 2026
f8528fc
Add 'reverb' to `TenancyBroadcastManager::$tenantBroadcasters`
lukinovec Apr 3, 2026
dc344b7
Merge branch 'master' into broadcasting-fixes
stancl Apr 12, 2026
9ab0a72
Fix typo
lukinovec Apr 13, 2026
4aeaa66
Remove unused `$broadcaster` parameter
lukinovec Apr 13, 2026
b3d5197
Correct channel prefix test comments
lukinovec Apr 13, 2026
a247bb0
Correct config bootstrapper test comment
lukinovec Apr 13, 2026
20d494f
Reset `BroadcastingConfigBootstrapper::$broadcaster` in tests
lukinovec Apr 14, 2026
7af2b8a
Improve BroadcastManager instance checks
lukinovec Apr 14, 2026
a3c5568
Add regression test for credentialsMap merge order
lukinovec Apr 15, 2026
f3652a8
Make `with()` formatting consistent
lukinovec Apr 15, 2026
a7acd07
Reset `BroadcastingConfigBootstrapper::$mapPresets` in tests
lukinovec Apr 15, 2026
7db486d
Extract cleanup in broadcasting config test file
lukinovec Apr 15, 2026
d885659
Use toEqualCanonicalizing instead of toBe for custom creators
lukinovec Apr 15, 2026
e592b37
Merge branch 'master' into broadcasting-fixes
lukinovec Apr 15, 2026
ddd8c68
Improve comments
lukinovec Apr 21, 2026
614344a
Add test for reverting the bound broadcaster when tenancy ends
lukinovec Jun 29, 2026
f20f801
Merge branch 'master' into broadcasting-fixes
lukinovec Jun 29, 2026
9af1735
Explain why we swap the bound Broadcaster singleton better, remove th…
lukinovec Jul 10, 2026
937c8c8
Delete the redundant BroadcastingFactory::class arg from the Broadcas…
lukinovec Jul 10, 2026
a19d3b5
Merge branch 'master' into broadcasting-fixes
lukinovec Jul 10, 2026
24aeb23
Remove unused `$app` param and `?BroadcastManager` typehint (`singlet…
lukinovec Jul 10, 2026
2a0464d
Resolve (and cache) tenant broadcasters once per tenancy initializati…
lukinovec Jul 10, 2026
d80fed1
Remove stale $tenantBroadcasters mention
lukinovec Jul 10, 2026
af14d40
Delete extra newline, fix comment inconsistency
lukinovec Jul 10, 2026
b8fddf8
Retrieve the re-registered channel closure to assert what tenant_chan…
lukinovec Jul 11, 2026
128a1ad
Improve BroadcastingConfigBootstrapper docblock
lukinovec Jul 11, 2026
9e6e0a9
Remove TenancyBroadcastManager, pass the channel auth closures direct…
lukinovec Jul 12, 2026
5911619
Simplify channel-passing test
lukinovec Jul 12, 2026
120cec4
Add spaces after `fn`/`function` for consistency
lukinovec Jul 12, 2026
12e51d0
Test that when a tenant doesn't have the property that should be mapp…
lukinovec Jul 12, 2026
0ba588d
Improve channel closure inheritance test
lukinovec Jul 12, 2026
e6c4c31
Test that using broadcasters that only implement the Broadcaster cont…
lukinovec Jul 12, 2026
4422df2
Test configuring BroadcastingConfigBootstrapper::$broadcaster
lukinovec Jul 12, 2026
1409d72
Make test name more specific
lukinovec Jul 12, 2026
31d1663
Fix typo in comment
lukinovec Jul 12, 2026
d535b28
Update src/Bootstrappers/BroadcastingConfigBootstrapper.php
lukinovec Jul 13, 2026
81d41b4
Use `$centralBroadcaster->getChannels()` to get the channels instead …
lukinovec Jul 13, 2026
c07996e
Explain extending BroadcasterContract better
lukinovec Jul 13, 2026
44b97ae
phpstan fix: comment ignore
stancl Jul 15, 2026
9efebf9
Test reverting to central credentials after a tenant with overrides
lukinovec Jul 16, 2026
50afc49
Test that the authenticated user resolvers get copied to tenant broad…
lukinovec Jul 16, 2026
fe7468a
Copy the broadcaster auth state directly
lukinovec Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ parameters:
ignoreErrors:
- identifier: trait.unused
- identifier: missingType.iterableValue
-
message: '#Spatie\\Invade\\Invader#'
identifier: method.notFound
#-
# message: '#Spatie\\Invade\\Invader#'
# identifier: method.notFound
-
message: '#Spatie\\Invade\\Invader#'
identifier: property.notFound
Expand Down
87 changes: 76 additions & 11 deletions src/Bootstrappers/BroadcastingConfigBootstrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

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.
*/
class BroadcastingConfigBootstrapper implements TenancyBootstrapper
{
/**
Expand All @@ -21,14 +26,16 @@ class BroadcastingConfigBootstrapper implements TenancyBootstrapper
* [
* 'config.key.name' => 'tenant_property',
* ]
*
* $tenant->tenant_property will be mapped to config('config.key.name') when tenancy is initialized.
*/
public static array $credentialsMap = [];

public static string|null $broadcaster = null;

protected array $originalConfig = [];
protected BroadcastManager|null $originalBroadcastManager = null;
protected Broadcaster|null $originalBroadcaster = null;
protected BroadcasterContract|null $originalBroadcaster = null;

public static array $mapPresets = [
'pusher' => [
Expand All @@ -54,27 +61,85 @@ public function __construct(
protected Application $app
) {
static::$broadcaster ??= $config->get('broadcasting.default');
static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$broadcaster] ?? []);
static::$credentialsMap = array_merge(static::$mapPresets[static::$broadcaster] ?? [], static::$credentialsMap);
}

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 custom BroadcastManager which makes the broadcasters use the tenant credentials
$this->app->extend(BroadcastManager::class, function (BroadcastManager $broadcastManager) {
return 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);

// 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 (invade($centralManager)->customCreators as $driver => $creator) {
$tenantManager->extend($driver, $creator);
}

return $tenantManager;
});
Comment on lines +77 to +87

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the reason for creating the instance manually like this that if we simply forgot the bound instance from the container, the custom creators that were already registered would be lost?

To confirm - when would BroadcastManager::extend() be used? Is this change simply because of how we do things in our tests or would real applications also extend the manager before tenancy bootstrap?

And what are some common use cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the reason for creating the instance manually like this that if we simply forgot the bound instance from the container, the custom creators that were already registered would be lost?

Exactly, yeah

To confirm - when would BroadcastManager::extend() be used? Is this change simply because of how we do things in our tests or would real applications also extend the manager before tenancy bootstrap?

I made the change because when I reviewed the tests, I realized that this could be a real problem with any custom driver.

Custom drivers are registered via Broadcast::extend('foobar', ...) in a service provider's boot(), which runs before tenancy is initialized, so in a real app the creators are always on the central manager before bootstrap. Without copying them, a tenant using a custom driver as the default broadcaster would get "Driver [foobar] is not supported" in the tenant context.

And what are some common use cases

Writing your own driver, or installing a package that ships one. I see that for example, the Centrifugo package registers its driver exactly like this, in its provider's boot() (it calls extend() on an injected BroadcastManager instead of the facade, but that's the same thing since Broadcast::extend() proxies to the manager): https://github.com/denis660/laravel-centrifugo/blob/bec36ea8323fb56dff42bfc3999600abc567afaf/src/CentrifugoServiceProvider.php#L27-L29

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we are recreating the manager from scratch, and then copying existing stuff into the new manager, instead of trying to reset parts of the manager - if possible. The ones that contain state that becomes utdated in the tenant context.

As a brief test, I looked at what forget* methods there are in the manager and what properties exist there. I saw forgetDrivers() and that there aren't really many other properties that could hold outdated state. If I replace this whole thing with just forgetDrivers() on the bound BroadcastManager, all tests except two assertions that are coupled to this specific implementation seem to pass.

Have we tried that before and was it insufficient to make everything work? Wondering why we'd be re-instantiating managers and copying private properties to new instances and things like that in this bootstrapper when we normally just try to reset state.

Thought of this when I looked at the invade()-related review since I checked where we use invade and it's only this bootstrapper and some tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a brief test ... If I replace this whole thing with just forgetDrivers() on the bound BroadcastManager, all tests except two assertions that are coupled to this specific implementation seem to pass.

Tested this too -- these tests are coupled to the specific implementation, so yeah, these fail. Other tests pass because we don't deal with the custom creator inheritance, the BroadcastManager extend and forgetDrivers have similar primary purpose (= make the manager resolve fresh broadcasters).

(actually, I think there's also a gap in the tests -- we don't have a test for whether revert restores the central credentials after a tenant with overrides, and that one would fail with forgetDrivers() -- more on that below)

But forgetDrivers is problematic.

Have we tried that before and was it insufficient to make everything work? Wondering why we'd be re-instantiating managers and copying private properties to new instances and things like that in this bootstrapper when we normally just try to reset state.

I don't think we have tried that at all because before, we were swapping the bound broadcast manager for the custom TenancyBroadcastManager (so there was no reason to try that). And after we removed that class, it didn't occur to me to test using just forgetDrivers() instead of the extend().

forgetDrivers() keeps the custom creators (since it only clears $drivers), so custom drivers would still work without the need to copy anything. The only custom creator difference is that a creator registered inside a tenant context would leak into later contexts instead of being isolated per tenant (that's why one of the tests fails), but that's an edge case.

But the worse thing is that we have to properly revert() what bootstrap() does. If we just did forgetDrivers() on the bound manager in bootstrap(), things will work just fine in tenant context (if we ignore custom creators). Then on revert(), if we did forgetDrivers(), we'd discard the tenant broadcaster, but nothing would copy the channel closures back to the central broadcaster. To make the full forgetDrivers() implementation actually correct, we'd have to copy the channel closures onto the rebuilt central broadcaster on revert too.

And if instead we kept instance() on revert (not forgetDrivers()), the manager's cached default driver would still hold the previous (tenant) broadcaster, so /broadcasting/auth in central context would use the old tenant's key (the channel closures were copied over during bootstrap, so /broadcasting/auth wouldn't 403, but the response would be signed using the tenant's key, and the websocket server would just reject it).

So both revert solutions need copying extra state to be correct, which is why extend() (restoring the untouched central instances via instance()) probably ends up cleaner. I'll try how that implementation could look, but like I wrote, I think the extend() may be cleaner in the end -- I'll let you know.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Keeping all the changes local for now since this review is tied to #1448 (comment), and we'll be doing more testing/code changes)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, I think there's also a gap in the tests -- we don't have a test for whether revert restores the central credentials after a tenant with overrides, and that one would fail with forgetDrivers()

Added assertions for this to the credential mapping test. They fail with the forgetDrivers() thing we tested in bootstrap(), pass with extend().

9efebf9

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So both revert solutions need copying extra state to be correct, which is why extend() (restoring the untouched central instances via instance()) probably ends up cleaner. I'll try how that implementation could look

Tried it, and it's like I wrote -- bootstrap() gets simpler (forgetDrivers() + the same auth state copying), but revert() has to re-resolve the central broadcaster and copy the auth state back onto it:

public function revert(): void
{
    $this->unsetConfig();

    // Clear the tenant broadcasters so they get re-resolved using the central config again
    $this->app->make(BroadcastManager::class)->forgetDrivers();

    // A freshly resolved broadcaster has no auth state, so copy it back from the original broadcaster
    $centralBroadcaster = $this->app->make(BroadcastManager::class)->connection();
    $this->copyAuthState($this->originalBroadcaster, $centralBroadcaster);
    $this->app->instance(BroadcasterContract::class, $centralBroadcaster);

    Broadcast::clearResolvedInstance();
}

So the copying happens in both directions instead of once (meaning invade() stays too), the original broadcaster isn't restored on revert -- a fresh instance replaces it, so instances resolved before initializing tenancy end up stale. This all would be a bit more messy than the current implementation, so imo, switching to this won't be worth it.

(About the new assertions I mentioned in the previous comment -- with the full implementation, these assertions pass too, since revert() re-resolves the broadcaster with the central config -- only the instance check assertions fail)

Also tested the "resetting the existing object" idea from the other review, that's more interesting. I'll comment on that in a minute.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also tested the "resetting the existing object" idea from the other review, that's more interesting. I'll comment on that

See #1448 (comment)


// 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.
// The closure runs immediately (the extended singleton is already resolved), and it's also what makes
// channel auth work in tenant context -- the broadcaster resolved here gets cached as the tenant
// manager's default driver and receives the central broadcaster's auth state (see copyAuthState()).
$this->app->extend(BroadcasterContract::class, function (BroadcasterContract $centralBroadcaster) {
$tenantBroadcaster = $this->app->make(BroadcastManager::class)->connection();

$this->copyAuthState($centralBroadcaster, $tenantBroadcaster);

return $tenantBroadcaster;
});

// Extending the binding doesn't update the Broadcast facade's cached instance,
// 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();
}

/**
* Copy the auth state (the channel auth closures, their options, and the authenticated user
* callback) from one broadcaster to another. A freshly resolved broadcaster has no auth state,
* so without the copying, channel auth and user auth would stop working (403) in tenant context.
*
* The auth state is stored on the abstract Broadcaster class, not in the Broadcaster
* contract, and it's stored in protected properties. Because of that, we have
* to check that both broadcasters are instances of the abstract Broadcaster class and
* use invade() to access the protected properties (for the $channels property, there
* is a public accessor -- getChannels() -- but since invade is already used here,
* we access the property directly for consistency).
*/
protected function copyAuthState(BroadcasterContract $from, BroadcasterContract $to): void
{
if (! $from instanceof Broadcaster || ! $to instanceof Broadcaster) {
return;
}

$fromState = invade($from);
$toState = invade($to);

$toState->channels = $fromState->channels;
$toState->channelOptions = $fromState->channelOptions;
$toState->authenticatedUserCallback = $fromState->authenticatedUserCallback;
}

public function revert(): void
{
// Change the BroadcastManager and Broadcaster singletons back to what they were before initializing tenancy
$this->app->singleton(BroadcastManager::class, fn (Application $app) => $this->originalBroadcastManager);
$this->app->singleton(Broadcaster::class, fn (Application $app) => $this->originalBroadcaster);
// Revert the bound BroadcastManager and Broadcaster singletons back to their original state
$this->app->instance(BroadcastManager::class, $this->originalBroadcastManager);
$this->app->instance(BroadcasterContract::class, $this->originalBroadcaster);

// Clear the resolved Broadcast facade instance so that it gets re-resolved as the central BroadcastManager
Broadcast::clearResolvedInstance();

$this->unsetConfig();
}
Expand Down
65 changes: 0 additions & 65 deletions src/Overrides/TenancyBroadcastManager.php

This file was deleted.

102 changes: 102 additions & 0 deletions tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
use function Stancl\Tenancy\Tests\pest;
use Illuminate\Broadcasting\Broadcasters\NullBroadcaster;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Collection;

beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Expand Down Expand Up @@ -137,3 +140,102 @@ protected function formatChannels(array $channels)
expect(app(BroadcastManager::class)->driver())->toBe($broadcaster);
expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqual($channelNames);
});

test('broadcasting channel helpers register channels correctly', function() {
config([
'broadcasting.default' => $driver = 'testing',
'broadcasting.connections.testing.driver' => $driver,
]);

config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);

Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});

$centralUser = User::create(['name' => 'central', 'email' => 'test@central.cz', 'password' => 'test']);
$tenant = Tenant::create();

migrateTenants();

tenancy()->initialize($tenant);

// Same ID as $centralUser
$tenantUser = User::create(['name' => 'tenant', 'email' => 'test@tenant.cz', 'password' => 'test']);

tenancy()->end();

/** @var BroadcastManager $broadcastManager */
$broadcastManager = app(BroadcastManager::class);

// Use a driver with no channels
$broadcastManager->extend($driver, fn () => new NullBroadcaster);

$getChannels = fn (): Collection => $broadcastManager->driver($driver)->getChannels();

expect($getChannels())->toBeEmpty();

// Basic channel registration
Broadcast::channel($channelName = 'user.{userName}', $channelClosure = function ($user, $userName) {
return User::firstWhere('name', $userName)?->is($user) ?? false;
});

// Check if the channel is registered
$centralChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === $channelName);
expect($centralChannelClosure)->not()->toBeNull();

// Channel closures work as expected (running in central context)
expect($centralChannelClosure($centralUser, $centralUser->name))->toBeTrue();
expect($centralChannelClosure($centralUser, $tenantUser->name))->toBeFalse();

// Register a tenant broadcasting channel (almost identical to the original channel, just able to accept the tenant key)
tenant_channel($channelName, $channelClosure);

// Tenant channel registered – its name is correctly prefixed ("{tenant}.user.{userName}")
$tenantChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === "{tenant}.$channelName");
expect($tenantChannelClosure)->toBe($centralChannelClosure);

// The tenant channels are prefixed with '{tenant}.'
// They accept the tenant key, but their closures only run in tenant context when tenancy is initialized
// The regular channels don't accept the tenant key, but they also respect the current context
// The tenant key is used solely for the name prefixing – the closures can still run in the central context
tenant_channel($channelName, $tenantChannelClosure = function ($user, $tenant, $userName) {
return User::firstWhere('name', $userName)?->is($user) ?? false;
});

// Retrieve the stored closure to verify that re-registering the channel replaced it
// (asserting on $tenantChannelClosure wouldn't tell us what tenant_channel() actually stored)
$reregisteredTenantChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === "{tenant}.$channelName");

expect($reregisteredTenantChannelClosure)
->toBe($tenantChannelClosure)
->not()->toBe($centralChannelClosure);

expect($reregisteredTenantChannelClosure($centralUser, $tenant->getTenantKey(), $centralUser->name))->toBeTrue();
expect($reregisteredTenantChannelClosure($centralUser, $tenant->getTenantKey(), $tenantUser->name))->toBeFalse();

tenancy()->initialize($tenant);

// The channel closure runs in the tenant context
// Only the tenant user is available
expect($tenantChannelClosure($centralUser, $tenant->getTenantKey(), $tenantUser->name))->toBeFalse();
expect($tenantChannelClosure($tenantUser, $tenant->getTenantKey(), $tenantUser->name))->toBeTrue();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Use a new channel instance to delete the previously registered channels before testing the global_channel helper
$broadcastManager->purge($driver);
$broadcastManager->extend($driver, fn () => new NullBroadcaster);

expect($getChannels())->toBeEmpty();

// Global channel helper prefixes the channel name with 'global__'
global_channel($channelName, $channelClosure);

// Channel prefixed with 'global__' found
$foundChannelClosure = $getChannels()->first(fn ($closure, $name) => $name === 'global__' . $channelName);
expect($foundChannelClosure)->not()->toBeNull();
});
Loading
Loading