mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 08:24:04 +00:00
Syncing: SyncedResourceDeleted event and DeleteResourceMapping listener
Also move pivot record deletion to that listener and improve tests The 'tenant pivot records are deleted along with the tenants to which they belong to' test is failing in this commit -- the listener for deleting mappings when a *tenant* is deleted is only implemented in the next commit. The only change done here is to re-add FKs (necessary for passing *in this commit* in that specific dataset variant) that were removed from the default test migration as we now have the DeleteResourceMapping listener that's enabled by default.
This commit is contained in:
parent
45cf7029af
commit
44e8ec8abf
10 changed files with 160 additions and 20 deletions
|
|
@ -129,6 +129,9 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
ResourceSyncing\Events\SyncedResourceSaved::class => [
|
ResourceSyncing\Events\SyncedResourceSaved::class => [
|
||||||
ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::class,
|
ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::class,
|
||||||
],
|
],
|
||||||
|
ResourceSyncing\Events\SyncedResourceDeleted::class => [
|
||||||
|
ResourceSyncing\Listeners\DeleteResourceMapping::class,
|
||||||
|
],
|
||||||
ResourceSyncing\Events\SyncMasterDeleted::class => [
|
ResourceSyncing\Events\SyncMasterDeleted::class => [
|
||||||
ResourceSyncing\Listeners\DeleteResourcesInTenants::class,
|
ResourceSyncing\Listeners\DeleteResourcesInTenants::class,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
18
src/ResourceSyncing/Events/SyncedResourceDeleted.php
Normal file
18
src/ResourceSyncing/Events/SyncedResourceDeleted.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\ResourceSyncing\Events;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||||
|
|
||||||
|
class SyncedResourceDeleted
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public Syncable&Model $model,
|
||||||
|
public TenantWithDatabase|null $tenant,
|
||||||
|
public bool $forceDelete,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
60
src/ResourceSyncing/Listeners/DeleteResourceMapping.php
Normal file
60
src/ResourceSyncing/Listeners/DeleteResourceMapping.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes pivot records when a synced resource is deleted.
|
||||||
|
*
|
||||||
|
* If a SyncMaster (central resource) is deleted, all pivot records for that resource are deleted.
|
||||||
|
* If a Syncable (tenant resource) is deleted, only delete the pivot record for that tenant.
|
||||||
|
*/
|
||||||
|
class DeleteResourceMapping extends QueueableListener
|
||||||
|
{
|
||||||
|
public static bool $shouldQueue = false;
|
||||||
|
|
||||||
|
public function handle(SyncedResourceDeleted $event): void
|
||||||
|
{
|
||||||
|
$centralResource = $this->getCentralResource($event->model);
|
||||||
|
|
||||||
|
if (! $centralResource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete pivot records if the central resource doesn't use soft deletes
|
||||||
|
// or the central resource was deleted using forceDelete()
|
||||||
|
if ($event->forceDelete || ! in_array(SoftDeletes::class, class_uses_recursive($centralResource::class), true)) {
|
||||||
|
Pivot::withoutEvents(function () use ($centralResource, $event) {
|
||||||
|
// If detach() is called with null -- if $event->tenant is null -- this means a central resource was deleted and detaches all tenants.
|
||||||
|
// If detach() is called with a specific tenant, it means the resource was deleted in that tenant, and we only delete that single mapping.
|
||||||
|
$centralResource->tenants()->detach($event->tenant);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCentralResource(Syncable&Model $resource): SyncMaster|null
|
||||||
|
{
|
||||||
|
if ($resource instanceof SyncMaster) {
|
||||||
|
return $resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
$centralResourceClass = $resource->getCentralModelName();
|
||||||
|
|
||||||
|
/** @var (SyncMaster&Model)|null $centralResource */
|
||||||
|
$centralResource = $centralResourceClass::firstWhere(
|
||||||
|
$resource->getGlobalIdentifierKeyName(),
|
||||||
|
$resource->getGlobalIdentifierKey()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $centralResource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||||
|
|
||||||
|
|
@ -21,12 +20,6 @@ class DeleteResourcesInTenants extends QueueableListener
|
||||||
|
|
||||||
tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource, $forceDelete) {
|
tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource, $forceDelete) {
|
||||||
$this->deleteSyncedResource($centralResource, $forceDelete);
|
$this->deleteSyncedResource($centralResource, $forceDelete);
|
||||||
|
|
||||||
// Delete pivot records if the central resource doesn't use soft deletes
|
|
||||||
// or the central resource was deleted using forceDelete()
|
|
||||||
if ($forceDelete || ! in_array(SoftDeletes::class, class_uses_recursive($centralResource::class), true)) {
|
|
||||||
$centralResource->tenants()->detach(tenant());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceAttachedToTenant;
|
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceAttachedToTenant;
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
|
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
|
||||||
|
|
@ -25,8 +26,8 @@ trait ResourceSyncing
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
static::deleting(function (Syncable&Model $model) {
|
static::deleted(function (Syncable&Model $model) {
|
||||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
if ($model->shouldSync()) {
|
||||||
$model->triggerDeleteEvent();
|
$model->triggerDeleteEvent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -42,14 +43,14 @@ trait ResourceSyncing
|
||||||
|
|
||||||
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
|
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
|
||||||
static::forceDeleting(function (Syncable&Model $model) {
|
static::forceDeleting(function (Syncable&Model $model) {
|
||||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
if ($model->shouldSync()) {
|
||||||
$model->triggerDeleteEvent(true);
|
$model->triggerDeleteEvent(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
static::restoring(function (Syncable&Model $model) {
|
static::restoring(function (Syncable&Model $model) {
|
||||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
if ($model instanceof SyncMaster && $model->shouldSync()) {
|
||||||
$model->triggerRestoredEvent();
|
$model->triggerRestoreEvent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -67,9 +68,11 @@ trait ResourceSyncing
|
||||||
/** @var SyncMaster&Model $this */
|
/** @var SyncMaster&Model $this */
|
||||||
event(new SyncMasterDeleted($this, $forceDelete));
|
event(new SyncMasterDeleted($this, $forceDelete));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event(new SyncedResourceDeleted($this, tenant(), $forceDelete));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function triggerRestoredEvent(): void
|
public function triggerRestoreEvent(): void
|
||||||
{
|
{
|
||||||
if ($this instanceof SyncMaster && in_array(SoftDeletes::class, class_uses_recursive($this), true)) {
|
if ($this instanceof SyncMaster && in_array(SoftDeletes::class, class_uses_recursive($this), true)) {
|
||||||
/** @var SyncMaster&Model $this */
|
/** @var SyncMaster&Model $this */
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,5 @@ interface SyncMaster extends Syncable
|
||||||
|
|
||||||
public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void;
|
public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void;
|
||||||
|
|
||||||
public function triggerDeleteEvent(bool $forceDelete = false): void;
|
public function triggerRestoreEvent(): void;
|
||||||
|
|
||||||
public function triggerRestoredEvent(): void;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ interface Syncable
|
||||||
|
|
||||||
public function triggerSyncEvent(): void;
|
public function triggerSyncEvent(): void;
|
||||||
|
|
||||||
|
public function triggerDeleteEvent(bool $forceDelete = false): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attributes used for creating the *other* model (i.e. tenant if this is the central one, and central if this is the tenant one).
|
* Get the attributes used for creating the *other* model (i.e. tenant if this is the central one, and central if this is the tenant one).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||||
class CentralUser extends Model implements SyncMaster
|
class CentralUser extends Model implements SyncMaster
|
||||||
{
|
{
|
||||||
use ResourceSyncing, CentralConnection;
|
use ResourceSyncing, CentralConnection;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@ class CreateTenantUsersTable extends Migration
|
||||||
$table->string('global_user_id');
|
$table->string('global_user_id');
|
||||||
|
|
||||||
$table->unique(['tenant_id', 'global_user_id']);
|
$table->unique(['tenant_id', 'global_user_id']);
|
||||||
|
|
||||||
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
|
|
||||||
$table->foreign('global_user_id')->references('global_id')->on('users')->onUpdate('cascade')->onDelete('cascade');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase;
|
||||||
use Illuminate\Database\Eloquent\Scope;
|
use Illuminate\Database\Eloquent\Scope;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use function Stancl\Tenancy\Tests\pest;
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\Listeners\DeleteResourceMapping;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.bootstrappers' => [
|
config(['tenancy.bootstrappers' => [
|
||||||
|
|
@ -92,6 +96,7 @@ beforeEach(function () {
|
||||||
CentralUser::$creationAttributes = $creationAttributes;
|
CentralUser::$creationAttributes = $creationAttributes;
|
||||||
|
|
||||||
Event::listen(SyncedResourceSaved::class, UpdateOrCreateSyncedResource::class);
|
Event::listen(SyncedResourceSaved::class, UpdateOrCreateSyncedResource::class);
|
||||||
|
Event::listen(SyncedResourceDeleted::class, DeleteResourceMapping::class);
|
||||||
Event::listen(SyncMasterDeleted::class, DeleteResourcesInTenants::class);
|
Event::listen(SyncMasterDeleted::class, DeleteResourcesInTenants::class);
|
||||||
Event::listen(SyncMasterRestored::class, RestoreResourcesInTenants::class);
|
Event::listen(SyncMasterRestored::class, RestoreResourcesInTenants::class);
|
||||||
Event::listen(CentralResourceAttachedToTenant::class, CreateTenantResource::class);
|
Event::listen(CentralResourceAttachedToTenant::class, CreateTenantResource::class);
|
||||||
|
|
@ -890,9 +895,13 @@ test('deleting SyncMaster automatically deletes its Syncables', function (bool $
|
||||||
'basic pivot' => false,
|
'basic pivot' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('tenant pivot records are deleted along with the tenants to which they belong to', function() {
|
test('tenant pivot records are deleted along with the tenants to which they belong to', function(bool $dbLevelOnCascadeDelete) {
|
||||||
[$tenant] = createTenantsAndRunMigrations();
|
[$tenant] = createTenantsAndRunMigrations();
|
||||||
|
|
||||||
|
if ($dbLevelOnCascadeDelete) {
|
||||||
|
addFkConstraintsToTenantUsersPivot();
|
||||||
|
}
|
||||||
|
|
||||||
$syncMaster = CentralUser::create([
|
$syncMaster = CentralUser::create([
|
||||||
'global_id' => 'cascade_user',
|
'global_id' => 'cascade_user',
|
||||||
'name' => 'Central user',
|
'name' => 'Central user',
|
||||||
|
|
@ -907,6 +916,54 @@ test('tenant pivot records are deleted along with the tenants to which they belo
|
||||||
|
|
||||||
// Deleting tenant deletes its pivot records
|
// Deleting tenant deletes its pivot records
|
||||||
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->getTenantKey()]))->toHaveCount(0);
|
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->getTenantKey()]))->toHaveCount(0);
|
||||||
|
})->with([
|
||||||
|
'db level on cascade delete' => true,
|
||||||
|
'event-based on cascade delete' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('pivot record is automatically deleted with the tenant resource', function() {
|
||||||
|
[$tenant] = createTenantsAndRunMigrations();
|
||||||
|
|
||||||
|
$syncMaster = CentralUser::create([
|
||||||
|
'global_id' => 'cascade_user',
|
||||||
|
'name' => 'Central user',
|
||||||
|
'email' => 'central@localhost',
|
||||||
|
'password' => 'password',
|
||||||
|
'role' => 'cascade_user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$syncMaster->tenants()->attach($tenant);
|
||||||
|
|
||||||
|
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->id]))->toHaveCount(1);
|
||||||
|
|
||||||
|
$tenant->run(function () {
|
||||||
|
TenantUser::firstWhere('global_id', 'cascade_user')->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deleting tenant resource deletes its pivot record
|
||||||
|
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->id]))->toHaveCount(0);
|
||||||
|
|
||||||
|
// The same works with forceDelete
|
||||||
|
addExtraColumns(true);
|
||||||
|
|
||||||
|
$syncMaster = CentralUserWithSoftDeletes::create([
|
||||||
|
'global_id' => 'force_cascade_user',
|
||||||
|
'name' => 'Central user',
|
||||||
|
'email' => 'central2@localhost',
|
||||||
|
'password' => 'password',
|
||||||
|
'role' => 'force_cascade_user',
|
||||||
|
'foo' => 'bar',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$syncMaster->tenants()->attach($tenant);
|
||||||
|
|
||||||
|
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->id]))->toHaveCount(1);
|
||||||
|
|
||||||
|
$tenant->run(function () {
|
||||||
|
TenantUserWithSoftDeletes::firstWhere('global_id', 'force_cascade_user')->forceDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->id]))->toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('trashed resources are synced correctly', function () {
|
test('trashed resources are synced correctly', function () {
|
||||||
|
|
@ -1265,6 +1322,14 @@ test('global scopes on syncable models can break resource syncing', function ()
|
||||||
expect($tenant1->run(fn () => TenantUser::first()->name))->toBe('tenant2 user');
|
expect($tenant1->run(fn () => TenantUser::first()->name))->toBe('tenant2 user');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function addFkConstraintsToTenantUsersPivot(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenant_users', function (Blueprint $table) {
|
||||||
|
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
|
||||||
|
$table->foreign('global_user_id')->references('global_id')->on('users')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create two tenants and run migrations for those tenants.
|
* Create two tenants and run migrations for those tenants.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue