Skip to content

[4.x] BroadcastingConfigBootstrapper: fix central instances persisting in tenant context#1448

Open
lukinovec wants to merge 57 commits into
masterfrom
broadcasting-fixes
Open

[4.x] BroadcastingConfigBootstrapper: fix central instances persisting in tenant context#1448
lukinovec wants to merge 57 commits into
masterfrom
broadcasting-fixes

Conversation

@lukinovec

@lukinovec lukinovec commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Broadcasting auth route problem (Broadcast facade always uses central BroadcastManager)

Note: Tested with Pusher and Reverb. Also, this is the primary problem that this PR solves.

When using BroadcastingConfigBootstrapper for broadcasting on private channels with multiple broadcasting apps (= each tenant has its own Pusher/Reverb/... app and credentials), the auth requests sent to the broadcasting server use the central broadcasting credentials. This is because the Broadcast facade (used in BroadcastController::authenticate like Broadcast::auth($request) to authorize the channels and then retrieve the auth key that's then sent to the broadcasting server) keeps using the BroadcastManager instance resolved in the central context instead of the tenant BroadcastManager. Calling withBroadcasting() in bootstrap/app.php results in calling Broadcast::routes() in the central context, which resolves and stores the central BroadcastManager (which is never cleared, and is used in tenant context).

Clearing the facade's resolved instance (Illuminate\Contracts\Broadcasting\Factory) in BroadcastingConfigBootstrapper::bootstrap() forces the facade to re-resolve Factory (= BroadcastManager) on the next use, so the next Broadcast::auth() call uses the tenant BroadcastManager in the tenant context (and clearing the resolved instance in revert() makes the Broadcast facade use the central BroadcastManager again), and the credentials from the current config will be used for authentication.

Central BroadcastManager doesn't pass custom driver creators to the tenant manager

When registering a custom driver creator (app(BroadcastManager::class)->extend('custom-driver', fn($app, $config) => new CustomBroadcaster(...))) in the central context (e.g. in BroadcastServiceProvider/AppServiceProvider, or in our case, in the tests), the creator won't be available in the tenant context. Calling e.g. app(BroadcastManager::class)->driver('custom-driver') in the tenant context (if the creator was registered only in the central context) would throw InvalidArgumentException ("Driver [custom-driver] is not supported.").

To fix that, register the original (central) BroadcastManager's custom creators in the new BroadcastManager in BroadcastingConfigBootstrapper::bootstrap().

Note: This was always a problem, the tests just never caught it. The original test where we used a custom driver creator actually registered the same creator on each context switch (see the $registerTestingBroadcaster() calls in the removed BroadcastingTest file) -- if the creator was registered just once (which is what we do now, after the fix), the test would fail. The test registered the driver creator repeatedly which worked around custom creators not persisting after switching between contexts.

Central Broadcaster instance bound in tenant context

Note: This concerns code that resolves or injects the Broadcaster contract directly (via DI or app(Broadcaster::class)).

The problem was that resolving Illuminate\Contracts\Broadcasting\Broadcaster::class in tenant context returned the broadcaster instance from the central context (Laravel binds the contract as a singleton, resolved from the default driver's config as it was at resolution time -- before tenancy was initialized). Fixed by making Broadcaster::class resolve to the current BroadcastManager's default broadcaster (= the tenant broadcaster in tenant context) via app->extend().

Since the manager caches its broadcasters (see the next section), app(Broadcaster::class) and Broadcast::driver() return the same instance in tenant context (consistent with how Laravel behaves in the central context).

Broadcasters are resolved once per tenancy initialization, TenancyBroadcastManager is removed

Originally, TenancyBroadcastManager re-resolved the broadcasters listed in its $tenantBroadcasters static property on every retrieval. Earlier, we considered that good since it meant direct config changes in tenant context were picked up immediately. As discussed in the review, there's probably no real use case for that, and the re-resolving actually caused a subtle bug: channels registered via Broadcast::channel() in tenant context got silently lost on the next retrieval (e.g. during a /broadcasting/auth request), because each re-resolution started over from a copy of the central channels -- making the auth request fail with a 403 as if the channel was never registered.

Now, TenancyBroadcastManager is removed entirely. BroadcastingConfigBootstrapper binds a fresh, base BroadcastManager with no cached broadcasters, so the broadcasters get resolved using the tenant's credentials on first use and stay cached (like in the parent manager) for the duration of the tenant's context. The central broadcaster's channel auth closures are passed to the tenant broadcaster directly in the Broadcaster contract's extend() closure -- since Laravel only ever uses the default broadcaster's channel auth closures for broadcasting auth (both Broadcast::channel() and Broadcast::auth() go through the default broadcaster), the closures only have to be passed to the default broadcaster. This all means that:

  • TenancyBroadcastManager is removed. Its $tenantBroadcasters property's purpose was listing the broadcasters to re-resolve (and pass the central channel auth closures to) -- custom drivers now work without the need to configure anything, and the closure copying is a few lines in the bootstrapper instead of a manager override.
  • Channels registered via Broadcast::channel() in tenant context persist for the duration of that context. They don't leak into other tenants' contexts or into the central context.
  • For broadcasters to use updated credentials, tenancy has to be reinitialized. Direct broadcasting config changes made in tenant context aren't picked up by the broadcasters, and tenant property changes are only mapped to config in bootstrap(). An already-injected (= stale) Broadcaster instance additionally needs to be obtained again after reinitialization -- reinitializing swaps the binding, so a previously injected instance keeps using the old credentials.

NOTE: Updating credentials affects connected clients

Updating a tenant's credentials also affects already-connected clients (tested with Reverb) -- they're connected using the old key, so they won't receive broadcasts sent with the new credentials.

The frontend has to reconnect using the new key (e.g. by refreshing the page). Meaning, clients can't be notified in response to broadcasting credential changes through websockets themselves -- a broadcast sent after the update already uses the new credentials, so it won't reach clients connected with the old key. So for things like notifying clients in response to the credential changes, a different mechanism is needed.

Credential map: overriding presets (BroadcastingConfigBootstrapper)

Previously, credential mappings from $mapPresets overrode mappings defined in $credentialsMap. If someone used e.g. Pusher and wanted to override some of that preset's mappings, e.g. use 'pusher_app_key' instead of 'pusher_key' by specifying 'pusher_app_key' in $credentialsMap, the preset's mapping ('pusher_key') would still be used.

Fixed that by reversing the array_merge() order in BroadcastingConfigBootstrapper::__construct().

Tests

Deleted the BroadcastingTest file, moved the tests to appropriate bootstrapper test files.

Added tests for mapping tenant properties to broadcaster credentials (including keeping the central config values when a tenant doesn't have a mapped property). Also added tests for the rest of the changes mentioned above (including a regression test for the tenant-context channel registration bug), plus tests for copying the channel options along with the auth closures, for broadcasters that only implement the Broadcaster contract (instead of extending the abstract class), and for configuring which map preset is used via the $broadcaster property. Improved the existing tests.

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes

    • Improved tenant-aware broadcasting configuration and broadcaster resolution during tenancy start/end, with more reliable restoration back to the central context.
    • Strengthened tenant and global channel helper handling, ensuring tenant-prefixed channels resolve correctly and don’t leak across tenants.
  • Tests

    • Added end-to-end coverage for broadcasting channel helpers, credential mapping overrides, custom driver-creator isolation, and tenant channel isolation.
    • Removed an older overlapping broadcasting test suite to avoid redundancy.

lukinovec and others added 9 commits March 31, 2026 15:32
Test that BroadcastingConfigBootstrapper correctly maps tenant properties to broadcasting config/credentials, and that the credentials don't leak when switching contexts. Also add the `$config` property to `TestingBroadcaster` so that we can access the credentials used by the broadcaster.
…ators

Test that TenancyBroadcastManager inherits the custom driver creators from the central BroadcastManager.
…ed `Broadcasting\Factory` instance

After initializing tenancy, calls like `Broadcast::auth()` use the central `BroadcastManager`. Clearing the facade's resolved `Broadcasting\Factory` instance fixes that problem.
@codecov

codecov Bot commented Mar 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.60%. Comparing base (76e5f96) to head (31d1663).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1448      +/-   ##
============================================
+ Coverage     86.57%   86.60%   +0.02%     
+ Complexity     1221     1219       -2     
============================================
  Files           187      186       -1     
  Lines          3575     3575              
============================================
+ Hits           3095     3096       +1     
+ Misses          480      479       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lukinovec lukinovec marked this pull request as ready for review March 31, 2026 16:03
@lukinovec lukinovec marked this pull request as draft March 31, 2026 16:04
lukinovec and others added 6 commits April 1, 2026 15:50
Moved binding `Broadcaster` to the bootstrapper.
Test that the bound Broadcaster instance inherits the channels too. Also test that the channels aren't lost when switching context to another tenant.
Tests from BroadcastingTest moved to the appropriate bootstrapper test files. The new tenant credentials test has assertions equal to both the original property -> config mapping test and the config -> credentials test.
@lukinovec lukinovec marked this pull request as ready for review April 2, 2026 14:54
lukinovec and others added 9 commits April 2, 2026 16:54
Tests now use datasets with all drivers that are in `TenancyBroadcastManager::$tenantBroadcasters` by default plus the custom driver. Also add assertions for updating the tenant properties/config in tenant context.
… order

Previously, credential mappings from `$mapPresets` overrode mappings defined in `$credentialsMap`. If someone used pusher/reverb/ably and wanted to override some of that preset's mappings, e.g. use 'pusher_app_key' instead of 'pusher_key' by specifying 'pusher_app_key' in `$credentialsMap`, the preset's mapping ('pusher_key') would still be used.
Adding 'reverb' to `TenancyBroadcastManager::$tenantBroadcasters` will make these tests pass.
@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Broadcasting now switches manager and broadcaster resolution during tenancy, refreshes facade bindings, applies tenant credential mappings, inherits channels, and restores central state. Tests cover multiple drivers, custom creators, channel persistence, isolation, helper registration, and lifecycle cleanup.

Changes

Tenant-aware broadcasting

Layer / File(s) Summary
Broadcast configuration precedence
src/Bootstrappers/BroadcastingConfigBootstrapper.php, tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php
Preset mappings are merged before explicit credential mappings, and tests verify explicit overrides and forced preset selection across supported drivers.
Tenant manager and broadcaster resolution
src/Bootstrappers/BroadcastingConfigBootstrapper.php
Tenant initialization creates tenant-aware manager and broadcaster bindings, preserves custom creators and central channel registrations, refreshes the facade, and restores central instances during revert.
Broadcast lifecycle and isolation validation
tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php, tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php, tests/Etc/TestingBroadcaster.php
Tests cover binding restoration, driver configuration propagation, custom creator isolation, channel persistence, helper registration, tenant isolation, broadcaster compatibility, and cleanup across tenant transitions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Tenancy
  participant BroadcastingConfigBootstrapper
  participant BroadcastManager
  participant Broadcaster
  participant BroadcastFacade
  Tenancy->>BroadcastingConfigBootstrapper: initialize tenant context
  BroadcastingConfigBootstrapper->>BroadcastManager: bind fresh tenant manager
  BroadcastingConfigBootstrapper->>Broadcaster: bind tenant default connection
  BroadcastingConfigBootstrapper->>BroadcastFacade: clear resolved instance
  BroadcastFacade->>BroadcastManager: resolve tenant broadcaster
  Tenancy->>BroadcastingConfigBootstrapper: end tenant context
  BroadcastingConfigBootstrapper->>BroadcastFacade: restore central resolution
Loading

Poem

I’m a rabbit hopping through the broadcast stream,
Tenant channels follow a burrowed dream.
Credentials shift, then homeward return,
While bright little signals continue to burn.
Reverb joins the chorus with cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: fixing central broadcasting instances from leaking into tenant context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch broadcasting-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Bootstrappers/BroadcastingConfigBootstrapper.php`:
- Around line 96-98: The closure passed to $this->app->extend currently declares
an unused Broadcaster $broadcaster parameter which triggers static analysis;
change the callback signature to remove that parameter so it becomes a
zero-argument closure (e.g., function () { ... } or an arrow fn) and return
$this->app->make(BroadcastManager::class)->connection(); leaving the extend call
and references to Broadcaster::class and BroadcastManager::class unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 36a92dc5-b8bc-4656-be92-2b81ddd77b4c

📥 Commits

Reviewing files that changed from the base of the PR and between 60dd522 and dc344b7.

📒 Files selected for processing (6)
  • src/Bootstrappers/BroadcastingConfigBootstrapper.php
  • src/Overrides/TenancyBroadcastManager.php
  • tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php
  • tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php
  • tests/BroadcastingTest.php
  • tests/Etc/TestingBroadcaster.php
💤 Files with no reviewable changes (1)
  • tests/BroadcastingTest.php

Comment thread src/Bootstrappers/BroadcastingConfigBootstrapper.php Outdated
// Change the BroadcastManager and Broadcaster singletons back to what they were before initializing tenancy
$this->app->singleton(BroadcastManager::class, fn (Application $app) => $this->originalBroadcastManager);
// Revert the bound BroadcastManager and Broadcaster singletons back to their original state
$this->app->singleton(BroadcastManager::class, fn (Application $app): ?BroadcastManager => $this->originalBroadcastManager);

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.

What's the reason for the added typehint here? We don't use it with the other singleton reset and it wasn't here before

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.

Definitely a leftover. My IDE shows typehints like these as gray text, and when I click that text accidentally, it just adds the typehint -- so my bad for including the change in a commit here..

The typehint adds nothing, so I'd just remove it (also for consistency with the other singleton reset)

Now that we're dealing with this, maybe fn (Application $app) => ... could be changed to fn () => ..., since that Application typehint adds nothing (and $app isn't used at all)? Seems like in code similar to this, we only add have $app in the closure's params when it's used, so removing this would make things more consistent.

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.

Took care of both the typehint and the unused $app here: 24aeb23

Comment on lines +24 to +34
* Names of broadcasters that
* - should always be recreated using $this->resolve(), even when they're cached and available
* in $this->drivers so that when you update broadcasting config in the tenant context,
* the updated config/credentials will be used for broadcasting immediately.
* Note that in cases like this, only direct config changes are reflected right away.
* For the broadcasters to reflect tenant property changes made in tenant context,
* you still have to reinitialize tenancy after updating the tenant properties intended
* to be mapped to broadcasting config, since the properties are only mapped to config
* on BroadcastingConfigBootstrapper::bootstrap().
* - should inherit the original broadcaster's channels (= the channels registered in
* the central context, e.g. in routes/channels.php, before this manager overrides the bound BroadcastManager).

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.

I'm thinking if there is a reason for breaking the cache like this. When would someone update the config? Only when tenancy is being initialized - the config is loaded from the tenant and then stays the same. There aren't many use cases for someone updating broadcasting config long after tenancy is initialized.

If they would make any change, it'd be through changes on the tenant itself which aren't reflected in the config anyway, so a reinitialization would be needed.

The only benefit of doing this that I can see is that it makes our tests a little easier maybe? Fewer reinit calls needed.

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.

Right. I agree that there probably aren't many use cases for updating the config after tenancy is initialized, so I guess we can forget about it entirely.

Even with this re-resolving logic, after direct broadcasting config updates, a page reload is necessary for the frontend to reconnect to Echo using the new credentials. The re-resolving logic here loses its purpose, the channel-passing logic is the thing we care about here.

Now I found out that when the drivers are re-resolved, only the central channels get passed to them. So when someone registers a channel in tenant context (pretty unlikely but possible), and the driver gets re-resolved later e.g. during the /broadcasting/auth request (during Broadcast::auth()), the driver will only have the channels registered in the central context again (since the driver got re-resolved, its channel list is fresh, until we pass the central channels to it -- and then, it'll only have the central channels in the list). So the /broadcasting/auth request would throw a 403 as if the channel never got registered because the driver just doesn't have it in its list at this point due to the re-resolution. This is probably not a huge problem, but it's not right.

If we don't care about direct config updates and just establish that "for broadcasters to use the up-to-date credentials/config, tenancy has to be reinitialized", TenancyBroadcastManager could actually get simplified a lot. Passing the channels to freshly resolved broadcasters could be its only purpose (after initializing tenancy, all drivers would get freshly resolved because we create a new TenancyBroadcastManager instance with no drivers cached). Instead of overriding get() (which we do now just so that the broadcasters are always re-resolved), we could override resolve() just so that the freshly resolved broadcasters receive the central channels (and if someone registers a channel in the tenant context, that'll work without a problem because we won't re-resolve the broadcasters).

We could get rid of TenancyBroadcastManager::$tenantBroadcasters entirely since its purpose was to list names of broadcasters that should get re-resolved and receive the central channels (the coupling here was weird anyway). Since in this theoretical implementation, we'd just be passing central channels to broadcasters newly resolved (and then cached) by TenancyBroadcastManager, I think it'd be pretty safe to just do this for all broadcasters, not just some specific ones.

Anyways, these are pretty big changes. I'll try changing the code around and see if the changes would make sense in the end.

(Also, yeah, the tests would have to get updated)

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.

I'll try changing the code around and see if the changes would make sense in the end.

Tried that, and it turned out good. Will push the changes after polishing everything.

Worth noting that now that there's no re-resolving going on, app(BroadcasterContract::class) and Broadcast::driver() will be the same instances in tenant context (which is consistent with Laravel's behavior).

Now I found out that when the drivers are re-resolved, only the central channels get passed to them. So when someone registers a channel in tenant context (pretty unlikely but possible), and the driver gets re-resolved later e.g. during the /broadcasting/auth request (during Broadcast::auth()), the driver will only have the channels registered in the central context again

Added a regression test for this and made it pass -- a channel registered via Broadcast::channel() in tenant context now survives subsequent driver() calls (the test fails with the original get() override, passes with the new resolve() one). Also tested that the channels registered in tenant context only live for the duration of that context, so the channels don't leak from tenant to central context, and they don't leak from one tenant's context to another's.

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.

Pushed the current changes: 2a0464d

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.

In the end, I deleted the entire TenancyBroadcastManager class in 9e6e0a9 as its only purpose was to pass the central channel auth closures to each newly resolved broadcaster in the tenant context. Only the default broadcaster's channel auth closures matter.

(see the PR's description)

…t::clearResolvedInstance calls

Also in bootstrap(), update the clearResolvedInstance comment. Remove the FQCN mention and explain why exactly do we need to clear the resolved instance
…on(...)` calls in `BroadcastingConfigBootstrapper::revert`)
…on instead of re-resolving them every time

TenancyBroadcastManager now overrides resolve() (only to pass the central channels to the newly resolved broadcasters) instead of get(), so broadcasters stay cached for the duration of the tenant's context.

Remove TenancyBroadcastManager::$tenantBroadcasters -- all broadcasters now inherit the central channels, so even custom drivers work without any additional configuration/registration.

Channels registered via Broadcast::channel() in tenant context are no longer lost on subsequent broadcaster retrievals (e.g. during /broadcasting/auth). Added a dedicated test for this,

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Overrides/TenancyBroadcastManager.php`:
- Around line 27-42: Update TenancyBroadcastManager::resolve() to avoid
resolving BroadcasterContract through the container after the manager swap,
which can recurse via BroadcastManager::connection(); instead, preserve the
already-resolved central broadcaster by injecting or otherwise passing it
directly into the manager before resolve() runs, then use that instance in
passChannelsFromOriginalBroadcaster() while retaining the existing Broadcaster
type checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c6f806ae-4d43-4d09-b639-9d9563a4faca

📥 Commits

Reviewing files that changed from the base of the PR and between 24aeb23 and 2a0464d.

📒 Files selected for processing (3)
  • src/Bootstrappers/BroadcastingConfigBootstrapper.php
  • src/Overrides/TenancyBroadcastManager.php
  • tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php

Comment thread src/Overrides/TenancyBroadcastManager.php Outdated
Use "userName" in the channel helper test's comment for consistency (the channel name uses "userName", the comment used "userId", fixed that)
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@lukinovec

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Bootstrappers/BroadcastingConfigBootstrapper.php`:
- Around line 83-106: Wrap the BroadcastManager and Broadcaster extension
mutations plus Broadcast facade reset in a try/catch within the bootstrapper
method, and call this bootstrapper’s revert logic before rethrowing any
exception from connection() resolution. Ensure rollback also handles partial
setup before the bootstrapper is tracked. Add a regression test using a custom
driver creator that throws during connection(), asserting the configuration and
manager bindings are restored.

In `@tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php`:
- Around line 196-221: Update the re-registration assertions in the
tenant_channel test to retrieve the callback actually stored under the prefixed
channel name from getChannels() after the second tenant_channel() call. Use that
retrieved closure, rather than the locally assigned $tenantChannelClosure, for
all subsequent central- and tenant-context invocations while retaining the
identity assertion against $centralChannelClosure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e8d31116-090a-437c-bb0d-e8304b55b593

📥 Commits

Reviewing files that changed from the base of the PR and between 76e5f96 and af14d40.

📒 Files selected for processing (6)
  • src/Bootstrappers/BroadcastingConfigBootstrapper.php
  • src/Overrides/TenancyBroadcastManager.php
  • tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php
  • tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php
  • tests/BroadcastingTest.php
  • tests/Etc/TestingBroadcaster.php
💤 Files with no reviewable changes (1)
  • tests/BroadcastingTest.php

Comment thread src/Bootstrappers/BroadcastingConfigBootstrapper.php Outdated
Comment thread tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php
lukinovec added 11 commits July 11, 2026 14:32
…ly 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.
Remove the with() -- the test ran four times with the same TestingBroadcaster, so the only tested broadcaster was actually the testing one. Running the test just once is enough, the same things are still covered.
…ed to config, the central config values are kept
Originally, the test only asserted that the broadcasters inherited the correct channel  names. Now, the test also asserts that the actual closure and the channel options are inherited too.
…ract (instead of the abstract class) don't break bootstrap()

Covers the `$centralBroadcaster instanceof Broadcaster && $tenantBroadcaster instanceof Broadcaster` check in extend(BroadcasterContract::class, ...)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Bootstrappers/BroadcastingConfigBootstrapper.php (1)

124-132: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear the configuration snapshot after reverting.

$originalConfig survives revert(). If central broadcasting configuration changes between tenancy sessions, the next setConfig() keeps the stale values because of ??=, and unsetConfig() later restores those old values instead of the current central configuration.

Reset the snapshot after restoring the config.

🛠️ Proposed fix
        $this->unsetConfig();
+       $this->originalConfig = [];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Bootstrappers/BroadcastingConfigBootstrapper.php` around lines 124 - 132,
Update revert() in BroadcastingConfigBootstrapper to clear the $originalConfig
snapshot after restoring the central broadcasting configuration. Ensure the
reset occurs before or alongside the existing unsetConfig() cleanup so the next
setConfig() captures current values instead of reusing stale data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/Bootstrappers/BroadcastingConfigBootstrapper.php`:
- Around line 124-132: Update revert() in BroadcastingConfigBootstrapper to
clear the $originalConfig snapshot after restoring the central broadcasting
configuration. Ensure the reset occurs before or alongside the existing
unsetConfig() cleanup so the next setConfig() captures current values instead of
reusing stale data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d36e5e68-49bd-448d-b3b5-cf6cfa38cf76

📥 Commits

Reviewing files that changed from the base of the PR and between 4422df2 and 31d1663.

📒 Files selected for processing (2)
  • src/Bootstrappers/BroadcastingConfigBootstrapper.php
  • tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php

Comment on lines +124 to +126
// Revert the bound BroadcastManager and Broadcaster singletons back to their original state
$this->app->singleton(BroadcastManager::class, fn () => $this->originalBroadcastManager);
$this->app->singleton(BroadcasterContract::class, fn () => $this->originalBroadcaster);

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.

Suggested change
// Revert the bound BroadcastManager and Broadcaster singletons back to their original state
$this->app->singleton(BroadcastManager::class, fn () => $this->originalBroadcastManager);
$this->app->singleton(BroadcasterContract::class, fn () => $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);

Would this be equivalent?

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

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.

There's a getChannels() method on Broadcaster I think

Comment on lines +77 to +87
$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;
});

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Modifications to BroadcastingConfigBootstrapper::$credentialsMap are overwritten with values from $mapPresets

3 participants