mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 06:44: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 the DeleteResourceMapping listener that's enabled by default.
This commit is contained in:
parent
45cf7029af
commit
5b15c67d9e
10 changed files with 138 additions and 20 deletions
|
|
@ -129,6 +129,9 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
ResourceSyncing\Events\SyncedResourceSaved::class => [
|
||||
ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::class,
|
||||
],
|
||||
ResourceSyncing\Events\SyncedResourceDeleted::class => [
|
||||
ResourceSyncing\Listeners\DeleteResourceMapping::class,
|
||||
],
|
||||
ResourceSyncing\Events\SyncMasterDeleted::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;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||
|
||||
|
|
@ -21,12 +20,6 @@ class DeleteResourcesInTenants extends QueueableListener
|
|||
|
||||
tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($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\ResourceSyncing\Events\CentralResourceAttachedToTenant;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
|
||||
|
|
@ -25,8 +26,8 @@ trait ResourceSyncing
|
|||
}
|
||||
});
|
||||
|
||||
static::deleting(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
static::deleted(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync()) {
|
||||
$model->triggerDeleteEvent();
|
||||
}
|
||||
});
|
||||
|
|
@ -42,14 +43,14 @@ trait ResourceSyncing
|
|||
|
||||
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
|
||||
static::forceDeleting(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
if ($model->shouldSync()) {
|
||||
$model->triggerDeleteEvent(true);
|
||||
}
|
||||
});
|
||||
|
||||
static::restoring(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
$model->triggerRestoredEvent();
|
||||
if ($model instanceof SyncMaster && $model->shouldSync()) {
|
||||
$model->triggerRestoreEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -67,9 +68,11 @@ trait ResourceSyncing
|
|||
/** @var SyncMaster&Model $this */
|
||||
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)) {
|
||||
/** @var SyncMaster&Model $this */
|
||||
|
|
|
|||
|
|
@ -25,7 +25,5 @@ interface SyncMaster extends Syncable
|
|||
|
||||
public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void;
|
||||
|
||||
public function triggerDeleteEvent(bool $forceDelete = false): void;
|
||||
|
||||
public function triggerRestoredEvent(): void;
|
||||
public function triggerRestoreEvent(): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ interface Syncable
|
|||
|
||||
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).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
|||
class CentralUser extends Model implements SyncMaster
|
||||
{
|
||||
use ResourceSyncing, CentralConnection;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ class CreateTenantUsersTable extends Migration
|
|||
$table->string('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\QueryException;
|
||||
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 () {
|
||||
config(['tenancy.bootstrappers' => [
|
||||
|
|
@ -92,6 +96,7 @@ beforeEach(function () {
|
|||
CentralUser::$creationAttributes = $creationAttributes;
|
||||
|
||||
Event::listen(SyncedResourceSaved::class, UpdateOrCreateSyncedResource::class);
|
||||
Event::listen(SyncedResourceDeleted::class, DeleteResourceMapping::class);
|
||||
Event::listen(SyncMasterDeleted::class, DeleteResourcesInTenants::class);
|
||||
Event::listen(SyncMasterRestored::class, RestoreResourcesInTenants::class);
|
||||
Event::listen(CentralResourceAttachedToTenant::class, CreateTenantResource::class);
|
||||
|
|
@ -890,9 +895,13 @@ test('deleting SyncMaster automatically deletes its Syncables', function (bool $
|
|||
'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();
|
||||
|
||||
if ($dbLevelOnCascadeDelete) {
|
||||
addFkConstraintsToTenantUsersPivot();
|
||||
}
|
||||
|
||||
$syncMaster = CentralUser::create([
|
||||
'global_id' => 'cascade_user',
|
||||
'name' => 'Central user',
|
||||
|
|
@ -907,6 +916,32 @@ test('tenant pivot records are deleted along with the tenants to which they belo
|
|||
|
||||
// Deleting tenant deletes its pivot records
|
||||
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);
|
||||
});
|
||||
|
||||
test('trashed resources are synced correctly', function () {
|
||||
|
|
@ -1265,6 +1300,14 @@ test('global scopes on syncable models can break resource syncing', function ()
|
|||
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.
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue