mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 14:34:04 +00:00
Resource syncing rework (#30)
* Add &Model to docblock * Fix code style (php-cs-fixer) * Only delete synced resource if the central resource shouldSync * Add central resource detached event and listener * Add SyncedTenant interface * Use the event & listener in the test file * Add getGlobalIdentifierKey(Name) to TenantMorphPivot * Refactor TriggerSyncingEvents * Fix code style (php-cs-fixer) * Test queueing the detaching listener * Move finding the central resource into the event, naming changes * Fix code style (php-cs-fixer) * Simplify listener code * Refactor detaching logic * Create tenant resource after attaching central to tenant, test queueing related listener * Delete dd() * Fix code style (php-cs-fixer) * Move triggerAttachEvent from SyncMaster * Update attach event-related code * Move findResource from SyncedTenant to the pivot trait * Add annotation * Update annotation * Simplify getAttributesForCreation in CreateTenantResourceFromSyncMaster * Update naming * Add tenant trait for attaching/detaching resources * Update test names * Move creation attribute parsing method to trait * Rename variable * Fix code style (php-cs-fixer) * Delete complete to-do * Delete event comment * Rename event property * Find tenant resource in detach listener * Use global ID key of tenant resource in cascade deletes listener * Use global ID key name of the central resource while creating/deleting tenant resources * Add getSyncedCreationAttributes example in the annotation * Fix inconsistencies in SyncedTenant methods * Improve annotation * Don't return the query in `$scopeGetModelQuery` Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Fix code style (php-cs-fixer) * Update scoping getModel query * Only use detach event instead of using both detach and delete events, refactor code * Test that detaching tenant from a central resource doesn't affect other tenants * Delete extra imports * Fix code style (php-cs-fixer) * Add PivotWithRelation, test attaching/detaching resources without polymorphic relations * Refactor TriggerSyncingEvents to work with non-polymorphic relations too * Fix code style (php-cs-fixer) * Rename synced resource changed event, fix tests * Enforce passing Tenant&Model to attach/detach events * Prevent firing saved event automatically in CreateTenantResource * Improve TriggerSyncingEvents trait * Delete unused import * Make TriggerSyncingEvents methods non-static, improve annotations * Pass saved model to event * Move attach/detach queueing tests to ResourceSyncingTest, pass models instead of IDs to attach/detach * Move events to ResourceSyncing\Events * Fix code style (php-cs-fixer) * Use SerializesModels in queueable listeners instead of events * Delete redundant $shouldQueue setting * Rename listener, test cascade deletes from both sides of many-to-many * Move creation attributes-related code to a separate test file, improve comments (wip) * Improve comments, fix variable name capitalization * Delete tracing comma * Extract duplicate code into a trait * Don't accept nullable tenant in SyncMasterDeleted * Fix annotation * Fix code style (php-cs-fixer) * Update annotation * Fix PHPStan error * Fix annotation * Update comments and test naming * Move triggerDeleteEvent to CascadeDeletes interface * Rename test file * Import TenantPivot in Tenant class (tests/Etc) * Add central resource not available in pivot exception * Rename SaveSyncedResource to UpdateOrCreateSyncedResource * Add new events and listeners to TSP stub * Improve comments and naming * Only keep SerializesModels in classes that utilize it * Use tenant->run() * Import events in stub * Move RS listeners to separate namespace, use `Event/`Listener/` in stub for consistency * Fix code style (php-cs-fixer) * Fix namespace changes * Use cursor instead of get * Update src/ResourceSyncing/ParsesCreationAttributes.php Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Update naming, structure (discussed on Discord) * Update uses in in test file * remove double ;; * Add comments * Test if static properties work fine with queueable listeners * Update $shouldQuery test * Update creation attributes * Work on updating the tests * Make synced attributes configurable using static properties * Update resource syncing tests * Get rid of mixed attribute classes * Get rid of TenantUserWIthCreationAttributes * Fix imports * Get rid of the conditionally synced classes, improve tests * Simplify resource creation tests (only test the more complex cases instead of each case - if the complex case works, the simpler cases work too) * Clean up ResourceSyncingTest (mostly duplicate tests that were already in AutomaticResourceCreationTest) * Simplify class naming in polymorhpic tests * Move automatic resource creation tests to ResourceSyncingTest * Test that the sync event doesn't get triggered excessively * Only trigger the sync event if the synced attributes were changed or if the resource was recently created * Update synced attribute instead of unsynced in test * Fix sync event test * Update static property redefining test * Use getGlobalIdentifierKeyName() instead of hardcoding the key name * Delete static properties from the ResourceSyncing trait * Reuse user classes in polymorphic tests * Update tests/ResourceSyncingTest.php Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Use the default tenants() method in central user, override the default in ResourceSyncingTest * Use BelongsToMany as tenants() return type * Fix code style (php-cs-fixer) * Delete extra static property from trait * Delete duplicate events/listeners from TSP stub * Delete weird expectation, use $model->trashed() * Change ResourceUser to TenantUser * Add defaults for getGlobalIdentifierKey(Name) * Use singular tenant in DeleteResourceInTenants name * Rename getSyncedCreationAttributes to getCreationAttributes * Fix comma position in comment * minor fixes in traits and interfaces * Fix code style (php-cs-fixer) * Correct comment * Use $tenant->run() * Update scopeGetModelQuery annotation * Use static property for testing shouldSync * Improve test * Get rid of datasets * Add trashed assertions * Always merge synced attributes with the creation attributes during parsing * Update creation attributes in test's beforeEach * Use only the necessary creation attributes (no need to include the synced attributes because they get merged automatically) * Rename ResourceTenant to MorphTenant * Add TriggerSyncingEvents docblock Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com> * Add force deletes test * Fix code style (php-cs-fixer) * Delete pivot if it can't access the resource class * Make parseCreationAttributes more readable * Comment out setting $scopeGetModelQuery in the stub * Add @var annotations to bootTriggerSyncingEvents * Fix attach()/detach() exception test * Interrupt creation of broken pivots instead of deleting the pivots at a later point * Add more comments * Update CreateTenantResource comment * Assert that forceDelete() doesn't affect other tenant resources * Rename test * Correct with() array formatting * Expand test with soft deletes * Merge SyncedResourceSaved tests * Improve naming, comments and minor details in the assertions * Move test * Fix failing test * Delete duplicate test * Minor test improvement * Delete duplicate test * Improve old test * Minor test improvement * Improve event test * Improve tests (naming, code, comments) * Delete extra test, add comments to the larger test * Refactor central -> tenant attach() test * Apply changes from central -> tenant attach() test on tenant -> central test * Fix assertions in central -> tenant * Correct comment and assertion * Refactor tenant -> central attach() test * Fix inconsistency * Delete unused import * Add comments * Update polymorphic test names * Rename polymorphic tests * Update listener test name * Delete redundant tenant ID assignments * Improve test names * Move polymorphic tests to ResourceSyncingTest * Mention alternative solutions in CentralResourceNotAvailableInPivotException * Add comments * Update test comments * minor changes to tests + review comments * Delete extra tests, update comments * Remove unneeded part of test * Fix comment * Improve comments * Add test for companies() realationship accessibility * Update test name * Complete to-do, add comment * Improve naming and comments (resolve some priority reviews) * Move test * Comment, resolve to-dos * Add low-level pivot assertions * Restore trashed resources if the central resource got restored, try improving tests * Fix code style (php-cs-fixer) * Dekete redundnat unsynced comments * Add to-do, test WIP * Fix restoring logic * Update todo * Add todo to fix phpdoc * Fix code style (php-cs-fixer) * PHPStan error fix wip * Fix PHPStan error * Add regression test * Delete unused trait * Add and test restoring WIP * Fix code style (php-cs-fixer) * Add to-do * Delete comment from test * Focus on restoring in the restore test * Improve maming * Fix stub * Delete redundant part of test * Delete incorrect test leftover * Add triggerRestoredEvent * Fix restore test * Correct tests and restore(() logic * Fix code style (php-cs-fixer) * Check if SoftDeletes are used before firing SyncMasterRestored * Fix comment * Revert restore action changes (phpstan errors) * Delete CascadeDeletes interface * Remove CascadeDeletes from most of the tests * Fix code style (php-cs-fixer) * Rename tests * Fix restoring + tests WIP * Fix restoring * Fix restoring tests * Fix code style (php-cs-fixer) * Test that detaching force deletes the tenant resources * Implement cacscade force deleting * Delete redundant changes * Fix typo * Fix SyncMaster * Improve test * Add force deleting logic back and fix tests * Improve comment * Delete extra assertion * Improve restoring test * Simplify assertion * Delete redundant query scoping from test * Test restore listener queueing * use strict in_array() checks * fix phpstan errors --------- Co-authored-by: lukinovec <lukinovec@gmail.com> Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
This commit is contained in:
parent
aa1437fb5e
commit
6784685054
39 changed files with 1903 additions and 1325 deletions
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Contracts;
|
||||
|
||||
interface Syncable
|
||||
{
|
||||
public function getGlobalIdentifierKeyName(): string;
|
||||
|
||||
public function getGlobalIdentifierKey(): string|int;
|
||||
|
||||
public function getCentralModelName(): string;
|
||||
|
||||
public function getSyncedAttributeNames(): array;
|
||||
|
||||
public function triggerSyncEvent(): 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). */
|
||||
public function getSyncedCreationAttributes(): array|null; // todo come up with a better name
|
||||
|
||||
public function shouldSync(): bool;
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
use Stancl\Tenancy\Database\Models\TenantMorphPivot;
|
||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||
|
||||
trait ResourceSyncing
|
||||
{
|
||||
public static function bootResourceSyncing(): void
|
||||
{
|
||||
static::saved(function (Syncable $model) {
|
||||
if ($model->shouldSync()) {
|
||||
$model->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
|
||||
static::creating(function (self $model) {
|
||||
if (! $model->getAttribute($model->getGlobalIdentifierKeyName()) && app()->bound(UniqueIdentifierGenerator::class)) {
|
||||
$model->setAttribute(
|
||||
$model->getGlobalIdentifierKeyName(),
|
||||
app(UniqueIdentifierGenerator::class)->generate($model)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function triggerSyncEvent(): void
|
||||
{
|
||||
/** @var Syncable $this */
|
||||
event(new SyncedResourceSaved($this, tenant()));
|
||||
}
|
||||
|
||||
public function getSyncedCreationAttributes(): array|null
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function shouldSync(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function tenants(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', 'global_id')
|
||||
->using(TenantMorphPivot::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
|
||||
trait TriggerSyncEvent
|
||||
{
|
||||
public static function booted(): void
|
||||
{
|
||||
static::saved(function (self $pivot) {
|
||||
$parent = $pivot->pivotParent;
|
||||
|
||||
if ($parent instanceof Syncable && $parent->shouldSync()) {
|
||||
$parent->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Stancl\Tenancy\Database\Concerns\TriggerSyncEvent;
|
||||
|
||||
class TenantPivot extends Pivot
|
||||
{
|
||||
use TriggerSyncEvent;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
|
||||
class SyncedResourceChangedInForeignDatabase
|
||||
{
|
||||
/** @var Syncable */
|
||||
public $model;
|
||||
|
||||
/** @var TenantWithDatabase|null */
|
||||
public $tenant;
|
||||
|
||||
public function __construct(Syncable $model, ?TenantWithDatabase $tenant)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,13 +8,12 @@ use Illuminate\Bus\Queueable;
|
|||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Commands\ClearPendingTenants as ClearPendingTenantsCommand;
|
||||
|
||||
class ClearPendingTenants implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
use Dispatchable, InteractsWithQueue, Queueable;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ use Illuminate\Bus\Queueable;
|
|||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Commands\CreatePendingTenants as CreatePendingTenantsCommand;
|
||||
|
||||
class CreatePendingTenants implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
use Dispatchable, InteractsWithQueue, Queueable;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CentralResourceNotAvailableInPivotException extends Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'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 make this work both ways, you can make your pivot implement PivotWithRelation and return the related model in getRelatedModel() or extend MorphPivot.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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\SyncMaster;
|
||||
|
||||
class CentralResourceAttachedToTenant
|
||||
{
|
||||
public function __construct(
|
||||
public SyncMaster&Model $centralResource,
|
||||
public TenantWithDatabase $tenant,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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\SyncMaster;
|
||||
|
||||
class CentralResourceDetachedFromTenant
|
||||
{
|
||||
public function __construct(
|
||||
public SyncMaster&Model $centralResource,
|
||||
public TenantWithDatabase $tenant,
|
||||
) {
|
||||
}
|
||||
}
|
||||
17
src/ResourceSyncing/Events/SyncMasterDeleted.php
Normal file
17
src/ResourceSyncing/Events/SyncMasterDeleted.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||
|
||||
class SyncMasterDeleted
|
||||
{
|
||||
public function __construct(
|
||||
public SyncMaster&Model $centralResource,
|
||||
public bool $forceDelete = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
16
src/ResourceSyncing/Events/SyncMasterRestored.php
Normal file
16
src/ResourceSyncing/Events/SyncMasterRestored.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||
|
||||
class SyncMasterRestored
|
||||
{
|
||||
public function __construct(
|
||||
public SyncMaster&Model $centralResource
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Events;
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||
|
||||
class SyncedResourceSaved
|
||||
{
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?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;
|
||||
|
||||
/** @internal Only used for test assertions, this event does not trigger any syncing logic. */
|
||||
class SyncedResourceSavedInForeignDatabase
|
||||
{
|
||||
public function __construct(
|
||||
public Syncable&Model $model,
|
||||
public TenantWithDatabase|null $tenant
|
||||
) {
|
||||
}
|
||||
}
|
||||
35
src/ResourceSyncing/Listeners/CreateTenantResource.php
Normal file
35
src/ResourceSyncing/Listeners/CreateTenantResource.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceAttachedToTenant;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase;
|
||||
use Stancl\Tenancy\ResourceSyncing\ParsesCreationAttributes;
|
||||
|
||||
/**
|
||||
* Create tenant resource synced to the central resource.
|
||||
*/
|
||||
class CreateTenantResource extends QueueableListener
|
||||
{
|
||||
use ParsesCreationAttributes;
|
||||
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
public function handle(CentralResourceAttachedToTenant $event): void
|
||||
{
|
||||
$tenantResourceClass = $event->centralResource->getTenantModelName();
|
||||
|
||||
$event->tenant->run(function () use ($event, $tenantResourceClass) {
|
||||
// Prevent $tenantResourceClass::create() from firing the SyncedResourceSaved event
|
||||
// Manually fire the SyncedResourceSavedInForeignDatabase event instead
|
||||
$tenantResourceClass::withoutEvents(function () use ($event, $tenantResourceClass) {
|
||||
$tenantResource = $tenantResourceClass::create($this->parseCreationAttributes($event->centralResource));
|
||||
|
||||
event(new SyncedResourceSavedInForeignDatabase($tenantResource, $event->tenant));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
23
src/ResourceSyncing/Listeners/DeleteResourceInTenant.php
Normal file
23
src/ResourceSyncing/Listeners/DeleteResourceInTenant.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
|
||||
|
||||
/**
|
||||
* When a central resource is detached from a tenant, delete the tenant resource.
|
||||
*/
|
||||
class DeleteResourceInTenant extends QueueableListener
|
||||
{
|
||||
use DeletesSyncedResources;
|
||||
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
public function handle(CentralResourceDetachedFromTenant $event): void
|
||||
{
|
||||
$event->tenant->run(fn () => $this->deleteSyncedResource($event->centralResource, true));
|
||||
}
|
||||
}
|
||||
25
src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php
Normal file
25
src/ResourceSyncing/Listeners/DeleteResourcesInTenants.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||
|
||||
class DeleteResourcesInTenants extends QueueableListener
|
||||
{
|
||||
use DeletesSyncedResources;
|
||||
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
public function handle(SyncMasterDeleted $event): void
|
||||
{
|
||||
$centralResource = $event->centralResource;
|
||||
$forceDelete = $event->forceDelete;
|
||||
|
||||
tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource, $forceDelete) {
|
||||
$this->deleteSyncedResource($centralResource, $forceDelete);
|
||||
});
|
||||
}
|
||||
}
|
||||
29
src/ResourceSyncing/Listeners/DeletesSyncedResources.php
Normal file
29
src/ResourceSyncing/Listeners/DeletesSyncedResources.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||
|
||||
trait DeletesSyncedResources
|
||||
{
|
||||
protected function deleteSyncedResource(SyncMaster&Model $centralResource, bool $force = false): void
|
||||
{
|
||||
$tenantResourceClass = $centralResource->getTenantModelName();
|
||||
|
||||
/** @var (Syncable&Model)|null $tenantResource */
|
||||
$tenantResource = $tenantResourceClass::firstWhere(
|
||||
$centralResource->getGlobalIdentifierKeyName(),
|
||||
$centralResource->getGlobalIdentifierKey()
|
||||
);
|
||||
|
||||
if ($force) {
|
||||
$tenantResource?->forceDelete();
|
||||
} else {
|
||||
$tenantResource?->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/ResourceSyncing/Listeners/RestoreResourcesInTenants.php
Normal file
43
src/ResourceSyncing/Listeners/RestoreResourcesInTenants.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
|
||||
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||
|
||||
class RestoreResourcesInTenants extends QueueableListener
|
||||
{
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
public function handle(SyncMasterRestored $event): void
|
||||
{
|
||||
/** @var SyncMaster&Model $centralResource */
|
||||
$centralResource = $event->centralResource;
|
||||
|
||||
if (! $centralResource::hasMacro('withTrashed')) {
|
||||
return;
|
||||
}
|
||||
|
||||
tenancy()->runForMultiple($centralResource->tenants()->cursor(), function () use ($centralResource) {
|
||||
$tenantResourceClass = $centralResource->getTenantModelName();
|
||||
/**
|
||||
* @var Syncable $centralResource
|
||||
* @var (SoftDeletes&Syncable)|null $tenantResource
|
||||
*/
|
||||
$tenantResource = $tenantResourceClass::withTrashed()->firstWhere(
|
||||
$centralResource->getGlobalIdentifierKeyName(),
|
||||
$centralResource->getGlobalIdentifierKey()
|
||||
);
|
||||
|
||||
if ($tenantResource) {
|
||||
$tenantResource->restore();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -2,24 +2,44 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
namespace Stancl\Tenancy\ResourceSyncing\Listeners;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Support\Arr;
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Contracts\SyncMaster;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Database\TenantCollection;
|
||||
use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase;
|
||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||
use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase;
|
||||
use Stancl\Tenancy\ResourceSyncing\ModelNotSyncMasterException;
|
||||
use Stancl\Tenancy\ResourceSyncing\ParsesCreationAttributes;
|
||||
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
class UpdateSyncedResource extends QueueableListener
|
||||
class UpdateOrCreateSyncedResource extends QueueableListener
|
||||
{
|
||||
use SerializesModels, ParsesCreationAttributes;
|
||||
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
/**
|
||||
* This static property allows you to scope the "get model query"
|
||||
* that's responsible for finding the resources that should get synced (in the getModel() method).
|
||||
*
|
||||
* For example, to include soft deleted records while syncing (excluded by default), you can use this closure:
|
||||
*
|
||||
* UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
|
||||
* if ($query->hasMacro('withTrashed')) {
|
||||
* $query->withTrashed();
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
public static Closure|null $scopeGetModelQuery = null;
|
||||
|
||||
public function handle(SyncedResourceSaved $event): void
|
||||
{
|
||||
$syncedAttributes = $event->model->only($event->model->getSyncedAttributeNames());
|
||||
|
|
@ -55,20 +75,20 @@ class UpdateSyncedResource extends QueueableListener
|
|||
|
||||
protected function updateResourceInCentralDatabaseAndGetTenants(SyncedResourceSaved $event, array $syncedAttributes): TenantCollection
|
||||
{
|
||||
/** @var (Model&SyncMaster)|null $centralModel */
|
||||
$centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey())
|
||||
->first();
|
||||
$centralModelClass = $event->model->getCentralModelName();
|
||||
|
||||
$centralModel = $this->getModel($centralModelClass, $event->model);
|
||||
|
||||
// We disable events for this call, to avoid triggering this event & listener again.
|
||||
$event->model->getCentralModelName()::withoutEvents(function () use (&$centralModel, $syncedAttributes, $event) {
|
||||
$centralModelClass::withoutEvents(function () use (&$centralModel, $syncedAttributes, $event, $centralModelClass) {
|
||||
if ($centralModel) {
|
||||
$centralModel->update($syncedAttributes);
|
||||
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
|
||||
} else {
|
||||
// If the resource doesn't exist at all in the central DB,we create
|
||||
$centralModel = $event->model->getCentralModelName()::create($this->getAttributesForCreation($event->model));
|
||||
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
|
||||
// If the resource doesn't exist at all in the central DB, we create it
|
||||
$centralModel = $centralModelClass::create($this->parseCreationAttributes($event->model));
|
||||
}
|
||||
|
||||
event(new SyncedResourceSavedInForeignDatabase($centralModel, null));
|
||||
});
|
||||
|
||||
// If the model was just created, the mapping of the tenant to the user likely doesn't exist, so we create it.
|
||||
|
|
@ -104,8 +124,8 @@ class UpdateSyncedResource extends QueueableListener
|
|||
protected function updateResourceInTenantDatabases(TenantCollection $tenants, SyncedResourceSaved $event, array $syncedAttributes): void
|
||||
{
|
||||
tenancy()->runForMultiple($tenants, function ($tenant) use ($event, $syncedAttributes) {
|
||||
// Forget instance state and find the model,
|
||||
// again in the current tenant's context.
|
||||
// Forget instance state and find the model again,
|
||||
// in the current tenant's context.
|
||||
|
||||
/** @var Model&Syncable $eventModel */
|
||||
$eventModel = $event->model;
|
||||
|
|
@ -117,8 +137,7 @@ class UpdateSyncedResource extends QueueableListener
|
|||
$localModelClass = get_class($eventModel);
|
||||
}
|
||||
|
||||
/** @var Model|null */
|
||||
$localModel = $localModelClass::firstWhere($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey());
|
||||
$localModel = $this->getModel($localModelClass, $eventModel);
|
||||
|
||||
// Also: We're syncing attributes, not columns, which is
|
||||
// why we're using Eloquent instead of direct DB queries.
|
||||
|
|
@ -128,53 +147,23 @@ class UpdateSyncedResource extends QueueableListener
|
|||
if ($localModel) {
|
||||
$localModel->update($syncedAttributes);
|
||||
} else {
|
||||
$localModel = $localModelClass::create($this->getAttributesForCreation($eventModel));
|
||||
$localModel = $localModelClass::create($this->parseCreationAttributes($eventModel));
|
||||
}
|
||||
|
||||
event(new SyncedResourceChangedInForeignDatabase($localModel, $tenant));
|
||||
event(new SyncedResourceSavedInForeignDatabase($localModel, $tenant));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected function getAttributesForCreation(Model&Syncable $model): array
|
||||
protected function getModel(string $modelClass, Syncable $eventModel): Model|null
|
||||
{
|
||||
if (! $model->getSyncedCreationAttributes()) {
|
||||
// Creation attributes are not specified so create the model as 1:1 copy
|
||||
// exclude the "primary key" because we want primary key to handle by the target model to avoid duplication errors
|
||||
$attributes = $model->getAttributes();
|
||||
unset($attributes[$model->getKeyName()]);
|
||||
/** @var Builder */
|
||||
$query = $modelClass::where($eventModel->getGlobalIdentifierKeyName(), $eventModel->getGlobalIdentifierKey());
|
||||
|
||||
return $attributes;
|
||||
if (static::$scopeGetModelQuery) {
|
||||
(static::$scopeGetModelQuery)($query);
|
||||
}
|
||||
|
||||
if (Arr::isAssoc($model->getSyncedCreationAttributes())) {
|
||||
// Developer provided the default values (key => value) or mix of default values and attribute names (values only)
|
||||
// We will merge the default values with provided attributes and sync attributes
|
||||
[$attributeNames, $defaultValues] = $this->getAttributeNamesAndDefaultValues($model);
|
||||
$attributes = $model->only(array_merge($model->getSyncedAttributeNames(), $attributeNames));
|
||||
|
||||
return array_merge($attributes, $defaultValues);
|
||||
}
|
||||
|
||||
// Developer provided the attribute names, so we'll use them to pick model attributes
|
||||
return $model->only($model->getSyncedCreationAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the attribute names (sequential index items) and default values (key => values).
|
||||
*/
|
||||
protected function getAttributeNamesAndDefaultValues(Model&Syncable $model): array
|
||||
{
|
||||
$syncedCreationAttributes = $model->getSyncedCreationAttributes() ?? [];
|
||||
|
||||
$attributes = Arr::where($syncedCreationAttributes, function ($value, $key) {
|
||||
return is_numeric($key);
|
||||
});
|
||||
|
||||
$defaultValues = Arr::where($syncedCreationAttributes, function ($value, $key) {
|
||||
return is_string($key);
|
||||
});
|
||||
|
||||
return [$attributes, $defaultValues];
|
||||
return $query->first();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Exceptions;
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Exception;
|
||||
|
||||
27
src/ResourceSyncing/ParsesCreationAttributes.php
Normal file
27
src/ResourceSyncing/ParsesCreationAttributes.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
trait ParsesCreationAttributes
|
||||
{
|
||||
protected function parseCreationAttributes(Syncable&Model $resource): array
|
||||
{
|
||||
$creationAttributes = $resource->getCreationAttributes();
|
||||
|
||||
// Merge the provided attribute names (['attribute']) with the provided defaults (['attribute2' => 'default_value'])
|
||||
// This allows mixing the two formats of providing the creation attributes (['attribute', 'attribute2' => 'default_value'])
|
||||
[$creationAttributeNames, $defaults] = [
|
||||
Arr::where($creationAttributes, fn ($value, $key) => is_numeric($key)),
|
||||
Arr::where($creationAttributes, fn ($value, $key) => is_string($key)),
|
||||
];
|
||||
|
||||
$attributeNames = array_merge($resource->getSyncedAttributeNames(), $creationAttributeNames);
|
||||
|
||||
return array_merge($resource->only($attributeNames), $defaults);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
123
src/ResourceSyncing/ResourceSyncing.php
Normal file
123
src/ResourceSyncing/ResourceSyncing.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceAttachedToTenant;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSaved;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterDeleted;
|
||||
use Stancl\Tenancy\ResourceSyncing\Events\SyncMasterRestored;
|
||||
|
||||
trait ResourceSyncing
|
||||
{
|
||||
public static function bootResourceSyncing(): void
|
||||
{
|
||||
static::saved(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && ($model->wasRecentlyCreated || $model->wasChanged($model->getSyncedAttributeNames()))) {
|
||||
$model->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
|
||||
static::deleting(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
$model->triggerDeleteEvent();
|
||||
}
|
||||
});
|
||||
|
||||
static::creating(function (Syncable&Model $model) {
|
||||
if (! $model->getAttribute($model->getGlobalIdentifierKeyName()) && app()->bound(UniqueIdentifierGenerator::class)) {
|
||||
$model->setAttribute(
|
||||
$model->getGlobalIdentifierKeyName(),
|
||||
app(UniqueIdentifierGenerator::class)->generate($model)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
|
||||
static::forceDeleting(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
$model->triggerDeleteEvent(true);
|
||||
}
|
||||
});
|
||||
|
||||
static::restoring(function (Syncable&Model $model) {
|
||||
if ($model->shouldSync() && $model instanceof SyncMaster) {
|
||||
$model->triggerRestoredEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function triggerSyncEvent(): void
|
||||
{
|
||||
/** @var Syncable&Model $this */
|
||||
event(new SyncedResourceSaved($this, tenant()));
|
||||
}
|
||||
|
||||
public function triggerDeleteEvent(bool $forceDelete = false): void
|
||||
{
|
||||
if ($this instanceof SyncMaster) {
|
||||
/** @var SyncMaster&Model $this */
|
||||
event(new SyncMasterDeleted($this, $forceDelete));
|
||||
}
|
||||
}
|
||||
|
||||
public function triggerRestoredEvent(): void
|
||||
{
|
||||
if ($this instanceof SyncMaster && in_array(SoftDeletes::class, class_uses_recursive($this), true)) {
|
||||
/** @var SyncMaster&Model $this */
|
||||
event(new SyncMasterRestored($this));
|
||||
}
|
||||
}
|
||||
|
||||
/** Default implementation for \Stancl\Tenancy\ResourceSyncing\SyncMaster */
|
||||
public function triggerAttachEvent(Tenant&Model $tenant): void
|
||||
{
|
||||
if ($this instanceof SyncMaster) {
|
||||
/** @var SyncMaster&Model $this */
|
||||
event(new CentralResourceAttachedToTenant($this, $tenant));
|
||||
}
|
||||
}
|
||||
|
||||
/** Default implementation for \Stancl\Tenancy\ResourceSyncing\SyncMaster */
|
||||
public function triggerDetachEvent(Tenant&Model $tenant): void
|
||||
{
|
||||
if ($this instanceof SyncMaster) {
|
||||
/** @var SyncMaster&Model $this */
|
||||
event(new CentralResourceDetachedFromTenant($this, $tenant));
|
||||
}
|
||||
}
|
||||
|
||||
public function getCreationAttributes(): array
|
||||
{
|
||||
return $this->getSyncedAttributeNames();
|
||||
}
|
||||
|
||||
public function shouldSync(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function tenants(): BelongsToMany
|
||||
{
|
||||
return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', $this->getGlobalIdentifierKeyName())
|
||||
->using(TenantMorphPivot::class);
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKeyName(): string
|
||||
{
|
||||
return 'global_id';
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKey(): string
|
||||
{
|
||||
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Contracts;
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
// todo move all resource syncing-related things to a separate namespace?
|
||||
|
||||
|
|
@ -17,4 +19,12 @@ interface SyncMaster extends Syncable
|
|||
public function tenants(): BelongsToMany;
|
||||
|
||||
public function getTenantModelName(): string;
|
||||
|
||||
public function triggerDetachEvent(Tenant&Model $tenant): void;
|
||||
|
||||
public function triggerAttachEvent(Tenant&Model $tenant): void;
|
||||
|
||||
public function triggerDeleteEvent(bool $forceDelete = false): void;
|
||||
|
||||
public function triggerRestoredEvent(): void;
|
||||
}
|
||||
36
src/ResourceSyncing/Syncable.php
Normal file
36
src/ResourceSyncing/Syncable.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
interface Syncable
|
||||
{
|
||||
public function getGlobalIdentifierKeyName(): string;
|
||||
|
||||
public function getGlobalIdentifierKey(): string|int;
|
||||
|
||||
public function getCentralModelName(): string;
|
||||
|
||||
public function getSyncedAttributeNames(): array;
|
||||
|
||||
public function triggerSyncEvent(): 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).
|
||||
*
|
||||
* You can also specify the default values for the attributes.
|
||||
*
|
||||
* E.g. return [
|
||||
* 'attribute',
|
||||
* 'attribute2' => 'default value',
|
||||
* ];
|
||||
*
|
||||
* In the ResourceSyncing trait, this method defaults to getSyncedAttributeNames().
|
||||
*
|
||||
* Note: These values are *merged into* getSyncedAttributeNames().
|
||||
*/
|
||||
public function getCreationAttributes(): array;
|
||||
|
||||
public function shouldSync(): bool;
|
||||
}
|
||||
|
|
@ -2,12 +2,11 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models;
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
||||
use Stancl\Tenancy\Database\Concerns\TriggerSyncEvent;
|
||||
|
||||
class TenantMorphPivot extends MorphPivot
|
||||
{
|
||||
use TriggerSyncEvent;
|
||||
use TriggerSyncingEvents;
|
||||
}
|
||||
12
src/ResourceSyncing/TenantPivot.php
Normal file
12
src/ResourceSyncing/TenantPivot.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
class TenantPivot extends Pivot
|
||||
{
|
||||
use TriggerSyncingEvents;
|
||||
}
|
||||
109
src/ResourceSyncing/TriggerSyncingEvents.php
Normal file
109
src/ResourceSyncing/TriggerSyncingEvents.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\ResourceSyncing;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
||||
|
||||
/**
|
||||
* Used on pivot models.
|
||||
*
|
||||
* @see TenantPivot
|
||||
* @see MorphPivot
|
||||
*/
|
||||
trait TriggerSyncingEvents
|
||||
{
|
||||
public static function bootTriggerSyncingEvents(): void
|
||||
{
|
||||
static::saving(function (self $pivot) {
|
||||
// Try getting the central resource to see if it is available
|
||||
// If it is not available, throw an exception to interrupt the saving process
|
||||
// And prevent creating a pivot record without a central resource
|
||||
$pivot->getCentralResourceAndTenant();
|
||||
});
|
||||
|
||||
static::saved(function (self $pivot) {
|
||||
/**
|
||||
* @var static&Pivot $pivot
|
||||
* @var SyncMaster|null $centralResource
|
||||
* @var (Tenant&Model)|null $tenant
|
||||
*/
|
||||
[$centralResource, $tenant] = $pivot->getCentralResourceAndTenant();
|
||||
|
||||
if ($tenant && $centralResource?->shouldSync()) {
|
||||
$centralResource->triggerAttachEvent($tenant);
|
||||
}
|
||||
});
|
||||
|
||||
static::deleting(function (self $pivot) {
|
||||
/**
|
||||
* @var static&Pivot $pivot
|
||||
* @var SyncMaster|null $centralResource
|
||||
* @var (Tenant&Model)|null $tenant
|
||||
*/
|
||||
[$centralResource, $tenant] = $pivot->getCentralResourceAndTenant();
|
||||
|
||||
if ($tenant && $centralResource?->shouldSync()) {
|
||||
$centralResource->triggerDetachEvent($tenant);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function getCentralResourceAndTenant(): array
|
||||
{
|
||||
/** @var static&Pivot $this */
|
||||
$parent = $this->pivotParent;
|
||||
|
||||
if ($parent instanceof Tenant) {
|
||||
// Tenant is the parent
|
||||
// $tenant->attach($resource) / $tenant->detach($resource)
|
||||
return [$this->findCentralResource(), $parent];
|
||||
}
|
||||
|
||||
// Central resource is the parent
|
||||
// $centralResource->attach($tenant) / $centralResource->detach($tenant)
|
||||
return [$parent, tenancy()->find($this->{$this->getOtherKey()})];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource class if available. Otherwise, throw an exception.
|
||||
*
|
||||
* Used in the `findCentralResource` method.
|
||||
*
|
||||
* @throws CentralResourceNotAvailableInPivotException
|
||||
*/
|
||||
protected function getResourceClass(): string
|
||||
{
|
||||
/** @var Pivot|MorphPivot|((Pivot|MorphPivot)&PivotWithRelation) $this */
|
||||
if ($this instanceof PivotWithRelation) {
|
||||
return $this->getRelatedModel()::class;
|
||||
}
|
||||
|
||||
if ($this instanceof MorphPivot) {
|
||||
return $this->morphClass;
|
||||
}
|
||||
|
||||
throw new CentralResourceNotAvailableInPivotException;
|
||||
}
|
||||
|
||||
protected function findCentralResource(): (SyncMaster&Model)|null
|
||||
{
|
||||
/**
|
||||
* Create an instance of the central resource class so that we can get the global identifier key name properly.
|
||||
*
|
||||
* @var SyncMaster&Model $centralResourceModel
|
||||
*/
|
||||
$centralResourceModel = new ($this->getResourceClass());
|
||||
|
||||
$globalId = $this->{$this->getOtherKey()};
|
||||
|
||||
/** @var (SyncMaster&Model)|null $centralResource */
|
||||
$centralResource = $centralResourceModel::firstWhere($centralResourceModel->getGlobalIdentifierKeyName(), $globalId);
|
||||
|
||||
return $centralResource;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue