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

26 commits

Author SHA1 Message Date
lukinovec
553f57a8ad
[MINOR BC] BroadcastingConfigBootstrapper rewrite, bugfixes (#1448)
> Minor breaking change: BroadcastingConfigBootstrapper::$broadcaster property removed, we now just use the default broadcaster for setting mappings based on a preset

### 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 section describes the problem with resolving/injecting the
`Broadcaster` contract directly (via DI or `app(Broadcaster::class)`).
The `extend()` call that fixes it also plays a second, bigger role --
see the "How `BroadcastingConfigBootstrapper::bootstrap()` works"
section below.

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 "Broadcasters are
resolved once per tenancy initialization" below),
`app(Broadcaster::class)` and `Broadcast::driver()` return the same
instance in every context -- same as Laravel's default behavior.

### How `BroadcastingConfigBootstrapper::bootstrap()` works (the
`extend()` calls)

Both `extend()` calls run immediately during `bootstrap()`, not lazily
on some later resolution -- `extend()` executes the closure right away
when the extended singleton is already resolved, and both singletons are
resolved at the top of `bootstrap()` (where we save the central
instances for `revert()`).

Since the singletons are already resolved, the container also doesn't
store the closures as extenders -- they run once during `bootstrap()`
and that's it. Once `revert()` restores the original instances,
resolving either singleton again (even after a `forgetInstance()`) goes
purely through the original bindings. So when tenancy initializes:

1. `setConfig()` maps the tenant's properties to the broadcasting
config.
2. The `BroadcastManager` extend swaps the bound manager for a fresh one
with an empty driver cache and passes the central manager's custom
driver creators to it.
3. The `Broadcaster` contract extend calls `connection()` on the tenant
manager, which resolves the default broadcaster using the updated
(tenant) config and caches it as the manager's default driver. The
central broadcaster's auth properties -- the channel auth closures,
their options, and the authenticated user callback -- are then copied
onto this tenant broadcaster.
4. `Broadcast::clearResolvedInstance()` makes the facade re-resolve on
the next call, so it returns the tenant manager instead of the stale
central one.

When `/broadcasting/auth` gets hit later, `Broadcast::auth()` goes
through the tenant manager's `driver()`, which returns the broadcaster
cached in step 3 (built with the tenant credentials, using the copied
channel auth closures). This means the `Broadcaster` contract extend
isn't just for code that resolves or injects the contract directly --
it's also what makes channel auth work in tenant context. Without it,
`app(Broadcaster::class)` would keep returning the stale central
broadcaster, and the tenant broadcaster's `$channels` would stay empty,
so `Broadcast::auth()` would throw a 403 for every channel registered in
the central context. The same goes for user authentication --
`resolveAuthenticatedUser()` has no fallback, so without the copied
authenticated user callback (registered via
`Broadcast::resolveAuthenticatedUserUsing()`), `/broadcasting/user-auth`
would throw a 403 in tenant context.

`revert()` restores the original central manager and broadcaster
instances via `instance()` (they're saved at the top of `bootstrap()`
and nothing touches them while tenancy is initialized) and clears the
facade's resolved instance again.

### 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](https://github.com/archtechx/tenancy/pull/1448#discussion_r3555941319),
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 auth properties (the channel auth closures, their options,
and the authenticated user callback) are copied 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 properties
only have to be copied to the default broadcaster.

The channel auth closures are always *only* on the default broadcaster,
even in plain Laravel. Every `Broadcast::channel()` call adds an entry
to the same `$channels` array on the default broadcaster -- a channel
with its own authorization rules is just one of those entries, so it
gets copied along with the rest. The only way to register a closure on a
non-default broadcaster is calling
`Broadcast::driver('foobar')->channel(...)` explicitly, and nothing in
Laravel's auth flow ever reads those (`/broadcasting/auth` always
authenticates using 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 bound instance, so a
previously injected instance keeps using the old credentials.

### NOTE: Already-connected clients stop receiving broadcasts after a
credential update

Updating a tenant's credentials doesn't disconnect already-connected
clients (tested with Reverb) -- they stay connected using the old key,
but they stop receiving broadcasts, since broadcasts sent after the
update are 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 about the credential change
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()`.

### **MINOR BC:** `BroadcastingConfigBootstrapper::$broadcaster`
property removed

`BroadcastingConfigBootstrapper::$broadcaster` determined which
`$mapPresets` preset to apply instead of just using
`broadcasting.default`. Its only effect was applying a preset for a
connection other than the default -- a connection nothing resolves, so
it did nothing useful. Set `$credentialsMap` directly if you need a
non-default mapping.

Removing it also let us drop the static mutation from the constructor,
which used to overwrite the user-configured `$credentialsMap`. The
preset merge now happens locally in `setConfig()`.

### 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, and reverting to the central credentials after a tenant with
credential overrides)
- for the rest of the changes mentioned above (including a regression
test for the tenant-context channel registration bug)
- for copying the channel options and the authenticated user callback
along with the channel auth closures
- for the bound `Broadcaster` and the manager's default broadcaster
being the same instance in central and tenant contexts
- 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

Also improved the existing tests.

---------

Co-authored-by: Samuel Stancl <samuel@archte.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-07-23 00:26:04 -07:00
45cf7029af
globalUrl: useAssetOrigin() instead of setAssetRoot()
This change was prompted by a phpstan failure after a recent update.
While making this change, I noticed we don't need the macro anymore
as useAssetOrigin() was added to the UrlGenerator earlier this year,
simplifying our implementation.
2025-11-14 10:59:31 +01:00
8f2cb894ce phpstan fixes for spatie invader 2025-01-21 14:35:00 +01:00
f955b38e2b phpstan fixes 2025-01-05 15:50:30 +01:00
b01c16f4fc phpstan fixes 2024-09-27 23:16:39 +02:00
87e02f207f more phpstan fixes 2024-08-06 18:26:46 +02:00
4372e1bef2 fix phpstan errors 2024-08-06 04:35:25 +02:00
6b6c883234 phpstan fixes 2024-07-04 20:48:55 +02:00
Samuel Štancl
7317d2638a
Postgres RLS + permission controlled database managers (#33)
This PR adds Postgres RLS (trait manager + table manager approach) and permission controlled managers for PostgreSQL.

---------

Co-authored-by: lukinovec <lukinovec@gmail.com>
Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-04-24 22:32:49 +02:00
Samuel Štancl
a41ad69023
Filesystem logic refactor, improved defaults for cache tenancy (#42)
* refactor FilesystemTenancyBootstrapper

* clean up tests and improve coverage

* minor maintenance mode changes

* Improve tenants:migrate --skip-failing logic

* make tenants:migrate output consistently formatted

* minor RootUrlBootstrapper + misc changes

* cache bootstrapper-related improvements

* Fix code style (php-cs-fixer)

* misc refactor

* Fix code style (php-cs-fixer)

* add %original_storage_path% to fs bootstrapper, improve default config for cache

* rename method

* inject concrete implementations where needed instead of abstracts

* Fix code style (php-cs-fixer)

* refactor DealsWithTenantSymlinks

* remove obsolete phpstan ignore

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-04-02 04:26:10 +02:00
Samuel Štancl
dc430666ba
Add more tenant key generators (#36)
* Add RandomHexGenerator, create new namespace

* phpstan ignore

* add base64 generator

* add note about base64 being case sensitive

* docblock updates

* replace old UUIDGenerator with a class that throws an exception

* replace base64 generator with a random string generator

* Fix namespace

* Fix code style (php-cs-fixer)

* add test for the deprecated uuid generator

* update comments

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-03-28 03:17:55 +01:00
e8c3c75d7c Simplify TenantWithDatabase interface, move tenantConfig() logic 2024-03-01 10:48:33 +01:00
56a6f25dac fix phpstan errors 2024-02-10 23:55:56 +01:00
Samuel Štancl
6c60a834d5
update larastan include 2023-12-12 11:06:43 +01:00
lukinovec
e61a26d604
Add L10 support to 4.x (merge 3.x to master) (#1071)
* exclude master from CI

* Add space after 'up' in 'docker-compose up-d' (#900)

* Fix ArgumentCountError on the TenantAssetsController (#894)

* Fix ArgumentCount exception on the TenantAssetsController when no `$path` is provided

* CS

* CS

* Handle null case explicitly

* code style

Co-authored-by: Bram Wubs <bram@sibi.nl>
Co-authored-by: Samuel Štancl <samuel@archte.ch>

* Add support for nested tenant config override (#920)

* feat: add support for nested tenant config override

* test: ensure nested tenant values are mapped

* fix: typo mistake (#954)

* [3.x] Add Vite helper for tenancy (#956)

* Add Vite helper for tenancy

* Move Vite bundler to an Optional Feature

* Rename to foundation vite

* Add ViteBundlerTest

* Add missing end of file

* Update tests

* remove unnecessary end() call

Co-authored-by: Samuel Štancl <samuel@archte.ch>

* rewrite ViteBundlerTest to phpunit syntax

* skip vite test in Laravel < 9

* convert ViteBundler to PHP 7 syntax

* remove import of nonexistent class in older Laravel versions

* remove import of Foundation\Vite in tests

* try to exclude Vite.php from coverage report

* remove typehint

* update channel name

* Cache crash fix (#1048)

* Don't prevent accessing missing Tenant attributes. (#1045)

* [3.x] L10 compatibility (#1065)

* Bump dependencies for Laravel 10

* Update GitHub Actions for Laravel 10

* ci: do not test L10 using PHP 7.3

* drop < L9 support

* use `dispatch_sync` instead of `dispatch_now`

* migrate phpunit configuration

* Update ci.yml

* drop laravel < 9 support

* misc L10 fixes, new docker image

* specify odbc version

* wip

* properly list php versions as strings

* minor changes

* Add `getValue($queryGrammar)` to raw query

* Clean up `isVersion8` code

* rewrite hasFailed assertion

* phpunit schema update

* Upgrade `doctrine/dbal`

---------

Co-authored-by: Samuel Štancl <samuel@archte.ch>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
Co-authored-by: lukinovec <lukinovec@gmail.com>

* Update ci.yml

* Fix code style (php-cs-fixer)

* Update dependencies

* Change invade version

* Delete ViteBundlerTest

* Fix PHPStan error

* Delete PHPStan error ignore

* Fix CONTRIBUTING.md

* Delete ViteBundler remains

* Bring back ViteBundler

* Convert ViteBundlerTest to Pest

* Update ci.yml

---------

Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
Co-authored-by: Bram Wubs <megawubs@users.noreply.github.com>
Co-authored-by: Bram Wubs <bram@sibi.nl>
Co-authored-by: Samuel Štancl <samuel@archte.ch>
Co-authored-by: George Bishop <email.georgebishop@gmail.com>
Co-authored-by: Anbuselvan Rocky <15264938+anburocky3@users.noreply.github.com>
Co-authored-by: Wilsen Hernández <13445515+wilsenhc@users.noreply.github.com>
Co-authored-by: Joel Stein <joel@mediatrix.digital>
Co-authored-by: Guilherme Saade <saade@outlook.com.br>
Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2023-02-20 23:47:10 +01:00
Samuel Štancl
0a205dd817 resolve phpstan issues 2023-02-01 06:17:19 +01:00
Samuel Štancl
48fc63fe02 switch to PHP 8.2 2023-01-06 02:38:31 +01:00
Samuel Štancl
24d71230e8 comment out php 8.2 phpstan ignores 2023-01-04 03:18:52 +01:00
Samuel Štancl
03ac1ef127 fix phpstan errors (seems like it started ignoring @property annotations on interfaces and abstract classes) 2023-01-04 02:23:48 +01:00
Samuel Štancl
942d79cbd7 resolve all phpstan issues 2022-11-08 13:34:04 +01:00
lukinovec
080b271bb3
[4.x] Drop tenant databases on migrate:fresh (#971)
* Test that `migrate:fresh` deletes tenant databases

* Delete tenants on `migrate:fresh`

* Fix code style (php-cs-fixer)

* Add config key for dropping tenant databases on `migrate:fresh`

* Add MigrateFreshOverride

* Try to override `migrate:fresh` in TenancyServiceProvider

* Update `migrate:fresh` test

* Fix code style (php-cs-fixer)

* Drop tenant databases by default

* Change `migrate:fresh` test to test if the tenant DBs are dropped by default

* Override `migrate:fresh` by extending `FreshCommand` in TenancyServiceProvider

* Update MigrateFreshOverride

* Fix code style (php-cs-fixer)

* Fix commands test

* Simplify handle method

* Fix code style (php-cs-fixer)

* Don't drop tenant DBs on migrate:fresh by default

* Change command overriding

* Update migrate:fresh test

* always register MigrateFreshOverride

* misc

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
2022-10-17 15:19:30 +02:00
Samuel Štancl
f98a901aeb get down to 21 phpstan errors 2022-09-29 23:39:35 +02:00
Samuel Štancl
a94227a19c get down to 59 phpstan errors 2022-09-29 22:20:55 +02:00
Samuel Štancl
87212e5390 phpstan, global_cache, resolver improvements, InitializationHelpers trait 2022-09-29 02:47:13 +02:00
Samuel Štancl
8af354c20e get phpstan errors down from 252 to 189 2022-08-30 05:44:23 +02:00
Abrar Ahmad
fb8b9c1614
add phpstan (#926) 2022-08-29 20:55:33 +02:00