mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-06 01:34:02 +00:00
Merge branch 'master' of github.com:archtechx/tenancy into unware-feature
This commit is contained in:
commit
b0560331ed
87 changed files with 2308 additions and 533 deletions
95
src/Bootstrappers/BroadcastTenancyBootstrapper.php
Normal file
95
src/Bootstrappers/BroadcastTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Illuminate\Broadcasting\BroadcastManager;
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Contracts\Broadcasting\Broadcaster;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\TenancyBroadcastManager;
|
||||
|
||||
class BroadcastTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/**
|
||||
* Tenant properties to be mapped to config (similarly to the TenantConfig feature).
|
||||
*
|
||||
* For example:
|
||||
* [
|
||||
* 'config.key.name' => 'tenant_property',
|
||||
* ]
|
||||
*/
|
||||
public static array $credentialsMap = [];
|
||||
|
||||
public static string|null $broadcaster = null;
|
||||
|
||||
protected array $originalConfig = [];
|
||||
protected BroadcastManager|null $originalBroadcastManager = null;
|
||||
protected Broadcaster|null $originalBroadcaster = null;
|
||||
|
||||
public static array $mapPresets = [
|
||||
'pusher' => [
|
||||
'broadcasting.connections.pusher.key' => 'pusher_key',
|
||||
'broadcasting.connections.pusher.secret' => 'pusher_secret',
|
||||
'broadcasting.connections.pusher.app_id' => 'pusher_app_id',
|
||||
'broadcasting.connections.pusher.options.cluster' => 'pusher_cluster',
|
||||
],
|
||||
'ably' => [
|
||||
'broadcasting.connections.ably.key' => 'ably_key',
|
||||
'broadcasting.connections.ably.public' => 'ably_public',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Repository $config,
|
||||
protected Application $app
|
||||
) {
|
||||
static::$broadcaster ??= $config->get('broadcasting.default');
|
||||
static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$broadcaster] ?? []);
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
$this->originalBroadcastManager = $this->app->make(BroadcastManager::class);
|
||||
$this->originalBroadcaster = $this->app->make(Broadcaster::class);
|
||||
|
||||
$this->setConfig($tenant);
|
||||
|
||||
// Make BroadcastManager resolve to a custom BroadcastManager which makes the broadcasters use the tenant credentials
|
||||
$this->app->extend(BroadcastManager::class, function (BroadcastManager $broadcastManager) {
|
||||
return new TenancyBroadcastManager($this->app);
|
||||
});
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
// Change the BroadcastManager and Broadcaster singletons back to what they were before initializing tenancy
|
||||
$this->app->singleton(BroadcastManager::class, fn (Application $app) => $this->originalBroadcastManager);
|
||||
$this->app->singleton(Broadcaster::class, fn (Application $app) => $this->originalBroadcaster);
|
||||
|
||||
$this->unsetConfig();
|
||||
}
|
||||
|
||||
protected function setConfig(Tenant $tenant): void
|
||||
{
|
||||
foreach (static::$credentialsMap as $configKey => $storageKey) {
|
||||
$override = $tenant->$storageKey;
|
||||
|
||||
if (array_key_exists($storageKey, $tenant->getAttributes())) {
|
||||
$this->originalConfig[$configKey] ??= $this->config->get($configKey);
|
||||
|
||||
$this->config->set($configKey, $override);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function unsetConfig(): void
|
||||
{
|
||||
foreach ($this->originalConfig as $key => $value) {
|
||||
$this->config->set($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
|||
/** @var TenantWithDatabase $tenant */
|
||||
|
||||
// Better debugging, but breaks cached lookup in prod
|
||||
if (app()->environment('local')) {
|
||||
if (app()->environment('local') || app()->environment('testing')) { // todo@docs mention this change in v4 upgrade guide https://github.com/archtechx/tenancy/pull/945#issuecomment-1268206149
|
||||
$database = $tenant->database()->getName();
|
||||
if (! $tenant->database()->manager()->databaseExists($database)) {
|
||||
throw new TenantDatabaseDoesNotExistException($database);
|
||||
|
|
|
|||
79
src/Bootstrappers/MailTenancyBootstrapper.php
Normal file
79
src/Bootstrappers/MailTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
class MailTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
/**
|
||||
* Tenant properties to be mapped to config (similarly to the TenantConfig feature).
|
||||
*
|
||||
* For example:
|
||||
* [
|
||||
* 'config.key.name' => 'tenant_property',
|
||||
* ]
|
||||
*/
|
||||
public static array $credentialsMap = [];
|
||||
|
||||
public static string|null $mailer = null;
|
||||
|
||||
protected array $originalConfig = [];
|
||||
|
||||
public static array $mapPresets = [
|
||||
'smtp' => [
|
||||
'mail.mailers.smtp.host' => 'smtp_host',
|
||||
'mail.mailers.smtp.port' => 'smtp_port',
|
||||
'mail.mailers.smtp.username' => 'smtp_username',
|
||||
'mail.mailers.smtp.password' => 'smtp_password',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Repository $config,
|
||||
protected Application $app
|
||||
) {
|
||||
static::$mailer ??= $config->get('mail.default');
|
||||
static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$mailer] ?? []);
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
// Forget the mail manager instance to clear the cached mailers
|
||||
$this->app->forgetInstance('mail.manager');
|
||||
|
||||
$this->setConfig($tenant);
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
$this->unsetConfig();
|
||||
|
||||
$this->app->forgetInstance('mail.manager');
|
||||
}
|
||||
|
||||
protected function setConfig(Tenant $tenant): void
|
||||
{
|
||||
foreach (static::$credentialsMap as $configKey => $storageKey) {
|
||||
$override = $tenant->$storageKey;
|
||||
|
||||
if (array_key_exists($storageKey, $tenant->getAttributes())) {
|
||||
$this->originalConfig[$configKey] ??= $this->config->get($configKey);
|
||||
|
||||
$this->config->set($configKey, $override);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function unsetConfig(): void
|
||||
{
|
||||
foreach ($this->originalConfig as $key => $value) {
|
||||
$this->config->set($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,9 +79,9 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
||||
}
|
||||
|
||||
protected static function initializeTenancyForQueue(string|int $tenantId): void
|
||||
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
|
||||
{
|
||||
if (! $tenantId) {
|
||||
if ($tenantId === null) {
|
||||
// The job is not tenant-aware
|
||||
if (tenancy()->initialized) {
|
||||
// Tenancy was initialized, so we revert back to the central context
|
||||
|
|
|
|||
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal file
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Session\DatabaseSessionHandler;
|
||||
use Illuminate\Session\SessionManager;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
/**
|
||||
* This resets the database connection used by the database session driver.
|
||||
*
|
||||
* It runs each time tenancy is initialized or ended.
|
||||
* That way the session driver always uses the current DB connection.
|
||||
*/
|
||||
class SessionTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
public function __construct(
|
||||
protected Repository $config,
|
||||
protected Container $container,
|
||||
protected SessionManager $session,
|
||||
) {
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
$this->resetDatabaseHandler();
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
// When ending tenancy, this runs *before* the DatabaseTenancyBootstrapper, so DB tenancy
|
||||
// is still bootstrapped. For that reason, we have to explicitly use the central connection
|
||||
$this->resetDatabaseHandler(config('tenancy.database.central_connection'));
|
||||
}
|
||||
|
||||
protected function resetDatabaseHandler(string $defaultConnection = null): void
|
||||
{
|
||||
$sessionDrivers = $this->session->getDrivers();
|
||||
|
||||
if (isset($sessionDrivers['database'])) {
|
||||
/** @var \Illuminate\Session\Store $databaseDriver */
|
||||
$databaseDriver = $sessionDrivers['database'];
|
||||
|
||||
$databaseDriver->setHandler($this->createDatabaseHandler($defaultConnection));
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDatabaseHandler(string $defaultConnection = null): DatabaseSessionHandler
|
||||
{
|
||||
// Typically returns null, so this falls back to the default DB connection
|
||||
$connection = $this->config->get('session.connection') ?? $defaultConnection;
|
||||
|
||||
// Based on SessionManager::createDatabaseDriver
|
||||
return new DatabaseSessionHandler(
|
||||
$this->container->make('db')->connection($connection),
|
||||
$this->config->get('session.table'),
|
||||
$this->config->get('session.lifetime'),
|
||||
$this->container,
|
||||
);
|
||||
}
|
||||
}
|
||||
41
src/Bootstrappers/UrlTenancyBootstrapper.php
Normal file
41
src/Bootstrappers/UrlTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Bootstrappers;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Contracts\Routing\UrlGenerator;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
||||
class UrlTenancyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
public static Closure|null $rootUrlOverride = null;
|
||||
protected string|null $originalRootUrl = null;
|
||||
|
||||
public function __construct(
|
||||
protected UrlGenerator $urlGenerator,
|
||||
protected Repository $config,
|
||||
) {
|
||||
}
|
||||
|
||||
public function bootstrap(Tenant $tenant): void
|
||||
{
|
||||
$this->originalRootUrl = $this->urlGenerator->to('/');
|
||||
|
||||
if (static::$rootUrlOverride) {
|
||||
$newRootUrl = (static::$rootUrlOverride)($tenant);
|
||||
|
||||
$this->urlGenerator->forceRootUrl($newRootUrl);
|
||||
$this->config->set('app.url', $newRootUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public function revert(): void
|
||||
{
|
||||
$this->urlGenerator->forceRootUrl($this->originalRootUrl);
|
||||
$this->config->set('app.url', $this->originalRootUrl);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Builder;
|
|||
class ClearPendingTenants extends Command
|
||||
{
|
||||
protected $signature = 'tenants:pending-clear
|
||||
{--all : Override the default settings and deletes all pending tenants}
|
||||
{--older-than-days= : Deletes all pending tenants older than the amount of days}
|
||||
{--older-than-hours= : Deletes all pending tenants older than the amount of hours}';
|
||||
|
||||
|
|
@ -18,38 +17,30 @@ class ClearPendingTenants extends Command
|
|||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Removing pending tenants.');
|
||||
$this->components->info('Removing pending tenants.');
|
||||
|
||||
$expirationDate = now();
|
||||
// We compare the original expiration date to the new one to check if the new one is different later
|
||||
$originalExpirationDate = $expirationDate->copy()->toImmutable();
|
||||
|
||||
// Skip the time constraints if the 'all' option is given
|
||||
if (! $this->option('all')) {
|
||||
/** @var ?int $olderThanDays */
|
||||
$olderThanDays = $this->option('older-than-days');
|
||||
$olderThanDays = (int) $this->option('older-than-days');
|
||||
$olderThanHours = (int) $this->option('older-than-hours');
|
||||
|
||||
/** @var ?int $olderThanHours */
|
||||
$olderThanHours = $this->option('older-than-hours');
|
||||
if ($olderThanDays && $olderThanHours) {
|
||||
$this->components->error("Cannot use '--older-than-days' and '--older-than-hours' together. Please, choose only one of these options.");
|
||||
|
||||
if ($olderThanDays && $olderThanHours) {
|
||||
$this->line("<options=bold,reverse;fg=red> Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components
|
||||
$this->line('Please, choose only one of these options.');
|
||||
|
||||
return 1; // Exit code for failure
|
||||
}
|
||||
|
||||
if ($olderThanDays) {
|
||||
$expirationDate->subDays($olderThanDays);
|
||||
}
|
||||
|
||||
if ($olderThanHours) {
|
||||
$expirationDate->subHours($olderThanHours);
|
||||
}
|
||||
return 1; // Exit code for failure
|
||||
}
|
||||
|
||||
$deletedTenantCount = tenancy()
|
||||
->query()
|
||||
if ($olderThanDays) {
|
||||
$expirationDate->subDays($olderThanDays);
|
||||
}
|
||||
|
||||
if ($olderThanHours) {
|
||||
$expirationDate->subHours($olderThanHours);
|
||||
}
|
||||
|
||||
$deletedTenantCount = tenancy()->query()
|
||||
->onlyPending()
|
||||
->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) {
|
||||
$query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp);
|
||||
|
|
@ -59,7 +50,7 @@ class ClearPendingTenants extends Command
|
|||
->delete()
|
||||
->count();
|
||||
|
||||
$this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.');
|
||||
$this->components->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class CreatePendingTenants extends Command
|
|||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Creating pending tenants.');
|
||||
$this->components->info('Creating pending tenants.');
|
||||
|
||||
$maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count'));
|
||||
$pendingTenantCount = $this->getPendingTenantCount();
|
||||
|
|
@ -30,8 +30,8 @@ class CreatePendingTenants extends Command
|
|||
$createdCount++;
|
||||
}
|
||||
|
||||
$this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.');
|
||||
$this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
||||
$this->components->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.');
|
||||
$this->components->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -39,8 +39,7 @@ class CreatePendingTenants extends Command
|
|||
/** Calculate the number of currently available pending tenants. */
|
||||
protected function getPendingTenantCount(): int
|
||||
{
|
||||
return tenancy()
|
||||
->query()
|
||||
return tenancy()->query()
|
||||
->onlyPending()
|
||||
->count();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Link extends Command
|
|||
$this->createLinks($tenants);
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
$this->components->error($exception->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ namespace Stancl\Tenancy\Commands;
|
|||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Stancl\Tenancy\Concerns\DealsWithMigrations;
|
||||
use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
|
||||
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||
use Stancl\Tenancy\Events\DatabaseMigrated;
|
||||
use Stancl\Tenancy\Events\MigratingDatabase;
|
||||
|
||||
|
|
@ -28,6 +30,8 @@ class Migrate extends MigrateCommand
|
|||
{
|
||||
parent::__construct($migrator, $dispatcher);
|
||||
|
||||
$this->addOption('skip-failing');
|
||||
|
||||
$this->specifyParameters();
|
||||
}
|
||||
|
||||
|
|
@ -43,16 +47,23 @@ class Migrate extends MigrateCommand
|
|||
return 1;
|
||||
}
|
||||
|
||||
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
|
||||
$this->components->info("Tenant: {$tenant->getTenantKey()}");
|
||||
foreach ($this->getTenants() as $tenant) {
|
||||
try {
|
||||
$tenant->run(function ($tenant) {
|
||||
$this->line("Tenant: {$tenant->getTenantKey()}");
|
||||
|
||||
event(new MigratingDatabase($tenant));
|
||||
event(new MigratingDatabase($tenant));
|
||||
// Migrate
|
||||
parent::handle();
|
||||
|
||||
// Migrate
|
||||
parent::handle();
|
||||
|
||||
event(new DatabaseMigrated($tenant));
|
||||
});
|
||||
event(new DatabaseMigrated($tenant));
|
||||
});
|
||||
} catch (TenantDatabaseDoesNotExistException|QueryException $th) {
|
||||
if (! $this->option('skip-failing')) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class TenantDump extends DumpCommand
|
|||
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int
|
||||
{
|
||||
if (is_null($this->option('path'))) {
|
||||
$this->input->setOption('path', database_path('schema/tenant-schema.dump'));
|
||||
$this->input->setOption('path', config('tenancy.migration_parameters.--schema-path') ?? database_path('schema/tenant-schema.dump'));
|
||||
}
|
||||
|
||||
$tenant = $this->option('tenant')
|
||||
|
|
@ -41,7 +41,7 @@ class TenantDump extends DumpCommand
|
|||
return 1;
|
||||
}
|
||||
|
||||
parent::handle($connections, $dispatcher);
|
||||
$tenant->run(fn () => parent::handle($connections, $dispatcher));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Stancl\Tenancy\Enums\LogMode;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
// todo finish this feature
|
||||
|
||||
/**
|
||||
* @mixin Tenancy
|
||||
*/
|
||||
trait Debuggable
|
||||
{
|
||||
protected LogMode $logMode = LogMode::NONE;
|
||||
protected array $eventLog = [];
|
||||
|
||||
public function log(LogMode $mode = LogMode::SILENT): static
|
||||
{
|
||||
$this->eventLog = [];
|
||||
$this->logMode = $mode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function logMode(): LogMode
|
||||
{
|
||||
return $this->logMode;
|
||||
}
|
||||
|
||||
public function getLog(): array
|
||||
{
|
||||
return $this->eventLog;
|
||||
}
|
||||
|
||||
public function logEvent(TenancyEvent $event): static
|
||||
{
|
||||
$this->eventLog[] = ['time' => now(), 'event' => $event::class, 'tenant' => $this->tenant];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dump(Closure $dump = null): static
|
||||
{
|
||||
$dump ??= dd(...);
|
||||
|
||||
// Dump the log if we were already logging in silent mode
|
||||
// Otherwise start logging in instant mode
|
||||
match ($this->logMode) {
|
||||
LogMode::NONE => $this->log(LogMode::INSTANT),
|
||||
LogMode::SILENT => $dump($this->eventLog),
|
||||
LogMode::INSTANT => null,
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dd(Closure $dump = null): void
|
||||
{
|
||||
$dump ??= dd(...);
|
||||
|
||||
if ($this->logMode === LogMode::SILENT) {
|
||||
$dump($this->eventLog);
|
||||
} else {
|
||||
$dump($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,7 @@ trait HasTenantOptions
|
|||
|
||||
protected function getTenants(): LazyCollection
|
||||
{
|
||||
return tenancy()
|
||||
->query()
|
||||
return tenancy()->query()
|
||||
->when($this->option('tenants'), function ($query) {
|
||||
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
*
|
||||
* @see \Stancl\Tenancy\Database\Models\Domain
|
||||
*
|
||||
* @method __call(string $method, array $parameters) IDE support. This will be a model.
|
||||
* @method __call(string $method, array $parameters) IDE support. This will be a model. // todo check if we can remove these now
|
||||
* @method static __callStatic(string $method, array $parameters) IDE support. This will be a model.
|
||||
* @mixin \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Database\TenantScope;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
|
@ -13,7 +14,7 @@ use Stancl\Tenancy\Tenancy;
|
|||
*/
|
||||
trait BelongsToTenant
|
||||
{
|
||||
public function tenant()
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
|
||||
use Stancl\Tenancy\Database\Models\TenantMorphPivot;
|
||||
use Stancl\Tenancy\Events\SyncedResourceSaved;
|
||||
|
||||
trait ResourceSyncing
|
||||
|
|
@ -43,4 +45,10 @@ trait ResourceSyncing
|
|||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function tenants(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(config('tenancy.models.tenant'), 'tenant_resources', 'tenant_resources', 'resource_global_id', 'tenant_id', 'global_id')
|
||||
->using(TenantMorphPivot::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/Database/Concerns/TriggerSyncEvent.php
Normal file
21
src/Database/Concerns/TriggerSyncEvent.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Database\Concerns;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
|
||||
trait TriggerSyncEvent
|
||||
{
|
||||
public static function booted(): void
|
||||
{
|
||||
static::saved(function (self $pivot) {
|
||||
$parent = $pivot->pivotParent;
|
||||
|
||||
if ($parent instanceof Syncable && $parent->shouldSync()) {
|
||||
$parent->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ class DatabaseConfig
|
|||
{
|
||||
$this->tenant->setInternal('db_name', $this->getName());
|
||||
|
||||
if ($this->connectionDriverManager($this->getTemplateConnectionName()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||
if ($this->connectionDriverManager($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant));
|
||||
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant));
|
||||
}
|
||||
|
|
@ -97,11 +97,29 @@ class DatabaseConfig
|
|||
}
|
||||
}
|
||||
|
||||
public function getTemplateConnectionName(): string
|
||||
public function getTemplateConnectionDriver(): string
|
||||
{
|
||||
return $this->tenant->getInternal('db_connection')
|
||||
?? config('tenancy.database.template_tenant_connection')
|
||||
?? config('tenancy.database.central_connection');
|
||||
return $this->getTemplateConnection()['driver'];
|
||||
}
|
||||
|
||||
public function getTemplateConnection(): array
|
||||
{
|
||||
if ($template = $this->tenant->getInternal('db_connection')) {
|
||||
return config("database.connections.{$template}");
|
||||
}
|
||||
|
||||
if ($template = config('tenancy.database.template_tenant_connection')) {
|
||||
return is_array($template) ? array_merge($this->getCentralConnection(), $template) : config("database.connections.{$template}");
|
||||
}
|
||||
|
||||
return $this->getCentralConnection();
|
||||
}
|
||||
|
||||
protected function getCentralConnection(): array
|
||||
{
|
||||
$centralConnectionName = config('tenancy.database.central_connection');
|
||||
|
||||
return config("database.connections.{$centralConnectionName}");
|
||||
}
|
||||
|
||||
public function getTenantHostConnectionName(): string
|
||||
|
|
@ -114,8 +132,7 @@ class DatabaseConfig
|
|||
*/
|
||||
public function connection(): array
|
||||
{
|
||||
$template = $this->getTemplateConnectionName();
|
||||
$templateConnection = config("database.connections.{$template}");
|
||||
$templateConnection = $this->getTemplateConnection();
|
||||
|
||||
return $this->manager()->makeConnectionConfig(
|
||||
array_merge($templateConnection, $this->tenantConfig()),
|
||||
|
|
@ -129,10 +146,9 @@ class DatabaseConfig
|
|||
public function hostConnection(): array
|
||||
{
|
||||
$config = $this->tenantConfig();
|
||||
$template = $this->getTemplateConnectionName();
|
||||
$templateConnection = config("database.connections.{$template}");
|
||||
$templateConnection = $this->getTemplateConnection();
|
||||
|
||||
if ($this->connectionDriverManager($template) instanceof Contracts\ManagesDatabaseUsers) {
|
||||
if ($this->connectionDriverManager($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||
// We're removing the username and password because user with these credentials is not created yet
|
||||
// If you need to provide username and password when using PermissionControlledMySQLDatabaseManager,
|
||||
// consider creating a new connection and use it as `tenancy_db_connection` tenant config key
|
||||
|
|
@ -196,7 +212,7 @@ class DatabaseConfig
|
|||
$tenantHostConnectionName = $this->getTenantHostConnectionName();
|
||||
config(["database.connections.{$tenantHostConnectionName}" => $this->hostConnection()]);
|
||||
|
||||
$manager = $this->connectionDriverManager($tenantHostConnectionName);
|
||||
$manager = $this->connectionDriverManager(config("database.connections.{$tenantHostConnectionName}.driver"));
|
||||
|
||||
if ($manager instanceof Contracts\StatefulTenantDatabaseManager) {
|
||||
$manager->setConnection($tenantHostConnectionName);
|
||||
|
|
@ -211,10 +227,8 @@ class DatabaseConfig
|
|||
*
|
||||
* @throws DatabaseManagerNotRegisteredException
|
||||
*/
|
||||
protected function connectionDriverManager(string $connectionName): Contracts\TenantDatabaseManager
|
||||
protected function connectionDriverManager(string $driver): Contracts\TenantDatabaseManager
|
||||
{
|
||||
$driver = config("database.connections.{$connectionName}.driver");
|
||||
|
||||
$databaseManagers = config('tenancy.database.managers');
|
||||
|
||||
if (! array_key_exists($driver, $databaseManagers)) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts;
|
|||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Database\Concerns;
|
||||
use Stancl\Tenancy\Events;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
/**
|
||||
* @property string $domain
|
||||
|
|
@ -28,7 +29,7 @@ class Domain extends Model implements Contracts\Domain
|
|||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(config('tenancy.models.tenant'));
|
||||
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
|
||||
}
|
||||
|
||||
protected $dispatchesEvents = [
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ class ImpersonationToken extends Model
|
|||
public $incrementing = false;
|
||||
|
||||
protected $table = 'tenant_user_impersonation_tokens';
|
||||
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public static function booted(): void
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ class Tenant extends Model implements Contracts\Tenant
|
|||
Concerns\InitializationHelpers,
|
||||
Concerns\InvalidatesResolverCache;
|
||||
|
||||
protected static $modelsShouldPreventAccessingMissingAttributes = false;
|
||||
|
||||
protected $table = 'tenants';
|
||||
protected $primaryKey = 'id';
|
||||
protected $guarded = [];
|
||||
|
|
|
|||
13
src/Database/Models/TenantMorphPivot.php
Normal file
13
src/Database/Models/TenantMorphPivot.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Database\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphPivot;
|
||||
use Stancl\Tenancy\Database\Concerns\TriggerSyncEvent;
|
||||
|
||||
class TenantMorphPivot extends MorphPivot
|
||||
{
|
||||
use TriggerSyncEvent;
|
||||
}
|
||||
|
|
@ -5,18 +5,9 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Database\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use Stancl\Tenancy\Contracts\Syncable;
|
||||
use Stancl\Tenancy\Database\Concerns\TriggerSyncEvent;
|
||||
|
||||
class TenantPivot extends Pivot
|
||||
{
|
||||
public static function booted(): void
|
||||
{
|
||||
static::saved(function (self $pivot) {
|
||||
$parent = $pivot->pivotParent;
|
||||
|
||||
if ($parent instanceof Syncable && $parent->shouldSync()) {
|
||||
$parent->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
use TriggerSyncEvent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
|
|||
|
||||
protected function isVersion8(): bool
|
||||
{
|
||||
$version = $this->database()->select($this->database()->raw('select version()'))[0]->{'version()'};
|
||||
$versionSelect = (string) $this->database()->raw('select version()')->getValue($this->database()->getQueryGrammar());
|
||||
$version = $this->database()->select($versionSelect)[0]->{'version()'};
|
||||
|
||||
return version_compare($version, '8.0.0') >= 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Enums;
|
||||
|
||||
enum LogMode
|
||||
{
|
||||
case NONE;
|
||||
case SILENT;
|
||||
case INSTANT;
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Features;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as Router;
|
||||
use Stancl\Tenancy\Contracts\Feature;
|
||||
use Stancl\Tenancy\Middleware;
|
||||
|
||||
class UniversalRoutes implements Feature
|
||||
{
|
||||
public static string $middlewareGroup = 'universal';
|
||||
|
||||
// todo docblock
|
||||
/** @var array<class-string<\Stancl\Tenancy\Middleware\IdentificationMiddleware>> */
|
||||
public static array $identificationMiddlewares = [
|
||||
Middleware\InitializeTenancyByDomain::class,
|
||||
Middleware\InitializeTenancyBySubdomain::class,
|
||||
];
|
||||
|
||||
public function bootstrap(): void
|
||||
{
|
||||
foreach (static::$identificationMiddlewares as $middleware) {
|
||||
$originalOnFail = $middleware::$onFail;
|
||||
|
||||
$middleware::$onFail = function ($exception, $request, $next) use ($originalOnFail) {
|
||||
if (static::routeHasMiddleware($request->route(), static::$middlewareGroup)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($originalOnFail) {
|
||||
return $originalOnFail($exception, $request, $next);
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static function routeHasMiddleware(Route $route, string $middleware): bool
|
||||
{
|
||||
/** @var array $routeMiddleware */
|
||||
$routeMiddleware = $route->middleware();
|
||||
|
||||
if (in_array($middleware, $routeMiddleware, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop one level deep and check if the route's middleware
|
||||
// groups have the searched middleware group inside them
|
||||
$middlewareGroups = Router::getMiddlewareGroups();
|
||||
foreach ($route->gatherMiddleware() as $inner) {
|
||||
if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function alwaysBootstrap(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -48,11 +48,26 @@ class UserImpersonation implements Feature
|
|||
|
||||
$token->delete();
|
||||
|
||||
session()->put('tenancy_impersonating', true);
|
||||
|
||||
return redirect($token->redirect_url);
|
||||
}
|
||||
|
||||
public static function alwaysBootstrap(): bool
|
||||
{
|
||||
return false;
|
||||
public static function isImpersonating(): bool
|
||||
{
|
||||
return session()->has('tenancy_impersonating');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout from the current domain and forget impersonation session.
|
||||
*/
|
||||
public static function leave(): void // todo possibly rename
|
||||
{
|
||||
auth()->logout();
|
||||
|
||||
session()->forget('tenancy_impersonating');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
src/Features/ViteBundler.php
Normal file
26
src/Features/ViteBundler.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Features;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Stancl\Tenancy\Contracts\Feature;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
use Stancl\Tenancy\Vite;
|
||||
|
||||
class ViteBundler implements Feature
|
||||
{
|
||||
/** @var Application */
|
||||
protected $app;
|
||||
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
public function bootstrap(Tenancy $tenancy): void
|
||||
{
|
||||
$this->app->singleton(\Illuminate\Foundation\Vite::class, Vite::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Listeners;
|
|||
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
|
||||
class CreateTenantConnection
|
||||
{
|
||||
|
|
@ -15,11 +15,12 @@ class CreateTenantConnection
|
|||
) {
|
||||
}
|
||||
|
||||
public function handle(TenantEvent $event): void
|
||||
public function handle(TenancyEvent $event): void
|
||||
{
|
||||
/** @var TenantWithDatabase */
|
||||
$tenant = $event->tenant;
|
||||
/** @var TenantWithDatabase $tenant */
|
||||
$tenant = $event->tenancy->tenant;
|
||||
|
||||
$this->database->purgeTenantConnection();
|
||||
$this->database->createTenantConnection($tenant);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/Listeners/CreateTenantStorage.php
Normal file
18
src/Listeners/CreateTenantStorage.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
|
||||
class CreateTenantStorage
|
||||
{
|
||||
public function handle(TenantCreated $event): void
|
||||
{
|
||||
$storage_path = $event->tenant->run(fn () => storage_path());
|
||||
|
||||
mkdir("$storage_path", 0777, true); // Create the tenant's folder inside storage/
|
||||
mkdir("$storage_path/framework/cache", 0777, true); // Create /framework/cache inside the tenant's storage (used for e.g. real-time facades)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,9 @@ class DeleteTenantStorage
|
|||
{
|
||||
public function handle(DeletingTenant $event): void
|
||||
{
|
||||
// todo@lukas since this is using the 'File' facade instead of low-level PHP functions, Tenancy might affect this?
|
||||
// Therefore, when Tenancy is initialized, this might look INSIDE the tenant's storage, instead of the main storage dir?
|
||||
// The DeletingTenant event will be fired in the central context in 99% of cases, but sometimes it might run in the tenant context (from another tenant) so we want to make sure this works well in all contexts.
|
||||
File::deleteDirectory($event->tenant->run(fn () => storage_path()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/Listeners/UseCentralConnection.php
Normal file
21
src/Listeners/UseCentralConnection.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
|
||||
class UseCentralConnection
|
||||
{
|
||||
public function __construct(
|
||||
protected DatabaseManager $database,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(TenancyEvent $event): void
|
||||
{
|
||||
$this->database->reconnectToCentral();
|
||||
}
|
||||
}
|
||||
21
src/Listeners/UseTenantConnection.php
Normal file
21
src/Listeners/UseTenantConnection.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
|
||||
class UseTenantConnection
|
||||
{
|
||||
public function __construct(
|
||||
protected DatabaseManager $database,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(TenancyEvent $event): void
|
||||
{
|
||||
$this->database->setDefaultConnection('tenant');
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,11 @@ class InitializeTenancyByDomain extends IdentificationMiddleware
|
|||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if (in_array($request->getHost(), config('tenancy.central_domains', []), true)) {
|
||||
// Always bypass tenancy initialization when host is in central domains
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return $this->initializeTenancy(
|
||||
$request,
|
||||
$next,
|
||||
|
|
|
|||
|
|
@ -28,14 +28,13 @@ class InitializeTenancyByPath extends IdentificationMiddleware
|
|||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $request->route();
|
||||
$route = $this->route($request);
|
||||
|
||||
// Only initialize tenancy if tenant is the first parameter
|
||||
// We don't want to initialize tenancy if the tenant is
|
||||
// simply injected into some route controller action.
|
||||
if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) {
|
||||
$this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized();
|
||||
$this->setDefaultTenantForRouteParametersWhenInitializingTenancy();
|
||||
|
||||
return $this->initializeTenancy(
|
||||
$request,
|
||||
|
|
@ -47,7 +46,26 @@ class InitializeTenancyByPath extends IdentificationMiddleware
|
|||
}
|
||||
}
|
||||
|
||||
protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void
|
||||
protected function route(Request $request): Route
|
||||
{
|
||||
/** @var ?Route $route */
|
||||
$route = $request->route();
|
||||
|
||||
if (! $route) {
|
||||
// Create a fake $route instance that has enough information for this middleware's needs
|
||||
$route = new Route($request->method(), $request->getUri(), []);
|
||||
/**
|
||||
* getPathInfo() returns the path except the root domain.
|
||||
* We fetch the first parameter because tenant parameter is *always* first.
|
||||
*/
|
||||
$route->parameters[PathTenantResolver::tenantParameterName()] = explode('/', ltrim($request->getPathInfo(), '/'))[0];
|
||||
$route->parameterNames[] = PathTenantResolver::tenantParameterName();
|
||||
}
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
protected function setDefaultTenantForRouteParametersWhenInitializingTenancy(): void
|
||||
{
|
||||
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
|
||||
/** @var Tenant $tenant */
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
|||
/** @return Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if (in_array($request->getHost(), config('tenancy.central_domains', []), true)) {
|
||||
// Always bypass tenancy initialization when host is in central domains
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$subdomain = $this->makeSubdomain($request->getHost());
|
||||
|
||||
if (is_object($subdomain) && $subdomain instanceof Exception) {
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PreventAccessFromCentralDomains
|
||||
{
|
||||
/**
|
||||
* Set this property if you want to customize the on-fail behavior.
|
||||
*/
|
||||
public static ?Closure $abortRequest;
|
||||
|
||||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if (in_array($request->getHost(), config('tenancy.central_domains'))) {
|
||||
$abortRequest = static::$abortRequest ?? function () {
|
||||
abort(404);
|
||||
};
|
||||
|
||||
return $abortRequest($request, $next);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
61
src/Middleware/PreventAccessFromUnwantedDomains.php
Normal file
61
src/Middleware/PreventAccessFromUnwantedDomains.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as Router;
|
||||
|
||||
// todo come up with a better name
|
||||
class PreventAccessFromUnwantedDomains
|
||||
{
|
||||
/**
|
||||
* Set this property if you want to customize the on-fail behavior.
|
||||
*/
|
||||
public static ?Closure $abortRequest;
|
||||
|
||||
/** @return \Illuminate\Http\Response|mixed */
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $request->route();
|
||||
|
||||
if ($this->routeHasMiddleware($route, 'universal')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (in_array($request->getHost(), config('tenancy.central_domains'), true)) {
|
||||
$abortRequest = static::$abortRequest ?? function () {
|
||||
abort(404);
|
||||
};
|
||||
|
||||
return $abortRequest($request, $next);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
protected function routeHasMiddleware(Route $route, string $middleware): bool
|
||||
{
|
||||
/** @var array $routeMiddleware */
|
||||
$routeMiddleware = $route->middleware();
|
||||
|
||||
if (in_array($middleware, $routeMiddleware, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop one level deep and check if the route's middleware
|
||||
// groups have the searched middleware group inside them
|
||||
$middlewareGroups = Router::getMiddlewareGroups();
|
||||
foreach ($route->gatherMiddleware() as $inner) {
|
||||
if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,9 +27,7 @@ abstract class CachedTenantResolver implements TenantResolver
|
|||
|
||||
$key = $this->getCacheKey(...$args);
|
||||
|
||||
if ($this->cache->has($key)) {
|
||||
$tenant = $this->cache->get($key);
|
||||
|
||||
if ($tenant = $this->cache->get($key)) {
|
||||
$this->resolved($tenant, ...$args);
|
||||
|
||||
return $tenant;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Resolvers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\Domain;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||
|
|
@ -39,14 +40,16 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
|||
|
||||
protected function setCurrentDomain(Tenant $tenant, string $domain): void
|
||||
{
|
||||
/** @var Tenant&Model $tenant */
|
||||
static::$currentDomain = $tenant->domains->where('domain', $domain)->first();
|
||||
}
|
||||
|
||||
public function getArgsForTenant(Tenant $tenant): array
|
||||
{
|
||||
/** @var Tenant&Model $tenant */
|
||||
$tenant->unsetRelation('domains');
|
||||
|
||||
return $tenant->domains->map(function (Domain $domain) {
|
||||
return $tenant->domains->map(function (Domain&Model $domain) {
|
||||
return [$domain->domain];
|
||||
})->toArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,21 +8,18 @@ use Closure;
|
|||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use Stancl\Tenancy\Concerns\Debuggable;
|
||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
|
||||
|
||||
class Tenancy
|
||||
{
|
||||
use Macroable, Debuggable;
|
||||
use Macroable;
|
||||
|
||||
/**
|
||||
* The current tenant.
|
||||
*
|
||||
* @var (Tenant&Model)|null
|
||||
*/
|
||||
public ?Tenant $tenant = null;
|
||||
public Tenant|null $tenant = null;
|
||||
|
||||
// todo docblock
|
||||
public ?Closure $getBootstrappersUsing = null;
|
||||
|
|
@ -97,9 +94,9 @@ class Tenancy
|
|||
|
||||
public static function model(): Tenant&Model
|
||||
{
|
||||
/** @var class-string<Tenant&Model> $class */
|
||||
$class = config('tenancy.models.tenant');
|
||||
|
||||
/** @var Tenant&Model $model */
|
||||
$model = new $class;
|
||||
|
||||
return $model;
|
||||
|
|
@ -113,8 +110,6 @@ class Tenancy
|
|||
|
||||
/**
|
||||
* Try to find a tenant using an ID.
|
||||
*
|
||||
* @return (Tenant&Model)|null
|
||||
*/
|
||||
public static function find(int|string $id): Tenant|null
|
||||
{
|
||||
|
|
|
|||
65
src/TenancyBroadcastManager.php
Normal file
65
src/TenancyBroadcastManager.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy; // todo new Overrides namespace?
|
||||
|
||||
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
|
||||
use Illuminate\Broadcasting\BroadcastManager;
|
||||
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
|
||||
class TenancyBroadcastManager extends BroadcastManager
|
||||
{
|
||||
/**
|
||||
* Names of broadcasters to always recreate using $this->resolve() (even when they're
|
||||
* cached and available in the $broadcasters property).
|
||||
*
|
||||
* The reason for recreating the broadcasters is
|
||||
* to make your app use the correct broadcaster credentials when tenancy is initialized.
|
||||
*/
|
||||
public static array $tenantBroadcasters = ['pusher', 'ably'];
|
||||
|
||||
/**
|
||||
* Override the get method so that the broadcasters in $tenantBroadcasters
|
||||
* always get freshly resolved even when they're cached and available in the $broadcasters property,
|
||||
* and that the resolved broadcaster will override the BroadcasterContract::class singleton.
|
||||
*
|
||||
* If there's a cached broadcaster with the same name as $name,
|
||||
* give its channels to the newly resolved bootstrapper.
|
||||
*/
|
||||
protected function get($name)
|
||||
{
|
||||
if (in_array($name, static::$tenantBroadcasters)) {
|
||||
/** @var Broadcaster|null $originalBroadcaster */
|
||||
$originalBroadcaster = $this->app->make(BroadcasterContract::class);
|
||||
$newBroadcaster = $this->resolve($name);
|
||||
|
||||
// If there is a current broadcaster, give its channels to the newly resolved one
|
||||
// Broadcasters only have to implement the Illuminate\Contracts\Broadcasting\Broadcaster contract
|
||||
// Which doesn't require the channels property
|
||||
// So passing the channels is only needed for Illuminate\Broadcasting\Broadcasters\Broadcaster instances
|
||||
if ($originalBroadcaster instanceof Broadcaster && $newBroadcaster instanceof Broadcaster) {
|
||||
$this->passChannelsFromOriginalBroadcaster($originalBroadcaster, $newBroadcaster);
|
||||
}
|
||||
|
||||
$this->app->singleton(BroadcasterContract::class, fn (Application $app) => $newBroadcaster);
|
||||
|
||||
return $newBroadcaster;
|
||||
}
|
||||
|
||||
return parent::get($name);
|
||||
}
|
||||
|
||||
// Because, unlike the original broadcaster, the newly resolved broadcaster won't have the channels registered using routes/channels.php
|
||||
// Using it for broadcasting won't work, unless we make it have the original broadcaster's channels
|
||||
protected function passChannelsFromOriginalBroadcaster(Broadcaster $originalBroadcaster, Broadcaster $newBroadcaster): void
|
||||
{
|
||||
// invade() because channels can't be retrieved through any of the broadcaster's public methods
|
||||
$originalBroadcaster = invade($originalBroadcaster);
|
||||
|
||||
foreach ($originalBroadcaster->channels as $channel => $callback) {
|
||||
$newBroadcaster->channel($channel, $callback, $originalBroadcaster->retrieveChannelOptions($channel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,10 @@ namespace Stancl\Tenancy;
|
|||
|
||||
use Illuminate\Cache\CacheManager;
|
||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Contracts\Domain;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Enums\LogMode;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||
|
||||
class TenancyServiceProvider extends ServiceProvider
|
||||
|
|
@ -62,6 +59,7 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
$this->app->singleton(Commands\Rollback::class, function ($app) {
|
||||
return new Commands\Rollback($app['migrator']);
|
||||
});
|
||||
|
||||
$this->app->singleton(Commands\Seed::class, function ($app) {
|
||||
return new Commands\Seed($app['db']);
|
||||
});
|
||||
|
|
@ -106,6 +104,10 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
__DIR__ . '/../assets/impersonation-migrations/' => database_path('migrations'),
|
||||
], 'impersonation-migrations');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__ . '/../assets/resource-syncing-migrations/' => database_path('migrations'),
|
||||
], 'resource-syncing-migrations');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__ . '/../assets/tenant_routes.stub.php' => base_path('routes/tenant.php'),
|
||||
], 'routes');
|
||||
|
|
@ -118,18 +120,6 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
$this->loadRoutesFrom(__DIR__ . '/../assets/routes.php');
|
||||
}
|
||||
|
||||
Event::listen('Stancl\\Tenancy\\Events\\*', function (string $name, array $data) {
|
||||
$event = $data[0];
|
||||
|
||||
if ($event instanceof TenancyEvent) {
|
||||
match (tenancy()->logMode()) {
|
||||
LogMode::SILENT => tenancy()->logEvent($event),
|
||||
LogMode::INSTANT => dump($event), // todo1 perhaps still log
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
$this->app->singleton('globalUrl', function ($app) {
|
||||
if ($app->bound(FilesystemTenancyBootstrapper::class)) {
|
||||
$instance = clone $app['url'];
|
||||
|
|
|
|||
22
src/Vite.php
Normal file
22
src/Vite.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Illuminate\Foundation\Vite as BaseVite;
|
||||
|
||||
class Vite extends BaseVite // todo move to a different directory in v4
|
||||
{
|
||||
/**
|
||||
* Generate an asset path for the application.
|
||||
*
|
||||
* @param string $path
|
||||
* @param bool|null $secure
|
||||
* @return string
|
||||
*/
|
||||
protected function assetPath($path, $secure = null)
|
||||
{
|
||||
return global_asset($path);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue