mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 09:34:05 +00:00
Merge branch 'master' into configurable-force-rls
This commit is contained in:
commit
f9f9e1814a
92 changed files with 1056 additions and 497 deletions
|
|
@ -92,7 +92,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,54 +7,52 @@ namespace Stancl\Tenancy\Bootstrappers\Integrations;
|
|||
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.
|
||||
* Fortify redirects that should be used in tenant context.
|
||||
*
|
||||
* 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,
|
||||
* ]
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* FortifyRouteBootstrapper::$fortifyRedirectMap = [
|
||||
* // On logout, redirect the user to the "bye" route in the central app
|
||||
* 'logout' => [
|
||||
* 'route_name' => 'bye',
|
||||
* 'context' => Context::CENTRAL,
|
||||
* ],
|
||||
*
|
||||
* // On login, redirect the user to the "welcome" route in the tenant app
|
||||
* 'login' => [
|
||||
* 'route_name' => 'welcome',
|
||||
* 'context' => Context::TENANT,
|
||||
* ],
|
||||
* ];
|
||||
* Syntax: ['redirect_name' => 'tenant_route_name']
|
||||
*/
|
||||
public static array $fortifyRedirectMap = [];
|
||||
|
||||
/**
|
||||
* Should the tenant parameter be passed to fortify routes in the tenant context.
|
||||
*
|
||||
* This should be enabled with path/query string identification and disabled with domain identification.
|
||||
*
|
||||
* 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 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 = [];
|
||||
|
||||
|
|
@ -76,27 +74,22 @@ class FortifyRouteBootstrapper implements TenancyBootstrapper
|
|||
|
||||
protected function useTenantRoutesInFortify(Tenant $tenant): void
|
||||
{
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
$tenantParameterName = PathTenantResolver::tenantParameterName();
|
||||
$tenantParameterName = static::$defaultParameterNames ? 'tenant' : PathTenantResolver::tenantParameterName();
|
||||
$tenantParameterValue = static::$defaultParameterNames ? $tenant->getTenantKey() : PathTenantResolver::tenantParameterValue($tenant);
|
||||
|
||||
$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;
|
||||
|
||||
// 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);
|
||||
|
|
|
|||
146
src/Bootstrappers/PersistentQueueTenancyBootstrapper.php
Normal file
146
src/Bootstrappers/PersistentQueueTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Queue\Events\JobFailed;
|
||||
use Illuminate\Queue\Events\JobProcessed;
|
||||
use Illuminate\Queue\Events\JobProcessing;
|
||||
use Illuminate\Queue\Events\JobRetryRequested;
|
||||
use Illuminate\Queue\QueueManager;
|
||||
use Illuminate\Support\Testing\Fakes\QueueFake;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
class PersistentQueueTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/** @var Repository */
|
||||
protected $config;
|
||||
|
||||
/** @var QueueManager */
|
||||
protected $queue;
|
||||
|
||||
/**
|
||||
* The normal constructor is only executed after tenancy is bootstrapped.
|
||||
* However, we're registering a hook to initialize tenancy. Therefore,
|
||||
* we need to register the hook at service provider execution time.
|
||||
*/
|
||||
public static function __constructStatic(Application $app): void
|
||||
{
|
||||
static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests());
|
||||
}
|
||||
|
||||
public function __construct(Repository $config, QueueManager $queue)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->queue = $queue;
|
||||
|
||||
$this->setUpPayloadGenerator();
|
||||
}
|
||||
|
||||
protected static function setUpJobListener(Dispatcher $dispatcher, bool $runningTests): void
|
||||
{
|
||||
$previousTenant = null;
|
||||
|
||||
$dispatcher->listen(JobProcessing::class, function ($event) use (&$previousTenant) {
|
||||
$previousTenant = tenant();
|
||||
|
||||
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
|
||||
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
||||
$previousTenant = tenant();
|
||||
|
||||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
|
||||
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
||||
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
|
||||
if ($runningTests) {
|
||||
static::revertToPreviousState($event->job->payload()['tenant_id'] ?? null, $previousTenant);
|
||||
|
||||
// We don't need to reset $previousTenant since the value will be set again when a job is processed.
|
||||
}
|
||||
|
||||
// If we're not running tests, we remain in the tenant's context. This makes other JobProcessed
|
||||
// listeners able to deserialize the job, including with SerializesModels, since the tenant connection
|
||||
// remains open.
|
||||
};
|
||||
|
||||
$dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds
|
||||
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
||||
}
|
||||
|
||||
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
|
||||
{
|
||||
if (! $tenantId) {
|
||||
// The job is not tenant-aware
|
||||
if (tenancy()->initialized) {
|
||||
// Tenancy was initialized, so we revert back to the central context
|
||||
tenancy()->end();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-initialize tenancy between all jobs even if the tenant is the same
|
||||
// so that we don't work with an outdated tenant() instance in case it
|
||||
// was updated outside the queue worker.
|
||||
tenancy()->end();
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = tenancy()->find($tenantId);
|
||||
tenancy()->initialize($tenant);
|
||||
}
|
||||
|
||||
protected static function revertToPreviousState(string|int|null $tenantId, ?Tenant $previousTenant): void
|
||||
{
|
||||
// The job was not tenant-aware
|
||||
if (! $tenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Revert back to the previous tenant
|
||||
if (tenant() && $previousTenant && $previousTenant->isNot(tenant())) {
|
||||
tenancy()->initialize($previousTenant);
|
||||
}
|
||||
|
||||
// End tenancy
|
||||
if (tenant() && (! $previousTenant)) {
|
||||
tenancy()->end();
|
||||
}
|
||||
}
|
||||
|
||||
protected function setUpPayloadGenerator(): void
|
||||
{
|
||||
$bootstrapper = &$this;
|
||||
|
||||
if (! $this->queue instanceof QueueFake) {
|
||||
$this->queue->createPayloadUsing(function ($connection) use (&$bootstrapper) {
|
||||
return $bootstrapper->getPayload($connection);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function getPayload(string $connection): array
|
||||
{
|
||||
if (! tenancy()->initialized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->config["queue.connections.$connection.central"]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'tenant_id' => tenant()->getTenantKey(),
|
||||
];
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void {}
|
||||
public function revert(): void {}
|
||||
}
|
||||
|
|
@ -24,16 +24,6 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
/** @var QueueManager */
|
||||
protected $queue;
|
||||
|
||||
/**
|
||||
* Don't persist the same tenant across multiple jobs even if they have the same tenant ID.
|
||||
*
|
||||
* This is useful when you're changing the tenant's state (e.g. properties in the `data` column) and want the next job to initialize tenancy again
|
||||
* with the new data. Features like the Tenant Config are only executed when tenancy is initialized, so the re-initialization is needed in some cases.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $forceRefresh = false;
|
||||
|
||||
/**
|
||||
* The normal constructor is only executed after tenancy is bootstrapped.
|
||||
* However, we're registering a hook to initialize tenancy. Therefore,
|
||||
|
|
@ -68,9 +58,12 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
|
||||
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
||||
$revertToPreviousState = function ($event) use (&$previousTenant) {
|
||||
static::revertToPreviousState($event, $previousTenant);
|
||||
// In queue worker context, this reverts to the central context.
|
||||
// In dispatchSync context, this reverts to the previous tenant's context.
|
||||
// There's no need to reset $previousTenant here since it's always first
|
||||
// set in the above listeners and the app is reverted back to that context.
|
||||
static::revertToPreviousState($event->job->payload()['tenant_id'] ?? null, $previousTenant);
|
||||
};
|
||||
|
||||
$dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds
|
||||
|
|
@ -79,61 +72,25 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
|
||||
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
|
||||
{
|
||||
if ($tenantId === null) {
|
||||
// The job is not tenant-aware
|
||||
if (tenancy()->initialized) {
|
||||
// Tenancy was initialized, so we revert back to the central context
|
||||
tenancy()->end();
|
||||
}
|
||||
|
||||
if (! $tenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (static::$forceRefresh) {
|
||||
// Re-initialize tenancy between all jobs
|
||||
if (tenancy()->initialized) {
|
||||
tenancy()->end();
|
||||
}
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = tenancy()->find($tenantId);
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (tenancy()->initialized) {
|
||||
// Tenancy is already initialized
|
||||
if (tenant()->getTenantKey() === $tenantId) {
|
||||
// It's initialized for the same tenant (e.g. dispatchSync was used, or the previous job also ran for this tenant)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Tenancy was either not initialized, or initialized for a different tenant.
|
||||
// Therefore, we initialize it for the correct tenant.
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = tenancy()->find($tenantId);
|
||||
tenancy()->initialize($tenant);
|
||||
}
|
||||
|
||||
protected static function revertToPreviousState(JobProcessed|JobFailed $event, ?Tenant &$previousTenant): void
|
||||
protected static function revertToPreviousState(string|int|null $tenantId, ?Tenant $previousTenant): void
|
||||
{
|
||||
$tenantId = $event->job->payload()['tenant_id'] ?? null;
|
||||
|
||||
// The job was not tenant-aware
|
||||
// The job was not tenant-aware so no context switch was done
|
||||
if (! $tenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Revert back to the previous tenant
|
||||
if (tenant() && $previousTenant?->isNot(tenant())) {
|
||||
tenancy()->initialize($previousTenant);
|
||||
}
|
||||
|
||||
// End tenancy
|
||||
if (tenant() && (! $previousTenant)) {
|
||||
// End tenancy when there's no previous tenant
|
||||
// (= when running in a queue worker, not dispatchSync)
|
||||
if (tenant() && ! $previousTenant) {
|
||||
tenancy()->end();
|
||||
}
|
||||
}
|
||||
|
|
@ -149,16 +106,6 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
}
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function getPayload(string $connection): array
|
||||
{
|
||||
if (! tenancy()->initialized) {
|
||||
|
|
@ -169,10 +116,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
return [];
|
||||
}
|
||||
|
||||
$id = tenant()->getTenantKey();
|
||||
|
||||
return [
|
||||
'tenant_id' => $id,
|
||||
'tenant_id' => tenant()->getTenantKey(),
|
||||
];
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void {}
|
||||
public function revert(): void {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Bootstrappers;
|
|||
use Closure;
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
|
|
@ -36,28 +35,43 @@ class RootUrlBootstrapper implements TenancyBootstrapper
|
|||
|
||||
protected string|null $originalRootUrl = null;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $urlGenerator,
|
||||
protected Repository $config,
|
||||
protected Application $app,
|
||||
) {}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
if ($this->app->runningInConsole() && static::$rootUrlOverride) {
|
||||
$this->originalRootUrl = $this->urlGenerator->to('/');
|
||||
|
||||
$newRootUrl = (static::$rootUrlOverride)($tenant, $this->originalRootUrl);
|
||||
|
||||
$this->urlGenerator->forceRootUrl($newRootUrl);
|
||||
$this->config->set('app.url', $newRootUrl);
|
||||
if (static::$rootUrlOverride === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->app->runningUnitTests() && ! static::$rootUrlOverrideInTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->originalRootUrl = $this->app['url']->to('/');
|
||||
|
||||
$newRootUrl = (static::$rootUrlOverride)($tenant, $this->originalRootUrl);
|
||||
|
||||
$this->app['url']->forceRootUrl($newRootUrl);
|
||||
$this->config->set('app.url', $newRootUrl);
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
if ($this->originalRootUrl) {
|
||||
$this->urlGenerator->forceRootUrl($this->originalRootUrl);
|
||||
$this->app['url']->forceRootUrl($this->originalRootUrl);
|
||||
$this->config->set('app.url', $this->originalRootUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Illuminate\Support\Facades\URL;
|
|||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||
|
||||
/**
|
||||
* Makes the app use TenancyUrlGenerator (instead of Illuminate\Routing\UrlGenerator) which:
|
||||
|
|
@ -19,10 +20,20 @@ use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
|||
* Used with path and query string identification.
|
||||
*
|
||||
* @see TenancyUrlGenerator
|
||||
* @see \Stancl\Tenancy\Resolvers\PathTenantResolver
|
||||
* @see PathTenantResolver
|
||||
*/
|
||||
class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/**
|
||||
* Should the tenant route parameter get added to TenancyUrlGenerator::defaults().
|
||||
*
|
||||
* This is recommended when using path identification since defaults() generally has better support in integrations,
|
||||
* namely Ziggy, compared to TenancyUrlGenerator::$passTenantParameterToRoutes.
|
||||
*
|
||||
* With query string identification, this has no effect since URL::defaults() only works for route paramaters.
|
||||
*/
|
||||
public static bool $addTenantParameterToDefaults = true;
|
||||
|
||||
public function __construct(
|
||||
protected Application $app,
|
||||
protected UrlGenerator $originalUrlGenerator,
|
||||
|
|
@ -32,12 +43,12 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
|||
{
|
||||
URL::clearResolvedInstances();
|
||||
|
||||
$this->useTenancyUrlGenerator();
|
||||
$this->useTenancyUrlGenerator($tenant);
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
$this->app->bind('url', fn () => $this->originalUrlGenerator);
|
||||
$this->app->extend('url', fn () => $this->originalUrlGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -45,26 +56,36 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
|
|||
*
|
||||
* @see \Illuminate\Routing\RoutingServiceProvider registerUrlGenerator()
|
||||
*/
|
||||
protected function useTenancyUrlGenerator(): void
|
||||
protected function useTenancyUrlGenerator(Tenant $tenant): void
|
||||
{
|
||||
$this->app->extend('url', function (UrlGenerator $urlGenerator, Application $app) {
|
||||
$newGenerator = new TenancyUrlGenerator(
|
||||
$app['router']->getRoutes(),
|
||||
$urlGenerator->getRequest(),
|
||||
$app['config']->get('app.asset_url'),
|
||||
$newGenerator = new TenancyUrlGenerator(
|
||||
$this->app['router']->getRoutes(),
|
||||
$this->originalUrlGenerator->getRequest(),
|
||||
$this->app['config']->get('app.asset_url'),
|
||||
);
|
||||
|
||||
$defaultParameters = $this->originalUrlGenerator->getDefaultParameters();
|
||||
|
||||
if (static::$addTenantParameterToDefaults) {
|
||||
$defaultParameters = array_merge(
|
||||
$defaultParameters,
|
||||
[
|
||||
PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue($tenant), // path identification
|
||||
'tenant' => $tenant->getTenantKey(), // query string identification
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
$newGenerator->defaults($urlGenerator->getDefaultParameters());
|
||||
$newGenerator->defaults($defaultParameters);
|
||||
|
||||
$newGenerator->setSessionResolver(function () {
|
||||
return $this->app['session'] ?? null;
|
||||
});
|
||||
|
||||
$newGenerator->setKeyResolver(function () {
|
||||
return $this->app->make('config')->get('app.key');
|
||||
});
|
||||
|
||||
return $newGenerator;
|
||||
$newGenerator->setSessionResolver(function () {
|
||||
return $this->app['session'] ?? null;
|
||||
});
|
||||
|
||||
$newGenerator->setKeyResolver(function () {
|
||||
return $this->app->make('config')->get('app.key');
|
||||
});
|
||||
|
||||
$this->app->extend('url', fn () => $newGenerator);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,6 @@ trait DealsWithTenantSymlinks
|
|||
/** Determine if the provided path is an existing symlink. */
|
||||
protected function symlinkExists(string $link): bool
|
||||
{
|
||||
return file_exists($link) && is_link($link);
|
||||
return is_link($link);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ interface UniqueIdentifierGenerator
|
|||
/**
|
||||
* Generate a unique identifier for a model.
|
||||
*/
|
||||
public static function generate(Model $model): string;
|
||||
public static function generate(Model $model): string|int;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ use Stancl\Tenancy\Events\PullingPendingTenant;
|
|||
*/
|
||||
trait HasPending
|
||||
{
|
||||
public static string $pendingSinceCast = 'timestamp';
|
||||
|
||||
/** Boot the trait. */
|
||||
public static function bootHasPending(): void
|
||||
{
|
||||
|
|
@ -32,7 +34,7 @@ trait HasPending
|
|||
/** 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. */
|
||||
|
|
|
|||
|
|
@ -4,16 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
trait InvalidatesResolverCache
|
||||
{
|
||||
public static function bootInvalidatesResolverCache(): void
|
||||
{
|
||||
static::saved(function (Tenant&Model $tenant) {
|
||||
Tenancy::invalidateResolverCache($tenant);
|
||||
});
|
||||
static::saved(Tenancy::invalidateResolverCache(...));
|
||||
static::deleting(Tenancy::invalidateResolverCache(...));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
/**
|
||||
|
|
@ -15,13 +14,9 @@ trait InvalidatesTenantsResolverCache
|
|||
{
|
||||
public static function bootInvalidatesTenantsResolverCache(): void
|
||||
{
|
||||
static::saved(function (Model $model) {
|
||||
foreach (Tenancy::cachedResolvers() as $resolver) {
|
||||
/** @var CachedTenantResolver $resolver */
|
||||
$resolver = app($resolver);
|
||||
$invalidateCache = static fn (Model $model) => Tenancy::invalidateResolverCache($model->tenant);
|
||||
|
||||
$resolver->invalidateCache($model->tenant);
|
||||
}
|
||||
});
|
||||
static::saved($invalidateCache);
|
||||
static::deleting($invalidateCache);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ class ImpersonationToken extends Model
|
|||
{
|
||||
use CentralConnection;
|
||||
|
||||
/** You can set this property to customize the table name */
|
||||
public static string $tableName = 'tenant_user_impersonation_tokens';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
|
@ -33,11 +36,15 @@ class ImpersonationToken extends Model
|
|||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $table = 'tenant_user_impersonation_tokens';
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return static::$tableName;
|
||||
}
|
||||
|
||||
public static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
|
|
|
|||
|
|
@ -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}]");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ class PermissionControlledPostgreSQLSchemaManager extends PostgreSQLSchemaManage
|
|||
// 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,11 @@ namespace Stancl\Tenancy\Events\Contracts;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
abstract class TenantEvent // todo we could add a feature to JobPipeline that automatically gets data for the send() from here
|
||||
abstract class TenantEvent
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/** @var Tenant */
|
||||
public $tenant;
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenant = $tenant;
|
||||
}
|
||||
public function __construct(
|
||||
public Tenant $tenant,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
// todo perhaps create Identification namespace
|
||||
|
||||
namespace Stancl\Tenancy\Exceptions;
|
||||
|
||||
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Features;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Stancl\Tenancy\Contracts\Feature;
|
||||
|
|
@ -18,8 +19,8 @@ class UserImpersonation implements Feature
|
|||
|
||||
public function bootstrap(Tenancy $tenancy): void
|
||||
{
|
||||
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): ImpersonationToken {
|
||||
return ImpersonationToken::create([
|
||||
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): Model {
|
||||
return UserImpersonation::modelClass()::create([
|
||||
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
|
||||
'user_id' => $userId,
|
||||
'redirect_url' => $redirectUrl,
|
||||
|
|
@ -30,10 +31,15 @@ class UserImpersonation implements Feature
|
|||
}
|
||||
|
||||
/** Impersonate a user and get an HTTP redirect response. */
|
||||
public static function makeResponse(#[\SensitiveParameter] string|ImpersonationToken $token, ?int $ttl = null): RedirectResponse
|
||||
public static function makeResponse(#[\SensitiveParameter] string|Model $token, ?int $ttl = null): RedirectResponse
|
||||
{
|
||||
/** @var ImpersonationToken $token */
|
||||
$token = $token instanceof ImpersonationToken ? $token : ImpersonationToken::findOrFail($token);
|
||||
/**
|
||||
* The model does NOT have to extend ImpersonationToken, but usually it WILL be a child
|
||||
* of ImpersonationToken and this makes it clear to phpstan that the model has a redirect_url property.
|
||||
*
|
||||
* @var ImpersonationToken $token
|
||||
*/
|
||||
$token = $token instanceof Model ? $token : static::modelClass()::findOrFail($token);
|
||||
$ttl ??= static::$ttl;
|
||||
|
||||
$tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl;
|
||||
|
|
@ -54,6 +60,12 @@ class UserImpersonation implements Feature
|
|||
return redirect($token->redirect_url);
|
||||
}
|
||||
|
||||
/** @return class-string<Model> */
|
||||
public static function modelClass(): string
|
||||
{
|
||||
return config('tenancy.models.impersonation_token');
|
||||
}
|
||||
|
||||
public static function isImpersonating(): bool
|
||||
{
|
||||
return session()->has('tenancy_impersonating');
|
||||
|
|
@ -62,7 +74,7 @@ class UserImpersonation implements Feature
|
|||
/**
|
||||
* Logout from the current domain and forget impersonation session.
|
||||
*/
|
||||
public static function leave(): void // todo@name possibly rename
|
||||
public static function stopImpersonating(): void
|
||||
{
|
||||
auth()->logout();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ use Illuminate\Routing\Events\RouteMatched;
|
|||
use Stancl\Tenancy\Enums\RouteMode;
|
||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||
|
||||
// todo@earlyIdReview
|
||||
|
||||
/**
|
||||
* Remove the tenant parameter from the matched route when path identification is used globally.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class PreventAccessFromUnwantedDomains
|
|||
return in_array($request->getHost(), config('tenancy.identification.central_domains'), true);
|
||||
}
|
||||
|
||||
// todo@samuel
|
||||
// todo@samuel technically not an identification middleware but probably ok to keep this here
|
||||
public function requestHasTenant(Request $request): bool
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ class ScopeSessions
|
|||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if (! tenancy()->initialized) {
|
||||
if (tenancy()->routeIsUniversal(tenancy()->getRoute($request))) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ namespace Stancl\Tenancy\Overrides;
|
|||
|
||||
use Illuminate\Cache\CacheManager as BaseCacheManager;
|
||||
|
||||
// todo@move move to Cache namespace?
|
||||
|
||||
class CacheManager extends BaseCacheManager
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,39 +13,85 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
|||
/**
|
||||
* This class is used in place of the default UrlGenerator when UrlGeneratorBootstrapper is enabled.
|
||||
*
|
||||
* TenancyUrlGenerator does two extra things:
|
||||
* 1. Autofill the {tenant} parameter in the tenant context with the current tenant if $passTenantParameterToRoutes is enabled (enabled by default)
|
||||
* 2. Prepend the route name with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled (disabled by default)
|
||||
* TenancyUrlGenerator does a few extra things:
|
||||
* - Autofills the tenant parameter in the tenant context with the current tenant.
|
||||
* This is done either by:
|
||||
* - URL::defaults() -- if UrlGeneratorBootstrapper::$addTenantParameterToDefaults is enabled.
|
||||
* This generally has the best support since tools like e.g. Ziggy read defaults().
|
||||
* - Automatically passing ['tenant' => ...] to each route() call -- if TenancyUrlGenerator::$passTenantParameterToRoutes is enabled
|
||||
* This is a more universal solution since it supports both path identification and query parameter identification.
|
||||
*
|
||||
* Both of these can be skipped by passing the $bypassParameter (`['central' => true]` by default)
|
||||
* - Prepends route names passed to route() and URL::temporarySignedRoute()
|
||||
* with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled.
|
||||
* This is primarily useful when using route cloning with path identification.
|
||||
*
|
||||
* To bypass this behavior on any single route() call, pass the $bypassParameter as true (['central' => true] by default).
|
||||
*/
|
||||
class TenancyUrlGenerator extends UrlGenerator
|
||||
{
|
||||
/**
|
||||
* Parameter which bypasses the behavior modification of route() and temporarySignedRoute().
|
||||
* Parameter which works as a flag for bypassing the behavior modification of route() and temporarySignedRoute().
|
||||
*
|
||||
* E.g. route('tenant') => app.test/{tenant}/tenant (or app.test/tenant?tenant=tenantKey if the route doesn't accept the tenant parameter)
|
||||
* route('tenant', [$bypassParameter => true]) => app.test/tenant.
|
||||
* For example, in tenant context:
|
||||
* Route::get('/', ...)->name('home');
|
||||
* // query string identification
|
||||
* Route::get('/tenant', ...)->middleware(InitializeTenancyByRequestData::class)->name('tenant.home');
|
||||
* - route('home') => app.test/tenant?tenant=tenantKey
|
||||
* - route('home', [$bypassParameter => true]) => app.test/
|
||||
* - route('tenant.home', [$bypassParameter => true]) => app.test/tenant -- no query string added
|
||||
*
|
||||
* Note: UrlGeneratorBootstrapper::$addTenantParameterToDefaults is not affected by this, though
|
||||
* it doesn't matter since it doesn't pass any extra parameters when not needed.
|
||||
*
|
||||
* @see UrlGeneratorBootstrapper
|
||||
*/
|
||||
public static string $bypassParameter = 'central';
|
||||
|
||||
/**
|
||||
* Determine if the route names passed to `route()` or `temporarySignedRoute()`
|
||||
* should get prefixed with the tenant route name prefix.
|
||||
* Should route names passed to route() or temporarySignedRoute()
|
||||
* get prefixed with the tenant route name prefix.
|
||||
*
|
||||
* This is useful when using path identification with packages that generate URLs,
|
||||
* like Jetstream, so that you don't have to manually prefix route names passed to each route() call.
|
||||
* This is useful when using e.g. path identification with third-party packages
|
||||
* where you don't have control over all route() calls or don't want to change
|
||||
* too many files. Often this will be when using route cloning.
|
||||
*/
|
||||
public static bool $prefixRouteNames = false;
|
||||
|
||||
/**
|
||||
* Determine if the tenant parameter should get passed
|
||||
* to the links generated by `route()` or `temporarySignedRoute()` whenever available
|
||||
* (enabled by default – works with both path and query string identification).
|
||||
* Should the tenant parameter be passed to route() or temporarySignedRoute() calls.
|
||||
*
|
||||
* With path identification, you can disable this and use URL::defaults() instead (as an alternative solution).
|
||||
* This is useful with path or query parameter identification. The former can be handled
|
||||
* more elegantly using UrlGeneratorBootstrapper::$addTenantParameterToDefaults.
|
||||
*
|
||||
* @see UrlGeneratorBootstrapper
|
||||
*/
|
||||
public static bool $passTenantParameterToRoutes = true;
|
||||
public static bool $passTenantParameterToRoutes = false;
|
||||
|
||||
/**
|
||||
* Route name overrides.
|
||||
*
|
||||
* Note: This behavior can be bypassed using $bypassParameter just like
|
||||
* $prefixRouteNames and $passTenantParameterToRoutes.
|
||||
*
|
||||
* Example from a Jetstream integration:
|
||||
* [
|
||||
* 'profile.show' => 'tenant.profile.show',
|
||||
* 'two-factor.login' => 'tenant.two-factor.login',
|
||||
* ]
|
||||
*
|
||||
* In the tenant context:
|
||||
* - `route('profile.show')` will return a URL as if you called `route('tenant.profile.show')`.
|
||||
* - `route('profile.show', ['central' => true])` will return a URL as if you called `route('profile.show')`.
|
||||
*/
|
||||
public static array $overrides = [];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Override the route() method so that the route name gets prefixed
|
||||
|
|
@ -99,7 +145,7 @@ class TenancyUrlGenerator extends UrlGenerator
|
|||
protected function prepareRouteInputs(string $name, array $parameters): array
|
||||
{
|
||||
if (! $this->routeBehaviorModificationBypassed($parameters)) {
|
||||
$name = $this->prefixRouteName($name);
|
||||
$name = $this->routeNameOverride($name) ?? $this->prefixRouteName($name);
|
||||
$parameters = $this->addTenantParameter($parameters);
|
||||
}
|
||||
|
||||
|
|
@ -124,10 +170,23 @@ class TenancyUrlGenerator extends UrlGenerator
|
|||
}
|
||||
|
||||
/**
|
||||
* If `tenant()` isn't null, add tenant paramter to the passed parameters.
|
||||
* If `tenant()` isn't null, add the tenant parameter to the passed parameters.
|
||||
*/
|
||||
protected function addTenantParameter(array $parameters): array
|
||||
{
|
||||
return tenant() && static::$passTenantParameterToRoutes ? array_merge($parameters, [PathTenantResolver::tenantParameterName() => tenant()->getTenantKey()]) : $parameters;
|
||||
if (tenant() && static::$passTenantParameterToRoutes) {
|
||||
if (static::$defaultParameterNames) {
|
||||
return array_merge($parameters, ['tenant' => tenant()->getTenantKey()]);
|
||||
} else {
|
||||
return array_merge($parameters, [PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue(tenant())]);
|
||||
}
|
||||
} else {
|
||||
return $parameters;
|
||||
}
|
||||
}
|
||||
|
||||
protected function routeNameOverride(string $name): string|null
|
||||
{
|
||||
return static::$overrides[$name] ?? null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,10 @@ class TableRLSManager implements RLSPolicyManager
|
|||
$builder = $this->database->getSchemaBuilder();
|
||||
|
||||
// We loop through each table in the database
|
||||
foreach ($builder->getTableListing() as $table) {
|
||||
foreach ($builder->getTableListing(schema: $this->database->getConfig('search_path')) as $table) {
|
||||
// E.g. "public.table_name" -> "table_name"
|
||||
$table = str($table)->afterLast('.')->toString();
|
||||
|
||||
// For each table, we get a list of all foreign key columns
|
||||
$foreignKeys = collect($builder->getForeignKeys($table))->map(function ($foreign) use ($table) {
|
||||
return $this->formatForeignKey($foreign, $table);
|
||||
|
|
@ -105,6 +108,12 @@ class TableRLSManager implements RLSPolicyManager
|
|||
|
||||
protected function generatePaths(string $table, array $foreign, array &$paths, array $currentPath = []): void
|
||||
{
|
||||
// If the foreign key has a comment of 'no-rls', we skip it
|
||||
// Also skip the foreign key if implicit scoping is off and the foreign key has no comment
|
||||
if ($foreign['comment'] === 'no-rls' || (! static::$scopeByDefault && $foreign['comment'] === null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($foreign['foreignTable'], array_column($currentPath, 'foreignTable'))) {
|
||||
throw new RecursiveRelationshipException;
|
||||
}
|
||||
|
|
@ -112,15 +121,7 @@ class TableRLSManager implements RLSPolicyManager
|
|||
$currentPath[] = $foreign;
|
||||
|
||||
if ($foreign['foreignTable'] === tenancy()->model()->getTable()) {
|
||||
$comments = array_column($currentPath, 'comment');
|
||||
$pathCanUseRls = static::$scopeByDefault ?
|
||||
! in_array('no-rls', $comments) :
|
||||
! in_array('no-rls', $comments) && ! in_array(null, $comments);
|
||||
|
||||
if ($pathCanUseRls) {
|
||||
// If the foreign table is the tenants table, add the current path to $paths
|
||||
$paths[] = $currentPath;
|
||||
}
|
||||
$paths[] = $currentPath;
|
||||
} else {
|
||||
// If not, recursively generate paths for the foreign table
|
||||
foreach ($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']) as $nextConstraint) {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
|||
|
||||
public static function tenantRouteNamePrefix(): string
|
||||
{
|
||||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_route_name_prefix') ?? static::tenantParameterName() . '.';
|
||||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_route_name_prefix') ?? 'tenant.';
|
||||
}
|
||||
|
||||
public static function tenantModelColumn(): string
|
||||
|
|
@ -81,6 +81,11 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
|||
return config('tenancy.identification.resolvers.' . static::class . '.tenant_model_column') ?? tenancy()->model()->getTenantKeyName();
|
||||
}
|
||||
|
||||
public static function tenantParameterValue(Tenant $tenant): string
|
||||
{
|
||||
return $tenant->getAttribute(static::tenantModelColumn());
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public static function allowedExtraModelColumns(): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ use Illuminate\Database\Eloquent\Model;
|
|||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
|
||||
// todo@move move all resource syncing-related things to a separate namespace?
|
||||
|
||||
/**
|
||||
* @property-read TenantWithDatabase[]|Collection<int, TenantWithDatabase&Model> $tenants
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -77,18 +77,21 @@ class Tenancy
|
|||
public function run(Tenant $tenant, Closure $callback): mixed
|
||||
{
|
||||
$originalTenant = $this->tenant;
|
||||
$result = null;
|
||||
|
||||
$this->initialize($tenant);
|
||||
$result = $callback($tenant);
|
||||
try {
|
||||
$this->initialize($tenant);
|
||||
$result = $callback($tenant);
|
||||
} finally {
|
||||
if ($result instanceof PendingDispatch) { // #1277
|
||||
$result = null;
|
||||
}
|
||||
|
||||
if ($result instanceof PendingDispatch) { // #1277
|
||||
$result = null;
|
||||
}
|
||||
|
||||
if ($originalTenant) {
|
||||
$this->initialize($originalTenant);
|
||||
} else {
|
||||
$this->end();
|
||||
if ($originalTenant) {
|
||||
$this->initialize($originalTenant);
|
||||
} else {
|
||||
$this->end();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
|
@ -204,8 +207,10 @@ class Tenancy
|
|||
// Wrap string in array
|
||||
$tenants = is_string($tenants) ? [$tenants] : $tenants;
|
||||
|
||||
// Use all tenants if $tenants is falsy
|
||||
$tenants = $tenants ?: $this->model()->cursor(); // todo@phpstan phpstan thinks this isn't needed, but tests fail without it
|
||||
// If $tenants is falsy by this point (e.g. an empty array) there's no work to be done
|
||||
if (! $tenants) {
|
||||
return;
|
||||
}
|
||||
|
||||
$originalTenant = $this->tenant;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Cache\CacheManager;
|
||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||
use Illuminate\Routing\Events\RouteMatched;
|
||||
|
|
@ -18,9 +19,15 @@ use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
|||
|
||||
class TenancyServiceProvider extends ServiceProvider
|
||||
{
|
||||
public static Closure|null $configure = null;
|
||||
|
||||
/* Register services. */
|
||||
public function register(): void
|
||||
{
|
||||
if (static::$configure) {
|
||||
(static::$configure)();
|
||||
}
|
||||
|
||||
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
|
||||
|
||||
$this->app->singleton(Database\DatabaseManager::class);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class RandomHexGenerator implements UniqueIdentifierGenerator
|
|||
{
|
||||
public static int $bytes = 6;
|
||||
|
||||
public static function generate(Model $model): string
|
||||
public static function generate(Model $model): string|int
|
||||
{
|
||||
return bin2hex(random_bytes(static::$bytes));
|
||||
}
|
||||
|
|
|
|||
22
src/UniqueIdentifierGenerators/RandomIntGenerator.php
Normal file
22
src/UniqueIdentifierGenerators/RandomIntGenerator.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\UniqueIdentifierGenerators;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
|
||||
/**
|
||||
* Generates a cryptographically secure random integer for the tenant key.
|
||||
*/
|
||||
class RandomIntGenerator implements UniqueIdentifierGenerator
|
||||
{
|
||||
public static int $min = 0;
|
||||
public static int $max = PHP_INT_MAX;
|
||||
|
||||
public static function generate(Model $model): string|int
|
||||
{
|
||||
return random_int(static::$min, static::$max);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ class RandomStringGenerator implements UniqueIdentifierGenerator
|
|||
{
|
||||
public static int $length = 8;
|
||||
|
||||
public static function generate(Model $model): string
|
||||
public static function generate(Model $model): string|int
|
||||
{
|
||||
return Str::random(static::$length);
|
||||
}
|
||||
|
|
|
|||
20
src/UniqueIdentifierGenerators/ULIDGenerator.php
Normal file
20
src/UniqueIdentifierGenerators/ULIDGenerator.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\UniqueIdentifierGenerators;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
|
||||
/**
|
||||
* Generates a UUID for the tenant key.
|
||||
*/
|
||||
class ULIDGenerator implements UniqueIdentifierGenerator
|
||||
{
|
||||
public static function generate(Model $model): string|int
|
||||
{
|
||||
return Str::ulid()->toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
|||
*/
|
||||
class UUIDGenerator implements UniqueIdentifierGenerator
|
||||
{
|
||||
public static function generate(Model $model): string
|
||||
public static function generate(Model $model): string|int
|
||||
{
|
||||
return Uuid::uuid4()->toString();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue