Skip to content

Commit 4578c9e

Browse files
committed
Features refactor
Features are now *always* bootstrapped, even if Tenancy is not resolved from the container. Previous implementations include tenancy-for-laravel/v4#19 #1021 Bug originally reported here #949 This implementation is much simpler, we do not distinguish between features that should be "always bootstrapped" and features that should only be bootstrapped after Tenancy is resolved. All features should work without issues if they're bootstrapped when TSP::boot() is called. We also add a Tenancy::bootstrapFeatures() method that can be used to bootstrap any features dynamically added at runtime that weren't bootstrapped in TSP::boot(). The function keeps track of which features were already bootstrapped so it doesn't bootstrap them again. The only potentialy risky thing in this implementation is that we're now resolving Tenancy in TSP::boot() (previously Tenancy was not being resolved) but that shouldn't be causing any issues.
1 parent 33e4a8e commit 4578c9e

15 files changed

+80
-45
lines changed

src/Contracts/Feature.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55
namespace Stancl\Tenancy\Contracts;
66

7-
use Stancl\Tenancy\Tenancy;
8-
97
/** Additional features, like Telescope tags and tenant redirects. */
108
interface Feature
119
{
12-
public function bootstrap(Tenancy $tenancy): void;
10+
public function bootstrap(): void;
1311
}

src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public function createDatabase(TenantWithDatabase $tenant): bool
9494
return false;
9595
}
9696

97+
// todo@sqlite we can just respect Laravel config for WAL now
9798
if (static::$WAL) {
9899
$pdo = new PDO('sqlite:' . $path);
99100
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
@@ -123,6 +124,7 @@ public function deleteDatabase(TenantWithDatabase $tenant): bool
123124
}
124125

125126
try {
127+
// todo@sqlite we should also remove any other files for the DB e.g. WAL
126128
return unlink($this->getPath($name));
127129
} catch (Throwable) {
128130
return false;

src/Features/CrossDomainRedirect.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66

77
use Illuminate\Http\RedirectResponse;
88
use Stancl\Tenancy\Contracts\Feature;
9-
use Stancl\Tenancy\Tenancy;
109

1110
class CrossDomainRedirect implements Feature
1211
{
13-
public function bootstrap(Tenancy $tenancy): void
12+
public function bootstrap(): void
1413
{
1514
RedirectResponse::macro('domain', function (string $domain) {
1615
/** @var RedirectResponse $this */

src/Features/DisallowSqliteAttach.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
use Illuminate\Support\Facades\DB;
1111
use PDO;
1212
use Stancl\Tenancy\Contracts\Feature;
13-
use Stancl\Tenancy\Tenancy;
1413

1514
class DisallowSqliteAttach implements Feature
1615
{
1716
protected static bool|null $loadExtensionSupported = null;
1817
public static string|false|null $extensionPath = null;
1918

20-
public function bootstrap(Tenancy $tenancy): void
19+
public function bootstrap(): void
2120
{
2221
// Handle any already resolved connections
2322
foreach (DB::getConnections() as $connection) {
@@ -40,16 +39,20 @@ public function bootstrap(Tenancy $tenancy): void
4039
protected function loadExtension(PDO $pdo): bool
4140
{
4241
if (static::$loadExtensionSupported === null) {
42+
// todo@sqlite refactor to local static
4343
static::$loadExtensionSupported = method_exists($pdo, 'loadExtension');
4444
}
4545

4646
if (static::$loadExtensionSupported === false) {
4747
return false;
4848
}
49+
4950
if (static::$extensionPath === false) {
5051
return false;
5152
}
5253

54+
// todo@sqlite we may want to check for 64 bit
55+
5356
$suffix = match (PHP_OS_FAMILY) {
5457
'Linux' => 'so',
5558
'Windows' => 'dll',

src/Features/TelescopeTags.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
use Laravel\Telescope\IncomingEntry;
88
use Laravel\Telescope\Telescope;
99
use Stancl\Tenancy\Contracts\Feature;
10-
use Stancl\Tenancy\Tenancy;
1110

1211
class TelescopeTags implements Feature
1312
{
14-
public function bootstrap(Tenancy $tenancy): void
13+
public function bootstrap(): void
1514
{
1615
if (! class_exists(Telescope::class)) {
1716
return;

src/Features/TenantConfig.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Stancl\Tenancy\Contracts\Tenant;
1313
use Stancl\Tenancy\Events\RevertedToCentralContext;
1414
use Stancl\Tenancy\Events\TenancyBootstrapped;
15-
use Stancl\Tenancy\Tenancy;
1615

1716
class TenantConfig implements Feature
1817
{
@@ -27,7 +26,7 @@ public function __construct(
2726
protected Repository $config,
2827
) {}
2928

30-
public function bootstrap(Tenancy $tenancy): void
29+
public function bootstrap(): void
3130
{
3231
Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) {
3332
/** @var Tenant $tenant */

src/Features/UserImpersonation.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ class UserImpersonation implements Feature
1717
/** The lifespan of impersonation tokens (in seconds). */
1818
public static int $ttl = 60;
1919

20-
public function bootstrap(Tenancy $tenancy): void
20+
public function bootstrap(): void
2121
{
22-
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): Model {
22+
Tenancy::macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): Model {
2323
return UserImpersonation::modelClass()::create([
2424
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
2525
'user_id' => $userId,

src/Features/ViteBundler.php

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,14 @@
77
use Illuminate\Foundation\Application;
88
use Illuminate\Support\Facades\Vite;
99
use Stancl\Tenancy\Contracts\Feature;
10-
use Stancl\Tenancy\Tenancy;
1110

1211
class ViteBundler implements Feature
1312
{
14-
/** @var Application */
15-
protected $app;
13+
public function __construct(
14+
protected Application $app,
15+
) {}
1616

17-
public function __construct(Application $app)
18-
{
19-
$this->app = $app;
20-
}
21-
22-
public function bootstrap(Tenancy $tenancy): void
17+
public function bootstrap(): void
2318
{
2419
Vite::createAssetPathsUsing(function ($path, $secure = null) {
2520
return global_asset($path);

src/Tenancy.php

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Illuminate\Support\Traits\Macroable;
1212
use Stancl\Tenancy\Concerns\DealsWithRouteContexts;
1313
use Stancl\Tenancy\Concerns\ManagesRLSPolicies;
14+
use Stancl\Tenancy\Contracts\Feature;
1415
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
1516
use Stancl\Tenancy\Contracts\Tenant;
1617
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
@@ -40,7 +41,7 @@ class Tenancy
4041
public static array $findWith = [];
4142

4243
/**
43-
* A list of bootstrappers that have been initialized.
44+
* List of bootstrappers that have been initialized.
4445
*
4546
* This is used when reverting tenancy, mainly if an exception
4647
* occurs during bootstrapping, to ensure we don't revert
@@ -53,6 +54,23 @@ class Tenancy
5354
*/
5455
public array $initializedBootstrappers = [];
5556

57+
/**
58+
* List of features that have been bootstrapped.
59+
*
60+
* Since features may be bootstrapped multiple times during
61+
* the request cycle (in TSP::boot() and any other times the user calls
62+
* bootstrapFeatures()), we keep track of which features have already
63+
* been bootstrapped so we do not bootstrap them again. Features are
64+
* bootstrapped once and irreversible.
65+
*
66+
* The main point of this is that some features *need* to be bootstrapped
67+
* very early (see #949), so we bootstrap them directly in TSP, but we
68+
* also need the ability to *change* which features are used at runtime
69+
* (mainly tests of this package) and bootstrap features again after making
70+
* changes to config('tenancy.features').
71+
*/
72+
protected array $bootstrappedFeatures = [];
73+
5674
/** Initialize tenancy for the passed tenant. */
5775
public function initialize(Tenant|int|string $tenant): void
5876
{
@@ -154,6 +172,27 @@ public function usingBootstrapper(string $bootstrapper): bool
154172
return in_array($bootstrapper, static::getBootstrappers(), true);
155173
}
156174

175+
/**
176+
* Bootstrap configured Tenancy features.
177+
*
178+
* Normally, features are bootstrapped directly in TSP::boot(). However, if
179+
* new features are enabled at runtime (e.g. during tests), this method may
180+
* be called to bootstrap new features. It's idempotent and keeps track of
181+
* which features have already been bootstrapped. Keep in mind that feature
182+
* bootstrapping is irreversible.
183+
*/
184+
public function bootstrapFeatures(): void
185+
{
186+
foreach (config('tenancy.features') ?? [] as $feature) {
187+
/** @var class-string<Feature> $feature */
188+
189+
if (! in_array($feature, $this->bootstrappedFeatures)) {
190+
app($feature)->bootstrap();
191+
$this->bootstrappedFeatures[] = $feature;
192+
}
193+
}
194+
}
195+
157196
/**
158197
* @return Builder<Tenant&Model>
159198
*/

src/TenancyServiceProvider.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,6 @@ public function register(): void
4040
// Make sure Tenancy is stateful.
4141
$this->app->singleton(Tenancy::class);
4242

43-
// Make sure features are bootstrapped as soon as Tenancy is instantiated.
44-
$this->app->extend(Tenancy::class, function (Tenancy $tenancy) {
45-
foreach ($this->app['config']['tenancy.features'] ?? [] as $feature) {
46-
$this->app[$feature]->bootstrap($tenancy);
47-
}
48-
49-
return $tenancy;
50-
});
51-
5243
// Make it possible to inject the current tenant by type hinting the Tenant contract.
5344
$this->app->bind(Tenant::class, function ($app) {
5445
return $app[Tenancy::class]->tenant;
@@ -176,6 +167,11 @@ public function boot(): void
176167
return $instance;
177168
});
178169

170+
// Bootstrap features that are already enabled in the config.
171+
// If more features are enabled at runtime, this method may be called
172+
// multiple times, it keeps track of which features have already been bootstrapped.
173+
$this->app->make(Tenancy::class)->bootstrapFeatures();
174+
179175
Route::middlewareGroup('clone', []);
180176
Route::middlewareGroup('universal', []);
181177
Route::middlewareGroup('tenant', []);

0 commit comments

Comments
 (0)