mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 15:34:03 +00:00
Add SyncedResourceDeleted event and DeleteResourceMapping listener
Also move pivot record deletion to that listener and improve tests
This commit is contained in:
parent
45cf7029af
commit
ff95c92134
10 changed files with 133 additions and 17 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) {
|
||||||
|
// $event->tenant is null when the deleted resource is a SyncMaster - all mappings are deleted in that case
|
||||||
|
// When $event->tenant is not null (= a Syncable was deleted), only delete the mapping for that tenant
|
||||||
|
$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,13 +43,13 @@ 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->shouldSync()) {
|
||||||
$model->triggerRestoredEvent();
|
$model->triggerRestoredEvent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -67,6 +68,8 @@ 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 triggerRestoredEvent(): void
|
||||||
|
|
|
||||||
|
|
@ -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 triggerRestoredEvent(): 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,30 @@ 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);
|
||||||
|
|
||||||
|
$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->getTenantKey()]))->toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('trashed resources are synced correctly', function () {
|
test('trashed resources are synced correctly', function () {
|
||||||
|
|
@ -1265,6 +1298,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