mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 22:14:03 +00:00
Compare commits
No commits in common. "a778e17686a2bc80704d68f868018f154a1a1e3c" and "45cf7029af2ed4785ed779005fd710c3e74f9b64" have entirely different histories.
a778e17686
...
45cf7029af
16 changed files with 72 additions and 342 deletions
|
|
@ -81,8 +81,6 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
])->send(function (Events\TenantDeleted $event) {
|
])->send(function (Events\TenantDeleted $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false),
|
})->shouldBeQueued(false),
|
||||||
|
|
||||||
// ResourceSyncing\Listeners\DeleteAllTenantMappings::class,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
Events\TenantMaintenanceModeEnabled::class => [],
|
Events\TenantMaintenanceModeEnabled::class => [],
|
||||||
|
|
@ -131,9 +129,6 @@ 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,
|
||||||
],
|
],
|
||||||
|
|
@ -146,9 +141,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
ResourceSyncing\Events\CentralResourceDetachedFromTenant::class => [
|
ResourceSyncing\Events\CentralResourceDetachedFromTenant::class => [
|
||||||
ResourceSyncing\Listeners\DeleteResourceInTenant::class,
|
ResourceSyncing\Listeners\DeleteResourceInTenant::class,
|
||||||
],
|
],
|
||||||
|
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
|
||||||
// Fired only when a synced resource is changed (as a result of syncing)
|
|
||||||
// in a different DB than DB from which the change originates (to avoid infinite loops)
|
|
||||||
ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase::class => [],
|
ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase::class => [],
|
||||||
|
|
||||||
// Storage symlinks
|
// Storage symlinks
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class CentralResourceNotAvailableInPivotException extends Exception
|
||||||
parent::__construct(
|
parent::__construct(
|
||||||
'Central resource is not accessible in pivot model.
|
'Central resource is not accessible in pivot model.
|
||||||
To attach a resource to a tenant, use $centralResource->tenants()->attach($tenant) instead of $tenant->resources()->attach($centralResource) (same for detaching).
|
To attach a resource to a tenant, use $centralResource->tenants()->attach($tenant) instead of $tenant->resources()->attach($centralResource) (same for detaching).
|
||||||
To make this work both ways, you can make your pivot implement PivotWithCentralResource and return the related model in getCentralResourceClass() or extend MorphPivot.'
|
To make this work both ways, you can make your pivot implement PivotWithRelation and return the related model in getRelatedModel() or extend MorphPivot.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?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,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Stancl\Tenancy\Events\TenantDeleted;
|
|
||||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up pivot records related to the deleted tenant.
|
|
||||||
*
|
|
||||||
* The listener only cleans up the pivot tables specified
|
|
||||||
* in the $pivotTables property (see the property for details),
|
|
||||||
* and is intended for use with tables that do not have tenant
|
|
||||||
* foreign key constraints with onDelete('cascade').
|
|
||||||
*/
|
|
||||||
class DeleteAllTenantMappings extends QueueableListener
|
|
||||||
{
|
|
||||||
public static bool $shouldQueue = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pivot tables to clean up after a tenant is deleted, in the
|
|
||||||
* ['table_name' => 'tenant_key_column'] format.
|
|
||||||
*
|
|
||||||
* Since we cannot automatically detect which pivot tables
|
|
||||||
* are being used, they have to be specified here manually.
|
|
||||||
*
|
|
||||||
* The default value follows the polymorphic table used by default.
|
|
||||||
*/
|
|
||||||
public static array $pivotTables = ['tenant_resources' => 'tenant_id'];
|
|
||||||
|
|
||||||
public function handle(TenantDeleted $event): void
|
|
||||||
{
|
|
||||||
foreach (static::$pivotTables as $table => $tenantKeyColumn) {
|
|
||||||
DB::table($table)->where($tenantKeyColumn, $event->tenant->getKey())->delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
<?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,6 +4,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -20,6 +21,12 @@ 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());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\ResourceSyncing;
|
|
||||||
|
|
||||||
interface PivotWithCentralResource
|
|
||||||
{
|
|
||||||
/** @return class-string<\Illuminate\Database\Eloquent\Model&Syncable> */
|
|
||||||
public function getCentralResourceClass(): string;
|
|
||||||
}
|
|
||||||
15
src/ResourceSyncing/PivotWithRelation.php
Normal file
15
src/ResourceSyncing/PivotWithRelation.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\ResourceSyncing;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
interface PivotWithRelation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* E.g. return $this->users()->getModel().
|
||||||
|
*/
|
||||||
|
public function getRelatedModel(): Model;
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,6 @@ 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;
|
||||||
|
|
@ -20,34 +19,37 @@ trait ResourceSyncing
|
||||||
{
|
{
|
||||||
public static function bootResourceSyncing(): void
|
public static function bootResourceSyncing(): void
|
||||||
{
|
{
|
||||||
static::saved(static function (Syncable&Model $model) {
|
static::saved(function (Syncable&Model $model) {
|
||||||
if ($model->shouldSync() && ($model->wasRecentlyCreated || $model->wasChanged($model->getSyncedAttributeNames()))) {
|
if ($model->shouldSync() && ($model->wasRecentlyCreated || $model->wasChanged($model->getSyncedAttributeNames()))) {
|
||||||
$model->triggerSyncEvent();
|
$model->triggerSyncEvent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
static::deleted(static function (Syncable&Model $model) {
|
static::deleting(function (Syncable&Model $model) {
|
||||||
if ($model->shouldSync()) {
|
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||||
$model->triggerDeleteEvent();
|
$model->triggerDeleteEvent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
static::creating(static function (Syncable&Model $model) {
|
static::creating(function (Syncable&Model $model) {
|
||||||
if (! $model->getAttribute($model->getGlobalIdentifierKeyName())) {
|
if (! $model->getAttribute($model->getGlobalIdentifierKeyName()) && app()->bound(UniqueIdentifierGenerator::class)) {
|
||||||
$model->generateGlobalIdentifierKey();
|
$model->setAttribute(
|
||||||
|
$model->getGlobalIdentifierKeyName(),
|
||||||
|
app(UniqueIdentifierGenerator::class)->generate($model)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
|
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
|
||||||
static::forceDeleting(static function (Syncable&Model $model) {
|
static::forceDeleting(function (Syncable&Model $model) {
|
||||||
if ($model->shouldSync()) {
|
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||||
$model->triggerDeleteEvent(true);
|
$model->triggerDeleteEvent(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
static::restoring(static function (Syncable&Model $model) {
|
static::restoring(function (Syncable&Model $model) {
|
||||||
if ($model instanceof SyncMaster && $model->shouldSync()) {
|
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||||
$model->triggerRestoreEvent();
|
$model->triggerRestoredEvent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -65,11 +67,9 @@ 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 triggerRestoreEvent(): void
|
public function triggerRestoredEvent(): 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 */
|
||||||
|
|
@ -116,18 +116,8 @@ trait ResourceSyncing
|
||||||
return 'global_id';
|
return 'global_id';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getGlobalIdentifierKey(): string|int
|
public function getGlobalIdentifierKey(): string
|
||||||
{
|
{
|
||||||
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function generateGlobalIdentifierKey(): void
|
|
||||||
{
|
|
||||||
if (! app()->bound(UniqueIdentifierGenerator::class)) return;
|
|
||||||
|
|
||||||
$this->setAttribute(
|
|
||||||
$this->getGlobalIdentifierKeyName(),
|
|
||||||
app(UniqueIdentifierGenerator::class)->generate($this),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,7 @@ interface SyncMaster extends Syncable
|
||||||
|
|
||||||
public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void;
|
public function triggerAttachEvent(TenantWithDatabase&Model $tenant): void;
|
||||||
|
|
||||||
public function triggerRestoreEvent(): void;
|
public function triggerDeleteEvent(bool $forceDelete = false): void;
|
||||||
|
|
||||||
|
public function triggerRestoredEvent(): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ 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).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ namespace Stancl\Tenancy\ResourceSyncing;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
|
|
||||||
|
|
@ -21,14 +20,14 @@ trait TriggerSyncingEvents
|
||||||
{
|
{
|
||||||
public static function bootTriggerSyncingEvents(): void
|
public static function bootTriggerSyncingEvents(): void
|
||||||
{
|
{
|
||||||
static::saving(static function (self $pivot) {
|
static::saving(function (self $pivot) {
|
||||||
// Try getting the central resource to see if it is available
|
// Try getting the central resource to see if it is available
|
||||||
// If it is not available, throw an exception to interrupt the saving process
|
// If it is not available, throw an exception to interrupt the saving process
|
||||||
// And prevent creating a pivot record without a central resource
|
// And prevent creating a pivot record without a central resource
|
||||||
$pivot->getCentralResourceAndTenant();
|
$pivot->getCentralResourceAndTenant();
|
||||||
});
|
});
|
||||||
|
|
||||||
static::saved(static function (self $pivot) {
|
static::saved(function (self $pivot) {
|
||||||
/**
|
/**
|
||||||
* @var static&Pivot $pivot
|
* @var static&Pivot $pivot
|
||||||
* @var SyncMaster|null $centralResource
|
* @var SyncMaster|null $centralResource
|
||||||
|
|
@ -41,7 +40,7 @@ trait TriggerSyncingEvents
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
static::deleting(static function (self $pivot) {
|
static::deleting(function (self $pivot) {
|
||||||
/**
|
/**
|
||||||
* @var static&Pivot $pivot
|
* @var static&Pivot $pivot
|
||||||
* @var SyncMaster|null $centralResource
|
* @var SyncMaster|null $centralResource
|
||||||
|
|
@ -80,13 +79,13 @@ trait TriggerSyncingEvents
|
||||||
*/
|
*/
|
||||||
protected function getResourceClass(): string
|
protected function getResourceClass(): string
|
||||||
{
|
{
|
||||||
/** @var $this&(Pivot|MorphPivot|((Pivot|MorphPivot)&PivotWithCentralResource)) $this */
|
/** @var $this&(Pivot|MorphPivot|((Pivot|MorphPivot)&PivotWithRelation)) $this */
|
||||||
if ($this instanceof PivotWithCentralResource) {
|
if ($this instanceof PivotWithRelation) {
|
||||||
return $this->getCentralResourceClass();
|
return $this->getRelatedModel()::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this instanceof MorphPivot) {
|
if ($this instanceof MorphPivot) {
|
||||||
return Relation::getMorphedModel($this->morphClass) ?? $this->morphClass;
|
return $this->morphClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new CentralResourceNotAvailableInPivotException;
|
throw new CentralResourceNotAvailableInPivotException;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,20 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Tests\Etc\ResourceSyncing;
|
namespace Stancl\Tenancy\Tests\Etc\ResourceSyncing;
|
||||||
|
|
||||||
use Stancl\Tenancy\ResourceSyncing\PivotWithCentralResource;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Stancl\Tenancy\ResourceSyncing\PivotWithRelation;
|
||||||
use Stancl\Tenancy\ResourceSyncing\TenantPivot;
|
use Stancl\Tenancy\ResourceSyncing\TenantPivot;
|
||||||
|
|
||||||
class CustomPivot extends TenantPivot implements PivotWithCentralResource
|
class CustomPivot extends TenantPivot implements PivotWithRelation
|
||||||
{
|
{
|
||||||
public function getCentralResourceClass(): string
|
public function users(): BelongsToMany
|
||||||
{
|
{
|
||||||
return CentralUser::class;
|
return $this->belongsToMany(CentralUser::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelatedModel(): Model
|
||||||
|
{
|
||||||
|
return $this->users()->getModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ 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,13 +46,6 @@ 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\Events\TenantDeleted;
|
|
||||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceDeleted;
|
|
||||||
use Stancl\Tenancy\ResourceSyncing\Listeners\DeleteAllTenantMappings;
|
|
||||||
use Stancl\Tenancy\ResourceSyncing\Listeners\DeleteResourceMapping;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.bootstrappers' => [
|
config(['tenancy.bootstrappers' => [
|
||||||
|
|
@ -76,7 +69,6 @@ beforeEach(function () {
|
||||||
CreateTenantResource::$shouldQueue = false;
|
CreateTenantResource::$shouldQueue = false;
|
||||||
DeleteResourceInTenant::$shouldQueue = false;
|
DeleteResourceInTenant::$shouldQueue = false;
|
||||||
UpdateOrCreateSyncedResource::$scopeGetModelQuery = null;
|
UpdateOrCreateSyncedResource::$scopeGetModelQuery = null;
|
||||||
DeleteAllTenantMappings::$pivotTables = ['tenant_resources' => 'tenant_id'];
|
|
||||||
|
|
||||||
// Reset global scopes on models (should happen automatically but to make this more explicit)
|
// Reset global scopes on models (should happen automatically but to make this more explicit)
|
||||||
Model::clearBootedModels();
|
Model::clearBootedModels();
|
||||||
|
|
@ -100,7 +92,6 @@ 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);
|
||||||
|
|
@ -264,7 +255,7 @@ test('attaching central resources to tenants or vice versa creates synced tenant
|
||||||
expect(TenantUser::all())->toHaveCount(0);
|
expect(TenantUser::all())->toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attaching resources to tenants requires using a pivot that implements the PivotWithCentralResource interface
|
// Attaching resources to tenants requires using a pivot that implements the PivotWithRelation interface
|
||||||
$tenant->customPivotUsers()->attach($createCentralUser());
|
$tenant->customPivotUsers()->attach($createCentralUser());
|
||||||
$createCentralUser()->tenants()->attach($tenant);
|
$createCentralUser()->tenants()->attach($tenant);
|
||||||
|
|
||||||
|
|
@ -288,7 +279,7 @@ test('detaching central users from tenants or vice versa force deletes the synce
|
||||||
migrateUsersTableForTenants();
|
migrateUsersTableForTenants();
|
||||||
|
|
||||||
if ($attachUserToTenant) {
|
if ($attachUserToTenant) {
|
||||||
// Attaching resources to tenants requires using a pivot that implements the PivotWithCentralResource interface
|
// Attaching resources to tenants requires using a pivot that implements the PivotWithRelation interface
|
||||||
$tenant->customPivotUsers()->attach($centralUser);
|
$tenant->customPivotUsers()->attach($centralUser);
|
||||||
} else {
|
} else {
|
||||||
$centralUser->tenants()->attach($tenant);
|
$centralUser->tenants()->attach($tenant);
|
||||||
|
|
@ -299,7 +290,7 @@ test('detaching central users from tenants or vice versa force deletes the synce
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($attachUserToTenant) {
|
if ($attachUserToTenant) {
|
||||||
// Detaching resources from tenants requires using a pivot that implements the PivotWithCentralResource interface
|
// Detaching resources from tenants requires using a pivot that implements the PivotWithRelation interface
|
||||||
$tenant->customPivotUsers()->detach($centralUser);
|
$tenant->customPivotUsers()->detach($centralUser);
|
||||||
} else {
|
} else {
|
||||||
$centralUser->tenants()->detach($tenant);
|
$centralUser->tenants()->detach($tenant);
|
||||||
|
|
@ -334,7 +325,7 @@ test('detaching central users from tenants or vice versa force deletes the synce
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($attachUserToTenant) {
|
if ($attachUserToTenant) {
|
||||||
// Detaching resources from tenants requires using a pivot that implements the PivotWithCentralResource interface
|
// Detaching resources from tenants requires using a pivot that implements the PivotWithRelation interface
|
||||||
$tenant->customPivotUsers()->detach($centralUserWithSoftDeletes);
|
$tenant->customPivotUsers()->detach($centralUserWithSoftDeletes);
|
||||||
} else {
|
} else {
|
||||||
$centralUserWithSoftDeletes->tenants()->detach($tenant);
|
$centralUserWithSoftDeletes->tenants()->detach($tenant);
|
||||||
|
|
@ -899,54 +890,7 @@ 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', function (bool $dbLevelOnCascadeDelete, bool $morphPivot) {
|
test('tenant pivot records are deleted along with the tenants to which they belong to', function() {
|
||||||
[$tenant] = createTenantsAndRunMigrations();
|
|
||||||
|
|
||||||
if ($morphPivot) {
|
|
||||||
config(['tenancy.models.tenant' => MorphTenant::class]);
|
|
||||||
$centralUserModel = BaseCentralUser::class;
|
|
||||||
|
|
||||||
// The default pivot table, no need to configure the listener
|
|
||||||
$pivotTable = 'tenant_resources';
|
|
||||||
} else {
|
|
||||||
$centralUserModel = CentralUser::class;
|
|
||||||
|
|
||||||
// Custom pivot table
|
|
||||||
$pivotTable = 'tenant_users';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dbLevelOnCascadeDelete) {
|
|
||||||
addTenantIdConstraintToPivot($pivotTable);
|
|
||||||
} else {
|
|
||||||
// Event-based cleanup
|
|
||||||
Event::listen(TenantDeleted::class, DeleteAllTenantMappings::class);
|
|
||||||
|
|
||||||
DeleteAllTenantMappings::$pivotTables = [$pivotTable => 'tenant_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$syncMaster = $centralUserModel::create([
|
|
||||||
'global_id' => 'user',
|
|
||||||
'name' => 'Central user',
|
|
||||||
'email' => 'central@localhost',
|
|
||||||
'password' => 'password',
|
|
||||||
'role' => 'user',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$syncMaster->tenants()->attach($tenant);
|
|
||||||
|
|
||||||
// Pivot records should be deleted along with the tenant
|
|
||||||
$tenant->delete();
|
|
||||||
|
|
||||||
expect(DB::select("SELECT * FROM {$pivotTable} WHERE tenant_id = ?", [$tenant->getTenantKey()]))->toHaveCount(0);
|
|
||||||
})->with([
|
|
||||||
'db level on cascade delete' => true,
|
|
||||||
'event-based on cascade delete' => false,
|
|
||||||
])->with([
|
|
||||||
'polymorphic pivot' => true,
|
|
||||||
'basic pivot' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
test('pivot record is automatically deleted with the tenant resource', function() {
|
|
||||||
[$tenant] = createTenantsAndRunMigrations();
|
[$tenant] = createTenantsAndRunMigrations();
|
||||||
|
|
||||||
$syncMaster = CentralUser::create([
|
$syncMaster = CentralUser::create([
|
||||||
|
|
@ -959,54 +903,10 @@ test('pivot record is automatically deleted with the tenant resource', function(
|
||||||
|
|
||||||
$syncMaster->tenants()->attach($tenant);
|
$syncMaster->tenants()->attach($tenant);
|
||||||
|
|
||||||
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->id]))->toHaveCount(1);
|
$tenant->delete();
|
||||||
|
|
||||||
$tenant->run(function () {
|
// Deleting tenant deletes its pivot records
|
||||||
TenantUser::firstWhere('global_id', 'cascade_user')->delete();
|
expect(DB::select("SELECT * FROM tenant_users WHERE tenant_id = ?", [$tenant->getTenantKey()]))->toHaveCount(0);
|
||||||
});
|
|
||||||
|
|
||||||
// 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('DeleteAllTenantMappings handles incorrect configuration correctly', function() {
|
|
||||||
Event::listen(TenantDeleted::class, DeleteAllTenantMappings::class);
|
|
||||||
|
|
||||||
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
|
|
||||||
|
|
||||||
// Existing table, non-existent tenant key column
|
|
||||||
// The listener should throw an 'unknown column' exception
|
|
||||||
DeleteAllTenantMappings::$pivotTables = ['tenant_users' => 'non_existent_column'];
|
|
||||||
|
|
||||||
// Should throw an exception when tenant is deleted
|
|
||||||
expect(fn() => $tenant1->delete())->toThrow(QueryException::class, "Unknown column 'non_existent_column' in 'where clause'");
|
|
||||||
|
|
||||||
// Non-existent table
|
|
||||||
DeleteAllTenantMappings::$pivotTables = ['nonexistent_pivot' => 'non_existent_column'];
|
|
||||||
|
|
||||||
expect(fn() => $tenant2->delete())->toThrow(QueryException::class, "Table 'main.nonexistent_pivot' doesn't exist");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('trashed resources are synced correctly', function () {
|
test('trashed resources are synced correctly', function () {
|
||||||
|
|
@ -1365,60 +1265,6 @@ 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');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('attach and detach events are handled correctly when using morph maps', function() {
|
|
||||||
config(['tenancy.models.tenant' => MorphTenant::class]);
|
|
||||||
[$tenant] = createTenantsAndRunMigrations();
|
|
||||||
migrateCompaniesTableForTenants();
|
|
||||||
|
|
||||||
Relation::morphMap([
|
|
||||||
'users' => BaseCentralUser::class,
|
|
||||||
'companies' => CentralCompany::class,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$centralUser = BaseCentralUser::create([
|
|
||||||
'global_id' => 'user',
|
|
||||||
'name' => 'Central user',
|
|
||||||
'email' => 'central@localhost',
|
|
||||||
'password' => 'password',
|
|
||||||
'role' => 'user',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$centralCompany = CentralCompany::create([
|
|
||||||
'global_id' => 'company',
|
|
||||||
'name' => 'Central company',
|
|
||||||
'email' => 'company@localhost',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant->users()->attach($centralUser);
|
|
||||||
$tenant->companies()->attach($centralCompany);
|
|
||||||
|
|
||||||
// Assert all tenant_resources mappings actually use the configured morph map
|
|
||||||
expect(DB::table('tenant_resources')->count())
|
|
||||||
->toBe(DB::table('tenant_resources')->whereIn('tenant_resources_type', ['users', 'companies'])->count());
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
expect(BaseTenantUser::whereGlobalId('user')->first())->not()->toBeNull();
|
|
||||||
expect(TenantCompany::whereGlobalId('company')->first())->not()->toBeNull();
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
$tenant->users()->detach($centralUser);
|
|
||||||
$tenant->companies()->detach($centralCompany);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
expect(BaseTenantUser::whereGlobalId('user')->first())->toBeNull();
|
|
||||||
expect(TenantCompany::whereGlobalId('company')->first())->toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
function addTenantIdConstraintToPivot(string $pivotTable): void
|
|
||||||
{
|
|
||||||
Schema::table($pivotTable, function (Blueprint $table) {
|
|
||||||
$table->foreign('tenant_id')->references('id')->on('tenants')->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