Skip to content

Commit a5b3111

Browse files
authored
Merge branch 'master' into override-toroute
2 parents b17f66f + ad4c924 commit a5b3111

22 files changed

Lines changed: 589 additions & 90 deletions

assets/TenancyServiceProvider.stub.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ public function events()
5353
])->send(function (Events\TenantCreated $event) {
5454
return $event->tenant;
5555
})->shouldBeQueued(false),
56-
57-
// Listeners\CreateTenantStorage::class,
5856
],
5957
Events\SavingTenant::class => [],
6058
Events\TenantSaved::class => [],
@@ -63,12 +61,11 @@ public function events()
6361
Events\DeletingTenant::class => [
6462
JobPipeline::make([
6563
Jobs\DeleteDomains::class,
64+
// Jobs\DeleteTenantStorage::class,
6665
// Jobs\RemoveStorageSymlinks::class,
6766
])->send(function (Events\DeletingTenant $event) {
6867
return $event->tenant;
6968
})->shouldBeQueued(false),
70-
71-
// Listeners\DeleteTenantStorage::class,
7269
],
7370
Events\TenantDeleted::class => [
7471
JobPipeline::make([

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ services:
8080
mssql:
8181
image: mcr.microsoft.com/mssql/server:2022-latest
8282
environment:
83-
- ACCEPT_EULA=Y
84-
- SA_PASSWORD=P@ssword # must be the same as TENANCY_TEST_SQLSRV_PASSWORD
83+
ACCEPT_EULA: "Y"
84+
SA_PASSWORD: "P@ssword" # must be the same as TENANCY_TEST_SQLSRV_PASSWORD
8585
healthcheck: # https://github.com/Microsoft/mssql-docker/issues/133#issuecomment-1995615432
8686
test: timeout 2 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/1433'
8787
interval: 10s

src/Concerns/HasTenantOptions.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ protected function getOptions()
1818
{
1919
return array_merge([
2020
new InputOption('tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, 'The tenants to run this command for. Leave empty for all tenants', null),
21-
new InputOption('with-pending', null, InputOption::VALUE_NONE, 'Include pending tenants in query'), // todo@pending should we also offer without-pending? if we add this, mention in docs
21+
new InputOption('with-pending', null, InputOption::VALUE_OPTIONAL, 'Include pending tenants in query if true/1, exclude if false/0. Defaults to the tenancy.pending.include_in_queries config value.'),
2222
], parent::getOptions());
2323
}
2424

@@ -43,7 +43,11 @@ protected function getTenantsQuery(?array $tenantKeys = null): Builder
4343
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
4444
})
4545
->when(tenancy()->model()::hasGlobalScope(PendingScope::class), function ($query) {
46-
$query->withPending(config('tenancy.pending.include_in_queries') ?: $this->option('with-pending'));
46+
$includePending = $this->input->hasParameterOption('--with-pending')
47+
? filter_var($this->option('with-pending') ?? true, FILTER_VALIDATE_BOOLEAN)
48+
: config('tenancy.pending.include_in_queries');
49+
50+
$query->withPending($includePending);
4751
});
4852
}
4953

src/Database/Concerns/HasPending.php

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ trait HasPending
2828
public static function bootHasPending(): void
2929
{
3030
static::addGlobalScope(new PendingScope());
31+
32+
static::creating(function (self $tenant): void {
33+
if ($tenant->pending()) {
34+
event(new CreatingPendingTenant($tenant));
35+
}
36+
});
37+
38+
static::created(function (self $tenant): void {
39+
if ($tenant->pending()) {
40+
event(new PendingTenantCreated($tenant));
41+
}
42+
});
3143
}
3244

3345
/** Initialize the trait. */
@@ -49,22 +61,11 @@ public function pending(): bool
4961
*/
5062
public static function createPending(array $attributes = []): Model&Tenant
5163
{
52-
$tenant = null;
53-
54-
try {
55-
$tenant = static::create(array_merge(static::getPendingAttributes($attributes), $attributes));
56-
event(new CreatingPendingTenant($tenant));
57-
} finally {
58-
// Update the pending_since value only after the tenant is created so it's
59-
// not marked as pending until after migrations, seeders, etc are run.
60-
$tenant?->update([
61-
'pending_since' => now()->timestamp,
62-
]);
63-
}
64-
65-
event(new PendingTenantCreated($tenant));
66-
67-
return $tenant;
64+
return static::create(array_merge(
65+
static::getPendingAttributes($attributes),
66+
$attributes,
67+
['pending_since' => now()->timestamp],
68+
));
6869
}
6970

7071
/**

src/Database/Concerns/PendingScope.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class PendingScope implements Scope
1414
/**
1515
* Apply the scope to a given Eloquent query builder.
1616
*
17-
* @param Builder<Model> $builder
17+
* @param Builder<covariant Model> $builder
1818
*
1919
* @return void
2020
*/
@@ -58,8 +58,10 @@ protected function addWithoutPending(Builder $builder): void
5858
{
5959
$builder->macro('withoutPending', function (Builder $builder) {
6060
$builder->withoutGlobalScope(static::class)
61-
->whereNull($builder->getModel()->getColumnForQuery('pending_since'))
62-
->orWhereNull($builder->getModel()->getDataColumn());
61+
->where(function (Builder $query) {
62+
$query->whereNull($query->getModel()->getColumnForQuery('pending_since'))
63+
->orWhereNull($query->getModel()->getDataColumn());
64+
});
6365

6466
return $builder;
6567
});

src/Database/ParentModelScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
class ParentModelScope implements Scope
1313
{
1414
/**
15-
* @param Builder<Model> $builder
15+
* @param Builder<covariant Model> $builder
1616
*/
1717
public function apply(Builder $builder, Model $model): void
1818
{

src/Database/TenantScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class TenantScope implements Scope
1414
{
1515
/**
16-
* @param Builder<Model> $builder
16+
* @param Builder<covariant Model> $builder
1717
*/
1818
public function apply(Builder $builder, Model $model)
1919
{

src/Features/UserImpersonation.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Stancl\Tenancy\Features;
66

7+
use Exception;
78
use Illuminate\Database\Eloquent\Model;
89
use Illuminate\Http\RedirectResponse;
910
use Illuminate\Support\Facades\Auth;
@@ -61,9 +62,9 @@ public static function makeResponse(#[\SensitiveParameter] string|Model $token,
6162

6263
Auth::guard($token->auth_guard)->loginUsingId($token->user_id, $token->remember);
6364

64-
$token->delete();
65+
session()->put('tenancy_impersonation_guard', $token->auth_guard);
6566

66-
session()->put('tenancy_impersonating', true);
67+
$token->delete();
6768

6869
return redirect($token->redirect_url);
6970
}
@@ -76,16 +77,30 @@ public static function modelClass(): string
7677

7778
public static function isImpersonating(): bool
7879
{
79-
return session()->has('tenancy_impersonating');
80+
return session()->has('tenancy_impersonation_guard');
8081
}
8182

8283
/**
83-
* Logout from the current domain and forget impersonation session.
84+
* Stop user impersonation by forgetting the impersonation session.
85+
*
86+
* When $logout is true, the user will also be logged out
87+
* from the impersonation guard stored in the session.
88+
*
89+
* Throws an exception if impersonation is not active
90+
* (= the impersonation guard is not in the session).
8491
*/
85-
public static function stopImpersonating(): void
92+
public static function stopImpersonating(bool $logout = true): void
8693
{
87-
auth()->logout();
94+
if (! static::isImpersonating()) {
95+
throw new Exception('Not currently impersonating any user.');
96+
}
97+
98+
if ($logout) {
99+
$guard = session()->get('tenancy_impersonation_guard');
100+
101+
auth($guard)->logout();
102+
}
88103

89-
session()->forget('tenancy_impersonating');
104+
session()->forget('tenancy_impersonation_guard');
90105
}
91106
}

src/Jobs/DeleteDatabase.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,34 @@ public function __construct(
2222
protected TenantWithDatabase&Model $tenant,
2323
) {}
2424

25+
/** Skip database deletion if the create_database internal attribute is false. */
26+
public static bool $skipWhenCreateDatabaseIsFalse = true;
27+
28+
/** Ignore exceptions thrown during database deletion and continue execution. */
29+
public static bool $ignoreFailures = false;
30+
2531
public function handle(): void
2632
{
33+
if (static::$skipWhenCreateDatabaseIsFalse && $this->tenant->getInternal('create_database') === false) {
34+
// If database creation was skipped, we presume deletion should also be skipped.
35+
// To avoid this skip, either unset the `create_database` attribute (or make it true), or
36+
// set the $skipWhenCreateDatabaseIsFalse static property to false.
37+
return;
38+
}
39+
2740
event(new DeletingDatabase($this->tenant));
2841

29-
$this->tenant->database()->manager()->deleteDatabase($this->tenant);
42+
$deleted = false;
43+
44+
try {
45+
$this->tenant->database()->manager()->deleteDatabase($this->tenant);
46+
$deleted = true;
47+
} catch (\Throwable $e) {
48+
if (! static::$ignoreFailures) {
49+
throw $e;
50+
}
51+
}
3052

31-
event(new DatabaseDeleted($this->tenant));
53+
if ($deleted) event(new DatabaseDeleted($this->tenant));
3254
}
3355
}

src/Jobs/DeleteTenantStorage.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Stancl\Tenancy\Jobs;
6+
7+
use Illuminate\Bus\Queueable;
8+
use Illuminate\Contracts\Queue\ShouldQueue;
9+
use Illuminate\Foundation\Bus\Dispatchable;
10+
use Illuminate\Queue\InteractsWithQueue;
11+
use Illuminate\Queue\SerializesModels;
12+
use Illuminate\Support\Facades\File;
13+
use Stancl\Tenancy\Contracts\Tenant;
14+
15+
class DeleteTenantStorage implements ShouldQueue
16+
{
17+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
18+
19+
public function __construct(
20+
public Tenant $tenant,
21+
) {}
22+
23+
public function handle(): void
24+
{
25+
if (config('tenancy.filesystem.suffix_storage_path') === false) {
26+
// Skip storage deletion if path suffixing is disabled
27+
return;
28+
}
29+
30+
$centralStoragePath = tenancy()->central(fn () => storage_path());
31+
$tenantStoragePath = tenancy()->run($this->tenant, fn () => storage_path());
32+
33+
if ($tenantStoragePath === $centralStoragePath) {
34+
// Check again to ensure the tenant storage path is distinct from the central storage path
35+
// to avoid any accidental central storage path deletion
36+
return;
37+
}
38+
39+
if (is_dir($tenantStoragePath)) {
40+
File::deleteDirectory($tenantStoragePath);
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)