Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.x] Cleanup #1317

Merged
merged 16 commits into from
Feb 20, 2025
Merged
2 changes: 1 addition & 1 deletion .github/workflows/queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- name: Prepare composer version constraint prefix
run: |
BRANCH=${GITHUB_REF#refs/heads/}
if [[ $BRANCH =~ ^[0-9] ]]; then
if [[ $BRANCH =~ ^[0-9]\.x$ ]]; then
echo "VERSION_PREFIX=${BRANCH}-dev" >> $GITHUB_ENV
else
echo "VERSION_PREFIX=dev-${BRANCH}" >> $GITHUB_ENV
Expand Down
90 changes: 32 additions & 58 deletions assets/TenancyServiceProvider.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,24 +145,19 @@ public function events()
*/
protected function overrideUrlInTenantContext(): void
{
/**
* Import your tenant model!
*
* \Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant, string $originalRootUrl) {
* $tenantDomain = $tenant instanceof \Stancl\Tenancy\Contracts\SingleDomainTenant
* ? $tenant->domain
* : $tenant->domains->first()->domain;
*
* $scheme = str($originalRootUrl)->before('://');
*
* // If you're using domain identification:
* return $scheme . '://' . $tenantDomain . '/';
*
* // If you're using subdomain identification:
* $originalDomain = str($originalRootUrl)->after($scheme . '://');
* return $scheme . '://' . $tenantDomain . '.' . $originalDomain . '/';
* };
*/
// \Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant, string $originalRootUrl) {
// $tenantDomain = $tenant instanceof \Stancl\Tenancy\Contracts\SingleDomainTenant
// ? $tenant->domain
// : $tenant->domains->first()->domain;
// $scheme = str($originalRootUrl)->before('://');
//
// // If you're using domain identification:
// return $scheme . '://' . $tenantDomain . '/';
//
// // If you're using subdomain identification:
// $originalDomain = str($originalRootUrl)->after($scheme . '://');
// return $scheme . '://' . $tenantDomain . '.' . $originalDomain . '/';
// };
}

public function register()
Expand All @@ -178,32 +173,17 @@ public function boot()
$this->makeTenancyMiddlewareHighestPriority();
$this->overrideUrlInTenantContext();

/**
* Include soft deleted resources in synced resource queries.
*
* ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
* if ($query->hasMacro('withTrashed')) {
* $query->withTrashed();
* }
* };
*/

/**
* To make Livewire v3 work with Tenancy, make the update route universal.
*
* Livewire::setUpdateRoute(function ($handle) {
* return RouteFacade::post('/livewire/update', $handle)->middleware(['web', 'universal']);
* });
*/

// if (InitializeTenancyByRequestData::inGlobalStack()) {
// FortifyRouteBootstrapper::$fortifyHome = 'dashboard';
// TenancyUrlGenerator::$prefixRouteNames = false;
// }

if (tenancy()->globalStackHasMiddleware(config('tenancy.identification.path_identification_middleware'))) {
TenancyUrlGenerator::$prefixRouteNames = true;
}
// // Include soft deleted resources in synced resource queries.
// ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
// if ($query->hasMacro('withTrashed')) {
// $query->withTrashed();
// }
// };

// // To make Livewire v3 work with Tenancy, make the update route universal.
// Livewire::setUpdateRoute(function ($handle) {
// return RouteFacade::post('/livewire/update', $handle)->middleware(['web', 'universal', \Stancl\Tenancy::defaultMiddleware()]);
// });
}

protected function bootEvents()
Expand All @@ -228,10 +208,7 @@ protected function mapRoutes()
->group(base_path('routes/tenant.php'));
}

// Delete this condition when using route-level path identification
if (tenancy()->globalStackHasMiddleware(config('tenancy.identification.path_identification_middleware'))) {
$this->cloneRoutes();
}
// $this->cloneRoutes();
});
}

Expand All @@ -245,16 +222,13 @@ protected function cloneRoutes(): void
/** @var CloneRoutesAsTenant $cloneRoutes */
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);

/**
* You can provide a closure for cloning a specific route, e.g.:
* $cloneRoutes->cloneUsing('welcome', function () {
* RouteFacade::get('/tenant-welcome', fn () => 'Current tenant: ' . tenant()->getTenantKey())
* ->middleware(['universal', InitializeTenancyByPath::class])
* ->name('tenant.welcome');
* });
*
* To see the default behavior of cloning the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant.
*/
// // You can provide a closure for cloning a specific route, e.g.:
// $cloneRoutes->cloneUsing('welcome', function () {
// RouteFacade::get('/tenant-welcome', fn () => 'Current tenant: ' . tenant()->getTenantKey())
// ->middleware(['universal', InitializeTenancyByPath::class])
// ->name('tenant.welcome');
// });
// // To see the default behavior of cloning the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant.

$cloneRoutes->handle();
}
Expand Down
17 changes: 11 additions & 6 deletions assets/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
/**
* Identification middleware tenancy recognizes as path identification middleware.
*
* This is used during determining whether whether a path identification is used
* This is used for determining if a path identification middleware is used
* during operations specific to path identification, e.g. forgetting the tenant parameter in ForgetTenantParameter.
*
* If you're using a custom path identification middleware, add it here.
Expand All @@ -118,6 +118,7 @@
Resolvers\PathTenantResolver::class => [
'tenant_parameter_name' => 'tenant',
'tenant_model_column' => null, // null = tenant key
'tenant_route_name_prefix' => null, // null = 'tenant.'
'allowed_extra_model_columns' => [], // used with binding route fields

'cache' => false,
Expand All @@ -130,8 +131,6 @@
'cache_store' => null, // null = default
],
],

// todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware()
],

/**
Expand Down Expand Up @@ -215,7 +214,14 @@
// 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager::class, // Also permission controlled
],

// todo@docblock
/*
* Drop tenant databases when `php artisan migrate:fresh` is used.
* You may want to use this locally since deleting tenants only
* deletes their databases when they're deleted individually, not
* when the records are mass deleted from the database.
*
* Note: This overrides the default MigrateFresh command.
*/
'drop_tenant_databases_on_migrate_fresh' => false,
],

Expand Down Expand Up @@ -320,7 +326,6 @@
*/
'url_override' => [
// Note that the local disk you add must exist in the tenancy.filesystem.root_override config
// todo@v4 Rename url_override to something that describes the config key better
'public' => 'public-%tenant%',
],

Expand Down Expand Up @@ -356,7 +361,7 @@
* leave asset() helper tenancy disabled and explicitly use tenant_asset() calls in places
* where you want to use tenant-specific assets (product images, avatars, etc).
*/
'asset_helper_tenancy' => false, // todo@rename asset_helper_override?
'asset_helper_override' => false,
],

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Bootstrappers/FilesystemTenancyBootstrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ protected function tenantStoragePath(string $suffix): string

protected function assetHelper(string|false $suffix): void
{
if (! $this->app['config']['tenancy.filesystem.asset_helper_tenancy']) {
if (! $this->app['config']['tenancy.filesystem.asset_helper_override']) {
return;
}

Expand Down
77 changes: 35 additions & 42 deletions src/Bootstrappers/Integrations/FortifyRouteBootstrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,52 @@
use Illuminate\Config\Repository;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Enums\Context;
use Stancl\Tenancy\Resolvers\PathTenantResolver;

/**
* Allows customizing Fortify action redirects
* so that they can also redirect to tenant routes instead of just the central routes.
* Allows customizing Fortify action redirects so that they can also redirect
* to tenant routes instead of just the central routes.
*
* Works with path and query string identification.
* This should be used with path/query string identification OR when using Fortify
* universally, including with domains.
*
* When using domain identification, there's no need to pass the tenant parameter,
* you only want to customize the routes being used, so you can set $passTenantParameter
* to false.
*/
class FortifyRouteBootstrapper implements TenancyBootstrapper
{
/**
* Make Fortify actions redirect to custom routes.
*
* For each route redirect, specify the intended route context (central or tenant).
* Based on the provided context, we pass the tenant parameter to the route (or not).
* The tenant parameter is only passed to the route when you specify its context as tenant.
*
* The route redirects should be in the following format:
*
* 'fortify_action' => [
* 'route_name' => 'tenant.route',
* 'context' => Context::TENANT,
* ]
* Fortify redirects that should be used in tenant context.
*
* For example:
* Syntax: ['redirect_name' => 'tenant_route_name']
*/
public static array $fortifyRedirectMap = [];

/**
* Should the tenant parameter be passed to fortify routes in the tenant context.
*
* FortifyRouteBootstrapper::$fortifyRedirectMap = [
* // On logout, redirect the user to the "bye" route in the central app
* 'logout' => [
* 'route_name' => 'bye',
* 'context' => Context::CENTRAL,
* ],
* This should be enabled with path/query string identification and disabled with domain identification.
*
* // On login, redirect the user to the "welcome" route in the tenant app
* 'login' => [
* 'route_name' => 'welcome',
* 'context' => Context::TENANT,
* ],
* ];
* You may also disable this when using path/query string identification if passing the tenant parameter
* is handled in another way (TenancyUrlGenerator::$passTenantParameter for both,
* UrlGeneratorBootstrapper:$addTenantParameterToDefaults for path identification).
*/
public static array $fortifyRedirectMap = [];
public static bool $passTenantParameter = true;

/**
* Tenant route that serves as Fortify's home (e.g. a tenant dashboard route).
* This route will always receive the tenant parameter.
*/
public static string $fortifyHome = 'tenant.dashboard';
public static string|null $fortifyHome = 'tenant.dashboard';

/**
* Use default parameter names ('tenant' name and tenant key value) instead of the parameter name
* and column name configured in the path resolver config.
*
* You want to enable this when using query string identification while having customized that config.
*/
public static bool $defaultParameterNames = false;

protected array $originalFortifyConfig = [];

Expand All @@ -76,27 +74,22 @@ public function revert(): void

protected function useTenantRoutesInFortify(Tenant $tenant): void
{
$tenantKey = $tenant->getTenantKey();
$tenantParameterName = PathTenantResolver::tenantParameterName();

$generateLink = function (array $redirect) use ($tenantKey, $tenantParameterName) {
// Specifying the context is only required with query string identification
// because with path identification, the tenant parameter should always present
$passTenantParameter = $redirect['context'] === Context::TENANT;
$tenantParameterName = static::$defaultParameterNames ? 'tenant' : PathTenantResolver::tenantParameterName();
$tenantParameterValue = static::$defaultParameterNames ? $tenant->getTenantKey() : PathTenantResolver::tenantParameterValue($tenant);

// Only pass the tenant parameter when the user should be redirected to a tenant route
return route($redirect['route_name'], $passTenantParameter ? [$tenantParameterName => $tenantKey] : []);
$generateLink = function (string $redirect) use ($tenantParameterValue, $tenantParameterName) {
return route($redirect, static::$passTenantParameter ? [$tenantParameterName => $tenantParameterValue] : []);
};

// Get redirect URLs for the configured redirect routes
$redirects = array_merge(
$this->originalFortifyConfig['redirects'] ?? [], // Fortify config redirects
array_map(fn (array $redirect) => $generateLink($redirect), static::$fortifyRedirectMap), // Mapped redirects
array_map(fn (string $redirect) => $generateLink($redirect), static::$fortifyRedirectMap), // Mapped redirects
);

if (static::$fortifyHome) {
// Generate the home route URL with the tenant parameter and make it the Fortify home route
$this->config->set('fortify.home', route(static::$fortifyHome, [$tenantParameterName => $tenantKey]));
$this->config->set('fortify.home', route(static::$fortifyHome, static::$passTenantParameter ? [$tenantParameterName => $tenantParameterValue] : []));
}

$this->config->set('fortify.redirects', $redirects);
Expand Down
9 changes: 2 additions & 7 deletions src/Bootstrappers/RootUrlBootstrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,8 @@ class RootUrlBootstrapper implements TenancyBootstrapper
protected string|null $originalRootUrl = null;

/**
* You may want to selectively enable or disable this bootstrapper in specific tests.
* For instance, when using `Livewire::test()` this bootstrapper can cause problems,
* due to an internal Livewire route, so you may want to disable it, while in tests
* that are generating URLs in things like mails, the bootstrapper should be used
* just like in any queued job.
*
* todo@revisit
* Overriding the root url may cause issues in *some* tests, so you can disable
* the behavior by setting this property to false.
*/
public static bool $rootUrlOverrideInTests = true;

Expand Down
5 changes: 4 additions & 1 deletion src/Bootstrappers/UrlGeneratorBootstrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ protected function useTenancyUrlGenerator(Tenant $tenant): void
if (static::$addTenantParameterToDefaults) {
$defaultParameters = array_merge(
$defaultParameters,
[PathTenantResolver::tenantParameterName() => $tenant->getTenantKey()]
[
PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue($tenant), // path identification
'tenant' => $tenant->getTenantKey(), // query string identification
],
);
}

Expand Down
9 changes: 2 additions & 7 deletions src/Database/Concerns/CreatesDatabaseUsers.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,11 @@ trait CreatesDatabaseUsers
{
public function createDatabase(TenantWithDatabase $tenant): bool
{
parent::createDatabase($tenant);

return $this->createUser($tenant->database());
return parent::createDatabase($tenant) && $this->createUser($tenant->database());
}

public function deleteDatabase(TenantWithDatabase $tenant): bool
{
// Some DB engines require the user to be deleted before the database (e.g. Postgres)
$this->deleteUser($tenant->database());

return parent::deleteDatabase($tenant);
return $this->deleteUser($tenant->database()) && parent::deleteDatabase($tenant);
}
}
4 changes: 3 additions & 1 deletion src/Database/Concerns/HasPending.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
trait HasPending
{
public static string $pendingSinceCast = 'timestamp';

/** Boot the trait. */
public static function bootHasPending(): void
{
Expand All @@ -32,7 +34,7 @@ public static function bootHasPending(): void
/** Initialize the trait. */
public function initializeHasPending(): void
{
$this->casts['pending_since'] = 'timestamp';
$this->casts['pending_since'] = static::$pendingSinceCast;
}

/** Determine if the model instance is in a pending state. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ class MicrosoftSQLDatabaseManager extends TenantDatabaseManager
public function createDatabase(TenantWithDatabase $tenant): bool
{
$database = $tenant->database()->getName();
$charset = $this->connection()->getConfig('charset');
$collation = $this->connection()->getConfig('collation'); // todo check why these are not used

return $this->connection()->statement("CREATE DATABASE [{$database}]");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ protected function grantPermissions(DatabaseConfig $databaseConfig): bool
// Grant permissions to any existing tables. This is used with RLS
// todo@samuel refactor this along with the todo in TenantDatabaseManager
// and move the grantPermissions() call inside the condition in `ManagesPostgresUsers::createUser()`
// but maybe moving it inside $createUser is wrong because some central user may migrate new tables
// while the RLS user should STILL get access to those tables
foreach ($tables as $table) {
$tableName = $table->table_name;

Expand Down
Loading
Loading