1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 13:54:03 +00:00

[4.x] Configure attributes for synced resources when creating models (#915)

* configure attributes for creating resource

* Update ResourceSyncingTest.php

* Update ci.yml

* Update ResourceSyncingTest.php

* Update ci.yml

* cs

* comments

* Update tests/ResourceSyncingTest.php

Co-authored-by: Samuel Štancl <samuel@archte.ch>

* improve comments, move method to `SyncMaster` interface

* Revert "improve comments, move method to `SyncMaster` interface"

This reverts commit 5ddd50deb9.

* Update ResourceSyncingTest.php

* Update ResourceSyncingTest.php

* update comment

* Update ResourceSyncingTest.php

* Update ResourceSyncingTest.php

* wip

* wip

* wip

* add a todo

* assert that creation attributes returns null

* classes at the end

* rename method to `getAttributesForCreation`

* Update ResourceSyncingTest.php

* update comments

* Fix little grammer

* merge default values with sync attributes and tests

* Update ResourceSyncingTest.php

* method rename

* method rename

* Update ResourceSyncingTest.php

* comments

* Update ResourceSyncingTest.php

* allow defining a mix of attribute names and default values

* add test

* code improvements

* Fix code style (php-cs-fixer)

* remove unused import

* fix all phpstan issues in resource syncing code

* Fix code style (php-cs-fixer)

* wip

* improve tests

* Update ResourceSyncingTest.php

* better names

* Update UpdateSyncedResource.php

* code style

* Update UpdateSyncedResource.php

* add comments above new tests

* methods dockblocks and correct names

* Update ResourceSyncingTest.php

* update comments

* remove different schema setup

* delete custom migrations

* self review

* grammar, code style

* refactor helpers for creating tenants

Co-authored-by: Samuel Štancl <samuel@archte.ch>
Co-authored-by: Samuel Štancl <samuel.stancl@gmail.com>
Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
This commit is contained in:
Abrar Ahmad 2022-11-03 21:51:29 +05:00 committed by GitHub
parent aa536529df
commit 77c5ae1f32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 469 additions and 37 deletions

View file

@ -15,4 +15,7 @@ interface Syncable
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
}

View file

@ -32,4 +32,9 @@ trait ResourceSyncing
/** @var Syncable $this */
event(new SyncedResourceSaved($this, tenant()));
}
public function getSyncedCreationAttributes(): array|null
{
return null;
}
}

View file

@ -10,14 +10,9 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
class SyncedResourceSaved
{
public Syncable&Model $model;
/** @var (TenantWithDatabase&Model)|null */
public TenantWithDatabase|null $tenant;
public function __construct(Syncable $model, TenantWithDatabase|null $tenant)
{
$this->model = $model;
$this->tenant = $tenant;
public function __construct(
public Syncable&Model $model,
public TenantWithDatabase|null $tenant,
) {
}
}

View file

@ -4,14 +4,19 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Listeners;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
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 Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Database\TenantCollection;
use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase;
use Stancl\Tenancy\Events\SyncedResourceSaved;
use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException;
// todo@v4 review all code related to resource syncing
class UpdateSyncedResource extends QueueableListener
{
public static bool $shouldQueue = false;
@ -30,25 +35,28 @@ class UpdateSyncedResource extends QueueableListener
$this->updateResourceInTenantDatabases($tenants, $event, $syncedAttributes);
}
protected function getTenantsForCentralModel($centralModel): EloquentCollection
protected function getTenantsForCentralModel(Syncable $centralModel): TenantCollection
{
if (! $centralModel instanceof SyncMaster) {
// If we're trying to use a tenant User model instead of the central User model, for example.
throw new ModelNotSyncMasterException(get_class($centralModel));
}
/** @var SyncMaster|Model $centralModel */
/** @var Tenant&Model&SyncMaster $centralModel */
// Since this model is "dirty" (taken by reference from the event), it might have the tenants
// relationship already loaded and cached. For this reason, we refresh the relationship.
$centralModel->load('tenants');
return $centralModel->tenants;
/** @var TenantCollection $tenants */
$tenants = $centralModel->tenants;
return $tenants;
}
protected function updateResourceInCentralDatabaseAndGetTenants($event, $syncedAttributes): EloquentCollection
protected function updateResourceInCentralDatabaseAndGetTenants(SyncedResourceSaved $event, array $syncedAttributes): TenantCollection
{
/** @var Model|SyncMaster $centralModel */
/** @var (Model&SyncMaster)|null $centralModel */
$centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey())
->first();
@ -59,15 +67,17 @@ class UpdateSyncedResource extends QueueableListener
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
} else {
// If the resource doesn't exist at all in the central DB,we create
// the record with all attributes, not just the synced ones.
$centralModel = $event->model->getCentralModelName()::create($event->model->getAttributes());
$centralModel = $event->model->getCentralModelName()::create($this->getAttributesForCreation($event->model));
event(new SyncedResourceChangedInForeignDatabase($event->model, null));
}
});
// If the model was just created, the mapping of the tenant to the user likely doesn't exist, so we create it.
$currentTenantMapping = function ($model) use ($event) {
return ((string) $model->pivot->tenant_id) === ((string) $event->tenant->getTenantKey());
/** @var Tenant */
$tenant = $event->tenant;
return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey());
};
$mappingExists = $centralModel->tenants->contains($currentTenantMapping);
@ -76,22 +86,29 @@ class UpdateSyncedResource extends QueueableListener
// Here we should call TenantPivot, but we call general Pivot, so that this works
// even if people use their own pivot model that is not based on our TenantPivot
Pivot::withoutEvents(function () use ($centralModel, $event) {
$centralModel->tenants()->attach($event->tenant->getTenantKey());
/** @var Tenant */
$tenant = $event->tenant;
$centralModel->tenants()->attach($tenant->getTenantKey());
});
}
return $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) {
/** @var TenantCollection $tenants */
$tenants = $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) {
// Remove the mapping for the current tenant.
return ! $currentTenantMapping($model);
});
return $tenants;
}
protected function updateResourceInTenantDatabases($tenants, $event, $syncedAttributes): void
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.
/** @var Model&Syncable $eventModel */
$eventModel = $event->model;
if ($eventModel instanceof SyncMaster) {
@ -112,12 +129,53 @@ class UpdateSyncedResource extends QueueableListener
if ($localModel) {
$localModel->update($syncedAttributes);
} else {
// When creating, we use all columns, not just the synced ones.
$localModel = $localModelClass::create($eventModel->getAttributes());
$localModel = $localModelClass::create($this->getAttributesForCreation($eventModel));
}
event(new SyncedResourceChangedInForeignDatabase($localModel, $tenant));
});
});
}
protected function getAttributesForCreation(Model&Syncable $model): array
{
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()]);
return $attributes;
}
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];
}
}

View file

@ -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 AddExtraColumnToCentralUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('foo');
});
}
public function down()
{
}
}

View file

@ -44,9 +44,10 @@ beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
UpdateSyncedResource::$shouldQueue = false; // global state cleanup
UpdateSyncedResource::$shouldQueue = false; // Global state cleanup
Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class);
// Run migrations on central connection
pest()->artisan('migrate', [
'--path' => [
__DIR__ . '/Etc/synced_resource_migrations',
@ -83,7 +84,7 @@ test('only the synced columns are updated in the central db', function () {
]);
$tenant = ResourceTenant::create();
migrateTenantsResource();
migrateUsersTableForTenants();
tenancy()->initialize($tenant);
@ -126,6 +127,231 @@ test('only the synced columns are updated in the central db', function () {
], ResourceUser::first()->getAttributes());
});
// This tests attribute list on the central side, and default values on the tenant side
// Those two don't depend on each other, we're just testing having each option on each side
// using tests that combine the two, to avoid having an excessively long and complex test suite
test('sync resource creation works when central model provides attributes and resource model provides default values', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
addExtraColumnToCentralDB();
$centralUser = CentralUserProvidingAttributeNames::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
'foo' => 'bar', // foo does not exist in resource model
]);
$tenant1->run(function () {
expect(ResourceUserProvidingDefaultValues::all())->toHaveCount(0);
});
// When central model provides the list of attributes, resource model will be created from the provided list of attributes' values
$centralUser->tenants()->attach('t1');
$tenant1->run(function () {
$resourceUser = ResourceUserProvidingDefaultValues::all();
expect($resourceUser)->toHaveCount(1);
expect($resourceUser->first()->global_id)->toBe('acme');
expect($resourceUser->first()->email)->toBe('john@localhost');
// 'foo' attribute is not provided by central model
expect($resourceUser->first()->foo)->toBeNull();
});
tenancy()->initialize($tenant2);
// When resource model provides the list of default values, central model will be created from the provided list of default values
ResourceUserProvidingDefaultValues::create([
'global_id' => 'asdf',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
]);
tenancy()->end();
// Assert central user was created using the list of default values
$centralUser = CentralUserProvidingAttributeNames::whereGlobalId('asdf')->first();
expect($centralUser)->not()->toBeNull();
expect($centralUser->name)->toBe('Default Name');
expect($centralUser->email)->toBe('default@localhost');
expect($centralUser->password)->toBe('password');
expect($centralUser->role)->toBe('admin');
expect($centralUser->foo)->toBe('bar');
});
// This tests default values on the central side, and attribute list on the tenant side
// Those two don't depend on each other, we're just testing having each option on each side
// using tests that combine the two, to avoid having an excessively long and complex test suite
test('sync resource creation works when central model provides default values and resource model provides attributes', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
addExtraColumnToCentralDB();
$centralUser = CentralUserProvidingDefaultValues::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
'foo' => 'bar', // foo does not exist in resource model
]);
$tenant1->run(function () {
expect(ResourceUserProvidingDefaultValues::all())->toHaveCount(0);
});
// When central model provides the list of default values, resource model will be created from the provided list of default values
$centralUser->tenants()->attach('t1');
$tenant1->run(function () {
// Assert resource user was created using the list of default values
$resourceUser = ResourceUserProvidingDefaultValues::first();
expect($resourceUser)->not()->toBeNull();
expect($resourceUser->global_id)->toBe('acme');
expect($resourceUser->email)->toBe('default@localhost');
expect($resourceUser->password)->toBe('password');
expect($resourceUser->role)->toBe('admin');
});
tenancy()->initialize($tenant2);
// When resource model provides the list of attributes, central model will be created from the provided list of attributes' values
ResourceUserProvidingAttributeNames::create([
'global_id' => 'asdf',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'secret',
'role' => 'commenter',
]);
tenancy()->end();
// Assert central user was created using the list of provided attributes
$centralUser = CentralUserProvidingAttributeNames::whereGlobalId('asdf')->first();
expect($centralUser)->not()->toBeNull();
expect($centralUser->email)->toBe('john@localhost');
expect($centralUser->password)->toBe('secret');
expect($centralUser->role)->toBe('commenter');
});
// This tests mixed attribute list/defaults on the central side, and no specified attributes on the tenant side
// Those two don't depend on each other, we're just testing having each option on each side
// using tests that combine the two, to avoid having an excessively long and complex test suite
test('sync resource creation works when central model provides mixture and resource model provides nothing', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
$centralUser = CentralUserProvidingMixture::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commentator'
]);
$tenant1->run(function () {
expect(ResourceUser::all())->toHaveCount(0);
});
// When central model provides the list of a mixture (attributes and default values), resource model will be created from the provided list of mixture (attributes and default values)
$centralUser->tenants()->attach('t1');
$tenant1->run(function () {
$resourceUser = ResourceUser::first();
// Assert resource user was created using the provided attributes and default values
expect($resourceUser->global_id)->toBe('acme');
expect($resourceUser->name)->toBe('John Doe');
expect($resourceUser->email)->toBe('john@localhost');
// default values
expect($resourceUser->role)->toBe('admin');
expect($resourceUser->password)->toBe('secret');
});
tenancy()->initialize($tenant2);
// When resource model provides nothing/null, the central model will be created as a 1:1 copy of resource model
$resourceUser = ResourceUser::create([
'global_id' => 'acmey',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commentator'
]);
tenancy()->end();
$centralUser = CentralUserProvidingMixture::whereGlobalId('acmey')->first();
expect($resourceUser->getSyncedCreationAttributes())->toBeNull();
$centralUser = $centralUser->toArray();
$resourceUser = $resourceUser->toArray();
unset($centralUser['id']);
unset($resourceUser['id']);
// Assert central user created as 1:1 copy of resource model except "id"
expect($centralUser)->toBe($resourceUser);
});
// This tests no specified attributes on the central side, and mixed attribute list/defaults on the tenant side
// Those two don't depend on each other, we're just testing having each option on each side
// using tests that combine the two, to avoid having an excessively long and complex test suite
test('sync resource creation works when central model provides nothing and resource model provides mixture', function () {
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
$centralUser = CentralUser::create([
'global_id' => 'acme',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
$tenant1->run(function () {
expect(ResourceUserProvidingMixture::all())->toHaveCount(0);
});
// When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model
$centralUser->tenants()->attach('t1');
expect($centralUser->getSyncedCreationAttributes())->toBeNull();
$tenant1->run(function () use ($centralUser) {
$resourceUser = ResourceUserProvidingMixture::first();
expect($resourceUser)->not()->toBeNull();
$resourceUser = $resourceUser->toArray();
$centralUser = $centralUser->withoutRelations()->toArray();
unset($resourceUser['id']);
unset($centralUser['id']);
expect($resourceUser)->toBe($centralUser);
});
tenancy()->initialize($tenant2);
// When resource model provides the list of a mixture (attributes and default values), central model will be created from the provided list of mixture (attributes and default values)
ResourceUserProvidingMixture::create([
'global_id' => 'absd',
'name' => 'John Doe',
'email' => 'john@localhost',
'password' => 'password',
'role' => 'commenter',
]);
tenancy()->end();
$centralUser = CentralUser::whereGlobalId('absd')->first();
// Assert central user was created using the provided list of attributes and default values
expect($centralUser->name)->toBe('John Doe');
expect($centralUser->email)->toBe('john@localhost');
// default values
expect($centralUser->role)->toBe('admin');
expect($centralUser->password)->toBe('secret');
});
test('creating the resource in tenant database creates it in central database and creates the mapping', function () {
creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase();
});
@ -152,7 +378,7 @@ test('attaching a tenant to the central resource triggers a pull from the tenant
$tenant = ResourceTenant::create([
'id' => 't1',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
$tenant->run(function () {
expect(ResourceUser::all())->toHaveCount(0);
@ -177,7 +403,7 @@ test('attaching users to tenants does not do anything', function () {
$tenant = ResourceTenant::create([
'id' => 't1',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
$tenant->run(function () {
expect(ResourceUser::all())->toHaveCount(0);
@ -212,7 +438,7 @@ test('resources are synced only to workspaces that have the resource', function
$t3 = ResourceTenant::create([
'id' => 't3',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
$centralUser->tenants()->attach('t1');
$centralUser->tenants()->attach('t2');
@ -250,7 +476,7 @@ test('when a resource exists in other tenant dbs but is created in a tenant db t
$t2 = ResourceTenant::create([
'id' => 't2',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
// Copy (cascade) user to t1 DB
$centralUser->tenants()->attach('t1');
@ -298,7 +524,7 @@ test('the synced columns are updated in other tenant dbs where the resource exis
$t3 = ResourceTenant::create([
'id' => 't3',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
// Copy (cascade) user to t1 DB
$centralUser->tenants()->attach('t1');
@ -353,7 +579,7 @@ test('when the resource doesnt exist in the tenant db non synced columns will ca
'id' => 't1',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
$centralUser->tenants()->attach('t1');
@ -367,7 +593,7 @@ test('when the resource doesnt exist in the central db non synced columns will b
'id' => 't1',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
$t1->run(function () {
ResourceUser::create([
@ -389,7 +615,7 @@ test('the listener can be queued', function () {
'id' => 't1',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
Queue::assertNothingPushed();
@ -428,7 +654,7 @@ test('an event is fired for all touched resources', function () {
$t3 = ResourceTenant::create([
'id' => 't3',
]);
migrateTenantsResource();
migrateUsersTableForTenants();
// Copy (cascade) user to t1 DB
$centralUser->tenants()->attach('t1');
@ -509,7 +735,7 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase()
expect(ResourceUser::all())->toHaveCount(0);
$tenant = ResourceTenant::create();
migrateTenantsResource();
migrateUsersTableForTenants();
tenancy()->initialize($tenant);
@ -524,7 +750,7 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase()
tenancy()->end();
// Asset user was created
// Assert user was created
expect(CentralUser::first()->global_id)->toBe('acme');
expect(CentralUser::first()->role)->toBe('commenter');
@ -537,7 +763,28 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase()
expect(ResourceUser::first()->role)->toBe('commenter');
}
function migrateTenantsResource()
/**
* Create two tenants and run migrations for those tenants.
*/
function createTenantsAndRunMigrations(): array
{
[$tenant1, $tenant2] = [ResourceTenant::create(['id' => 't1']), ResourceTenant::create(['id' => 't2'])];
migrateUsersTableForTenants();
return [$tenant1, $tenant2];
}
function addExtraColumnToCentralDB(): void
{
// migrate extra column "foo" in central DB
pest()->artisan('migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users_extra',
'--realpath' => true,
])->assertExitCode(0);
}
function migrateUsersTableForTenants(): void
{
pest()->artisan('tenants:migrate', [
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users',
@ -593,6 +840,7 @@ class CentralUser extends Model implements SyncMaster
public function getSyncedAttributeNames(): array
{
return [
'global_id',
'name',
'password',
'email',
@ -628,9 +876,106 @@ class ResourceUser extends Model implements Syncable
public function getSyncedAttributeNames(): array
{
return [
'global_id',
'name',
'password',
'email',
];
}
}
// override method in ResourceUser class to return default attribute values
class ResourceUserProvidingDefaultValues extends ResourceUser
{
public function getSyncedCreationAttributes(): array
{
// Default values when creating resources from tenant to central DB
return
[
'name' => 'Default Name',
'email' => 'default@localhost',
'password' => 'password',
'role' => 'admin',
'foo' => 'bar'
];
}
}
// override method in ResourceUser class to return attribute names
class ResourceUserProvidingAttributeNames extends ResourceUser
{
public function getSyncedCreationAttributes(): array
{
// Attributes used when creating resources from tenant to central DB
// Notice here we are not adding "code" filed because it doesn't
// exist in central model
return
[
'name',
'password',
'email',
'role',
'foo' => 'bar'
];
}
}
// override method in CentralUser class to return attribute default values
class CentralUserProvidingDefaultValues extends CentralUser
{
public function getSyncedCreationAttributes(): array
{
// Attributes default values when creating resources from central to tenant model
return
[
'name' => 'Default User',
'email' => 'default@localhost',
'password' => 'password',
'role' => 'admin',
];
}
}
// override method in CentralUser class to return attribute names
class CentralUserProvidingAttributeNames extends CentralUser
{
public function getSyncedCreationAttributes(): array
{
// Attributes used when creating resources from central to tenant DB
return
[
'global_id',
'name',
'password',
'email',
'role',
];
}
}
class CentralUserProvidingMixture extends CentralUser
{
public function getSyncedCreationAttributes(): array
{
return [
'name',
'email',
'role' => 'admin',
'password' => 'secret',
];
}
}
class ResourceUserProvidingMixture extends ResourceUser
{
public function getSyncedCreationAttributes(): array
{
return [
'name',
'email',
'role' => 'admin',
'password' => 'secret',
];
}
}