mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 11:14:03 +00:00
Merge branch 'master' into poly-sync
This commit is contained in:
commit
dc69a112eb
34 changed files with 231 additions and 125 deletions
|
|
@ -10,10 +10,16 @@ Run `composer docker-up` to start the containers. Then run `composer test` to ru
|
||||||
|
|
||||||
If you need to pass additional flags to phpunit, use `./test --foo` instead of `composer test --foo`. Composer scripts unfortunately don't pass CLI arguments.
|
If you need to pass additional flags to phpunit, use `./test --foo` instead of `composer test --foo`. Composer scripts unfortunately don't pass CLI arguments.
|
||||||
|
|
||||||
If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'`.
|
If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'` (`--no-coverage` speeds up the execution time).
|
||||||
|
|
||||||
When you're done testing, run `composer docker-down` to shut down the containers.
|
When you're done testing, run `composer docker-down` to shut down the containers.
|
||||||
|
|
||||||
|
### Debugging tests
|
||||||
|
|
||||||
|
If you're developing some feature and you encounter `SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry` errors, it's likely that some PHP errors were thrown in past test runs and prevented the test cleanup from running properly.
|
||||||
|
|
||||||
|
To fix this, simply delete the database memory by shutting down containers and starting them again: `composer docker-down && composer docker-up`.
|
||||||
|
|
||||||
### Docker on M1
|
### Docker on M1
|
||||||
|
|
||||||
Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1.
|
Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1.
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,29 @@ use Stancl\Tenancy\Middleware;
|
||||||
use Stancl\Tenancy\Resolvers;
|
use Stancl\Tenancy\Resolvers;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class,
|
/**
|
||||||
'domain_model' => Stancl\Tenancy\Database\Models\Domain::class,
|
* Configuration for the models used by Tenancy.
|
||||||
|
*/
|
||||||
|
'models' => [
|
||||||
|
'tenant' => Stancl\Tenancy\Database\Models\Tenant::class,
|
||||||
|
'domain' => Stancl\Tenancy\Database\Models\Domain::class,
|
||||||
|
|
||||||
'id_generator' => Stancl\Tenancy\UUIDGenerator::class,
|
/**
|
||||||
|
* Name of the column used to relate models to tenants.
|
||||||
|
*
|
||||||
|
* This is used by the HasDomains trait, and models that use the BelongsToTenant trait (used in single-database tenancy).
|
||||||
|
*/
|
||||||
|
'tenant_key_column' => 'tenant_id',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for generating tenant IDs.
|
||||||
|
*
|
||||||
|
* - Feel free to override this with a custom class that implements the UniqueIdentifierGenerator interface.
|
||||||
|
* - To use autoincrement IDs, set this to null and update the `tenants` table migration to use an autoincrement column.
|
||||||
|
* SECURITY NOTE: Keep in mind that autoincrement IDs come with *potential* enumeration issues (such as tenant storage URLs).
|
||||||
|
*/
|
||||||
|
'id_generator' => Stancl\Tenancy\UUIDGenerator::class,
|
||||||
|
],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of domains hosting your central app.
|
* The list of domains hosting your central app.
|
||||||
|
|
@ -293,12 +312,4 @@ return [
|
||||||
'--class' => 'Database\Seeders\DatabaseSeeder', // root seeder class
|
'--class' => 'Database\Seeders\DatabaseSeeder', // root seeder class
|
||||||
// '--force' => true,
|
// '--force' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
/**
|
|
||||||
* Single-database tenancy config.
|
|
||||||
*/
|
|
||||||
'single_db' => [
|
|
||||||
/** The name of the column used by models with the BelongsToTenant trait. */
|
|
||||||
'tenant_id_column' => 'tenant_id',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ declare(strict_types=1);
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
class CreateTenantUserImpersonationTokensTable extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
|
|
@ -17,13 +18,13 @@ class CreateTenantUserImpersonationTokensTable extends Migration
|
||||||
{
|
{
|
||||||
Schema::create('tenant_user_impersonation_tokens', function (Blueprint $table) {
|
Schema::create('tenant_user_impersonation_tokens', function (Blueprint $table) {
|
||||||
$table->string('token', 128)->primary();
|
$table->string('token', 128)->primary();
|
||||||
$table->string('tenant_id');
|
$table->string(Tenancy::tenantKeyColumn());
|
||||||
$table->string('user_id');
|
$table->string('user_id');
|
||||||
$table->string('auth_guard');
|
$table->string('auth_guard');
|
||||||
$table->string('redirect_url');
|
$table->string('redirect_url');
|
||||||
$table->timestamp('created_at');
|
$table->timestamp('created_at');
|
||||||
|
|
||||||
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
|
$table->foreign(Tenancy::tenantKeyColumn())->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,4 +37,4 @@ class CreateTenantUserImpersonationTokensTable extends Migration
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('tenant_user_impersonation_tokens');
|
Schema::dropIfExists('tenant_user_impersonation_tokens');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateTenantsTable extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
|
|
@ -34,4 +34,4 @@ class CreateTenantsTable extends Migration
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('tenants');
|
Schema::dropIfExists('tenants');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ declare(strict_types=1);
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
class CreateDomainsTable extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
|
|
@ -18,10 +19,10 @@ class CreateDomainsTable extends Migration
|
||||||
Schema::create('domains', function (Blueprint $table) {
|
Schema::create('domains', function (Blueprint $table) {
|
||||||
$table->increments('id');
|
$table->increments('id');
|
||||||
$table->string('domain', 255)->unique();
|
$table->string('domain', 255)->unique();
|
||||||
$table->string('tenant_id');
|
$table->string(Tenancy::tenantKeyColumn());
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade');
|
$table->foreign(Tenancy::tenantKeyColumn())->references('id')->on('tenants')->onUpdate('cascade');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,4 +35,4 @@ class CreateDomainsTable extends Migration
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('domains');
|
Schema::dropIfExists('domains');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Controllers\TenantAssetController;
|
use Stancl\Tenancy\Controllers\TenantAssetController;
|
||||||
|
|
||||||
|
// todo make this work with path identification
|
||||||
Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset'])
|
Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset'])
|
||||||
->where('path', '(.*)')
|
->where('path', '(.*)')
|
||||||
->name('stancl.tenancy.asset');
|
->name('stancl.tenancy.asset');
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Builder;
|
||||||
class ClearPendingTenants extends Command
|
class ClearPendingTenants extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'tenants:pending-clear
|
protected $signature = 'tenants:pending-clear
|
||||||
{--all : Override the default settings and deletes all pending tenants}
|
|
||||||
{--older-than-days= : Deletes all pending tenants older than the amount of days}
|
{--older-than-days= : Deletes all pending tenants older than the amount of days}
|
||||||
{--older-than-hours= : Deletes all pending tenants older than the amount of hours}';
|
{--older-than-hours= : Deletes all pending tenants older than the amount of hours}';
|
||||||
|
|
||||||
|
|
@ -24,32 +23,25 @@ class ClearPendingTenants extends Command
|
||||||
// We compare the original expiration date to the new one to check if the new one is different later
|
// We compare the original expiration date to the new one to check if the new one is different later
|
||||||
$originalExpirationDate = $expirationDate->copy()->toImmutable();
|
$originalExpirationDate = $expirationDate->copy()->toImmutable();
|
||||||
|
|
||||||
// Skip the time constraints if the 'all' option is given
|
$olderThanDays = (int) $this->option('older-than-days');
|
||||||
if (! $this->option('all')) {
|
$olderThanHours = (int) $this->option('older-than-hours');
|
||||||
/** @var ?int $olderThanDays */
|
|
||||||
$olderThanDays = $this->option('older-than-days');
|
|
||||||
|
|
||||||
/** @var ?int $olderThanHours */
|
if ($olderThanDays && $olderThanHours) {
|
||||||
$olderThanHours = $this->option('older-than-hours');
|
$this->line("<options=bold,reverse;fg=red> Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components
|
||||||
|
$this->line('Please, choose only one of these options.');
|
||||||
|
|
||||||
if ($olderThanDays && $olderThanHours) {
|
return 1; // Exit code for failure
|
||||||
$this->line("<options=bold,reverse;fg=red> Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components
|
|
||||||
$this->line('Please, choose only one of these options.');
|
|
||||||
|
|
||||||
return 1; // Exit code for failure
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($olderThanDays) {
|
|
||||||
$expirationDate->subDays($olderThanDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($olderThanHours) {
|
|
||||||
$expirationDate->subHours($olderThanHours);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$deletedTenantCount = tenancy()
|
if ($olderThanDays) {
|
||||||
->query()
|
$expirationDate->subDays($olderThanDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($olderThanHours) {
|
||||||
|
$expirationDate->subHours($olderThanHours);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletedTenantCount = tenancy()->query()
|
||||||
->onlyPending()
|
->onlyPending()
|
||||||
->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) {
|
->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) {
|
||||||
$query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp);
|
$query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp);
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ class CreatePendingTenants extends Command
|
||||||
$createdCount++;
|
$createdCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.');
|
$this->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.');
|
||||||
$this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
$this->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -39,8 +39,7 @@ class CreatePendingTenants extends Command
|
||||||
/** Calculate the number of currently available pending tenants. */
|
/** Calculate the number of currently available pending tenants. */
|
||||||
protected function getPendingTenantCount(): int
|
protected function getPendingTenantCount(): int
|
||||||
{
|
{
|
||||||
return tenancy()
|
return tenancy()->query()
|
||||||
->query()
|
|
||||||
->onlyPending()
|
->onlyPending()
|
||||||
->count();
|
->count();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,18 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Commands;
|
namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class MigrateFreshOverride extends FreshCommand
|
class MigrateFreshOverride extends FreshCommand
|
||||||
{
|
{
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
if (config('tenancy.database.drop_tenant_databases_on_migrate_fresh')) {
|
if (config('tenancy.database.drop_tenant_databases_on_migrate_fresh')) {
|
||||||
tenancy()->model()::cursor()->each->delete();
|
$tenantModel = tenancy()->model();
|
||||||
|
|
||||||
|
if (Schema::hasTable($tenantModel->getTable())) {
|
||||||
|
$tenantModel::cursor()->each->delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::handle();
|
return parent::handle();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class TenantDump extends DumpCommand
|
||||||
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int
|
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int
|
||||||
{
|
{
|
||||||
if (is_null($this->option('path'))) {
|
if (is_null($this->option('path'))) {
|
||||||
$this->input->setOption('path', database_path('schema/tenant-schema.dump'));
|
$this->input->setOption('path', config('tenancy.migration_parameters.--schema-path') ?? database_path('schema/tenant-schema.dump'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$tenant = $this->option('tenant')
|
$tenant = $this->option('tenant')
|
||||||
|
|
@ -41,7 +41,7 @@ class TenantDump extends DumpCommand
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
parent::handle($connections, $dispatcher);
|
$tenant->run(fn () => parent::handle($connections, $dispatcher));
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ trait HasTenantOptions
|
||||||
|
|
||||||
protected function getTenants(): LazyCollection
|
protected function getTenants(): LazyCollection
|
||||||
{
|
{
|
||||||
return tenancy()
|
return tenancy()->query()
|
||||||
->query()
|
|
||||||
->when($this->option('tenants'), function ($query) {
|
->when($this->option('tenants'), function ($query) {
|
||||||
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Database\TenantScope;
|
use Stancl\Tenancy\Database\TenantScope;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property-read Tenant $tenant
|
* @property-read Tenant $tenant
|
||||||
|
|
@ -14,12 +15,7 @@ trait BelongsToTenant
|
||||||
{
|
{
|
||||||
public function tenant()
|
public function tenant()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(config('tenancy.tenant_model'), static::tenantIdColumn());
|
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
|
||||||
}
|
|
||||||
|
|
||||||
public static function tenantIdColumn(): string
|
|
||||||
{
|
|
||||||
return config('tenancy.single_db.tenant_id_column');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function bootBelongsToTenant(): void
|
public static function bootBelongsToTenant(): void
|
||||||
|
|
@ -27,9 +23,9 @@ trait BelongsToTenant
|
||||||
static::addGlobalScope(new TenantScope);
|
static::addGlobalScope(new TenantScope);
|
||||||
|
|
||||||
static::creating(function ($model) {
|
static::creating(function ($model) {
|
||||||
if (! $model->getAttribute(static::tenantIdColumn()) && ! $model->relationLoaded('tenant')) {
|
if (! $model->getAttribute(Tenancy::tenantKeyColumn()) && ! $model->relationLoaded('tenant')) {
|
||||||
if (tenancy()->initialized) {
|
if (tenancy()->initialized) {
|
||||||
$model->setAttribute(static::tenantIdColumn(), tenant()->getTenantKey());
|
$model->setAttribute(Tenancy::tenantKeyColumn(), tenant()->getTenantKey());
|
||||||
$model->setRelation('tenant', tenant());
|
$model->setRelation('tenant', tenant());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// todo not sure if this should be in Database\
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\Concerns;
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Stancl\Tenancy\Contracts\Domain;
|
use Stancl\Tenancy\Contracts\Domain;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains
|
* @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains
|
||||||
|
|
@ -17,12 +16,12 @@ trait HasDomains
|
||||||
{
|
{
|
||||||
public function domains()
|
public function domains()
|
||||||
{
|
{
|
||||||
return $this->hasMany(config('tenancy.domain_model'), 'tenant_id');
|
return $this->hasMany(config('tenancy.models.domain'), Tenancy::tenantKeyColumn());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createDomain($data): Domain
|
public function createDomain($data): Domain
|
||||||
{
|
{
|
||||||
$class = config('tenancy.domain_model');
|
$class = config('tenancy.models.domain');
|
||||||
|
|
||||||
if (! is_array($data)) {
|
if (! is_array($data)) {
|
||||||
$data = ['domain' => $data];
|
$data = ['domain' => $data];
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,17 @@ namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Illuminate\Validation\Rules\Exists;
|
use Illuminate\Validation\Rules\Exists;
|
||||||
use Illuminate\Validation\Rules\Unique;
|
use Illuminate\Validation\Rules\Unique;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
trait HasScopedValidationRules
|
trait HasScopedValidationRules
|
||||||
{
|
{
|
||||||
public function unique($table, $column = 'NULL')
|
public function unique($table, $column = 'NULL')
|
||||||
{
|
{
|
||||||
return (new Unique($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey());
|
return (new Unique($table, $column))->where(Tenancy::tenantKeyColumn(), $this->getTenantKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exists($table, $column = 'NULL')
|
public function exists($table, $column = 'NULL')
|
||||||
{
|
{
|
||||||
return (new Exists($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey());
|
return (new Exists($table, $column))->where(Tenancy::tenantKeyColumn(), $this->getTenantKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class Domain extends Model implements Contracts\Domain
|
||||||
|
|
||||||
public function tenant(): BelongsTo
|
public function tenant(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(config('tenancy.tenant_model'));
|
return $this->belongsTo(config('tenancy.models.tenant'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $dispatchesEvents = [
|
protected $dispatchesEvents = [
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Database;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Scope;
|
use Illuminate\Database\Eloquent\Scope;
|
||||||
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
class TenantScope implements Scope
|
class TenantScope implements Scope
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +17,7 @@ class TenantScope implements Scope
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder->where($model->qualifyColumn(BelongsToTenant::tenantIdColumn()), tenant()->getTenantKey());
|
$builder->where($model->qualifyColumn(Tenancy::tenantKeyColumn()), tenant()->getTenantKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function extend(Builder $builder): void
|
public function extend(Builder $builder): void
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class UserImpersonation implements Feature
|
||||||
{
|
{
|
||||||
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string $authGuard = null): ImpersonationToken {
|
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string $authGuard = null): ImpersonationToken {
|
||||||
return ImpersonationToken::create([
|
return ImpersonationToken::create([
|
||||||
'tenant_id' => $tenant->getTenantKey(),
|
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'redirect_url' => $redirectUrl,
|
'redirect_url' => $redirectUrl,
|
||||||
'auth_guard' => $authGuard,
|
'auth_guard' => $authGuard,
|
||||||
|
|
@ -39,7 +39,7 @@ class UserImpersonation implements Feature
|
||||||
|
|
||||||
abort_if($tokenExpired, 403);
|
abort_if($tokenExpired, 403);
|
||||||
|
|
||||||
$tokenTenantId = (string) $token->tenant_id;
|
$tokenTenantId = (string) $token->getAttribute(Tenancy::tenantKeyColumn());
|
||||||
$currentTenantId = (string) tenant()->getTenantKey();
|
$currentTenantId = (string) tenant()->getTenantKey();
|
||||||
|
|
||||||
abort_unless($tokenTenantId === $currentTenantId, 403);
|
abort_unless($tokenTenantId === $currentTenantId, 403);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Listeners;
|
||||||
|
|
||||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||||
use Stancl\Tenancy\Database\DatabaseManager;
|
use Stancl\Tenancy\Database\DatabaseManager;
|
||||||
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||||
|
|
||||||
class CreateTenantConnection
|
class CreateTenantConnection
|
||||||
{
|
{
|
||||||
|
|
@ -15,11 +15,12 @@ class CreateTenantConnection
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(TenantEvent $event): void
|
public function handle(TenancyEvent $event): void
|
||||||
{
|
{
|
||||||
/** @var TenantWithDatabase */
|
/** @var TenantWithDatabase $tenant */
|
||||||
$tenant = $event->tenant;
|
$tenant = $event->tenancy->tenant;
|
||||||
|
|
||||||
|
$this->database->purgeTenantConnection();
|
||||||
$this->database->createTenantConnection($tenant);
|
$this->database->createTenantConnection($tenant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use Stancl\Tenancy\Database\TenantCollection;
|
||||||
use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase;
|
use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase;
|
||||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||||
use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException;
|
use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
// todo@v4 review all code related to resource syncing
|
// todo@v4 review all code related to resource syncing
|
||||||
|
|
||||||
|
|
@ -77,7 +78,7 @@ class UpdateSyncedResource extends QueueableListener
|
||||||
/** @var Tenant */
|
/** @var Tenant */
|
||||||
$tenant = $event->tenant;
|
$tenant = $event->tenant;
|
||||||
|
|
||||||
return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey());
|
return ((string) $model->pivot->getAttribute(Tenancy::tenantKeyColumn())) === ((string) $tenant->getTenantKey());
|
||||||
};
|
};
|
||||||
|
|
||||||
$mappingExists = $centralModel->tenants->contains($currentTenantMapping);
|
$mappingExists = $centralModel->tenants->contains($currentTenantMapping);
|
||||||
|
|
|
||||||
21
src/Listeners/UseCentralConnection.php
Normal file
21
src/Listeners/UseCentralConnection.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Listeners;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Database\DatabaseManager;
|
||||||
|
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||||
|
|
||||||
|
class UseCentralConnection
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected DatabaseManager $database,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(TenancyEvent $event): void
|
||||||
|
{
|
||||||
|
$this->database->reconnectToCentral();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Listeners/UseTenantConnection.php
Normal file
21
src/Listeners/UseTenantConnection.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Listeners;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Database\DatabaseManager;
|
||||||
|
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||||
|
|
||||||
|
class UseTenantConnection
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected DatabaseManager $database,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(TenancyEvent $event): void
|
||||||
|
{
|
||||||
|
$this->database->setDefaultConnection('tenant');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
||||||
{
|
{
|
||||||
$domain = $args[0];
|
$domain = $args[0];
|
||||||
|
|
||||||
$tenant = config('tenancy.tenant_model')::query()
|
$tenant = config('tenancy.models.tenant')::query()
|
||||||
->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain))
|
->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain))
|
||||||
->with('domains')
|
->with('domains')
|
||||||
->first();
|
->first();
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ class Tenancy
|
||||||
|
|
||||||
public static function model(): Tenant&Model
|
public static function model(): Tenant&Model
|
||||||
{
|
{
|
||||||
$class = config('tenancy.tenant_model');
|
$class = config('tenancy.models.tenant');
|
||||||
|
|
||||||
/** @var Tenant&Model $model */
|
/** @var Tenant&Model $model */
|
||||||
$model = new $class;
|
$model = new $class;
|
||||||
|
|
@ -105,6 +105,12 @@ class Tenancy
|
||||||
return $model;
|
return $model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Name of the column used to relate models to tenants. */
|
||||||
|
public static function tenantKeyColumn(): string
|
||||||
|
{
|
||||||
|
return config('tenancy.models.tenant_key_column') ?? 'tenant_id';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to find a tenant using an ID.
|
* Try to find a tenant using an ID.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,9 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
$this->app->singleton($bootstrapper);
|
$this->app->singleton($bootstrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind the class in the tenancy.id_generator config to the UniqueIdentifierGenerator abstract.
|
// Bind the class in the tenancy.models.id_generator config to the UniqueIdentifierGenerator abstract.
|
||||||
if (! is_null($this->app['config']['tenancy.id_generator'])) {
|
if (! is_null($this->app['config']['tenancy.models.id_generator'])) {
|
||||||
$this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.id_generator']);
|
$this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.models.id_generator']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->app->singleton(Commands\Migrate::class, function ($app) {
|
$this->app->singleton(Commands\Migrate::class, function ($app) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ beforeEach(function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
config(['tenancy.tenant_model' => CombinedTenant::class]);
|
config(['tenancy.models.tenant' => CombinedTenant::class]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant can be identified by subdomain', function () {
|
test('tenant can be identified by subdomain', function () {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) {
|
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
||||||
unlink($schemaPath);
|
unlink($schemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,28 +111,44 @@ test('migrate command loads schema state', function () {
|
||||||
|
|
||||||
test('dump command works', function () {
|
test('dump command works', function () {
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
|
$schemaPath = 'tests/Etc/tenant-schema-test.dump';
|
||||||
|
|
||||||
Artisan::call('tenants:migrate');
|
Artisan::call('tenants:migrate');
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
expect($schemaPath)->not()->toBeFile();
|
||||||
|
|
||||||
Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"');
|
Artisan::call('tenants:dump ' . "--tenant='$tenant->id' --path='$schemaPath'");
|
||||||
expect('tests/Etc/tenant-schema-test.dump')->toBeFile();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tenant dump file gets created as tenant-schema.dump in the database schema folder by default', function() {
|
|
||||||
config(['tenancy.migration_parameters.--schema-path' => $schemaPath = database_path('schema/tenant-schema.dump')]);
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
Artisan::call('tenants:migrate');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
Artisan::call('tenants:dump');
|
|
||||||
|
|
||||||
expect($schemaPath)->toBeFile();
|
expect($schemaPath)->toBeFile();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('migrate command uses the correct schema path by default', function () {
|
test('dump command generates dump at the passed path', function() {
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
Artisan::call('tenants:migrate');
|
||||||
|
|
||||||
|
expect($schemaPath = 'tests/Etc/tenant-schema-test.dump')->not()->toBeFile();
|
||||||
|
|
||||||
|
Artisan::call("tenants:dump --tenant='$tenant->id' --path='$schemaPath'");
|
||||||
|
|
||||||
|
expect($schemaPath)->toBeFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dump command generates dump at the path specified in the tenancy migration parameters config', function() {
|
||||||
|
config(['tenancy.migration_parameters.--schema-path' => $schemaPath = 'tests/Etc/tenant-schema-test.dump']);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
Artisan::call('tenants:migrate');
|
||||||
|
|
||||||
|
expect($schemaPath)->not()->toBeFile();
|
||||||
|
|
||||||
|
Artisan::call("tenants:dump --tenant='$tenant->id'");
|
||||||
|
|
||||||
|
expect($schemaPath)->toBeFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('migrate command correctly uses the schema dump located at the configured schema path by default', function () {
|
||||||
config(['tenancy.migration_parameters.--schema-path' => 'tests/Etc/tenant-schema.dump']);
|
config(['tenancy.migration_parameters.--schema-path' => 'tests/Etc/tenant-schema.dump']);
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
|
@ -146,6 +162,7 @@ test('migrate command uses the correct schema path by default', function () {
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// schema_users is a table included in the tests/Etc/tenant-schema dump
|
||||||
// Check for both tables to see if missing migrations also get executed
|
// Check for both tables to see if missing migrations also get executed
|
||||||
expect(Schema::hasTable('schema_users'))->toBeTrue();
|
expect(Schema::hasTable('schema_users'))->toBeTrue();
|
||||||
expect(Schema::hasTable('users'))->toBeTrue();
|
expect(Schema::hasTable('users'))->toBeTrue();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||||
use Stancl\Tenancy\Jobs\DeleteDomains;
|
use Stancl\Tenancy\Jobs\DeleteDomains;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.tenant_model' => DatabaseAndDomainTenant::class]);
|
config(['tenancy.models.tenant' => DatabaseAndDomainTenant::class]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('job delete domains successfully', function (){
|
test('job delete domains successfully', function (){
|
||||||
|
|
@ -29,4 +29,4 @@ test('job delete domains successfully', function (){
|
||||||
class DatabaseAndDomainTenant extends \Stancl\Tenancy\Tests\Etc\Tenant
|
class DatabaseAndDomainTenant extends \Stancl\Tenancy\Tests\Etc\Tenant
|
||||||
{
|
{
|
||||||
use HasDomains;
|
use HasDomains;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ beforeEach(function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
config(['tenancy.tenant_model' => DomainTenant::class]);
|
config(['tenancy.models.tenant' => DomainTenant::class]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant can be identified using hostname', function () {
|
test('tenant can be identified using hostname', function () {
|
||||||
|
|
|
||||||
45
tests/ManualModeTest.php
Normal file
45
tests/ManualModeTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Listeners\CreateTenantConnection;
|
||||||
|
use Stancl\Tenancy\Listeners\UseCentralConnection;
|
||||||
|
use Stancl\Tenancy\Listeners\UseTenantConnection;
|
||||||
|
use \Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
test('manual tenancy initialization works', function () {
|
||||||
|
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener());
|
||||||
|
|
||||||
|
Event::listen(TenancyInitialized::class, CreateTenantConnection::class);
|
||||||
|
Event::listen(TenancyInitialized::class, UseTenantConnection::class);
|
||||||
|
Event::listen(TenancyEnded::class, UseCentralConnection::class);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
expect(app('db')->getDefaultConnection())->toBe('central');
|
||||||
|
expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant_host_connection']);
|
||||||
|
pest()->assertArrayNotHasKey('tenant', config('database.connections'));
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// Trigger creation of the tenant connection
|
||||||
|
createUsersTable();
|
||||||
|
|
||||||
|
expect(app('db')->getDefaultConnection())->toBe('tenant');
|
||||||
|
expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant']);
|
||||||
|
pest()->assertArrayHasKey('tenant', config('database.connections'));
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
expect(array_keys(app('db')->getConnections()))->toBe(['central']);
|
||||||
|
expect(config('database.connections.tenant'))->toBeNull();
|
||||||
|
expect(app('db')->getDefaultConnection())->toBe(config('tenancy.database.central_connection'));
|
||||||
|
});
|
||||||
|
|
@ -67,23 +67,6 @@ test('CreatePendingTenants command cannot run with both time constraints', funct
|
||||||
->assertFailed();
|
->assertFailed();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('CreatePendingTenants commands all option overrides any config constraints', function () {
|
|
||||||
Tenant::createPending();
|
|
||||||
Tenant::createPending();
|
|
||||||
|
|
||||||
tenancy()->model()->query()->onlyPending()->first()->update([
|
|
||||||
'pending_since' => now()->subDays(10)
|
|
||||||
]);
|
|
||||||
|
|
||||||
config(['tenancy.pending.older_than_days' => 4]);
|
|
||||||
|
|
||||||
Artisan::call(ClearPendingTenants::class, [
|
|
||||||
'--all' => true
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(Tenant::onlyPending()->count())->toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tenancy can check if there are any pending tenants', function () {
|
test('tenancy can check if there are any pending tenants', function () {
|
||||||
expect(Tenant::onlyPending()->exists())->toBeFalse();
|
expect(Tenant::onlyPending()->exists())->toBeFalse();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ beforeEach(function () {
|
||||||
$table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade');
|
$table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade');
|
||||||
});
|
});
|
||||||
|
|
||||||
config(['tenancy.tenant_model' => Tenant::class]);
|
config(['tenancy.models.tenant' => Tenant::class]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('primary models are scoped to the current tenant', function () {
|
test('primary models are scoped to the current tenant', function () {
|
||||||
|
|
@ -142,7 +142,7 @@ test('tenant id is not auto added when creating primary resources in central con
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant id column name can be customized', function () {
|
test('tenant id column name can be customized', function () {
|
||||||
config(['tenancy.single_db.tenant_id_column' => 'team_id']);
|
config(['tenancy.models.tenant_key_column' => 'team_id']);
|
||||||
|
|
||||||
Schema::drop('comments');
|
Schema::drop('comments');
|
||||||
Schema::drop('posts');
|
Schema::drop('posts');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ beforeEach(function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
config(['tenancy.tenant_model' => SubdomainTenant::class]);
|
config(['tenancy.models.tenant' => SubdomainTenant::class]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant can be identified by subdomain', function () {
|
test('tenant can be identified by subdomain', function () {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ test('current tenant can be resolved from service container using typehint', fun
|
||||||
});
|
});
|
||||||
|
|
||||||
test('id is generated when no id is supplied', function () {
|
test('id is generated when no id is supplied', function () {
|
||||||
config(['tenancy.id_generator' => UUIDGenerator::class]);
|
config(['tenancy.models.id_generator' => UUIDGenerator::class]);
|
||||||
|
|
||||||
$this->mock(UUIDGenerator::class, function ($mock) {
|
$this->mock(UUIDGenerator::class, function ($mock) {
|
||||||
return $mock->shouldReceive('generate')->once();
|
return $mock->shouldReceive('generate')->once();
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
'central' => true,
|
'central' => true,
|
||||||
],
|
],
|
||||||
'tenancy.seeder_parameters' => [],
|
'tenancy.seeder_parameters' => [],
|
||||||
'tenancy.tenant_model' => Tenant::class, // Use test tenant w/ DBs & domains
|
'tenancy.models.tenant' => Tenant::class, // Use test tenant w/ DBs & domains
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
|
$app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue