mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 09:34:04 +00:00
Add SessionTenancyBootstrapper (#2)
* Add SessionTenancyBootstrapper * Fix code style (php-cs-fixer) * add value to bootstrappers config * tenant aware call test * reproduce issue in tests * fix logic for calling tenant run from central context, finish tests * change laravel version back * bump laravel to ^9.38 * add listener to create tenant DBs Co-authored-by: PHP CS Fixer <phpcsfixer@example.com> Co-authored-by: Abrar Ahmad <abrar.dev99@gmail.com>
This commit is contained in:
parent
ff46bcfe20
commit
5849089373
7 changed files with 251 additions and 3 deletions
|
|
@ -102,6 +102,7 @@ return [
|
||||||
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
|
||||||
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
||||||
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
|
||||||
|
// Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper::class,
|
||||||
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"illuminate/support": "^9.0",
|
"illuminate/support": "^9.38",
|
||||||
"spatie/ignition": "^1.4",
|
"spatie/ignition": "^1.4",
|
||||||
"ramsey/uuid": "^4.0",
|
"ramsey/uuid": "^4.0",
|
||||||
"stancl/jobpipeline": "^1.0",
|
"stancl/jobpipeline": "^1.0",
|
||||||
"stancl/virtualcolumn": "^1.3"
|
"stancl/virtualcolumn": "^1.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "^9.0",
|
"laravel/framework": "^9.38",
|
||||||
"orchestra/testbench": "^7.0",
|
"orchestra/testbench": "^7.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"doctrine/dbal": "^2.10",
|
"doctrine/dbal": "^2.10",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
</filter>
|
</filter>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_KEY" value="base64:uYVmTs9lrQbXWfHgSSiG0VZMjc2KG/fBbjV1i1JDVos="/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="redis"/>
|
<env name="CACHE_DRIVER" value="redis"/>
|
||||||
<env name="MAIL_DRIVER" value="array"/>
|
<env name="MAIL_DRIVER" value="array"/>
|
||||||
|
|
|
||||||
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal file
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Bootstrappers;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository;
|
||||||
|
use Illuminate\Contracts\Container\Container;
|
||||||
|
use Illuminate\Session\DatabaseSessionHandler;
|
||||||
|
use Illuminate\Session\SessionManager;
|
||||||
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This resets the database connection used by the database session driver.
|
||||||
|
*
|
||||||
|
* It runs each time tenancy is initialized or ended.
|
||||||
|
* That way the session driver always uses the current DB connection.
|
||||||
|
*/
|
||||||
|
class SessionTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected Repository $config,
|
||||||
|
protected Container $container,
|
||||||
|
protected SessionManager $session,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bootstrap(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
$this->resetDatabaseHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revert(): void
|
||||||
|
{
|
||||||
|
// When ending tenancy, this runs *before* the DatabaseTenancyBootstrapper, so DB tenancy
|
||||||
|
// is still bootstrapped. For that reason, we have to explicitly use the central connection
|
||||||
|
$this->resetDatabaseHandler(config('tenancy.database.central_connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resetDatabaseHandler(string $defaultConnection = null): void
|
||||||
|
{
|
||||||
|
$sessionDrivers = $this->session->getDrivers();
|
||||||
|
|
||||||
|
if (isset($sessionDrivers['database'])) {
|
||||||
|
/** @var \Illuminate\Session\Store $databaseDriver */
|
||||||
|
$databaseDriver = $sessionDrivers['database'];
|
||||||
|
|
||||||
|
$databaseDriver->setHandler($this->createDatabaseHandler($defaultConnection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDatabaseHandler(string $defaultConnection = null): DatabaseSessionHandler
|
||||||
|
{
|
||||||
|
// Typically returns null, so this falls back to the default DB connection
|
||||||
|
$connection = $this->config->get('session.connection') ?? $defaultConnection;
|
||||||
|
|
||||||
|
// Based on SessionManager::createDatabaseDriver
|
||||||
|
return new DatabaseSessionHandler(
|
||||||
|
$this->container->make('db')->connection($connection),
|
||||||
|
$this->config->get('session.table'),
|
||||||
|
$this->config->get('session.lifetime'),
|
||||||
|
$this->container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ class HttpKernel extends Kernel
|
||||||
*/
|
*/
|
||||||
protected $middlewareGroups = [
|
protected $middlewareGroups = [
|
||||||
'web' => [
|
'web' => [
|
||||||
\Orchestra\Testbench\Http\Middleware\EncryptCookies::class,
|
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
\Illuminate\Session\Middleware\StartSession::class,
|
||||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateSessionsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->text('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
}
|
||||||
145
tests/SessionBootstrapperTest.php
Normal file
145
tests/SessionBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Listeners;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This collection of regression tests verifies that SessionTenancyBootstrapper
|
||||||
|
* fully fixes the issue described here https://github.com/archtechx/tenancy/issues/547
|
||||||
|
*
|
||||||
|
* This means: using the DB session driver and:
|
||||||
|
* 1) switching to the central context from tenant requests, OR
|
||||||
|
* 2) switching to the tenant context from central requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config(['session.driver' => 'database']);
|
||||||
|
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
TenantCreated::class,
|
||||||
|
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener()
|
||||||
|
);
|
||||||
|
|
||||||
|
Event::listen(Events\TenancyInitialized::class, Listeners\BootstrapTenancy::class);
|
||||||
|
Event::listen(Events\TenancyEnded::class, Listeners\RevertToCentralContext::class);
|
||||||
|
|
||||||
|
// Sessions table for central database
|
||||||
|
pest()->artisan('migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/session_migrations',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('central helper can be used in tenant requests', function (bool $enabled, bool $shouldThrow) {
|
||||||
|
if ($enabled) {
|
||||||
|
config()->set(
|
||||||
|
'tenancy.bootstrappers',
|
||||||
|
array_merge(config('tenancy.bootstrappers'), [SessionTenancyBootstrapper::class]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
$tenant->domains()->create(['domain' => 'foo.localhost']);
|
||||||
|
|
||||||
|
// run for tenants
|
||||||
|
pest()->artisan('tenants:migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/session_migrations',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
Route::middleware(['web', InitializeTenancyByDomain::class])->get('/bar', function () {
|
||||||
|
session(['message' => 'tenant session']);
|
||||||
|
|
||||||
|
tenancy()->central(function () {
|
||||||
|
return 'central results';
|
||||||
|
});
|
||||||
|
|
||||||
|
return session('message');
|
||||||
|
});
|
||||||
|
|
||||||
|
// We initialize tenancy before making the request, since sessions work a bit differently in tests
|
||||||
|
// and we need the DB session handler to use the tenant connection (as it does in a real app on tenant requests).
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->withoutExceptionHandling()
|
||||||
|
->get('http://foo.localhost/bar')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('tenant session');
|
||||||
|
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->fail('Exception not thrown');
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->assertTrue(true); // empty assertion to make the test pass
|
||||||
|
} else {
|
||||||
|
pest()->fail('Exception thrown: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})->with([
|
||||||
|
['enabled' => false, 'shouldThrow' => true],
|
||||||
|
['enabled' => true, 'shouldThrow' => false],
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('tenant run helper can be used on central requests', function (bool $enabled, bool $shouldThrow) {
|
||||||
|
if ($enabled) {
|
||||||
|
config()->set(
|
||||||
|
'tenancy.bootstrappers',
|
||||||
|
array_merge(config('tenancy.bootstrappers'), [SessionTenancyBootstrapper::class]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tenant::create();
|
||||||
|
|
||||||
|
// run for tenants
|
||||||
|
pest()->artisan('tenants:migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/session_migrations',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
Route::middleware(['web'])->get('/bar', function () {
|
||||||
|
session(['message' => 'central session']);
|
||||||
|
|
||||||
|
Tenant::first()->run(function () {
|
||||||
|
return 'tenant results';
|
||||||
|
});
|
||||||
|
|
||||||
|
return session('message');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->withoutExceptionHandling()
|
||||||
|
->get('http://localhost/bar')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('central session');
|
||||||
|
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->fail('Exception not thrown');
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->assertTrue(true); // empty assertion to make the test pass
|
||||||
|
} else {
|
||||||
|
pest()->fail('Exception thrown: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})->with([
|
||||||
|
['enabled' => false, 'shouldThrow' => true],
|
||||||
|
['enabled' => true, 'shouldThrow' => false],
|
||||||
|
]);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue