1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-05-06 15:24:03 +00:00

Merge branch 'master' into boilerplate-dev

This commit is contained in:
lukinovec 2026-04-22 14:47:28 +02:00
commit 5d047089ea
8 changed files with 95 additions and 7 deletions

View file

@ -32,7 +32,7 @@
"league/flysystem-aws-s3-v3": "^3.12.2",
"doctrine/dbal": "^3.6.0",
"spatie/valuestore": "^1.2.5",
"pestphp/pest": "^3.0",
"pestphp/pest": "^4.0",
"larastan/larastan": "^3.0",
"league/flysystem-path-prefixing": "^3.0"
},

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Exception;
use Illuminate\Foundation\Application;
use Illuminate\Session\FileSessionHandler;
use Illuminate\Support\Facades\Storage;
@ -75,8 +76,13 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
: $this->originalStoragePath . '/framework/cache';
if (! is_dir($path)) {
// Create tenant framework/cache directory if it does not exist
mkdir($path, 0750, true);
// Create tenant framework/cache directory if it does not exist.
// We ignore errors due to TOCTOU race conditions, instead we check for success below.
@mkdir($path, 0750, true);
if (! is_dir($path)) {
throw new Exception("Unable to create tenant storage directory [{$path}].");
}
}
if ($suffix === false) {
@ -222,8 +228,13 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
: $this->originalStoragePath . '/framework/sessions';
if (! is_dir($path)) {
// Create tenant framework/sessions directory if it does not exist
mkdir($path, 0750, true);
// Create tenant framework/sessions directory if it does not exist.
// We ignore errors due to TOCTOU race conditions, instead we check for success below.
@mkdir($path, 0750, true);
if (! is_dir($path)) {
throw new Exception("Unable to create tenant session directory [{$path}].");
}
}
$this->app['config']['session.files'] = $path;

View file

@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
/** @implements Scope<Model> */
class PendingScope implements Scope
{
/**
@ -57,8 +58,10 @@ class PendingScope implements Scope
{
$builder->macro('withoutPending', function (Builder $builder) {
$builder->withoutGlobalScope(static::class)
->whereNull($builder->getModel()->getColumnForQuery('pending_since'))
->orWhereNull($builder->getModel()->getDataColumn());
->where(function (Builder $query) {
$query->whereNull($query->getModel()->getColumnForQuery('pending_since'))
->orWhereNull($query->getModel()->getDataColumn());
});
return $builder;
});

View file

@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
/** @implements Scope<Model> */
class ParentModelScope implements Scope
{
/**

View file

@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Stancl\Tenancy\Tenancy;
/** @implements Scope<Model> */
class TenantScope implements Scope
{
/**

View file

@ -152,6 +152,26 @@ class Tenancy
$this->initialized = false;
}
/**
* End tenancy and initialize it again for the current tenant.
*
* This can be helpful when changing "dependencies" of bootstrappers such as
* attributes of the current tenant that are only read once, during bootstrap().
*
* If tenancy is not initialized, this method is a no-op.
*/
public function reinitialize(): void
{
if ($this->tenant === null) {
return;
}
$tenant = $this->tenant;
$this->end();
$this->initialize($tenant);
}
/** @return TenancyBootstrapper[] */
public function getBootstrappers(): array
{

View file

@ -103,6 +103,33 @@ test('central helper doesnt change tenancy state when called in central context'
expect(tenant())->toBeNull();
});
test('reinitialize method does nothing in the central context', function () {
expect(tenancy()->initialized)->toBe(false);
expect(fn () => tenancy()->reinitialize())->not()->toThrow(\Throwable::class);
expect(tenancy()->initialized)->toBe(false);
});
test('reinitialize method runs bootstrappers again for the current tenant', function () {
config(['tenancy.bootstrappers' => [
ReinitBootstrapper::class,
]]);
tenancy()->initialize($tenant = Tenant::create(['reinit_bootstrapper_key' => 'foo']));
expect(tenant()->getKey())->toBe($tenant->getKey());
expect(app('tenancy_reinit_bootstrapper_key'))->toBe('foo');
$tenant->update(['reinit_bootstrapper_key' => 'bar']);
// Unchanged until we reinitialize...
expect(app('tenancy_reinit_bootstrapper_key'))->toBe('foo');
tenancy()->reinitialize();
expect(tenant()->getKey())->toBe($tenant->getKey());
expect(app('tenancy_reinit_bootstrapper_key'))->toBe('bar');
});
class MyBootstrapper implements TenancyBootstrapper
{
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void
@ -115,3 +142,16 @@ class MyBootstrapper implements TenancyBootstrapper
app()->instance('tenancy_ended', true);
}
}
class ReinitBootstrapper implements TenancyBootstrapper
{
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void
{
app()->instance('tenancy_reinit_bootstrapper_key', $tenant->getAttribute('reinit_bootstrapper_key'));
}
public function revert(): void
{
app()->instance('tenancy_reinit_bootstrapper_key', null);
}
}

View file

@ -111,6 +111,18 @@ test('a new tenant gets created while pulling a pending tenant if the pending po
expect(Tenant::withPending()->get()->count())->toBe(1); // All tenants
});
test('withoutPending chained with where clauses returns correct results', function () {
$tenant = Tenant::create();
$pendingTenant = Tenant::createPending();
// The query returned the correct tenant
expect(Tenant::withoutPending()->where('id', $tenant->id)->first()->id)->toBe($tenant->id);
// No tenant with this ID exists, the query returns null
expect(Tenant::withoutPending()->where('id', Str::random(8) . 'nonexistent-id')->first())->toBeNull();
// withoutPending() correctly excludes the pending tenant from the query
expect(Tenant::withoutPending()->where('id', $pendingTenant->id)->first())->toBeNull();
});
test('pending tenants are included in all queries based on the include_in_queries config', function () {
Tenant::createPending();