1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 18:34:04 +00:00
This commit is contained in:
Abrar Ahmad 2022-11-08 14:49:01 +05:00
parent b7a6953231
commit 8c81ef2a8d
6 changed files with 428 additions and 7 deletions

View file

@ -14,7 +14,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
*/ */
interface SyncMaster extends Syncable interface SyncMaster extends Syncable
{ {
public function tenants(): BelongsToMany; //public function tenants(): BelongsToMany;
public function getTenantModelName(): string; public function getTenantModelName(): string;
} }

View file

@ -4,10 +4,11 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Models; namespace Stancl\Tenancy\Database\Models;
use Illuminate\Database\Eloquent\Relations\MorphPivot;
use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Pivot;
use Stancl\Tenancy\Contracts\Syncable; use Stancl\Tenancy\Contracts\Syncable;
class TenantPivot extends Pivot class TenantPivot extends MorphPivot
{ {
public static function booted(): void public static function booted(): void
{ {

View file

@ -46,10 +46,10 @@ class UpdateSyncedResource extends QueueableListener
// Since this model is "dirty" (taken by reference from the event), it might have the tenants // Since this model is "dirty" (taken by reference from the event), it might have the tenants
// relationship already loaded and cached. For this reason, we refresh the relationship. // relationship already loaded and cached. For this reason, we refresh the relationship.
$centralModel->load('tenants'); $centralModel->load('resources');
/** @var TenantCollection $tenants */ /** @var TenantCollection $tenants */
$tenants = $centralModel->tenants; $tenants = $centralModel->resources;
return $tenants; return $tenants;
} }
@ -80,7 +80,7 @@ class UpdateSyncedResource extends QueueableListener
return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey()); return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey());
}; };
$mappingExists = $centralModel->tenants->contains($currentTenantMapping); $mappingExists = $centralModel->resources->contains($currentTenantMapping);
if (! $mappingExists) { if (! $mappingExists) {
// Here we should call TenantPivot, but we call general Pivot, so that this works // Here we should call TenantPivot, but we call general Pivot, so that this works
@ -89,12 +89,12 @@ class UpdateSyncedResource extends QueueableListener
/** @var Tenant */ /** @var Tenant */
$tenant = $event->tenant; $tenant = $event->tenant;
$centralModel->tenants()->attach($tenant->getTenantKey()); $centralModel->resources()->attach($tenant->getTenantKey());
}); });
} }
/** @var TenantCollection $tenants */ /** @var TenantCollection $tenants */
$tenants = $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) { $tenants = $centralModel->resources->filter(function ($model) use ($currentTenantMapping) {
// Remove the mapping for the current tenant. // Remove the mapping for the current tenant.
return ! $currentTenantMapping($model); return ! $currentTenantMapping($model);
}); });

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTenantResourcesTable extends Migration
{
public function up()
{
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()
{
Schema::dropIfExists('tenant_resources');
}
}

View file

@ -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');
}
}

View file

@ -0,0 +1,365 @@
<?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\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,
]]);
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__ . '/Etc/synced_resource_migrations',
__DIR__ . '/Etc/synced_resource_migrations/users',
],
'--realpath' => true,
])->assertExitCode(0);
});
test('polymorphic relationship works for every model when syncing resources from central to tenant', function (){
$tenant1 = ResourceTenantForPolymorphic::create(['id' => 't1']);
migrateUsersTableForPloymorphicTenants();
// Assert User resource is synced
$centralUser = CentralUserForPolymorphic::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
$tenant1->run(function () {
expect(ResourceUserForPolymorphic::all())->toHaveCount(0);
});
// When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model
$centralUser->resources()->attach('t1');
expect($centralUser->getSyncedCreationAttributes())->toBeNull();
$tenant1->run(function () use ($centralUser) {
$resourceUser = ResourceUserForPolymorphic::first();
expect($resourceUser)->not()->toBeNull();
$resourceUser = $resourceUser->toArray();
$centralUser = $centralUser->withoutRelations()->toArray();
// remove id from comparison, because we don't copy id and let target model handle it
unset($resourceUser['id']);
unset($centralUser['id']);
expect($resourceUser)->toBe($centralUser);
});
$tenant2 = ResourceTenantForPolymorphic::create(['id' => 't2']);
migrateCompaniesTableForTenants();
// Assert Company resource is synced
$centralCompany = CentralCompanyForPolymorphic::create([
'global_id' => 'acme',
'name' => 'ArchTech',
'email' => 'archtech@localhost',
]);
$tenant2->run(function () {
expect(ResourceCompanyForPolymorphic::all())->toHaveCount(0);
});
// When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model
$centralCompany->resources()->attach('t2');
expect($centralCompany->getSyncedCreationAttributes())->toBeNull();
$tenant2->run(function () use ($centralCompany) {
$resourceCompany = ResourceCompanyForPolymorphic::first();
expect($resourceCompany)->not()->toBeNull();
$resourceCompany = $resourceCompany->toArray();
$centralCompany = $centralCompany->withoutRelations()->toArray();
// remove id from comparison, because we don't copy id and let target model handle it
unset($resourceCompany['id']);
unset($centralCompany['id']);
expect($resourceCompany)->toBe($centralCompany);
});
});
test('polymorphic relationship works for multiple models when syncing resources from tenant to central', function () {
$tenant1 = ResourceTenantForPolymorphic::create(['id' => 't1']);
migrateUsersTableForPloymorphicTenants();
tenancy()->initialize($tenant1);
// Assert User resource is synced
$resourceUser = ResourceUserForPolymorphic::create([
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
tenancy()->end();
$centralUser = CentralUserForPolymorphic::first()->only(['name', 'email', 'password', 'role']);
$resourceUser = $resourceUser->only(['name', 'email', 'password', 'role']);
expect($resourceUser)->toBe($centralUser);
// Assert Company resource is synced
$tenant2 = ResourceTenantForPolymorphic::create(['id' => 't2']);
migrateCompaniesTableForTenants();
tenancy()->initialize($tenant2);
// Assert User resource is synced
$resourceCompany = ResourceCompanyForPolymorphic::create([
'global_id' => 'acme',
'name' => 'tenant comp',
'email' => 'company@localhost',
]);
tenancy()->end();
$centralCompany = CentralCompanyForPolymorphic::first()->only(['name', 'email']);
$resourceCompany = $resourceCompany->only(['name', 'email']);
expect($resourceCompany)->toBe($centralCompany);
});
function migrateUsersTableForPloymorphicTenants(): void
{
pest()->artisan('tenants:migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users',
'--realpath' => true,
])->assertExitCode(0);
}
function migrateCompaniesTableForTenants(): void
{
// Run migrations on central connection
pest()->artisan('migrate', [
'--path' => [
__DIR__ . '/Etc/synced_resource_migrations/companies',
],
'--realpath' => true,
])->assertExitCode(0);
pest()->artisan('tenants:migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/companies',
'--realpath' => true,
])->assertExitCode(0);
}
class ResourceTenantForPolymorphic extends Tenant
{
public function users()
{
return $this->morphedByMany(CentralUserForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
->using(TenantPivot::class);
}
public function companies()
{
return $this->morphedByMany(CentralCompanyForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
->using(TenantPivot::class);
}
}
class CentralUserForPolymorphic extends Model implements SyncMaster
{
use ResourceSyncing, CentralConnection;
protected $guarded = [];
public $timestamps = false;
public $table = 'users';
public function resources(): MorphToMany
{
return $this->morphToMany(ResourceTenantForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', 'global_id')
->using(TenantPivot::class);
}
public function getTenantModelName(): string
{
return ResourceUserForPolymorphic::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 ResourceUserForPolymorphic 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 CentralUserForPolymorphic::class;
}
public function getSyncedAttributeNames(): array
{
return [
'global_id',
'name',
'password',
'email',
];
}
}
class CentralCompanyForPolymorphic extends Model implements SyncMaster
{
use ResourceSyncing, CentralConnection;
protected $guarded = [];
public $timestamps = false;
public $table = 'companies';
public function resources(): MorphToMany
{
return $this->morphToMany(ResourceTenantForPolymorphic::class, 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', 'global_id')
->using(TenantPivot::class);
}
public function getTenantModelName(): string
{
return ResourceCompanyForPolymorphic::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 ResourceCompanyForPolymorphic 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 CentralCompanyForPolymorphic::class;
}
public function getSyncedAttributeNames(): array
{
return [
'global_id',
'name',
'email',
];
}
}