mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 21:14:03 +00:00
Merge branch 'master' of github.com:archtechx/tenancy into cache-prefix
This commit is contained in:
commit
93aaba3e11
22 changed files with 709 additions and 49 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -111,7 +111,6 @@ jobs:
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: '8.2'
|
php-version: '8.2'
|
||||||
extensions: imagick, swoole
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install composer dependencies
|
- name: Install composer dependencies
|
||||||
run: composer install
|
run: composer install
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('tenant_resources', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->string('tenant_id');
|
||||||
|
$table->string('resource_global_id');
|
||||||
|
$table->string('tenant_resources_type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tenant_resources');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -23,6 +23,7 @@ parameters:
|
||||||
- src/Commands/ClearPendingTenants.php
|
- src/Commands/ClearPendingTenants.php
|
||||||
- src/Database/Concerns/PendingScope.php
|
- src/Database/Concerns/PendingScope.php
|
||||||
- src/Database/ParentModelScope.php
|
- src/Database/ParentModelScope.php
|
||||||
|
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder\:\:withPending\(\)#'
|
||||||
-
|
-
|
||||||
message: '#invalid type Laravel\\Telescope\\IncomingEntry#'
|
message: '#invalid type Laravel\\Telescope\\IncomingEntry#'
|
||||||
paths:
|
paths:
|
||||||
|
|
@ -47,9 +48,14 @@ parameters:
|
||||||
message: '#Trying to invoke Closure\|null but it might not be a callable#'
|
message: '#Trying to invoke Closure\|null but it might not be a callable#'
|
||||||
paths:
|
paths:
|
||||||
- src/Database/DatabaseConfig.php
|
- src/Database/DatabaseConfig.php
|
||||||
|
-
|
||||||
|
message: '#Unable to resolve the template type (TMapWithKeysKey|TMapWithKeysValue) in call to method#'
|
||||||
|
paths:
|
||||||
|
- src/Concerns/DealsWithTenantSymlinks.php
|
||||||
- '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#'
|
- '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#'
|
||||||
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#'
|
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#'
|
||||||
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#'
|
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#'
|
||||||
|
|
||||||
checkMissingIterableValueType: false
|
checkMissingIterableValueType: false
|
||||||
|
checkGenericClassInNonGenericObjectType: false # later we may want to enable this
|
||||||
treatPhpDocTypesAsCertain: false
|
treatPhpDocTypesAsCertain: false
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||||
/** @var TenantWithDatabase $tenant */
|
/** @var TenantWithDatabase $tenant */
|
||||||
|
|
||||||
// Better debugging, but breaks cached lookup in prod
|
// Better debugging, but breaks cached lookup in prod
|
||||||
if (app()->environment('local')) {
|
if (app()->environment('local') || app()->environment('testing')) { // todo@docs mention this change in v4 upgrade guide https://github.com/archtechx/tenancy/pull/945#issuecomment-1268206149
|
||||||
$database = $tenant->database()->getName();
|
$database = $tenant->database()->getName();
|
||||||
if (! $tenant->database()->manager()->databaseExists($database)) {
|
if (! $tenant->database()->manager()->databaseExists($database)) {
|
||||||
throw new TenantDatabaseDoesNotExistException($database);
|
throw new TenantDatabaseDoesNotExistException($database);
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ namespace Stancl\Tenancy\Commands;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||||
use Illuminate\Database\Migrations\Migrator;
|
use Illuminate\Database\Migrations\Migrator;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||||
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||||
|
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||||
use Stancl\Tenancy\Events\MigratingDatabase;
|
use Stancl\Tenancy\Events\MigratingDatabase;
|
||||||
|
|
||||||
|
|
@ -28,6 +30,8 @@ class Migrate extends MigrateCommand
|
||||||
{
|
{
|
||||||
parent::__construct($migrator, $dispatcher);
|
parent::__construct($migrator, $dispatcher);
|
||||||
|
|
||||||
|
$this->addOption('skip-failing');
|
||||||
|
|
||||||
$this->specifyParameters();
|
$this->specifyParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,16 +47,23 @@ class Migrate extends MigrateCommand
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
foreach ($this->getTenants() as $tenant) {
|
||||||
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
try {
|
||||||
|
$tenant->run(function ($tenant) {
|
||||||
|
$this->line("Tenant: {$tenant->getTenantKey()}");
|
||||||
|
|
||||||
event(new MigratingDatabase($tenant));
|
event(new MigratingDatabase($tenant));
|
||||||
|
|
||||||
// Migrate
|
// Migrate
|
||||||
parent::handle();
|
parent::handle();
|
||||||
|
|
||||||
event(new DatabaseMigrated($tenant));
|
event(new DatabaseMigrated($tenant));
|
||||||
});
|
});
|
||||||
|
} catch (TenantDatabaseDoesNotExistException|QueryException $th) {
|
||||||
|
if (! $this->option('skip-failing')) {
|
||||||
|
throw $th;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\Concerns;
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||||
use Stancl\Tenancy\Contracts\Syncable;
|
use Stancl\Tenancy\Contracts\Syncable;
|
||||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||||
|
use Stancl\Tenancy\Database\Models\TenantMorphPivot;
|
||||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||||
|
|
||||||
trait ResourceSyncing
|
trait ResourceSyncing
|
||||||
|
|
@ -43,4 +45,10 @@ trait ResourceSyncing
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tenants(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', 'global_id')
|
||||||
|
->using(TenantMorphPivot::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
src/Database/Concerns/TriggerSyncEvent.php
Normal file
21
src/Database/Concerns/TriggerSyncEvent.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Contracts\Syncable;
|
||||||
|
|
||||||
|
trait TriggerSyncEvent
|
||||||
|
{
|
||||||
|
public static function booted(): void
|
||||||
|
{
|
||||||
|
static::saved(function (self $pivot) {
|
||||||
|
$parent = $pivot->pivotParent;
|
||||||
|
|
||||||
|
if ($parent instanceof Syncable && $parent->shouldSync()) {
|
||||||
|
$parent->triggerSyncEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ class DatabaseConfig
|
||||||
{
|
{
|
||||||
$this->tenant->setInternal('db_name', $this->getName());
|
$this->tenant->setInternal('db_name', $this->getName());
|
||||||
|
|
||||||
if ($this->connectionDriverManager($this->getTemplateConnectionName()) instanceof Contracts\ManagesDatabaseUsers) {
|
if ($this->connectionDriverManager($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
||||||
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant));
|
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant));
|
||||||
}
|
}
|
||||||
|
|
@ -97,11 +97,29 @@ class DatabaseConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTemplateConnectionName(): string
|
public function getTemplateConnectionDriver(): string
|
||||||
{
|
{
|
||||||
return $this->tenant->getInternal('db_connection')
|
return $this->getTemplateConnection()['driver'];
|
||||||
?? config('tenancy.database.template_tenant_connection')
|
}
|
||||||
?? config('tenancy.database.central_connection');
|
|
||||||
|
public function getTemplateConnection(): array
|
||||||
|
{
|
||||||
|
if ($template = $this->tenant->getInternal('db_connection')) {
|
||||||
|
return config("database.connections.{$template}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($template = config('tenancy.database.template_tenant_connection')) {
|
||||||
|
return is_array($template) ? array_merge($this->getCentralConnection(), $template) : config("database.connections.{$template}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getCentralConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCentralConnection(): array
|
||||||
|
{
|
||||||
|
$centralConnectionName = config('tenancy.database.central_connection');
|
||||||
|
|
||||||
|
return config("database.connections.{$centralConnectionName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTenantHostConnectionName(): string
|
public function getTenantHostConnectionName(): string
|
||||||
|
|
@ -114,8 +132,7 @@ class DatabaseConfig
|
||||||
*/
|
*/
|
||||||
public function connection(): array
|
public function connection(): array
|
||||||
{
|
{
|
||||||
$template = $this->getTemplateConnectionName();
|
$templateConnection = $this->getTemplateConnection();
|
||||||
$templateConnection = config("database.connections.{$template}");
|
|
||||||
|
|
||||||
return $this->manager()->makeConnectionConfig(
|
return $this->manager()->makeConnectionConfig(
|
||||||
array_merge($templateConnection, $this->tenantConfig()),
|
array_merge($templateConnection, $this->tenantConfig()),
|
||||||
|
|
@ -129,10 +146,9 @@ class DatabaseConfig
|
||||||
public function hostConnection(): array
|
public function hostConnection(): array
|
||||||
{
|
{
|
||||||
$config = $this->tenantConfig();
|
$config = $this->tenantConfig();
|
||||||
$template = $this->getTemplateConnectionName();
|
$templateConnection = $this->getTemplateConnection();
|
||||||
$templateConnection = config("database.connections.{$template}");
|
|
||||||
|
|
||||||
if ($this->connectionDriverManager($template) instanceof Contracts\ManagesDatabaseUsers) {
|
if ($this->connectionDriverManager($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||||
// We're removing the username and password because user with these credentials is not created yet
|
// We're removing the username and password because user with these credentials is not created yet
|
||||||
// If you need to provide username and password when using PermissionControlledMySQLDatabaseManager,
|
// If you need to provide username and password when using PermissionControlledMySQLDatabaseManager,
|
||||||
// consider creating a new connection and use it as `tenancy_db_connection` tenant config key
|
// consider creating a new connection and use it as `tenancy_db_connection` tenant config key
|
||||||
|
|
@ -196,7 +212,7 @@ class DatabaseConfig
|
||||||
$tenantHostConnectionName = $this->getTenantHostConnectionName();
|
$tenantHostConnectionName = $this->getTenantHostConnectionName();
|
||||||
config(["database.connections.{$tenantHostConnectionName}" => $this->hostConnection()]);
|
config(["database.connections.{$tenantHostConnectionName}" => $this->hostConnection()]);
|
||||||
|
|
||||||
$manager = $this->connectionDriverManager($tenantHostConnectionName);
|
$manager = $this->connectionDriverManager(config("database.connections.{$tenantHostConnectionName}.driver"));
|
||||||
|
|
||||||
if ($manager instanceof Contracts\StatefulTenantDatabaseManager) {
|
if ($manager instanceof Contracts\StatefulTenantDatabaseManager) {
|
||||||
$manager->setConnection($tenantHostConnectionName);
|
$manager->setConnection($tenantHostConnectionName);
|
||||||
|
|
@ -211,10 +227,8 @@ class DatabaseConfig
|
||||||
*
|
*
|
||||||
* @throws DatabaseManagerNotRegisteredException
|
* @throws DatabaseManagerNotRegisteredException
|
||||||
*/
|
*/
|
||||||
protected function connectionDriverManager(string $connectionName): Contracts\TenantDatabaseManager
|
protected function connectionDriverManager(string $driver): Contracts\TenantDatabaseManager
|
||||||
{
|
{
|
||||||
$driver = config("database.connections.{$connectionName}.driver");
|
|
||||||
|
|
||||||
$databaseManagers = config('tenancy.database.managers');
|
$databaseManagers = config('tenancy.database.managers');
|
||||||
|
|
||||||
if (! array_key_exists($driver, $databaseManagers)) {
|
if (! array_key_exists($driver, $databaseManagers)) {
|
||||||
|
|
|
||||||
13
src/Database/Models/TenantMorphPivot.php
Normal file
13
src/Database/Models/TenantMorphPivot.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Database\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
||||||
|
use Stancl\Tenancy\Database\Concerns\TriggerSyncEvent;
|
||||||
|
|
||||||
|
class TenantMorphPivot extends MorphPivot
|
||||||
|
{
|
||||||
|
use TriggerSyncEvent;
|
||||||
|
}
|
||||||
|
|
@ -5,18 +5,9 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Database\Models;
|
namespace Stancl\Tenancy\Database\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
use Stancl\Tenancy\Contracts\Syncable;
|
use Stancl\Tenancy\Database\Concerns\TriggerSyncEvent;
|
||||||
|
|
||||||
class TenantPivot extends Pivot
|
class TenantPivot extends Pivot
|
||||||
{
|
{
|
||||||
public static function booted(): void
|
use TriggerSyncEvent;
|
||||||
{
|
|
||||||
static::saved(function (self $pivot) {
|
|
||||||
$parent = $pivot->pivotParent;
|
|
||||||
|
|
||||||
if ($parent instanceof Syncable && $parent->shouldSync()) {
|
|
||||||
$parent->triggerSyncEvent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class Tenancy
|
||||||
/**
|
/**
|
||||||
* The current tenant.
|
* The current tenant.
|
||||||
*/
|
*/
|
||||||
public (Tenant&Model)|null $tenant = null;
|
public Tenant|null $tenant = null;
|
||||||
|
|
||||||
// todo docblock
|
// todo docblock
|
||||||
public ?Closure $getBootstrappersUsing = null;
|
public ?Closure $getBootstrappersUsing = null;
|
||||||
|
|
@ -111,8 +111,9 @@ class Tenancy
|
||||||
/**
|
/**
|
||||||
* Try to find a tenant using an ID.
|
* Try to find a tenant using an ID.
|
||||||
*/
|
*/
|
||||||
public static function find(int|string $id): (Tenant&Model)|null
|
public static function find(int|string $id): Tenant|null
|
||||||
{
|
{
|
||||||
|
/** @var (Tenant&Model)|null */
|
||||||
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
|
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,10 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
__DIR__ . '/../assets/impersonation-migrations/' => database_path('migrations'),
|
__DIR__ . '/../assets/impersonation-migrations/' => database_path('migrations'),
|
||||||
], 'impersonation-migrations');
|
], 'impersonation-migrations');
|
||||||
|
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__ . '/../assets/resource-syncing-migrations/' => database_path('migrations'),
|
||||||
|
], 'resource-syncing-migrations');
|
||||||
|
|
||||||
$this->publishes([
|
$this->publishes([
|
||||||
__DIR__ . '/../assets/tenant_routes.stub.php' => base_path('routes/tenant.php'),
|
__DIR__ . '/../assets/tenant_routes.stub.php' => base_path('routes/tenant.php'),
|
||||||
], 'routes');
|
], 'routes');
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ test('context is switched when tenancy is reinitialized', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('central helper runs callbacks in the central state', function () {
|
test('central helper runs callbacks in the central state', function () {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
tenancy()->initialize($tenant = Tenant::create());
|
tenancy()->initialize($tenant = Tenant::create());
|
||||||
|
|
||||||
tenancy()->central(function () {
|
tenancy()->central(function () {
|
||||||
|
|
@ -60,6 +62,8 @@ test('central helper runs callbacks in the central state', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('central helper returns the value from the callback', function () {
|
test('central helper returns the value from the callback', function () {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
tenancy()->initialize(Tenant::create());
|
tenancy()->initialize(Tenant::create());
|
||||||
|
|
||||||
pest()->assertSame('foo', tenancy()->central(function () {
|
pest()->assertSame('foo', tenancy()->central(function () {
|
||||||
|
|
@ -68,6 +72,8 @@ test('central helper returns the value from the callback', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('central helper reverts back to tenant context', function () {
|
test('central helper reverts back to tenant context', function () {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
tenancy()->initialize($tenant = Tenant::create());
|
tenancy()->initialize($tenant = Tenant::create());
|
||||||
|
|
||||||
tenancy()->central(function () {
|
tenancy()->central(function () {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ beforeEach(function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('batch repository is set to tenant connection and reverted', function () {
|
test('batch repository is set to tenant connection and reverted', function () {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
$tenant2 = Tenant::create();
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,13 @@ use Stancl\Tenancy\Events\TenantCreated;
|
||||||
use Stancl\Tenancy\Events\TenantDeleted;
|
use Stancl\Tenancy\Events\TenantDeleted;
|
||||||
use Stancl\Tenancy\Tests\Etc\TestSeeder;
|
use Stancl\Tenancy\Tests\Etc\TestSeeder;
|
||||||
use Stancl\Tenancy\Events\DeletingTenant;
|
use Stancl\Tenancy\Events\DeletingTenant;
|
||||||
|
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||||
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
|
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
||||||
|
|
@ -109,6 +111,46 @@ test('migrate command loads schema state', function () {
|
||||||
expect(Schema::hasTable('users'))->toBeTrue();
|
expect(Schema::hasTable('users'))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('migrate command only throws exceptions if skip-failing is not passed', function() {
|
||||||
|
Tenant::create();
|
||||||
|
|
||||||
|
$tenantWithoutDatabase = Tenant::create();
|
||||||
|
$databaseToDrop = $tenantWithoutDatabase->run(fn() => DB::connection()->getDatabaseName());
|
||||||
|
|
||||||
|
DB::statement("DROP DATABASE `$databaseToDrop`");
|
||||||
|
|
||||||
|
Tenant::create();
|
||||||
|
|
||||||
|
expect(fn() => pest()->artisan('tenants:migrate --schema-path="tests/Etc/tenant-schema.dump"'))->toThrow(TenantDatabaseDoesNotExistException::class);
|
||||||
|
expect(fn() => pest()->artisan('tenants:migrate --schema-path="tests/Etc/tenant-schema.dump" --skip-failing'))->not()->toThrow(TenantDatabaseDoesNotExistException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('migrate command does not stop after the first failure if skip-failing is passed', function() {
|
||||||
|
$tenants = collect([
|
||||||
|
Tenant::create(),
|
||||||
|
$tenantWithoutDatabase = Tenant::create(),
|
||||||
|
Tenant::create(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$migratedTenants = 0;
|
||||||
|
|
||||||
|
Event::listen(DatabaseMigrated::class, function() use (&$migratedTenants) {
|
||||||
|
$migratedTenants++;
|
||||||
|
});
|
||||||
|
|
||||||
|
$databaseToDrop = $tenantWithoutDatabase->run(fn() => DB::connection()->getDatabaseName());
|
||||||
|
|
||||||
|
DB::statement("DROP DATABASE `$databaseToDrop`");
|
||||||
|
|
||||||
|
Artisan::call('tenants:migrate', [
|
||||||
|
'--schema-path' => '"tests/Etc/tenant-schema.dump"',
|
||||||
|
'--skip-failing' => true,
|
||||||
|
'--tenants' => $tenants->pluck('id')->toArray(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($migratedTenants)->toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
test('dump command works', function () {
|
test('dump command works', function () {
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
$schemaPath = 'tests/Etc/tenant-schema-test.dump';
|
$schemaPath = 'tests/Etc/tenant-schema-test.dump';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class TestCreateCompaniesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('companies', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->string('global_id')->unique();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('companies');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,8 @@ function assertMailerTransportUsesPassword(string|null $password) {
|
||||||
};
|
};
|
||||||
|
|
||||||
test('mailer transport uses the correct credentials', function() {
|
test('mailer transport uses the correct credentials', function() {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'DEFAULT']);
|
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'DEFAULT']);
|
||||||
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
||||||
|
|
||||||
|
|
@ -52,6 +54,8 @@ test('mailer transport uses the correct credentials', function() {
|
||||||
|
|
||||||
|
|
||||||
test('initializing and ending tenancy binds a fresh MailManager instance without cached mailers', function() {
|
test('initializing and ending tenancy binds a fresh MailManager instance without cached mailers', function() {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
$mailers = fn() => invade(app(MailManager::class))->mailers;
|
$mailers = fn() => invade(app(MailManager::class))->mailers;
|
||||||
|
|
||||||
app(MailManager::class)->mailer('smtp');
|
app(MailManager::class)->mailer('smtp');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Stancl\Tenancy\Tests\TestCase;
|
use Stancl\Tenancy\Tests\TestCase;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
|
||||||
uses(TestCase::class)->in(__DIR__);
|
uses(TestCase::class)->in(__DIR__);
|
||||||
|
|
||||||
|
|
@ -8,3 +12,10 @@ function pest(): TestCase
|
||||||
{
|
{
|
||||||
return Pest\TestSuite::getInstance()->test;
|
return Pest\TestSuite::getInstance()->test;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withTenantDatabases()
|
||||||
|
{
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener());
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,23 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Spatie\Valuestore\Valuestore;
|
use Spatie\Valuestore\Valuestore;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Stancl\Tenancy\Tests\Etc\User;
|
use Stancl\Tenancy\Tests\Etc\User;
|
||||||
use Stancl\JobPipeline\JobPipeline;
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Stancl\Tenancy\Events\TenantCreated;
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Queue\Events\JobProcessed;
|
use Illuminate\Queue\Events\JobProcessed;
|
||||||
use Illuminate\Queue\Events\JobProcessing;
|
use Illuminate\Queue\Events\JobProcessing;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
@ -48,6 +48,8 @@ afterEach(function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant id is passed to tenant queues', function () {
|
test('tenant id is passed to tenant queues', function () {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
config(['queue.default' => 'sync']);
|
config(['queue.default' => 'sync']);
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
|
|
@ -64,6 +66,8 @@ test('tenant id is passed to tenant queues', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant id is not passed to central queues', function () {
|
test('tenant id is not passed to central queues', function () {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
@ -156,6 +160,8 @@ test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenan
|
||||||
})->with([true, false]);
|
})->with([true, false]);
|
||||||
|
|
||||||
test('the tenant used by the job doesnt change when the current tenant changes', function () {
|
test('the tenant used by the job doesnt change when the current tenant changes', function () {
|
||||||
|
withTenantDatabases();
|
||||||
|
|
||||||
$tenant1 = Tenant::create([
|
$tenant1 = Tenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
]);
|
]);
|
||||||
|
|
@ -217,13 +223,6 @@ function withUsers()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function withTenantDatabases()
|
|
||||||
{
|
|
||||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
|
||||||
return $event->tenant;
|
|
||||||
})->toListener());
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestJob implements ShouldQueue
|
class TestJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
|
||||||
|
|
@ -832,7 +832,7 @@ function migrateUsersTableForTenants(): void
|
||||||
// Tenant model used for resource syncing setup
|
// Tenant model used for resource syncing setup
|
||||||
class ResourceTenant extends Tenant
|
class ResourceTenant extends Tenant
|
||||||
{
|
{
|
||||||
public function users()
|
public function users(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id')
|
return $this->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id')
|
||||||
->using(TenantPivot::class);
|
->using(TenantPivot::class);
|
||||||
|
|
|
||||||
398
tests/ResourceSyncingUsingPolymorphicTest.php
Normal file
398
tests/ResourceSyncingUsingPolymorphicTest.php
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Contracts\Syncable;
|
||||||
|
use Stancl\Tenancy\Contracts\SyncMaster;
|
||||||
|
use Stancl\Tenancy\Database\Concerns\CentralConnection;
|
||||||
|
use Stancl\Tenancy\Database\Concerns\ResourceSyncing;
|
||||||
|
use Stancl\Tenancy\Database\DatabaseConfig;
|
||||||
|
use Stancl\Tenancy\Database\Models\TenantMorphPivot;
|
||||||
|
use Stancl\Tenancy\Database\Models\TenantPivot;
|
||||||
|
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use Stancl\Tenancy\Listeners\UpdateSyncedResource;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [
|
||||||
|
DatabaseTenancyBootstrapper::class,
|
||||||
|
],
|
||||||
|
'tenancy.models.tenant' => ResourceTenantUsingPolymorphic::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener());
|
||||||
|
|
||||||
|
DatabaseConfig::generateDatabaseNamesUsing(function () {
|
||||||
|
return 'db' . Str::random(16);
|
||||||
|
});
|
||||||
|
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
|
||||||
|
UpdateSyncedResource::$shouldQueue = false; // Global state cleanup
|
||||||
|
Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class);
|
||||||
|
|
||||||
|
// Run migrations on central connection
|
||||||
|
pest()->artisan('migrate', [
|
||||||
|
'--path' => [
|
||||||
|
__DIR__ . '/../assets/resource-syncing-migrations',
|
||||||
|
__DIR__ . '/Etc/synced_resource_migrations/users',
|
||||||
|
__DIR__ . '/Etc/synced_resource_migrations/companies',
|
||||||
|
],
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resource syncing works using a single pivot table for multiple models when syncing from central to tenant', function () {
|
||||||
|
$tenant1 = ResourceTenantUsingPolymorphic::create(['id' => 't1']);
|
||||||
|
migrateUsersTableForTenants();
|
||||||
|
|
||||||
|
$centralUser = CentralUserUsingPolymorphic::create([
|
||||||
|
'global_id' => 'acme',
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@localhost',
|
||||||
|
'password' => 'password',
|
||||||
|
'role' => 'commenter',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant1->run(function () {
|
||||||
|
expect(TenantUserUsingPolymorphic::all())->toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$centralUser->tenants()->attach('t1');
|
||||||
|
|
||||||
|
// Assert `tenants` are accessible
|
||||||
|
expect($centralUser->tenants->pluck('id')->toArray())->toBe(['t1']);
|
||||||
|
|
||||||
|
// Users are accessible from tenant
|
||||||
|
expect($tenant1->users()->pluck('email')->toArray())->toBe(['john@localhost']);
|
||||||
|
|
||||||
|
// Assert User resource is synced
|
||||||
|
$tenant1->run(function () use ($centralUser) {
|
||||||
|
$tenantUser = TenantUserUsingPolymorphic::first()->toArray();
|
||||||
|
$centralUser = $centralUser->withoutRelations()->toArray();
|
||||||
|
unset($centralUser['id'], $tenantUser['id']);
|
||||||
|
|
||||||
|
expect($tenantUser)->toBe($centralUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
$tenant2 = ResourceTenantUsingPolymorphic::create(['id' => 't2']);
|
||||||
|
migrateCompaniesTableForTenants();
|
||||||
|
|
||||||
|
$centralCompany = CentralCompanyUsingPolymorphic::create([
|
||||||
|
'global_id' => 'acme',
|
||||||
|
'name' => 'ArchTech',
|
||||||
|
'email' => 'archtech@localhost',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant2->run(function () {
|
||||||
|
expect(TenantCompanyUsingPolymorphic::all())->toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$centralCompany->tenants()->attach('t2');
|
||||||
|
|
||||||
|
// Assert `tenants` are accessible
|
||||||
|
expect($centralCompany->tenants->pluck('id')->toArray())->toBe(['t2']);
|
||||||
|
|
||||||
|
// Companies are accessible from tenant
|
||||||
|
expect($tenant2->companies()->pluck('email')->toArray())->toBe(['archtech@localhost']);
|
||||||
|
|
||||||
|
// Assert Company resource is synced
|
||||||
|
$tenant2->run(function () use ($centralCompany) {
|
||||||
|
$tenantCompany = TenantCompanyUsingPolymorphic::first()->toArray();
|
||||||
|
$centralCompany = $centralCompany->withoutRelations()->toArray();
|
||||||
|
|
||||||
|
unset($centralCompany['id'], $tenantCompany['id']);
|
||||||
|
|
||||||
|
expect($tenantCompany)->toBe($centralCompany);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resource syncing works using a single pivot table for multiple models when syncing from tenant to central', function () {
|
||||||
|
$tenant1 = ResourceTenantUsingPolymorphic::create(['id' => 't1']);
|
||||||
|
migrateUsersTableForTenants();
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
$tenantUser = TenantUserUsingPolymorphic::create([
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@localhost',
|
||||||
|
'password' => 'password',
|
||||||
|
'role' => 'commenter',
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// Assert User resource is synced
|
||||||
|
$centralUser = CentralUserUsingPolymorphic::first();
|
||||||
|
|
||||||
|
// Assert `tenants` are accessible
|
||||||
|
expect($centralUser->tenants->pluck('id')->toArray())->toBe(['t1']);
|
||||||
|
|
||||||
|
// Users are accessible from tenant
|
||||||
|
expect($tenant1->users()->pluck('email')->toArray())->toBe(['john@localhost']);
|
||||||
|
|
||||||
|
$centralUser = $centralUser->withoutRelations()->toArray();
|
||||||
|
$tenantUser = $tenantUser->toArray();
|
||||||
|
unset($centralUser['id'], $tenantUser['id']);
|
||||||
|
|
||||||
|
// array keys use a different order here
|
||||||
|
expect($tenantUser)->toEqualCanonicalizing($centralUser);
|
||||||
|
|
||||||
|
$tenant2 = ResourceTenantUsingPolymorphic::create(['id' => 't2']);
|
||||||
|
migrateCompaniesTableForTenants();
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
|
||||||
|
$tenantCompany = TenantCompanyUsingPolymorphic::create([
|
||||||
|
'global_id' => 'acme',
|
||||||
|
'name' => 'tenant comp',
|
||||||
|
'email' => 'company@localhost',
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// Assert Company resource is synced
|
||||||
|
$centralCompany = CentralCompanyUsingPolymorphic::first();
|
||||||
|
|
||||||
|
// Assert `tenants` are accessible
|
||||||
|
expect($centralCompany->tenants->pluck('id')->toArray())->toBe(['t2']);
|
||||||
|
|
||||||
|
// Companies are accessible from tenant
|
||||||
|
expect($tenant2->companies()->pluck('email')->toArray())->toBe(['company@localhost']);
|
||||||
|
|
||||||
|
$centralCompany = $centralCompany->withoutRelations()->toArray();
|
||||||
|
$tenantCompany = $tenantCompany->toArray();
|
||||||
|
unset($centralCompany['id'], $tenantCompany['id']);
|
||||||
|
|
||||||
|
expect($tenantCompany)->toBe($centralCompany);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('right resources are accessible from the tenant', function () {
|
||||||
|
$tenant1 = ResourceTenantUsingPolymorphic::create(['id' => 't1']);
|
||||||
|
$tenant2 = ResourceTenantUsingPolymorphic::create(['id' => 't2']);
|
||||||
|
migrateUsersTableForTenants();
|
||||||
|
|
||||||
|
$user1 = CentralUserUsingPolymorphic::create([
|
||||||
|
'global_id' => 'user1',
|
||||||
|
'name' => 'user1',
|
||||||
|
'email' => 'user1@localhost',
|
||||||
|
'password' => 'password',
|
||||||
|
'role' => 'commenter',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user2 = CentralUserUsingPolymorphic::create([
|
||||||
|
'global_id' => 'user2',
|
||||||
|
'name' => 'user2',
|
||||||
|
'email' => 'user2@localhost',
|
||||||
|
'password' => 'password',
|
||||||
|
'role' => 'commenter',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user3 = CentralUserUsingPolymorphic::create([
|
||||||
|
'global_id' => 'user3',
|
||||||
|
'name' => 'user3',
|
||||||
|
'email' => 'user3@localhost',
|
||||||
|
'password' => 'password',
|
||||||
|
'role' => 'commenter',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user1->tenants()->attach('t1');
|
||||||
|
$user2->tenants()->attach('t1');
|
||||||
|
$user3->tenants()->attach('t2');
|
||||||
|
|
||||||
|
expect($tenant1->users()->pluck('email')->toArray())->toBe([$user1->email, $user2->email]);
|
||||||
|
expect($tenant2->users()->pluck('email')->toArray())->toBe([$user3->email]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function migrateCompaniesTableForTenants(): void
|
||||||
|
{
|
||||||
|
pest()->artisan('tenants:migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/synced_resource_migrations/companies',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenant model used for resource syncing setup
|
||||||
|
class ResourceTenantUsingPolymorphic extends Tenant
|
||||||
|
{
|
||||||
|
public function users(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(CentralUserUsingPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
|
||||||
|
->using(TenantMorphPivot::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function companies(): MorphToMany
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(CentralCompanyUsingPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
|
||||||
|
->using(TenantMorphPivot::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CentralUserUsingPolymorphic extends Model implements SyncMaster
|
||||||
|
{
|
||||||
|
use ResourceSyncing, CentralConnection;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
public $table = 'users';
|
||||||
|
|
||||||
|
public function getTenantModelName(): string
|
||||||
|
{
|
||||||
|
return TenantUserUsingPolymorphic::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKey(): string|int
|
||||||
|
{
|
||||||
|
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKeyName(): string
|
||||||
|
{
|
||||||
|
return 'global_id';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCentralModelName(): string
|
||||||
|
{
|
||||||
|
return static::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSyncedAttributeNames(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'global_id',
|
||||||
|
'name',
|
||||||
|
'password',
|
||||||
|
'email',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TenantUserUsingPolymorphic extends Model implements Syncable
|
||||||
|
{
|
||||||
|
use ResourceSyncing;
|
||||||
|
|
||||||
|
protected $table = 'users';
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKey(): string|int
|
||||||
|
{
|
||||||
|
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKeyName(): string
|
||||||
|
{
|
||||||
|
return 'global_id';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCentralModelName(): string
|
||||||
|
{
|
||||||
|
return CentralUserUsingPolymorphic::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSyncedAttributeNames(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'global_id',
|
||||||
|
'name',
|
||||||
|
'password',
|
||||||
|
'email',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CentralCompanyUsingPolymorphic extends Model implements SyncMaster
|
||||||
|
{
|
||||||
|
use ResourceSyncing, CentralConnection;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
public $table = 'companies';
|
||||||
|
|
||||||
|
public function getTenantModelName(): string
|
||||||
|
{
|
||||||
|
return TenantCompanyUsingPolymorphic::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKey(): string|int
|
||||||
|
{
|
||||||
|
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKeyName(): string
|
||||||
|
{
|
||||||
|
return 'global_id';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCentralModelName(): string
|
||||||
|
{
|
||||||
|
return static::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSyncedAttributeNames(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'global_id',
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TenantCompanyUsingPolymorphic extends Model implements Syncable
|
||||||
|
{
|
||||||
|
use ResourceSyncing;
|
||||||
|
|
||||||
|
protected $table = 'companies';
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKey(): string|int
|
||||||
|
{
|
||||||
|
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGlobalIdentifierKeyName(): string
|
||||||
|
{
|
||||||
|
return 'global_id';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCentralModelName(): string
|
||||||
|
{
|
||||||
|
return CentralCompanyUsingPolymorphic::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSyncedAttributeNames(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'global_id',
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -390,6 +390,81 @@ test('path used by sqlite manager can be customized', function () {
|
||||||
expect(file_exists($customPath . '/' . $name))->toBeTrue();
|
expect(file_exists($customPath . '/' . $name))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the tenant connection template can be specified either by name or as a connection array', function () {
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener());
|
||||||
|
|
||||||
|
config([
|
||||||
|
'tenancy.database.managers.mysql' => MySQLDatabaseManager::class,
|
||||||
|
'tenancy.database.template_tenant_connection' => 'mysql',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$name = 'foo' . Str::random(8);
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenancy_db_name' => $name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var MySQLDatabaseManager $manager */
|
||||||
|
$manager = $tenant->database()->manager();
|
||||||
|
expect($manager->databaseExists($name))->toBeTrue();
|
||||||
|
expect($manager->database()->getConfig('host'))->toBe('mysql');
|
||||||
|
|
||||||
|
config([
|
||||||
|
'tenancy.database.template_tenant_connection' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => null,
|
||||||
|
'host' => 'mysql2',
|
||||||
|
'port' => '3306',
|
||||||
|
'database' => 'main',
|
||||||
|
'username' => 'root',
|
||||||
|
'password' => 'password',
|
||||||
|
'unix_socket' => '',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => [],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenancy_db_name' => $name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var MySQLDatabaseManager $manager */
|
||||||
|
$manager = $tenant->database()->manager();
|
||||||
|
expect($manager->databaseExists($name))->toBeTrue(); // tenant connection works
|
||||||
|
expect($manager->database()->getConfig('host'))->toBe('mysql2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('partial tenant connection templates get merged into the central connection template', function () {
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener());
|
||||||
|
|
||||||
|
config([
|
||||||
|
'database.connections.central.url' => 'example.com',
|
||||||
|
'tenancy.database.template_tenant_connection' => [
|
||||||
|
'url' => null,
|
||||||
|
'host' => 'mysql2',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$name = 'foo' . Str::random(8);
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenancy_db_name' => $name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var MySQLDatabaseManager $manager */
|
||||||
|
$manager = $tenant->database()->manager();
|
||||||
|
expect($manager->databaseExists($name))->toBeTrue(); // tenant connection works
|
||||||
|
expect($manager->database()->getConfig('host'))->toBe('mysql2');
|
||||||
|
expect($manager->database()->getConfig('url'))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
// Datasets
|
// Datasets
|
||||||
dataset('database_managers', [
|
dataset('database_managers', [
|
||||||
['mysql', MySQLDatabaseManager::class],
|
['mysql', MySQLDatabaseManager::class],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue