1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 15:34:03 +00:00
tenancy/tests/ResourceSyncingTest.php
Samuel Štancl 6784685054
Resource syncing rework (#30)
* Add &Model to docblock

* Fix code style (php-cs-fixer)

* Only delete synced resource if the central resource shouldSync

* Add central resource detached event and listener

* Add SyncedTenant interface

* Use the event & listener in the test file

* Add getGlobalIdentifierKey(Name) to TenantMorphPivot

* Refactor TriggerSyncingEvents

* Fix code style (php-cs-fixer)

* Test queueing the detaching listener

* Move finding the central resource into the event, naming changes

* Fix code style (php-cs-fixer)

* Simplify listener code

* Refactor detaching logic

* Create tenant resource after attaching central to tenant, test queueing related listener

* Delete dd()

* Fix code style (php-cs-fixer)

* Move triggerAttachEvent from SyncMaster

* Update attach event-related code

* Move findResource from SyncedTenant to the pivot trait

* Add annotation

* Update annotation

* Simplify getAttributesForCreation in CreateTenantResourceFromSyncMaster

* Update naming

* Add tenant trait for attaching/detaching resources

* Update test names

* Move creation attribute parsing method to trait

* Rename variable

* Fix code style (php-cs-fixer)

* Delete complete to-do

* Delete event comment

* Rename event property

* Find tenant resource in detach listener

* Use global ID key of tenant resource in cascade deletes listener

* Use global ID key name of the central resource while creating/deleting tenant resources

* Add getSyncedCreationAttributes example in the annotation

* Fix inconsistencies in SyncedTenant methods

* Improve annotation

* Don't return the query in `$scopeGetModelQuery`

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

* Fix code style (php-cs-fixer)

* Update scoping getModel query

* Only use detach event instead of using both detach and delete events, refactor code

* Test that detaching tenant from a central resource doesn't affect other tenants

* Delete extra imports

* Fix code style (php-cs-fixer)

* Add PivotWithRelation, test attaching/detaching resources without polymorphic relations

* Refactor TriggerSyncingEvents to work with non-polymorphic relations too

* Fix code style (php-cs-fixer)

* Rename synced resource changed event, fix tests

* Enforce passing Tenant&Model to attach/detach events

* Prevent firing saved event automatically in CreateTenantResource

* Improve TriggerSyncingEvents trait

* Delete unused import

* Make TriggerSyncingEvents methods non-static, improve annotations

* Pass saved model to event

* Move attach/detach queueing tests to ResourceSyncingTest, pass models instead of IDs to attach/detach

* Move events to ResourceSyncing\Events

* Fix code style (php-cs-fixer)

* Use SerializesModels in queueable listeners instead of events

* Delete redundant $shouldQueue setting

* Rename listener, test cascade deletes from both sides of many-to-many

* Move creation attributes-related code to a separate test file, improve comments (wip)

* Improve comments, fix variable name capitalization

* Delete tracing comma

* Extract duplicate code into a trait

* Don't accept nullable tenant in SyncMasterDeleted

* Fix annotation

* Fix code style (php-cs-fixer)

* Update annotation

* Fix PHPStan error

* Fix annotation

* Update comments and test naming

* Move triggerDeleteEvent to CascadeDeletes interface

* Rename test file

* Import TenantPivot in Tenant class (tests/Etc)

* Add central resource not available in pivot exception

* Rename SaveSyncedResource to UpdateOrCreateSyncedResource

* Add new events and listeners to TSP stub

* Improve comments and naming

* Only keep SerializesModels in classes that utilize it

* Use tenant->run()

* Import events in stub

* Move RS listeners to separate namespace, use `Event/`Listener/` in stub for consistency

* Fix code style (php-cs-fixer)

* Fix namespace changes

* Use cursor instead of get

* Update src/ResourceSyncing/ParsesCreationAttributes.php

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

* Update naming, structure (discussed on Discord)

* Update uses in in test file

* remove double ;;

* Add comments

* Test if static properties work fine with queueable listeners

* Update $shouldQuery test

* Update creation attributes

* Work on updating the tests

* Make synced attributes configurable using static properties

* Update resource syncing tests

* Get rid of mixed attribute classes

* Get rid of TenantUserWIthCreationAttributes

* Fix imports

* Get rid of the conditionally synced classes, improve tests

* Simplify resource creation tests (only test the more complex cases instead of each case - if the complex case works, the simpler cases work too)

* Clean up ResourceSyncingTest (mostly duplicate tests that were already in AutomaticResourceCreationTest)

* Simplify class naming in polymorhpic tests

* Move automatic resource creation tests to ResourceSyncingTest

* Test that the sync event doesn't get triggered excessively

* Only trigger the sync event if the synced attributes were changed or if the resource was recently created

* Update synced attribute instead of unsynced in test

* Fix sync event test

* Update static property redefining test

* Use getGlobalIdentifierKeyName() instead of hardcoding the key name

* Delete static properties from the ResourceSyncing trait

* Reuse user classes in polymorphic tests

* Update tests/ResourceSyncingTest.php

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

* Use the default tenants() method in central user, override the default in ResourceSyncingTest

* Use BelongsToMany as tenants() return type

* Fix code style (php-cs-fixer)

* Delete extra static property from trait

* Delete duplicate events/listeners from TSP stub

* Delete weird expectation,  use $model->trashed()

* Change ResourceUser to TenantUser

* Add defaults for getGlobalIdentifierKey(Name)

* Use singular tenant in DeleteResourceInTenants name

* Rename getSyncedCreationAttributes to getCreationAttributes

* Fix comma position in comment

* minor fixes in traits and interfaces

* Fix code style (php-cs-fixer)

* Correct comment

* Use $tenant->run()

* Update scopeGetModelQuery annotation

* Use static property for testing shouldSync

* Improve test

* Get rid of datasets

* Add trashed assertions

* Always merge synced attributes with the creation attributes during parsing

* Update creation attributes in test's beforeEach

* Use only the necessary creation attributes (no need to include the synced attributes because they get merged automatically)

* Rename ResourceTenant to MorphTenant

* Add TriggerSyncingEvents docblock

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

* Add force deletes test

* Fix code style (php-cs-fixer)

* Delete pivot if it can't access the resource class

* Make parseCreationAttributes more readable

* Comment out setting $scopeGetModelQuery in the stub

* Add @var annotations to bootTriggerSyncingEvents

* Fix attach()/detach() exception test

* Interrupt creation of broken pivots instead of deleting the pivots at a later point

* Add more comments

* Update CreateTenantResource comment

* Assert that forceDelete() doesn't affect other tenant resources

* Rename test

* Correct with() array formatting

* Expand test with soft deletes

* Merge SyncedResourceSaved tests

* Improve naming, comments and minor details in the assertions

* Move test

* Fix failing test

* Delete duplicate test

* Minor test improvement

* Delete duplicate test

* Improve old test

* Minor test improvement

* Improve event test

* Improve tests (naming, code, comments)

* Delete extra test, add comments to the larger test

* Refactor central -> tenant attach() test

* Apply changes from central -> tenant attach() test on tenant -> central test

* Fix assertions in central -> tenant

* Correct comment and assertion

* Refactor tenant -> central attach() test

* Fix inconsistency

* Delete unused import

* Add comments

* Update polymorphic test names

* Rename polymorphic tests

* Update listener test name

* Delete redundant tenant ID assignments

* Improve test names

* Move polymorphic tests to ResourceSyncingTest

* Mention alternative solutions in CentralResourceNotAvailableInPivotException

* Add comments

* Update test comments

* minor changes to tests + review comments

* Delete extra tests, update comments

* Remove unneeded part of test

* Fix comment

* Improve comments

* Add test for companies() realationship accessibility

* Update test name

* Complete to-do, add comment

* Improve naming and comments (resolve some priority reviews)

* Move test

* Comment, resolve to-dos

* Add low-level pivot assertions

* Restore trashed resources if the central resource got restored, try improving tests

* Fix code style (php-cs-fixer)

* Dekete redundnat unsynced comments

* Add to-do, test WIP

* Fix restoring logic

* Update todo

* Add todo to fix phpdoc

* Fix code style (php-cs-fixer)

* PHPStan error fix wip

* Fix PHPStan error

* Add regression test

* Delete unused trait

* Add and test restoring WIP

* Fix code style (php-cs-fixer)

* Add to-do

* Delete comment from test

* Focus on restoring in the restore test

* Improve maming

* Fix stub

* Delete redundant part of test

* Delete incorrect test leftover

* Add triggerRestoredEvent

* Fix restore test

* Correct tests and restore(() logic

* Fix code style (php-cs-fixer)

* Check if SoftDeletes are used before firing SyncMasterRestored

* Fix comment

* Revert restore action changes (phpstan errors)

* Delete CascadeDeletes interface

* Remove CascadeDeletes from most of the tests

* Fix code style (php-cs-fixer)

* Rename tests

* Fix restoring + tests WIP

* Fix restoring

* Fix restoring tests

* Fix code style (php-cs-fixer)

* Test that detaching force deletes the tenant resources

* Implement cacscade force deleting

* Delete redundant changes

* Fix typo

* Fix SyncMaster

* Improve test

* Add force deleting logic back and fix tests

* Improve comment

* Delete extra assertion

* Improve restoring test

* Simplify assertion

* Delete redundant query scoping from test

* Test restore listener queueing

* use strict in_array() checks

* fix phpstan errors

---------

Co-authored-by: lukinovec <lukinovec@gmail.com>
Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
2024-02-10 19:08:37 +01:00

1313 lines
42 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Stancl\JobPipeline\JobPipeline;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Events\TenantCreated;
use Illuminate\Events\CallQueuedListener;
use Stancl\Tenancy\Database\DatabaseConfig;
use Stancl\Tenancy\ResourceSyncing\Syncable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Stancl\Tenancy\ResourceSyncing\ResourceSyncing;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\ResourceSyncing\TenantMorphPivot;
use Stancl\Tenancy\Tests\Etc\ResourceSyncing\Tenant;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Stancl\Tenancy\Database\Concerns\CentralConnection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
use Stancl\Tenancy\ResourceSyncing\TenantPivot as BasePivot;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
use Stancl\Tenancy\ResourceSyncing\ModelNotSyncMasterException;
use Stancl\Tenancy\ResourceSyncing\Listeners\CreateTenantResource;
use Stancl\Tenancy\ResourceSyncing\Listeners\DeleteResourceInTenant;
use Stancl\Tenancy\ResourceSyncing\Listeners\DeleteResourcesInTenants;
use Stancl\Tenancy\ResourceSyncing\Listeners\RestoreResourcesInTenants;
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceAttachedToTenant;
use Stancl\Tenancy\ResourceSyncing\Listeners\UpdateOrCreateSyncedResource;
use Stancl\Tenancy\Tests\Etc\ResourceSyncing\TenantUser as BaseTenantUser;
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
use Stancl\Tenancy\Tests\Etc\ResourceSyncing\CentralUser as BaseCentralUser;
use Stancl\Tenancy\ResourceSyncing\CentralResourceNotAvailableInPivotException;
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase;
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);
// Global state cleanup
UpdateOrCreateSyncedResource::$shouldQueue = false;
DeleteResourcesInTenants::$shouldQueue = false;
CreateTenantResource::$shouldQueue = false;
DeleteResourceInTenant::$shouldQueue = false;
UpdateOrCreateSyncedResource::$scopeGetModelQuery = null;
$syncedAttributes = [
'global_id',
'name',
'password',
'email',
];
TenantUser::$shouldSync = true;
CentralUser::$shouldSync = true;
TenantUser::$syncedAttributes = $syncedAttributes;
CentralUser::$syncedAttributes = $syncedAttributes;
$creationAttributes = ['role' => 'commenter'];
TenantUser::$creationAttributes = $creationAttributes;
CentralUser::$creationAttributes = $creationAttributes;
Event::listen(SyncedResourceSaved::class, UpdateOrCreateSyncedResource::class);
Event::listen(SyncMasterDeleted::class, DeleteResourcesInTenants::class);
Event::listen(SyncMasterRestored::class, RestoreResourcesInTenants::class);
Event::listen(CentralResourceAttachedToTenant::class, CreateTenantResource::class);
Event::listen(CentralResourceDetachedFromTenant::class, DeleteResourceInTenant::class);
// Run migrations on central connection
pest()->artisan('migrate', [
'--path' => [
__DIR__ . '/../assets/resource-syncing-migrations',
__DIR__ . '/Etc/synced_resource_migrations',
__DIR__ . '/Etc/synced_resource_migrations/users',
__DIR__ . '/Etc/synced_resource_migrations/companies',
],
'--realpath' => true,
])->assertExitCode(0);
});
afterEach(function () {
UpdateOrCreateSyncedResource::$scopeGetModelQuery = null;
});
test('SyncedResourceSaved event gets triggered when resource gets created or when its synced attributes get updated', function () {
Event::fake(SyncedResourceSaved::class);
// Create resource
$user = TenantUser::create([
'name' => 'Foo',
'email' => 'foo@email.com',
'password' => 'secret',
'global_id' => 'foo',
'role' => 'foo',
]);
Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) {
return $event->model === $user;
});
// Flush
Event::fake(SyncedResourceSaved::class);
// Update resource's synced attribute
$user->update(['name' => 'Bar']);
Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) {
return $event->model === $user;
});
// Flush
Event::fake(SyncedResourceSaved::class);
// Refetch the model to reset $user->wasRecentlyCreated
$user = TenantUser::firstWhere('name', 'Bar');
// Update resource's unsynced attribute
$user->update(['role' => 'bar']);
Event::assertNotDispatched(SyncedResourceSaved::class); // regression test for #1168
});
test('only synced columns get updated by the syncing logic', function () {
// Create user in central DB
$user = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'superadmin', // Unsynced
]);
$tenant = Tenant::create();
migrateUsersTableForTenants();
tenancy()->initialize($tenant);
// Create the same user in tenant DB
$user = TenantUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // Unsynced
]);
// Update user in tenant DB
$user->update([
'name' => 'John Foo', // Synced
'email' => 'john@foreignhost', // Synced
'role' => 'admin', // Unsynced
]);
tenancy()->end();
// Assert changes bubbled up
pest()->assertEquals([
'id' => 1,
'global_id' => 'acme',
'name' => 'John Foo', // Synced
'email' => 'john@foreignhost', // Synced
'password' => 'secret',
'role' => 'superadmin', // Unsynced
], TenantUser::first()->getAttributes());
});
test('updating tenant resources from central context throws an exception', function () {
$tenant = Tenant::create();
migrateUsersTableForTenants();
tenancy()->initialize($tenant);
TenantUser::create([
'global_id' => 'foo',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
]);
tenancy()->end();
pest()->expectException(ModelNotSyncMasterException::class);
TenantUser::first()->update(['password' => 'foobar']);
});
test('attaching central resources to tenants or vice versa creates synced tenant resource', function () {
$createCentralUser = fn () => CentralUser::create([
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // Unsynced
]);
$tenant = Tenant::create();
migrateUsersTableForTenants();
$tenant->run(function () {
expect(TenantUser::all())->toHaveCount(0);
});
// Attaching resources to tenants requires using a pivot that implements the PivotWithRelation interface
$tenant->customPivotUsers()->attach($createCentralUser());
$createCentralUser()->tenants()->attach($tenant);
$tenant->run(function () {
// two (separate) central users were created, so there are now two separate tenant users in the tenant's database
expect(TenantUser::all())->toHaveCount(2);
});
});
test('detaching central users from tenants or vice versa force deletes the synced tenant resource', function (bool $attachUserToTenant) {
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // Unsynced
]);
$tenant = Tenant::create();
migrateUsersTableForTenants();
if ($attachUserToTenant) {
// Attaching resources to tenants requires using a pivot that implements the PivotWithRelation interface
$tenant->customPivotUsers()->attach($centralUser);
} else {
$centralUser->tenants()->attach($tenant);
}
$tenant->run(function () {
expect(TenantUser::all())->toHaveCount(1);
});
if ($attachUserToTenant) {
// Detaching resources from tenants requires using a pivot that implements the PivotWithRelation interface
$tenant->customPivotUsers()->detach($centralUser);
} else {
$centralUser->tenants()->detach($tenant);
}
$tenant->run(function () {
expect(TenantUser::all())->toHaveCount(0);
});
addExtraColumns(true);
// Detaching *force deletes* the tenant resource
CentralUserWithSoftDeletes::$creationAttributes = ['role' => 'commenter', 'foo' => 'bar'];
$centralUserWithSoftDeletes = CentralUserWithSoftDeletes::create([
'global_id' => 'bar',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // Unsynced
'foo' => 'bar',
]);
if ($attachUserToTenant) {
$tenant->customPivotUsers()->attach($centralUserWithSoftDeletes);
} else {
$centralUserWithSoftDeletes->tenants()->attach($tenant);
}
$tenant->run(function () {
expect(TenantUserWithSoftDeletes::withTrashed()->count())->toBe(1);
});
if ($attachUserToTenant) {
// Detaching resources from tenants requires using a pivot that implements the PivotWithRelation interface
$tenant->customPivotUsers()->detach($centralUserWithSoftDeletes);
} else {
$centralUserWithSoftDeletes->tenants()->detach($tenant);
}
$tenant->run(function () {
expect(TenantUserWithSoftDeletes::withTrashed()->count())->toBe(0);
});
})->with([
true,
false,
]);
test('attaching tenant to central resource works correctly even when using a single pivot table for multiple models', function () {
config(['tenancy.models.tenant' => MorphTenant::class]);
$tenant1 = MorphTenant::create();
$tenant2 = MorphTenant::create();
migrateUsersTableForTenants();
migrateCompaniesTableForTenants();
// Use BaseCentralUser and BaseTenantUser in this test
// These models use the polymorphic relationship for tenants(), which is the default
$centralUser = BaseCentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
$tenant1->run(function () {
expect(BaseTenantUser::count())->toBe(0);
});
// Create tenant resource from central resource
// By attaching a tenant to the central resource
$centralUser->tenants()->attach($tenant1);
// Central users are accessible from the tenant using the relationship method
expect($tenant1->users()->count())->toBe(1);
// Tenants are accessible from the central resource using the relationship method
expect($centralUser->tenants()->count())->toBe(1);
// The tenant resource got created with the correct attributes
$tenant1->run(function () use ($centralUser) {
$tenantUser = BaseTenantUser::first()->getAttributes();
$centralUser = $centralUser->getAttributes();
expect($tenantUser)->toEqualCanonicalizing($centralUser);
});
// Test that the company resource can use the same pivot as the user resource
$centralCompany = CentralCompany::create([
'global_id' => 'acme',
'name' => 'ArchTech',
'email' => 'archtech@localhost',
]);
// Central company wasn't attached yet
$tenant2->run(function () {
expect(TenantCompany::count())->toBe(0);
});
$centralCompany->tenants()->attach($tenant2);
// Tenant company got created during the attaching
$tenant2->run(function () {
expect(TenantCompany::count())->toBe(1);
});
// The tenant companies are accessible from tenant using the relationship method
expect($tenant2->companies()->count())->toBe(1);
// Tenants are accessible from the central resource using the relationship method
expect($centralCompany->tenants()->count())->toBe(1);
// The TenantCompany resource got created with the correct attributes
$tenant2->run(function () use ($centralCompany) {
$tenantCompany = TenantCompany::first()->getAttributes();
$centralCompany = $centralCompany->getAttributes();
expect($tenantCompany)->toEqualCanonicalizing($centralCompany);
});
// Detaching tenant from a central resource deletes the resource of that tenant
$centralUser->tenants()->detach($tenant1);
$tenant1->run(function () {
expect(BaseTenantUser::count())->toBe(0);
});
$centralUser->tenants()->attach($tenant2);
// Detaching tenant from a central resource doesn't affect resources of other tenants
$tenant2->run(function () {
expect(BaseTenantUser::count())->toBe(1);
});
$centralCompany->tenants()->detach($tenant2);
$tenant1->run(function () {
expect(TenantCompany::count())->toBe(0);
});
});
test('attaching central resource to tenant works correctly even when using a single pivot table for multiple models', function () {
config(['tenancy.models.tenant' => MorphTenant::class]);
$tenant1 = MorphTenant::create();
migrateUsersTableForTenants();
// Use BaseCentralUser and BaseTenantUser in this test
// These models use the polymorphic relationship for tenants(), which is the default
$centralUser = BaseCentralUser::create([
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
expect(DB::table('tenant_resources')->count())->toBe(0);
// Create tenant resource from central resource
// By attaching a central resource to the tenant
$tenant1->users()->attach($centralUser);
// The tenant resource got created
// And with the correct attributes
$tenantUser = $tenant1->run(function () {
return BaseTenantUser::first();
});
expect($tenantUser?->getAttributes())->toEqualCanonicalizing($centralUser->getAttributes());
// Tenants are accessible from the central resource using the relationship method
expect($centralUser->tenants()->count())->toBe(1);
expect(DB::table('tenant_resources')->count())->toBe(1);
// Central users are accessible from the tenant using the relationship method
expect($tenant1->users()->count())->toBe(1);
$tenant1->users()->detach($centralUser);
$tenant1->run(function () {
expect(BaseTenantUser::count())->toBe(0);
});
expect(DB::table('tenant_resources')->count())->toBe(0);
// Test that the company resource can use the same pivot as the user resource
$tenant2 = MorphTenant::create();
migrateCompaniesTableForTenants();
expect($tenant2->companies()->count())->toBe(0);
expect(DB::table('tenant_resources')->count())->toBe(0);
// Company resource uses the same pivot as the user resource
// Creating a tenant resource creates the central resource automatically if it doesn't exist
$tenantCompany = $tenant2->run(function () {
return TenantCompany::create([
'global_id' => 'acme',
'name' => 'tenant comp',
'email' => 'company@localhost',
]);
});
$centralCompany = CentralCompany::first();
// The central resource got created
// And with the correct attributes
expect($centralCompany?->getAttributes())->toEqualCanonicalizing($tenantCompany->getAttributes());
// The pivot table got created
expect(DB::table('tenant_resources')->count())->toBe(1);
// Tenants are accessible from the central resource using the relationship method
expect($centralCompany->tenants()->count())->toBe(1);
// Central companies are accessible from the tenant using the relationship method
expect($tenant2->companies()->count())->toBe(1);
// Detaching central resource from a tenant deletes the resource of that tenant
$tenant2->companies()->detach($centralCompany);
expect($tenant2->companies()->count())->toBe(0);
// Tenant resource got deleted
$tenant2->run(function () {
expect(TenantCompany::count())->toBe(0);
});
});
test('attaching or detaching users to or from tenant throws an exception when the pivot cannot access the central resource', function() {
$tenant = Tenant::create();
migrateUsersTableForTenants();
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
]);
expect(TenantPivot::count())->toBe(0);
expect(fn () => $tenant->users()->attach($centralUser))->toThrow(CentralResourceNotAvailableInPivotException::class);
expect(TenantPivot::count())->toBe(0);
$centralUser->tenants()->attach($tenant); // central->tenants() direction works
expect(TenantPivot::count())->toBe(1);
expect(fn () => $tenant->users()->detach($centralUser))->toThrow(CentralResourceNotAvailableInPivotException::class);
expect(TenantPivot::count())->toBe(1);
});
test('resources only get created in tenant databases they were attached to', function () {
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // Unsynced
]);
$t1 = Tenant::create();
$t2 = Tenant::create();
$t3 = Tenant::create();
migrateUsersTableForTenants();
$centralUser->tenants()->attach($t1);
$centralUser->tenants()->attach($t2);
// t3 is not attached
$t1->run(fn () => expect(TenantUser::count())->toBe(1)); // assert user exists
$t2->run(fn () => expect(TenantUser::count())->toBe(1)); // assert user exists
$t3->run(fn () => expect(TenantUser::count())->toBe(0)); // assert user does NOT exist
});
test('synced columns are updated in other tenant dbs where the resource exists', function () {
// Create central resource
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // Unsynced
]);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
$tenant3 = Tenant::create();
migrateUsersTableForTenants();
// Create tenant users by attaching tenants to the central user
$centralUser->tenants()->attach($tenant1);
$centralUser->tenants()->attach($tenant2);
$centralUser->tenants()->attach($tenant3);
// Update first tenant's resource
$tenant1->run(function () {
TenantUser::first()->update([
'name' => 'John 1',
'role' => 'employee', // Unsynced
]);
expect(TenantUser::first()->role)->toBe('employee');
});
// Check that the resources of the other tenants got updated too
tenancy()->runForMultiple([$tenant2, $tenant3], function () {
$user = TenantUser::first();
expect($user->name)->toBe('John 1');
expect($user->role)->toBe('commenter');
});
// Check that change bubbled up to central DB
expect(CentralUser::count())->toBe(1);
$centralUser = CentralUser::first();
expect($centralUser->name)->toBe('John 1'); // Synced
expect($centralUser->role)->toBe('commenter'); // Unsynced
// This works when the change comes from the central DB all tenant resources get updated
$centralUser->update(['name' => 'John 0']);
tenancy()->runForMultiple([$tenant1, $tenant2, $tenant3], function () {
expect(TenantUser::first()->name)->toBe('John 0');
});
});
test('the global id is generated using the id generator when the global id is not supplied when creating the resource', function () {
$user = CentralUser::create([
'name' => 'John Doe',
'email' => 'john@doe',
'password' => 'secret',
'role' => 'employee',
]);
pest()->assertNotNull($user->global_id);
});
test('the update or create listener can be queued', function () {
Queue::fake();
UpdateOrCreateSyncedResource::$shouldQueue = true;
$tenant = Tenant::create();
migrateUsersTableForTenants();
Queue::assertNothingPushed();
$tenant->run(function () {
TenantUser::create([
'name' => 'John Doe',
'email' => 'john@doe',
'password' => 'secret',
'role' => 'employee',
]);
});
Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
return $job->class === UpdateOrCreateSyncedResource::class;
});
UpdateOrCreateSyncedResource::$shouldQueue = false;
});
test('the cascade deletes and restore listeners can be queued', function () {
Queue::fake();
DeleteResourcesInTenants::$shouldQueue = true;
RestoreResourcesInTenants::$shouldQueue = true;
CentralUserWithSoftDeletes::$creationAttributes = [
'foo' => 'foo',
'role' => 'role',
];
$tenant = Tenant::create();
migrateUsersTableForTenants();
addExtraColumns(true);
Queue::assertNothingPushed();
$centralUser = fn () => CentralUserWithSoftDeletes::create([
'name' => 'John Doe',
'email' => 'john@doe',
'password' => 'secret',
'role' => 'employee',
'foo' => 'foo',
]);
[$user1, $user2] = [$centralUser(), $centralUser()];
$tenant->softDeletesUsers()->attach($user1); // Custom pivot method
$user2->tenants()->attach($tenant);
$user1->delete();
$user2->delete();
Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
return $job->class === DeleteResourcesInTenants::class;
});
$user1->restore();
$user2->restore();
Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
return $job->class === RestoreResourcesInTenants::class;
});
DeleteResourcesInTenants::$shouldQueue = false;
RestoreResourcesInTenants::$shouldQueue = false;
});
test('the attach and detach listeners can be queued', function () {
Queue::fake();
CreateTenantResource::$shouldQueue = true;
DeleteResourceInTenant::$shouldQueue = true;
$tenant = Tenant::create();
migrateUsersTableForTenants();
$centralUser = fn () => CentralUser::create([
'name' => 'John Doe',
'email' => 'john@doe',
'password' => 'secret',
'role' => 'employee',
]);
$user1 = $centralUser();
$user2 = $centralUser();
$tenant->customPivotUsers()->attach($user1);
$user2->tenants()->attach($tenant);
Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
return $job->class === CreateTenantResource::class;
});
$tenant->customPivotUsers()->detach($user1);
$user2->tenants()->detach($tenant);
Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
return $job->class === DeleteResourceInTenant::class;
});
CreateTenantResource::$shouldQueue = false;
DeleteResourceInTenant::$shouldQueue = false;
});
test('the SyncedResourceSavedInForeignDatabase event is fired for all touched resources', function () {
Event::fake([SyncedResourceSavedInForeignDatabase::class]);
// Create central resource
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter', // Unsynced
]);
$t1 = Tenant::create(['id' => 't1']);
$t2 = Tenant::create(['id' => 't2']);
$t3 = Tenant::create(['id' => 't3']);
migrateUsersTableForTenants();
// Create tenant resources by attaching tenants to the central user
foreach ([$t1, $t2, $t3] as $tenant) {
$centralUser->tenants()->attach($tenant);
Event::assertDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) use ($tenant) {
return $event->tenant->getTenantKey() === $tenant->getTenantKey();
});
}
// Event wasn't dispatched in the central app (No tenant present in the event)
Event::assertNotDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) {
return $event->tenant === null;
});
// Flush event log
Event::fake([SyncedResourceSavedInForeignDatabase::class]);
$t3->run(function () {
TenantUser::first()->update(['name' => 'John 3']);
});
Event::assertDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) {
return $event->tenant?->getTenantKey() === 't1';
});
Event::assertDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) {
return $event->tenant?->getTenantKey() === 't2';
});
// Event wasn't dispatched for t3
Event::assertNotDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) {
return $event->tenant?->getTenantKey() === 't3';
});
// Event wasn't dispatched in the central app (No tenant present in the event)
Event::assertDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) {
return is_null($event->tenant);
});
// Flush
Event::fake([SyncedResourceSavedInForeignDatabase::class]);
$centralUser->update([
'name' => 'John Central',
]);
foreach ([$t1, $t2, $t3] as $tenant) {
Event::assertDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) use ($tenant) {
return $event->tenant->getTenantKey() === $tenant->getTenantKey();
});
}
// Event wasn't dispatched in the central app
Event::assertNotDispatched(SyncedResourceSavedInForeignDatabase::class, function (SyncedResourceSavedInForeignDatabase $event) {
return is_null($event->tenant);
});
});
test('resources are synced only when the shouldSync method returns true', function (bool $enabled) {
TenantUser::$shouldSync = $enabled;
CentralUser::$shouldSync = $enabled;
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
migrateUsersTableForTenants();
tenancy()->initialize($tenant1);
TenantUser::create([
'global_id' => 'absd',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
tenancy()->end();
expect(CentralUser::all())->toHaveCount($enabled ? 1 : 0);
expect(CentralUser::whereGlobalId('absd')->exists())->toBe($enabled);
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
$centralUser->tenants()->attach($tenant2);
$tenant2->run(function () use ($enabled) {
expect(TenantUser::all())->toHaveCount($enabled ? 1 : 0);
expect(TenantUser::whereGlobalId('acme')->exists())->toBe($enabled);
});
})->with([
true,
false,
]);
test('deleting SyncMaster automatically deletes its Syncables', function () {
[$tenant1] = createTenantsAndRunMigrations();
$syncMaster = CentralUser::create([
'global_id' => 'cascade_user',
'name' => 'Central user',
'email' => 'central@localhost',
'password' => 'password',
'role' => 'cascade_user',
]);
$syncMaster->tenants()->attach($tenant1);
$syncMaster->delete();
tenancy()->initialize($tenant1);
expect(TenantUser::firstWhere('global_id', 'cascade_user'))->toBeNull(); // Delete cascaded
});
test('trashed resources are synced correctly', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
migrateUsersTableForTenants();
addExtraColumns(true);
// Include trashed resources in syncing queries
UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
if ($query->hasMacro('withTrashed')) {
$query->withTrashed();
}
};
$centralUser = CentralUserWithSoftDeletes::create([
'global_id' => 'user',
'name' => 'Central user',
'email' => 'central@localhost',
'password' => 'password',
'role' => 'commenter',
'foo' => 'foo',
]);
$centralUser->tenants()->attach($tenant1);
$centralUser->tenants()->attach($tenant2);
tenancy()->initialize($tenant1);
// Synced resources aren't soft deleted from other tenants
TenantUserWithSoftDeletes::first()->delete();
tenancy()->initialize($tenant2);
expect(TenantUserWithSoftDeletes::withTrashed()->first()->trashed())->toBeFalse();
tenancy()->end();
// Synced resources are soft deleted from tenants when the central resource gets deleted
expect(CentralUserWithSoftDeletes::first())->delete();
tenancy()->initialize($tenant2);
expect(TenantUserWithSoftDeletes::withTrashed()->first()->trashed())->toBeTrue();
// Update soft deleted synced resource
TenantUserWithSoftDeletes::withTrashed()->first()->update(['name' => $newName = 'Updated name']);
tenancy()->initialize($tenant1);
$tenantResource = TenantUserWithSoftDeletes::withTrashed()->first();
expect($tenantResource->name)->toBe($newName); // Value synced
expect($tenantResource->trashed())->toBeTrue();
tenancy()->end();
$centralResource = CentralUserWithSoftDeletes::withTrashed()->first();
expect($centralResource->name)->toBe($newName); // Value synced
expect($centralResource->trashed())->toBeTrue();
tenancy()->initialize($tenant2);
$tenantResource = TenantUserWithSoftDeletes::withTrashed()->first();
expect($tenantResource->name)->toBe($newName);
// The trashed status is not synced even after updating another tenant resource
expect($tenantResource->trashed())->toBeTrue();
});
test('restoring soft deleted resources works', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
migrateUsersTableForTenants();
addExtraColumns(true);
CentralUserWithSoftDeletes::create([
'global_id' => 'user',
'name' => 'Central user',
'email' => 'central@localhost',
'password' => 'password',
'role' => 'commenter',
'foo' => 'foo',
]);
tenancy()->initialize($tenant1);
TenantUserWithSoftDeletes::create([
'global_id' => 'user',
'name' => 'Tenant user',
'email' => 'tenant@localhost',
'password' => 'password',
'role' => 'commenter',
'foo' => 'foo',
]);
tenancy()->initialize($tenant2);
TenantUserWithSoftDeletes::create([
'global_id' => 'user',
'name' => 'Tenant user',
'email' => 'tenant@localhost',
'password' => 'password',
'role' => 'commenter',
'foo' => 'foo',
]);
tenancy()->end();
// Synced resources are deleted from all tenants if the central resource gets deleted
CentralUserWithSoftDeletes::first()->delete();
expect(CentralUserWithSoftDeletes::withTrashed()->first()->trashed())->toBeTrue();
tenancy()->runForMultiple([$tenant1, $tenant2], function () {
expect(TenantUserWithSoftDeletes::withTrashed()->first()->trashed())->toBeTrue();
});
// Restoring a central resource restores tenant resources
CentralUserWithSoftDeletes::withTrashed()->first()->restore();
tenancy()->runForMultiple([$tenant1, $tenant2], function () {
expect(TenantUserWithSoftDeletes::withTrashed()->first()->trashed())->toBeFalse();
});
});
test('using forceDelete on a central resource with soft deletes force deletes the tenant resources', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
migrateUsersTableForTenants();
addExtraColumns(true);
$centralUser = CentralUserWithSoftDeletes::create([
'global_id' => 'user',
'name' => 'Central user',
'email' => 'central@localhost',
'password' => 'password',
'role' => 'commenter',
'foo' => 'foo',
]);
$centralUser->tenants()->attach($tenant1);
$centralUser->tenants()->attach($tenant2);
// Force deleting a tenant resource does not affect resources of other tenants
tenancy()->initialize($tenant1);
TenantUserWithSoftDeletes::firstWhere('global_id', 'user')->forceDelete();
tenancy()->initialize($tenant2);
expect(TenantUserWithSoftDeletes::firstWhere('global_id', 'user'))->not()->toBeNull();
tenancy()->end();
// Synced resources are deleted from all tenants if the central resource gets force deleted
CentralUserWithSoftDeletes::firstWhere('global_id', 'user')->forceDelete();
expect(CentralUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user'))->toBeNull();
tenancy()->initialize($tenant2);
expect(TenantUserWithSoftDeletes::withTrashed()->firstWhere('global_id', 'user'))->toBeNull();
});
test('resource creation works correctly when tenant resource provides defaults in the creation attributes', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
addExtraColumns();
// Attribute names
CentralUser::$creationAttributes = ['role'];
// Mixed (attribute name + defaults)
TenantUser::$creationAttributes = [
'name' => 'Default Name',
'email' => 'default@localhost',
'password' => 'password',
'role' => 'admin',
'foo' => 'bar',
];
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
'foo' => 'bar', // foo does not exist in tenant resource
]);
$tenant1->run(function () {
expect(TenantUser::all())->toHaveCount(0);
});
// When central resource provides the attribute names in $creationAttributes
// The tenant resource will be created using them
$centralUser->tenants()->attach($tenant1);
$tenant1->run(function () {
$tenantUser = TenantUser::all();
expect($tenantUser)->toHaveCount(1);
expect($tenantUser->first()->global_id)->toBe('acme');
expect($tenantUser->first()->email)->toBe('john@localhost');
// 'foo' attribute is not provided by central model
expect($tenantUser->first()->foo)->toBeNull();
});
tenancy()->initialize($tenant2);
// Creating a tenant resource creates a central resource
// Using resource's creation attributes
TenantUser::create([
'global_id' => 'asdf',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
]);
tenancy()->end();
// Central user was created using the defaults provided in the tenant resource
// Creating TenantUser created a CentralUser with the same global_id
$centralUser = CentralUser::whereGlobalId('asdf')->first();
expect($centralUser)->not()->toBeNull();
expect($centralUser->name)->toBe('Default Name');
expect($centralUser->email)->toBe('default@localhost');
expect($centralUser->password)->toBe('password');
expect($centralUser->role)->toBe('admin');
expect($centralUser->foo)->toBe('bar');
});
test('resource creation works correctly when central resource provides defaults in the creation attributes', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
addExtraColumns();
CentralUser::$creationAttributes = [
'name' => 'Default User',
'email' => 'default@localhost',
'password' => 'password',
'role' => 'admin',
];
TenantUser::$creationAttributes = [
// Central user requires 'foo', but tenant user does not have it
// So we provide a default here in order for the central resource to be created when creating the tenant resource
'foo' => 'bar',
'role',
];
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
'foo' => 'bar',
]);
$tenant1->run(function () {
expect(TenantUser::all())->toHaveCount(0);
});
$centralUser->tenants()->attach($tenant1);
$tenant1->run(function () {
// Assert tenant resource was created using the defaults provided in the central resource
$tenantUser = TenantUser::first();
expect($tenantUser)->not()->toBeNull();
expect($tenantUser->global_id)->toBe('acme');
expect($tenantUser->email)->toBe('default@localhost');
expect($tenantUser->password)->toBe('password');
expect($tenantUser->role)->toBe('admin');
});
tenancy()->initialize($tenant2);
// The creation attributes provided in the tenant resource will be used in the newly created central resource
TenantUser::create([
'global_id' => 'asdf',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
]);
tenancy()->end();
// Assert central resource was created using the provided attributes
$centralUser = CentralUser::whereGlobalId('asdf')->first();
expect($centralUser)->not()->toBeNull();
expect($centralUser->email)->toBe('john@localhost');
expect($centralUser->password)->toBe('secret');
expect($centralUser->role)->toBe('commenter');
expect($centralUser->foo)->toBe('bar');
});
/**
* Create two tenants and run migrations for those tenants.
*
* @return Tenant[]
*/
function createTenantsAndRunMigrations(): array
{
[$tenant1, $tenant2] = [Tenant::create(), Tenant::create()];
migrateUsersTableForTenants();
return [$tenant1, $tenant2];
}
function addExtraColumns(bool $tenantDbs = false): void
{
// Migrate extra column "foo" in central DB
pest()->artisan('migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users_extra',
'--realpath' => true,
])->assertExitCode(0);
if ($tenantDbs) {
pest()->artisan('tenants:migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users_extra',
'--realpath' => true,
])->assertExitCode(0);
}
}
function migrateUsersTableForTenants(): void
{
pest()->artisan('tenants:migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users',
'--realpath' => true,
])->assertExitCode(0);
}
function migrateCompaniesTableForTenants(): void
{
pest()->artisan('tenants:migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/companies',
'--realpath' => true,
])->assertExitCode(0);
}
class CentralUser extends BaseCentralUser
{
public function tenants(): BelongsToMany
{
return $this->belongsToMany(Tenant::class, 'tenant_users', 'global_user_id', 'tenant_id', 'global_id')
->using(TenantPivot::class);
}
}
class TenantUser extends BaseTenantUser
{
public function getCentralModelName(): string
{
return CentralUser::class;
}
public function tenants(): BelongsToMany
{
return $this->belongsToMany(Tenant::class, 'tenant_users', 'global_user_id', 'tenant_id', 'global_id')
->using(TenantPivot::class);
}
}
class TenantPivot extends BasePivot
{
public $table = 'tenant_users';
}
class CentralUserWithSoftDeletes extends CentralUser
{
use SoftDeletes;
public function getTenantModelName(): string
{
return TenantUserWithSoftDeletes::class;
}
public function getCreationAttributes(): array
{
return array_merge($this->getSyncedAttributeNames(), [
'role' => 'role',
'foo' => 'foo', // extra column
]);
}
}
class TenantUserWithSoftDeletes extends TenantUser
{
use SoftDeletes;
public function getCentralModelName(): string
{
return CentralUserWithSoftDeletes::class;
}
}
class MorphTenant extends Tenant
{
public function users(): MorphToMany
{
return $this->morphedByMany(BaseCentralUser::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
->using(TenantMorphPivot::class);
}
public function companies(): MorphToMany
{
return $this->morphedByMany(CentralCompany::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
->using(TenantMorphPivot::class);
}
}
class CentralCompany extends Model implements SyncMaster
{
use ResourceSyncing, CentralConnection;
protected $guarded = [];
public $timestamps = false;
public $table = 'companies';
public function getTenantModelName(): string
{
return TenantCompany::class;
}
public function getCentralModelName(): string
{
return static::class;
}
public function getSyncedAttributeNames(): array
{
return [
'global_id',
'name',
'email',
];
}
}
class TenantCompany extends Model implements Syncable
{
use ResourceSyncing;
protected $table = 'companies';
protected $guarded = [];
public $timestamps = false;
public function getCentralModelName(): string
{
return CentralCompany::class;
}
public function getSyncedAttributeNames(): array
{
return [
'global_id',
'name',
'email',
];
}
}