mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 21: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
|
|
@ -7,14 +7,16 @@ namespace App\Providers;
|
|||
use Illuminate\Routing\Route;
|
||||
use Stancl\Tenancy\Jobs;
|
||||
use Stancl\Tenancy\Events;
|
||||
use Stancl\Tenancy\ResourceSyncing;
|
||||
use Stancl\Tenancy\Listeners;
|
||||
use Stancl\Tenancy\Middleware;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
||||
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||
|
||||
|
|
@ -108,18 +110,29 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
Events\RevertedToCentralContext::class => [],
|
||||
|
||||
// Resource syncing
|
||||
Events\SyncedResourceSaved::class => [
|
||||
Listeners\UpdateSyncedResource::class,
|
||||
ResourceSyncing\Events\SyncedResourceSaved::class => [
|
||||
ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::class,
|
||||
],
|
||||
ResourceSyncing\Events\SyncMasterDeleted::class => [
|
||||
ResourceSyncing\Listeners\DeleteResourcesInTenants::class,
|
||||
],
|
||||
ResourceSyncing\Events\SyncMasterRestored::class => [
|
||||
ResourceSyncing\Listeners\RestoreResourcesInTenants::class,
|
||||
],
|
||||
ResourceSyncing\Events\CentralResourceAttachedToTenant::class => [
|
||||
ResourceSyncing\Listeners\CreateTenantResource::class,
|
||||
],
|
||||
ResourceSyncing\Events\CentralResourceDetachedFromTenant::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)
|
||||
ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase::class => [],
|
||||
|
||||
// Storage symlinks
|
||||
Events\CreatingStorageSymlink::class => [],
|
||||
Events\StorageSymlinkCreated::class => [],
|
||||
Events\RemovingStorageSymlink::class => [],
|
||||
Events\StorageSymlinkRemoved::class => [],
|
||||
|
||||
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
|
||||
Events\SyncedResourceChangedInForeignDatabase::class => [],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +164,16 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
$this->makeTenancyMiddlewareHighestPriority();
|
||||
$this->overrideUrlInTenantContext();
|
||||
|
||||
/**
|
||||
* Include soft deleted resources in synced resource queries.
|
||||
*
|
||||
* ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
|
||||
* if ($query->hasMacro('withTrashed')) {
|
||||
* $query->withTrashed();
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
|
||||
/**
|
||||
* To make Livewire v3 work with Tenancy, make the update route universal.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
51
tests/Etc/ResourceSyncing/CentralUser.php
Normal file
51
tests/Etc/ResourceSyncing/CentralUser.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Database\Concerns\CentralConnection;
|
||||
use Stancl\Tenancy\ResourceSyncing\ResourceSyncing;
|
||||
use Stancl\Tenancy\ResourceSyncing\SyncMaster;
|
||||
|
||||
class CentralUser extends Model implements SyncMaster
|
||||
{
|
||||
use ResourceSyncing, CentralConnection;
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public $table = 'users';
|
||||
|
||||
public static array $syncedAttributes = [];
|
||||
|
||||
public static array $creationAttributes = [];
|
||||
|
||||
public static bool $shouldSync = true;
|
||||
|
||||
public function getTenantModelName(): string
|
||||
{
|
||||
return TenantUser::class;
|
||||
}
|
||||
|
||||
public function shouldSync(): bool
|
||||
{
|
||||
return static::$shouldSync;
|
||||
}
|
||||
|
||||
public function getCentralModelName(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return static::$syncedAttributes;
|
||||
}
|
||||
|
||||
public function getCreationAttributes(): array
|
||||
{
|
||||
return count(static::$creationAttributes) ? static::$creationAttributes : $this->getSyncedAttributeNames();
|
||||
}
|
||||
}
|
||||
23
tests/Etc/ResourceSyncing/CustomPivot.php
Normal file
23
tests/Etc/ResourceSyncing/CustomPivot.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Stancl\Tenancy\ResourceSyncing\PivotWithRelation;
|
||||
use Stancl\Tenancy\ResourceSyncing\TenantPivot;
|
||||
|
||||
class CustomPivot extends TenantPivot implements PivotWithRelation
|
||||
{
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(CentralUser::class);
|
||||
}
|
||||
|
||||
public function getRelatedModel(): Model
|
||||
{
|
||||
return $this->users()->getModel();
|
||||
}
|
||||
}
|
||||
31
tests/Etc/ResourceSyncing/Tenant.php
Normal file
31
tests/Etc/ResourceSyncing/Tenant.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc\ResourceSyncing;
|
||||
|
||||
use CentralUserWithSoftDeletes;
|
||||
use Stancl\Tenancy\ResourceSyncing\TenantPivot;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant as BaseTenant;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Tenant extends BaseTenant
|
||||
{
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id')
|
||||
->using(TenantPivot::class);
|
||||
}
|
||||
|
||||
public function customPivotUsers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id', 'users')
|
||||
->using(CustomPivot::class);
|
||||
}
|
||||
|
||||
public function softDeletesUsers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(CentralUserWithSoftDeletes::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id', 'users')
|
||||
->using(CustomPivot::class);
|
||||
}
|
||||
}
|
||||
46
tests/Etc/ResourceSyncing/TenantUser.php
Normal file
46
tests/Etc/ResourceSyncing/TenantUser.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc\ResourceSyncing;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\ResourceSyncing\ResourceSyncing;
|
||||
use Stancl\Tenancy\ResourceSyncing\Syncable;
|
||||
|
||||
class TenantUser extends Model implements Syncable
|
||||
{
|
||||
use ResourceSyncing;
|
||||
|
||||
protected $table = 'users';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public static array $syncedAttributes = [];
|
||||
|
||||
public static array $creationAttributes = [];
|
||||
|
||||
public static bool $shouldSync = true;
|
||||
|
||||
public function shouldSync(): bool
|
||||
{
|
||||
return static::$shouldSync;
|
||||
}
|
||||
|
||||
public function getCentralModelName(): string
|
||||
{
|
||||
return CentralUser::class;
|
||||
}
|
||||
|
||||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return static::$syncedAttributes;
|
||||
}
|
||||
|
||||
public function getCreationAttributes(): array
|
||||
{
|
||||
return count(static::$creationAttributes) ? static::$creationAttributes : $this->getSyncedAttributeNames();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddDeletedAtToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dateTime('deleted_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -2,23 +2,23 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Events\CallQueuedListener;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Events\BootstrappingTenancy;
|
||||
use Stancl\Tenancy\Events\CreatingDatabase;
|
||||
use Stancl\Tenancy\Events\CreatingTenant;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\UpdatingDomain;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||
use Illuminate\Events\CallQueuedListener;
|
||||
use Stancl\Tenancy\Events\CreatingTenant;
|
||||
use Stancl\Tenancy\Events\UpdatingDomain;
|
||||
use Stancl\Tenancy\Events\CreatingDatabase;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Events\BootstrappingTenancy;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
|
||||
beforeEach(function () {
|
||||
FooListener::$shouldQueue = false;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Listeners\QueueableListener;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->mockConsoleOutput = false;
|
||||
|
|
@ -117,7 +118,30 @@ test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) {
|
|||
$tenant->run(function () use ($user) {
|
||||
expect($user->fresh()->name)->toBe('Bar');
|
||||
});
|
||||
})->with([true, false]);;
|
||||
})->with([true, false]);
|
||||
|
||||
test('changing the shouldQueue static property in parent class affects child classes unless the property is redefined', function () {
|
||||
// Parent – $shouldQueue is true
|
||||
expect(app(ShouldQueueListener::class)->shouldQueue(new stdClass()))->toBeTrue();
|
||||
|
||||
// Child – $shouldQueue is redefined and set to false
|
||||
expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
||||
|
||||
// Child – inherits $shouldQueue from ShouldQueueListener (true)
|
||||
expect(app(InheritedQueueListener::class)->shouldQueue(new stdClass()))->toBeTrue();
|
||||
|
||||
// Update $shouldQueue of InheritedQueueListener's parent to see if it affects the child
|
||||
ShouldQueueListener::$shouldQueue = false;
|
||||
|
||||
// Parent's $shouldQueue changed to false
|
||||
expect(app(InheritedQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
||||
|
||||
ShouldQueueListener::$shouldQueue = true;
|
||||
|
||||
// Parent's $shouldQueue changed back to true
|
||||
// Child's $shouldQueue is still false because it was redefined
|
||||
expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
|
||||
});
|
||||
|
||||
test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy) {
|
||||
withFailedJobs();
|
||||
|
|
@ -254,3 +278,31 @@ class TestJob implements ShouldQueue
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShouldQueueListener extends QueueableListener
|
||||
{
|
||||
public static bool $shouldQueue = true;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
return static::$shouldQueue;
|
||||
}
|
||||
}
|
||||
|
||||
class ShouldNotQueueListener extends ShouldQueueListener
|
||||
{
|
||||
public static bool $shouldQueue = false;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
return static::$shouldQueue;
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedQueueListener extends ShouldQueueListener
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
return static::$shouldQueue;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,395 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Contracts\SyncMaster;
|
||||
use Stancl\Tenancy\Database\Concerns\CentralConnection;
|
||||
use Stancl\Tenancy\Database\Concerns\ResourceSyncing;
|
||||
use Stancl\Tenancy\Database\DatabaseConfig;
|
||||
use Stancl\Tenancy\Database\Models\TenantMorphPivot;
|
||||
use Stancl\Tenancy\Database\Models\TenantPivot;
|
||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Listeners\UpdateSyncedResource;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.models.tenant' => ResourceTenantUsingPolymorphic::class,
|
||||
]);
|
||||
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
DatabaseConfig::generateDatabaseNamesUsing(function () {
|
||||
return 'db' . Str::random(16);
|
||||
});
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class);
|
||||
|
||||
// Run migrations on central connection
|
||||
pest()->artisan('migrate', [
|
||||
'--path' => [
|
||||
__DIR__ . '/../assets/resource-syncing-migrations',
|
||||
__DIR__ . '/Etc/synced_resource_migrations/users',
|
||||
__DIR__ . '/Etc/synced_resource_migrations/companies',
|
||||
],
|
||||
'--realpath' => true,
|
||||
])->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('resource syncing works using a single pivot table for multiple models when syncing from central to tenant', function () {
|
||||
$tenant1 = ResourceTenantUsingPolymorphic::create(['id' => 't1']);
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
$centralUser = CentralUserUsingPolymorphic::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
$tenant1->run(function () {
|
||||
expect(TenantUserUsingPolymorphic::all())->toHaveCount(0);
|
||||
});
|
||||
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
// Assert `tenants` are accessible
|
||||
expect($centralUser->tenants->pluck('id')->toArray())->toBe(['t1']);
|
||||
|
||||
// Users are accessible from tenant
|
||||
expect($tenant1->users()->pluck('email')->toArray())->toBe(['john@localhost']);
|
||||
|
||||
// Assert User resource is synced
|
||||
$tenant1->run(function () use ($centralUser) {
|
||||
$tenantUser = TenantUserUsingPolymorphic::first()->toArray();
|
||||
$centralUser = $centralUser->withoutRelations()->toArray();
|
||||
unset($centralUser['id'], $tenantUser['id']);
|
||||
|
||||
expect($tenantUser)->toBe($centralUser);
|
||||
});
|
||||
|
||||
$tenant2 = ResourceTenantUsingPolymorphic::create(['id' => 't2']);
|
||||
migrateCompaniesTableForTenants();
|
||||
|
||||
$centralCompany = CentralCompanyUsingPolymorphic::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'ArchTech',
|
||||
'email' => 'archtech@localhost',
|
||||
]);
|
||||
|
||||
$tenant2->run(function () {
|
||||
expect(TenantCompanyUsingPolymorphic::all())->toHaveCount(0);
|
||||
});
|
||||
|
||||
$centralCompany->tenants()->attach('t2');
|
||||
|
||||
// Assert `tenants` are accessible
|
||||
expect($centralCompany->tenants->pluck('id')->toArray())->toBe(['t2']);
|
||||
|
||||
// Companies are accessible from tenant
|
||||
expect($tenant2->companies()->pluck('email')->toArray())->toBe(['archtech@localhost']);
|
||||
|
||||
// Assert Company resource is synced
|
||||
$tenant2->run(function () use ($centralCompany) {
|
||||
$tenantCompany = TenantCompanyUsingPolymorphic::first()->toArray();
|
||||
$centralCompany = $centralCompany->withoutRelations()->toArray();
|
||||
|
||||
unset($centralCompany['id'], $tenantCompany['id']);
|
||||
|
||||
expect($tenantCompany)->toBe($centralCompany);
|
||||
});
|
||||
});
|
||||
|
||||
test('resource syncing works using a single pivot table for multiple models when syncing from tenant to central', function () {
|
||||
$tenant1 = ResourceTenantUsingPolymorphic::create(['id' => 't1']);
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
$tenantUser = TenantUserUsingPolymorphic::create([
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
// Assert User resource is synced
|
||||
$centralUser = CentralUserUsingPolymorphic::first();
|
||||
|
||||
// Assert `tenants` are accessible
|
||||
expect($centralUser->tenants->pluck('id')->toArray())->toBe(['t1']);
|
||||
|
||||
// Users are accessible from tenant
|
||||
expect($tenant1->users()->pluck('email')->toArray())->toBe(['john@localhost']);
|
||||
|
||||
$centralUser = $centralUser->withoutRelations()->toArray();
|
||||
$tenantUser = $tenantUser->toArray();
|
||||
unset($centralUser['id'], $tenantUser['id']);
|
||||
|
||||
// array keys use a different order here
|
||||
expect($tenantUser)->toEqualCanonicalizing($centralUser);
|
||||
|
||||
$tenant2 = ResourceTenantUsingPolymorphic::create(['id' => 't2']);
|
||||
migrateCompaniesTableForTenants();
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
$tenantCompany = TenantCompanyUsingPolymorphic::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'tenant comp',
|
||||
'email' => 'company@localhost',
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
// Assert Company resource is synced
|
||||
$centralCompany = CentralCompanyUsingPolymorphic::first();
|
||||
|
||||
// Assert `tenants` are accessible
|
||||
expect($centralCompany->tenants->pluck('id')->toArray())->toBe(['t2']);
|
||||
|
||||
// Companies are accessible from tenant
|
||||
expect($tenant2->companies()->pluck('email')->toArray())->toBe(['company@localhost']);
|
||||
|
||||
$centralCompany = $centralCompany->withoutRelations()->toArray();
|
||||
$tenantCompany = $tenantCompany->toArray();
|
||||
unset($centralCompany['id'], $tenantCompany['id']);
|
||||
|
||||
expect($tenantCompany)->toBe($centralCompany);
|
||||
});
|
||||
|
||||
test('right resources are accessible from the tenant', function () {
|
||||
$tenant1 = ResourceTenantUsingPolymorphic::create(['id' => 't1']);
|
||||
$tenant2 = ResourceTenantUsingPolymorphic::create(['id' => 't2']);
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
$user1 = CentralUserUsingPolymorphic::create([
|
||||
'global_id' => 'user1',
|
||||
'name' => 'user1',
|
||||
'email' => 'user1@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
$user2 = CentralUserUsingPolymorphic::create([
|
||||
'global_id' => 'user2',
|
||||
'name' => 'user2',
|
||||
'email' => 'user2@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
$user3 = CentralUserUsingPolymorphic::create([
|
||||
'global_id' => 'user3',
|
||||
'name' => 'user3',
|
||||
'email' => 'user3@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
$user1->tenants()->attach('t1');
|
||||
$user2->tenants()->attach('t1');
|
||||
$user3->tenants()->attach('t2');
|
||||
|
||||
expect($tenant1->users()->pluck('email')->toArray())->toBe([$user1->email, $user2->email]);
|
||||
expect($tenant2->users()->pluck('email')->toArray())->toBe([$user3->email]);
|
||||
});
|
||||
|
||||
function migrateCompaniesTableForTenants(): void
|
||||
{
|
||||
pest()->artisan('tenants:migrate', [
|
||||
'--path' => __DIR__ . '/Etc/synced_resource_migrations/companies',
|
||||
'--realpath' => true,
|
||||
])->assertExitCode(0);
|
||||
}
|
||||
|
||||
// Tenant model used for resource syncing setup
|
||||
class ResourceTenantUsingPolymorphic extends Tenant
|
||||
{
|
||||
public function users(): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(CentralUserUsingPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
|
||||
->using(TenantMorphPivot::class);
|
||||
}
|
||||
|
||||
public function companies(): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(CentralCompanyUsingPolymorphic::class, 'tenant_resources', 'tenant_resources', 'tenant_id', 'resource_global_id', 'id', 'global_id')
|
||||
->using(TenantMorphPivot::class);
|
||||
}
|
||||
}
|
||||
|
||||
class CentralUserUsingPolymorphic extends Model implements SyncMaster
|
||||
{
|
||||
use ResourceSyncing, CentralConnection;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public $table = 'users';
|
||||
|
||||
public function getTenantModelName(): string
|
||||
{
|
||||
return TenantUserUsingPolymorphic::class;
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKey(): string|int
|
||||
{
|
||||
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKeyName(): string
|
||||
{
|
||||
return 'global_id';
|
||||
}
|
||||
|
||||
public function getCentralModelName(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return [
|
||||
'global_id',
|
||||
'name',
|
||||
'password',
|
||||
'email',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class TenantUserUsingPolymorphic extends Model implements Syncable
|
||||
{
|
||||
use ResourceSyncing;
|
||||
|
||||
protected $table = 'users';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public function getGlobalIdentifierKey(): string|int
|
||||
{
|
||||
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKeyName(): string
|
||||
{
|
||||
return 'global_id';
|
||||
}
|
||||
|
||||
public function getCentralModelName(): string
|
||||
{
|
||||
return CentralUserUsingPolymorphic::class;
|
||||
}
|
||||
|
||||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return [
|
||||
'global_id',
|
||||
'name',
|
||||
'password',
|
||||
'email',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CentralCompanyUsingPolymorphic extends Model implements SyncMaster
|
||||
{
|
||||
use ResourceSyncing, CentralConnection;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public $table = 'companies';
|
||||
|
||||
public function getTenantModelName(): string
|
||||
{
|
||||
return TenantCompanyUsingPolymorphic::class;
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKey(): string|int
|
||||
{
|
||||
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKeyName(): string
|
||||
{
|
||||
return 'global_id';
|
||||
}
|
||||
|
||||
public function getCentralModelName(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return [
|
||||
'global_id',
|
||||
'name',
|
||||
'email',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class TenantCompanyUsingPolymorphic extends Model implements Syncable
|
||||
{
|
||||
use ResourceSyncing;
|
||||
|
||||
protected $table = 'companies';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public function getGlobalIdentifierKey(): string|int
|
||||
{
|
||||
return $this->getAttribute($this->getGlobalIdentifierKeyName());
|
||||
}
|
||||
|
||||
public function getGlobalIdentifierKeyName(): string
|
||||
{
|
||||
return 'global_id';
|
||||
}
|
||||
|
||||
public function getCentralModelName(): string
|
||||
{
|
||||
return CentralCompanyUsingPolymorphic::class;
|
||||
}
|
||||
|
||||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return [
|
||||
'global_id',
|
||||
'name',
|
||||
'email',
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue