1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 08:24:04 +00:00

[4.x] Make broadcasting work with Tenancy (#1027)

* Add BroadcastTenancyBootstrapper and TenancyBroadcastManager

* Fix code style (php-cs-fixer)

* Bind original BroadcastManager again on `revert()`

* Fix code style (php-cs-fixer)

* Move manager to correct directory

* Fix property type

* Make BroadcastTenancyBootstrapper a singleton in tests

* Fix code style (php-cs-fixer)

* Bind the original broadcaster instance on `revert()`

* Instead of just forgetting the old broadcaster instance, bind the new one

* Add BroadcastTenancyBootstrapper tests

* Separate the test

* Fix code style (php-cs-fixer)

* Add bootstrapper test

* Add broadcaster channels test

* Clean up BootstrapperTest

* Fix BroadcastingTest

* Add comments to TenancyBroadcastManager

* Add BroadcastTenancyBootstrapper comments

* Simplify BroadcastManager extension, remove setDriver method

* Add comment

* Fix PHPStan errors

* Fix PHPStan errors

* Remove duplicate import

* Fix test

* Delete `::class` from test name

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

* Create databases for newly created tenants in BroadcastingTest

* move spatie/invade to require

---------

Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
This commit is contained in:
lukinovec 2023-02-18 15:52:55 +01:00 committed by GitHub
parent fbdb13f392
commit d7a4982cd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 336 additions and 2 deletions

View file

@ -21,7 +21,8 @@
"spatie/ignition": "^1.4",
"ramsey/uuid": "^4.0",
"stancl/jobpipeline": "^1.0",
"stancl/virtualcolumn": "^1.3"
"stancl/virtualcolumn": "^1.3",
"spatie/invade": "^1.1"
},
"require-dev": {
"laravel/framework": "^9.38",

View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Broadcasting\BroadcastManager;
use Illuminate\Config\Repository;
use Illuminate\Contracts\Broadcasting\Broadcaster;
use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\TenancyBroadcastManager;
class BroadcastTenancyBootstrapper implements TenancyBootstrapper
{
/**
* Tenant properties to be mapped to config (similarly to the TenantConfig feature).
*
* For example:
* [
* 'config.key.name' => 'tenant_property',
* ]
*/
public static array $credentialsMap = [];
public static string|null $broadcaster = null;
protected array $originalConfig = [];
protected BroadcastManager|null $originalBroadcastManager = null;
protected Broadcaster|null $originalBroadcaster = null;
public static array $mapPresets = [
'pusher' => [
'broadcasting.connections.pusher.key' => 'pusher_key',
'broadcasting.connections.pusher.secret' => 'pusher_secret',
'broadcasting.connections.pusher.app_id' => 'pusher_app_id',
'broadcasting.connections.pusher.options.cluster' => 'pusher_cluster',
],
'ably' => [
'broadcasting.connections.ably.key' => 'ably_key',
'broadcasting.connections.ably.public' => 'ably_public',
],
];
public function __construct(
protected Repository $config,
protected Application $app
) {
static::$broadcaster ??= $config->get('broadcasting.default');
static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$broadcaster] ?? []);
}
public function bootstrap(Tenant $tenant): void
{
$this->originalBroadcastManager = $this->app->make(BroadcastManager::class);
$this->originalBroadcaster = $this->app->make(Broadcaster::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);
});
}
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);
$this->unsetConfig();
}
protected function setConfig(Tenant $tenant): void
{
foreach (static::$credentialsMap as $configKey => $storageKey) {
$override = $tenant->$storageKey;
if (array_key_exists($storageKey, $tenant->getAttributes())) {
$this->originalConfig[$configKey] ??= $this->config->get($configKey);
$this->config->set($configKey, $override);
}
}
}
protected function unsetConfig(): void
{
foreach ($this->originalConfig as $key => $value) {
$this->config->set($key, $value);
}
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy; // todo new Overrides namespace?
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
use Illuminate\Broadcasting\BroadcastManager;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
use Illuminate\Contracts\Foundation\Application;
class TenancyBroadcastManager extends BroadcastManager
{
/**
* Names of broadcasters to always recreate using $this->resolve() (even when they're
* cached and available in the $broadcasters property).
*
* The reason for recreating the broadcasters is
* to make your app use the correct broadcaster credentials when tenancy is initialized.
*/
public static array $tenantBroadcasters = ['pusher', 'ably'];
/**
* Override the get method so that the broadcasters in $tenantBroadcasters
* always get freshly resolved even when they're cached and available in the $broadcasters property,
* and that the resolved broadcaster will override the BroadcasterContract::class singleton.
*
* If there's a cached broadcaster with the same name as $name,
* give its channels to the newly resolved bootstrapper.
*/
protected function get($name)
{
if (in_array($name, static::$tenantBroadcasters)) {
/** @var Broadcaster|null $originalBroadcaster */
$originalBroadcaster = $this->app->make(BroadcasterContract::class);
$newBroadcaster = $this->resolve($name);
// If there is a current broadcaster, give its channels to the newly resolved one
// Broadcasters only have to implement the Illuminate\Contracts\Broadcasting\Broadcaster contract
// Which doesn't require the channels property
// So passing the channels is only needed for Illuminate\Broadcasting\Broadcasters\Broadcaster instances
if ($originalBroadcaster instanceof Broadcaster && $newBroadcaster instanceof Broadcaster) {
$this->passChannelsFromOriginalBroadcaster($originalBroadcaster, $newBroadcaster);
}
$this->app->singleton(BroadcasterContract::class, fn (Application $app) => $newBroadcaster);
return $newBroadcaster;
}
return parent::get($name);
}
// Because, unlike the original broadcaster, the newly resolved broadcaster won't have the channels registered using routes/channels.php
// Using it for broadcasting won't work, unless we make it have the original broadcaster's channels
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

@ -18,11 +18,14 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Events\TenantDeleted;
use Stancl\Tenancy\Events\DeletingTenant;
use Stancl\Tenancy\TenancyBroadcastManager;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Broadcasting\BroadcastManager;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\UrlTenancyBootstrapper;
@ -31,6 +34,7 @@ use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
beforeEach(function () {
@ -331,6 +335,82 @@ test('local storage public urls are generated correctly', function() {
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
});
test('BroadcastTenancyBootstrapper binds TenancyBroadcastManager to BroadcastManager and reverts the binding when tenancy is ended', function() {
expect(app(BroadcastManager::class))->toBeInstanceOf(BroadcastManager::class);
tenancy()->initialize(Tenant::create());
expect(app(BroadcastManager::class))->toBeInstanceOf(TenancyBroadcastManager::class);
tenancy()->end();
expect(app(BroadcastManager::class))->toBeInstanceOf(BroadcastManager::class);
});
test('BroadcastTenancyBootstrapper maps tenant broadcaster credentials to config as specified in the $credentialsMap property and reverts the config after ending tenancy', function() {
config([
'broadcasting.connections.testing.driver' => 'testing',
'broadcasting.connections.testing.message' => $defaultMessage = 'default',
]);
BroadcastTenancyBootstrapper::$credentialsMap = [
'broadcasting.connections.testing.message' => 'testing_broadcaster_message',
];
$tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']);
$tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']);
tenancy()->initialize($tenant);
expect(array_key_exists('testing_broadcaster_message', tenant()->getAttributes()))->toBeTrue();
expect(config('broadcasting.connections.testing.message'))->toBe($tenantMessage);
tenancy()->initialize($tenant2);
expect(config('broadcasting.connections.testing.message'))->toBe($secondTenantMessage);
tenancy()->end();
expect(config('broadcasting.connections.testing.message'))->toBe($defaultMessage);
});
test('BroadcastTenancyBootstrapper makes the app use broadcasters with the correct credentials', function() {
config([
'broadcasting.default' => 'testing',
'broadcasting.connections.testing.driver' => 'testing',
'broadcasting.connections.testing.message' => $defaultMessage = 'default',
]);
TenancyBroadcastManager::$tenantBroadcasters[] = 'testing';
BroadcastTenancyBootstrapper::$credentialsMap = [
'broadcasting.connections.testing.message' => 'testing_broadcaster_message',
];
$registerTestingBroadcaster = fn() => app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster($config['message']));
$registerTestingBroadcaster();
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage);
$tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']);
$tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']);
tenancy()->initialize($tenant);
$registerTestingBroadcaster();
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($tenantMessage);
tenancy()->initialize($tenant2);
$registerTestingBroadcaster();
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($secondTenantMessage);
tenancy()->end();
$registerTestingBroadcaster();
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage);
});
test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() {
MailTenancyBootstrapper::$credentialsMap = [
'mail.mailers.smtp.username' => 'smtp_username',

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded;
use Illuminate\Support\Facades\Broadcast;
use Stancl\Tenancy\TenancyBroadcastManager;
use Illuminate\Broadcasting\BroadcastManager;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
beforeEach(function() {
withTenantDatabases();
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
});
test('bound broadcaster instance is the same before initializing tenancy and after ending it', function() {
config(['broadcasting.default' => 'null']);
TenancyBroadcastManager::$tenantBroadcasters[] = 'null';
$originalBroadcaster = app(BroadcasterContract::class);
tenancy()->initialize(Tenant::create());
// TenancyBroadcastManager binds new broadcaster
$tenantBroadcaster = app(BroadcastManager::class)->driver();
expect($tenantBroadcaster)->not()->toBe($originalBroadcaster);
tenancy()->end();
expect($originalBroadcaster)->toBe(app(BroadcasterContract::class));
});
test('new broadcasters get the channels from the previously bound broadcaster', function() {
config([
'broadcasting.default' => $driver = 'testing',
'broadcasting.connections.testing.driver' => $driver,
]);
TenancyBroadcastManager::$tenantBroadcasters[] = $driver;
$registerTestingBroadcaster = fn() => app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster('testing'));
$getCurrentChannels = fn() => array_keys(invade(app(BroadcastManager::class)->driver())->channels);
$registerTestingBroadcaster();
Broadcast::channel($channel = 'testing-channel', fn() => true);
expect($channel)->toBeIn($getCurrentChannels());
tenancy()->initialize(Tenant::create());
$registerTestingBroadcaster();
expect($channel)->toBeIn($getCurrentChannels());
tenancy()->end();
$registerTestingBroadcaster();
expect($channel)->toBeIn($getCurrentChannels());
});

View file

@ -0,0 +1,25 @@
<?php
namespace Stancl\Tenancy\Tests\Etc;
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
class TestingBroadcaster extends Broadcaster {
public function __construct(
public string $message
) {}
public function auth($request)
{
return true;
}
public function validAuthenticationResponse($request, $result)
{
return true;
}
public function broadcast(array $channels, $event, array $payload = [])
{
}
}

View file

@ -12,9 +12,10 @@ use Illuminate\Support\Facades\Redis;
use Illuminate\Foundation\Application;
use Stancl\Tenancy\Facades\GlobalCache;
use Stancl\Tenancy\TenancyServiceProvider;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\UrlTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
abstract class TestCase extends \Orchestra\Testbench\TestCase
{
@ -105,6 +106,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'--force' => true,
],
'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that
'tenancy.bootstrappers.broadcast' => BroadcastTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that
'tenancy.bootstrappers.mail' => MailTenancyBootstrapper::class,
'tenancy.bootstrappers.url' => UrlTenancyBootstrapper::class,
'queue.connections.central' => [
@ -116,6 +118,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
]);
$app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
$app->singleton(BroadcastTenancyBootstrapper::class);
$app->singleton(MailTenancyBootstrapper::class);
$app->singleton(UrlTenancyBootstrapper::class);
}