Resource Syncing (draft)
The resource syncing feature allows you to sync specific columns between the central database and the tenant databases. Meaning, you can make specific attributes of the tenant models update as soon as the attributes get updated on the central model (and vice versa).
Usage
To use resource syncing, first, ensure you’re listening to the resource syncing events, (either using our default listeners – recommended, or your own custom listeners), e.g. in the TenancyServiceProvider’s events()
method:
Next, you’ll need two models: one for the central resource (SyncMaster), and one for the tenant resource (Syncable). You’ll also need to add a column for the global identifier to the tables of both resources – the central and tenant resources will be associated using this column. The default name for the column is “global_id” (to use a different column name, override the getGlobalIdentifierKeyName()
method). It is recommended to make the global ID a string – in the ResourceSyncing trait, we hook into the resource’s creating
event and if the global ID isn’t set, we generate it using the currently bound UniqueIdentifierGenerator.
Make the central resource model implement SyncMaster and the tenant resource Syncable. Then, use the ResourceSyncing trait in both models. The trait provides defaults for the methods required by the SyncMaster and Syncable interfaces. An important default to notice is the tenants()
relationship default – it uses a pivot table named tenant_resources
. You can use php artisan vendor:publish --tag=resource-syncing-migrations
to publish the migration for the tenant_resources
table. You’ll either need that table, or you’ll need to override the tenants()
method to use a custom table.
You’ll need to add a few methods for which we can’t provide the defaults in the ResourceSyncing trait (feel free to override the defaults – e.g. if you want your resource to have a different global identifier key than global_id
, override the getGlobalIdentifierKeyName()
method), and for which you’ll likely want a custom value. Here’s an example of the models with the least possible configuration:
Before looking into the more advanced things covered in the next sections, you can test that you got the implemenatation right by creating a central resource (e.g. a user in the central database) and a tenant resource (e.g. a user in the database of some tenant) with the same global ID, update some of the synced attributes and see if the other resource gets updated automatically.
Synced attributes
An important requirement of the Syncable interface is the getSyncedAttributeNames()
method. In the method, you return a list of the attributes you want to keep in sync with the other resource – when the current resource gets updated, the synced attributes will get updated on the other resource.
Creation attributes
When you create a tenant resource which doesn’t have any corresponding central resource, the central resource will get created using the tenant resource’s creation attributes automatically.
The synced attributes are used as the creation attributes by default, but you can override the getCreationAttributes()
method and control which attributes will the automatically created central resource get from the tenant resource. You can also set defaults for the attributes – this is useful if one resource requires attributes that the other resource doesn’t have.
Creation attributes are also important for specifying what attributes of a central resource will be used for creating the tenant resource when attaching a central resource to a tenant.
Cascade on deleting the central resource
When the central resource gets deleted, all related tenant resources can get deleted automatically. To enable this behavior, make your SyncMaster models implement the CascadeDeletes interface.
The interface requires the triggerDeleteEvent()
method (provided by the ResourceSyncing trait) in which the SyncMasterDeleted event gets dispatched. The DeleteResourcesInTenants listener then handles deleting the resource from each tenant connected to the central resource via a pivot record.
Conditional sync
Whether or not a resource’s attributes will be synced (updated when the other resource gets saved) depends on the resource’s shouldSync()
method. If the method returns true
, the attributes will be synced, and if it returns false
, they won’t. By default, the shouldSync()
method returns true
, but you can override it. For example, sync the attributes only if the resource’s email
attribute is not null
.
Soft deletes with resource syncing
By default, attributes of trashed resources aren’t synced. The trashed records aren’t included when we’re syncing the resources. To change that, assign the UpdateOrCreateSyncedResource::$scopeGetModelQuery
property a closure that scopes the query using $query->withTrashed()
(like in the example below). Now, the queries performed when syncing are going to include the trashed resources.
How it works
Resource syncing is built on events that are triggered when you save, delete, attach, or detach resources. The syncing happens while handling theses events.
The default event flow
Saving synced resources
$centralResource->create/update(...)
calls triggerSyncEvent()
in your Syncable resource, which triggers the SyncedResourceSaved
event handled by the UpdateOrCreateSyncedResource
listener. The SyncedResourceSavedInForeignDatabase
event is dispatched after that – it’s only used in the package tests.
Deleting central resource when it implements CascadeDeletes
$centralResource->delete()
calls triggerDeleteEvent()
in your CascadeDeletes SyncMaster resource, which triggers the SyncMasterDeleted
event handled by the DeleteResourcesInTenants
listener.
Attaching tenants to central resources (and vice versa)
$centralResource->tenants()->attach($tenant)
calls triggerAttachEvent()
in your SyncMaster resource, which triggers the CentralResourceAttachedToTenant
event handled by the CreateTenantResource
listener. The SyncedResourceSavedInForeignDatabase
event is dispatched after that – it’s only used in the package tests.
Detaching tenants from central resources (and vice versa)
$centralResource->tenants()->detach($tenant)
calls triggerDetachEvent()
in your SyncMaster resource, which triggers the CentralResourceDetachedFromTenant
event handled by the DeleteResourceInTenant
listener.
The event flow is fully customizable – you can override the triggerXEvent() methods and trigger any event you want. You can also use listeners other than the default ones used in the TenancyServiceProvider.
UpdateOrCreateSyncedResource
The UpdateOrCreateSyncedResource listener is responsible for handling the core of resource syncing – updating and creating resources.
When a resource gets saved, the listener determines if the resource is central or tenant.
If the resource is central, the listener loops through all tenants associated with the central resource ($centralResource->tenants
) to check for tenant resources with the same global ID as the central resource’s. If the resource exists in the tenant database, update its synced attributes, or if it doesn’t exist, create the tenant resource from the central resource’s creation attributes.
If the resource is tenant, update the central resource’s synced attributes.
DeleteResourcesInTenants (cascade deletes)
When a central resource that implements the CascadeDeletes interface gets deleted, the DeleteResourcesInTenants listener deletes related tenant resources from the database of each tenant returned by $centralResource->tenants
.
CreateTenantResource and DeleteResourceInTenant (attach/detach events)
When a central resource gets attached to a tenant, the CreateTenantResource listener creates a resource using the central resource’s creation attributes in the tenant’s database.
When a central resource gets detached from a tenant, the DeleteResourceInTenant listener deletes the resource from the tenant database.
Database schema
The main idea: have one resource in the central database and one resource in the tenant database (or in many different tenant databases), associated by the global identifier. The central resource belongs to many tenants (one central resource can have an associated resource in many tenant databases) – by default, we’re using a polymorphic relationship (MorphToMany, TenantMorphPivot model) with the tenant_resources
pivot table for that.
TriggerSyncingEvents trait (attaching/detaching)
The default pivot (TenantMorphPivot) uses the TriggerSyncingEvents trait, which fires events on attaching/detaching resources to/from tenants (CentralResourceAttachedToTenant/CentralResourceDetachedFromTenant).
In the default event flow, attaching a tenant to a central resource creates a tenant resource associated with the central resource, and detaching a tenant from a central resource deletes the associated tenant resource from the tenant’s database.
Custom pivots
When using TenantMorphPivot as the pivot model, everything should work fine. But if you’re overriding the tenants()
relationship method, make sure you use a pivot that uses the TriggerSyncingEvents trait, so that attaching/detaching works as expected. Also, note that with custom pivot classes (other than ones extending MorphPivot), attaching and detaching doesn’t work in both directions right away. $resource->tenants()->attach($tenant)
works just fine, but $tenant->resources()->attach($resource)
doesn’t work with non-polymorphic pivots because while attaching, we can’t reach the resource passed to attach()
.
To fix this, the pivot needs to implement the PivotWithRelation interface, which means you’ll need to add the getRelatedModel()
method. In this method, return the model of the central resource. The method makes it possible to access the central resource when attaching the central resource to a tenant using $tenant->resources()->attach($resource)
– while handling attaching, there’s no way of getting the related model ($resource
). We’re only able to get the parent resource ($tenant
), unless you provide the resource’s model in the getRelatedModel()
method.
Changes compared to 3.x
You can provide creation attributes in the resource’s getCreationAttributes()
method
This method allows specifying which attributes of the current resource will be used when the other resource is being created (e.g. when attaching a central resource to a tenant, create an associated tenant resource using the central resource’s creation attributes).
The method also allows defining defaults for creating the other resource, e.g. when the other resource requires some attribute that the current resource doesn’t have (explained in the Usage -> Creation attributes section).
Creating a tenant resource also creates the central resource if it doesn’t exist yet.
Now, the sync event gets triggered:
- only when
shouldSync()
returns true - when the resource got recently created or its synced attributes got updated (updating non-synced attributes doesn’t trigger the sync event now)
Cascading deletes If a class that implements SyncMaster and CascadeDeletes gets deleted, also delete its child models (in tenant DBs).
Soft deletes
The UpdateOrCreateSyncedResource::$scopeGetModelQuery
property lets you scope the queries performed while saving synced resources. Primarily, this allows soft deletes to work with resource syncing – syncing trashed resources works too now (soft deletes also work with CascadeDeletes).
Polymorphic relationships (attaching/detaching)
Syncable records are now created/deleted when attaching/detaching central resources. $centralResource->tenants()->attach($tenant)
now creates a tenant resource synced to $centralResource
(same for $tenant->resources()->attach($centralResource)
when using PivotWithRelation or MorphPivot). Detaching tenants from central resources also deletes the synced tenant resources – $centralResource->tenants()->detach($tenant)
deletes the tenant resource (same for $tenant->resources()->detach($centralResource)
when using PivotWithRelation or MorphPivot).
Polymorphic many-to-many is now the default for the tenants()
relationship method provided by the ResourceSyncing trait (using TenantMorphPivot).
Global identifier defaults
The resource syncing trait now provides defaults for the methods concerning the resource’s global identifier. Now, the getGlobalIdentifierKeyName()
method returns “global_id”, and getGlobalIdentifierKey()
now returns $this->getAttribute($this->getGlobalIdentifierKeyName())
. So unless you want to override the key name, you don’t have to add the methods manually to your resources anymore.