Skip to content

Commit a778e17

Browse files
authored
Merge pull request #1411 from archtechx/resource-syncing-refactor
[4.x] Improve resource syncing (refactor + mapping cleanup + morph maps)
2 parents 45cf702 + 159e600 commit a778e17

16 files changed

+342
-72
lines changed

assets/TenancyServiceProvider.stub.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public function events()
8181
])->send(function (Events\TenantDeleted $event) {
8282
return $event->tenant;
8383
})->shouldBeQueued(false),
84+
85+
// ResourceSyncing\Listeners\DeleteAllTenantMappings::class,
8486
],
8587

8688
Events\TenantMaintenanceModeEnabled::class => [],
@@ -129,6 +131,9 @@ public function events()
129131
ResourceSyncing\Events\SyncedResourceSaved::class => [
130132
ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::class,
131133
],
134+
ResourceSyncing\Events\SyncedResourceDeleted::class => [
135+
ResourceSyncing\Listeners\DeleteResourceMapping::class,
136+
],
132137
ResourceSyncing\Events\SyncMasterDeleted::class => [
133138
ResourceSyncing\Listeners\DeleteResourcesInTenants::class,
134139
],
@@ -141,7 +146,9 @@ public function events()
141146
ResourceSyncing\Events\CentralResourceDetachedFromTenant::class => [
142147
ResourceSyncing\Listeners\DeleteResourceInTenant::class,
143148
],
144-
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
149+
150+
// Fired only when a synced resource is changed (as a result of syncing)
151+
// in a different DB than DB from which the change originates (to avoid infinite loops)
145152
ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase::class => [],
146153

147154
// Storage symlinks

src/ResourceSyncing/CentralResourceNotAvailableInPivotException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function __construct()
1313
parent::__construct(
1414
'Central resource is not accessible in pivot model.
1515
To attach a resource to a tenant, use $centralResource->tenants()->attach($tenant) instead of $tenant->resources()->attach($centralResource) (same for detaching).
16-
To make this work both ways, you can make your pivot implement PivotWithRelation and return the related model in getRelatedModel() or extend MorphPivot.'
16+
To make this work both ways, you can make your pivot implement PivotWithCentralResource and return the related model in getCentralResourceClass() or extend MorphPivot.'
1717
);
1818
}
1919
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Stancl\Tenancy\ResourceSyncing\Events;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
9+
use Stancl\Tenancy\ResourceSyncing\Syncable;
10+
11+
class SyncedResourceDeleted
12+
{
13+
public function __construct(
14+
public Syncable&Model $model,
15+
public TenantWithDatabase|null $tenant,
16+
public bool $forceDelete,
17+
) {}
18+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
6+
7+
use Illuminate\Support\Facades\DB;
8+
use Stancl\Tenancy\Events\TenantDeleted;
9+
use Stancl\Tenancy\Listeners\QueueableListener;
10+
11+
/**
12+
* Cleans up pivot records related to the deleted tenant.
13+
*
14+
* The listener only cleans up the pivot tables specified
15+
* in the $pivotTables property (see the property for details),
16+
* and is intended for use with tables that do not have tenant
17+
* foreign key constraints with onDelete('cascade').
18+
*/
19+
class DeleteAllTenantMappings extends QueueableListener
20+
{
21+
public static bool $shouldQueue = false;
22+
23+
/**
24+
* Pivot tables to clean up after a tenant is deleted, in the
25+
* ['table_name' => 'tenant_key_column'] format.
26+
*
27+
* Since we cannot automatically detect which pivot tables
28+
* are being used, they have to be specified here manually.
29+
*
30+
* The default value follows the polymorphic table used by default.
31+
*/
32+
public static array $pivotTables = ['tenant_resources' => 'tenant_id'];
33+
34+
public function handle(TenantDeleted $event): void
35+
{
36+
foreach (static::$pivotTables as $table => $tenantKeyColumn) {
37+
DB::table($table)->where($tenantKeyColumn, $event->tenant->getKey())->delete();
38+
}
39+
}
40+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\Pivot;
9+
use Illuminate\Database\Eloquent\SoftDeletes;
10+
use Stancl\Tenancy\Listeners\QueueableListener;
11+
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
12+
use Stancl\Tenancy\ResourceSyncing\Syncable;
13+
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
14+
15+
/**
16+
* Deletes pivot records when a synced resource is deleted.
17+
*
18+
* If a SyncMaster (central resource) is deleted, all pivot records for that resource are deleted.
19+
* If a Syncable (tenant resource) is deleted, only delete the pivot record for that tenant.
20+
*/
21+
class DeleteResourceMapping extends QueueableListener
22+
{
23+
public static bool $shouldQueue = false;
24+
25+
public function handle(SyncedResourceDeleted $event): void
26+
{
27+
$centralResource = $this->getCentralResource($event->model);
28+
29+
if (! $centralResource) {
30+
return;
31+
}
32+
33+
// Delete pivot records if the central resource doesn't use soft deletes
34+
// or the central resource was deleted using forceDelete()
35+
if ($event->forceDelete || ! in_array(SoftDeletes::class, class_uses_recursive($centralResource::class), true)) {
36+
Pivot::withoutEvents(function () use ($centralResource, $event) {
37+
// If detach() is called with null -- if $event->tenant is null -- this means a central resource was deleted and detaches all tenants.
38+
// If detach() is called with a specific tenant, it means the resource was deleted in that tenant, and we only delete that single mapping.
39+
$centralResource->tenants()->detach($event->tenant);
40+
});
41+
}
42+
}
43+
44+
public function getCentralResource(Syncable&Model $resource): SyncMaster|null
45+
{
46+
if ($resource instanceof SyncMaster) {
47+
return $resource;
48+
}
49+
50+
$centralResourceClass = $resource->getCentralModelName();
51+
52+
/** @var (SyncMaster&Model)|null $centralResource */
53+
$centralResource = $centralResourceClass::firstWhere(
54+
$resource->getGlobalIdentifierKeyName(),
55+
$resource->getGlobalIdentifierKey()
56+
);
57+
58+
return $centralResource;
59+
}
60+
}

src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
66

7-
use Illuminate\Database\Eloquent\SoftDeletes;
87
use Stancl\Tenancy\Listeners\QueueableListener;
98
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
109

@@ -21,12 +20,6 @@ public function handle(SyncMasterDeleted $event): void
2120

2221
tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource, $forceDelete) {
2322
$this->deleteSyncedResource($centralResource, $forceDelete);
24-
25-
// Delete pivot records if the central resource doesn't use soft deletes
26-
// or the central resource was deleted using forceDelete()
27-
if ($forceDelete || ! in_array(SoftDeletes::class, class_uses_recursive($centralResource::class), true)) {
28-
$centralResource->tenants()->detach(tenant());
29-
}
3023
});
3124
}
3225
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Stancl\Tenancy\ResourceSyncing;
6+
7+
interface PivotWithCentralResource
8+
{
9+
/** @return class-string<\Illuminate\Database\Eloquent\Model&Syncable> */
10+
public function getCentralResourceClass(): string;
11+
}

src/ResourceSyncing/PivotWithRelation.php

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/ResourceSyncing/ResourceSyncing.php

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
1212
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceAttachedToTenant;
1313
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
14+
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
1415
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
1516
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
1617
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
@@ -19,37 +20,34 @@ trait ResourceSyncing
1920
{
2021
public static function bootResourceSyncing(): void
2122
{
22-
static::saved(function (Syncable&Model $model) {
23+
static::saved(static function (Syncable&Model $model) {
2324
if ($model->shouldSync() && ($model->wasRecentlyCreated || $model->wasChanged($model->getSyncedAttributeNames()))) {
2425
$model->triggerSyncEvent();
2526
}
2627
});
2728

28-
static::deleting(function (Syncable&Model $model) {
29-
if ($model->shouldSync() && $model instanceof SyncMaster) {
29+
static::deleted(static function (Syncable&Model $model) {
30+
if ($model->shouldSync()) {
3031
$model->triggerDeleteEvent();
3132
}
3233
});
3334

34-
static::creating(function (Syncable&Model $model) {
35-
if (! $model->getAttribute($model->getGlobalIdentifierKeyName()) && app()->bound(UniqueIdentifierGenerator::class)) {
36-
$model->setAttribute(
37-
$model->getGlobalIdentifierKeyName(),
38-
app(UniqueIdentifierGenerator::class)->generate($model)
39-
);
35+
static::creating(static function (Syncable&Model $model) {
36+
if (! $model->getAttribute($model->getGlobalIdentifierKeyName())) {
37+
$model->generateGlobalIdentifierKey();
4038
}
4139
});
4240

4341
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
44-
static::forceDeleting(function (Syncable&Model $model) {
45-
if ($model->shouldSync() && $model instanceof SyncMaster) {
42+
static::forceDeleting(static function (Syncable&Model $model) {
43+
if ($model->shouldSync()) {
4644
$model->triggerDeleteEvent(true);
4745
}
4846
});
4947

50-
static::restoring(function (Syncable&Model $model) {
51-
if ($model->shouldSync() && $model instanceof SyncMaster) {
52-
$model->triggerRestoredEvent();
48+
static::restoring(static function (Syncable&Model $model) {
49+
if ($model instanceof SyncMaster && $model->shouldSync()) {
50+
$model->triggerRestoreEvent();
5351
}
5452
});
5553
}
@@ -67,9 +65,11 @@ public function triggerDeleteEvent(bool $forceDelete = false): void
6765
/** @var SyncMaster&Model $this */
6866
event(new SyncMasterDeleted($this, $forceDelete));
6967
}
68+
69+
event(new SyncedResourceDeleted($this, tenant(), $forceDelete));
7070
}
7171

72-
public function triggerRestoredEvent(): void
72+
public function triggerRestoreEvent(): void
7373
{
7474
if ($this instanceof SyncMaster && in_array(SoftDeletes::class, class_uses_recursive($this), true)) {
7575
/** @var SyncMaster&Model $this */
@@ -116,8 +116,18 @@ public function getGlobalIdentifierKeyName(): string
116116
return 'global_id';
117117
}
118118

119-
public function getGlobalIdentifierKey(): string
119+
public function getGlobalIdentifierKey(): string|int
120120
{
121121
return $this->getAttribute($this->getGlobalIdentifierKeyName());
122122
}
123+
124+
protected function generateGlobalIdentifierKey(): void
125+
{
126+
if (! app()->bound(UniqueIdentifierGenerator::class)) return;
127+
128+
$this->setAttribute(
129+
$this->getGlobalIdentifierKeyName(),
130+
app(UniqueIdentifierGenerator::class)->generate($this),
131+
);
132+
}
123133
}

src/ResourceSyncing/SyncMaster.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,5 @@ public function triggerDetachEvent(TenantWithDatabase&Model $tenant): void;
2525

2626
public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void;
2727

28-
public function triggerDeleteEvent(bool $forceDelete = false): void;
29-
30-
public function triggerRestoredEvent(): void;
28+
public function triggerRestoreEvent(): void;
3129
}

0 commit comments

Comments
 (0)