1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 15:14:04 +00:00

Merge branch 'master' into poly-sync

This commit is contained in:
Abrar Ahmad 2022-12-06 11:35:40 +05:00
commit dc69a112eb
34 changed files with 231 additions and 125 deletions

View file

@ -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.

View file

@ -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,
/**
* 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, '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',
],
]; ];

View file

@ -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');
} }
} };

View file

@ -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');
} }
} };

View file

@ -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');
} }
} };

View file

@ -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');

View file

@ -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,13 +23,8 @@ 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 */
$olderThanHours = $this->option('older-than-hours');
if ($olderThanDays && $olderThanHours) { if ($olderThanDays && $olderThanHours) {
$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("<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
@ -46,10 +40,8 @@ class ClearPendingTenants extends Command
if ($olderThanHours) { if ($olderThanHours) {
$expirationDate->subHours($olderThanHours); $expirationDate->subHours($olderThanHours);
} }
}
$deletedTenantCount = tenancy() $deletedTenantCount = tenancy()->query()
->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);

View file

@ -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();
} }

View file

@ -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();

View file

@ -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;
} }

View file

@ -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'));
}) })

View file

@ -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());
} }
} }

View file

@ -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];

View file

@ -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());
} }
} }

View file

@ -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 = [

View file

@ -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

View file

@ -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);

View file

@ -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);
} }
} }

View file

@ -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);

View 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();
}
}

View 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');
}
}

View file

@ -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();

View file

@ -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.
* *

View file

@ -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) {

View file

@ -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 () {

View file

@ -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();

View file

@ -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 (){

View file

@ -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
View 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'));
});

View file

@ -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();

View file

@ -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');

View file

@ -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 () {

View file

@ -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();

View file

@ -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