mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 21:54:03 +00:00
Merge dev branch (minor breaking changes)
From the perspective of the master branch, this commit merges in a few small breaking changes from the dev branch:6b0066c5ef- Make pullPendingFromPool() $firstOrCreate arg default to false (pullPending() is now a direct alias for pullPendingFromPool() with default $firstOrCreate=true) - See full commit message for other changes. They shouldn't be breaking though.13a2209f11- Remove $WAL static property. We instead just let Laravel use its journal_mode config now This merge also adds a deprecation:b320f8f33d- Deprecate TenantConfig feature in favor of TenantConfigBootstrapper
This commit is contained in:
commit
151e81b412
43 changed files with 443 additions and 223 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -10,6 +10,7 @@
|
||||||
/.nvim.lua export-ignore
|
/.nvim.lua export-ignore
|
||||||
/art export-ignore
|
/art export-ignore
|
||||||
/coverage export-ignore
|
/coverage export-ignore
|
||||||
|
/CLAUDE.md export-ignore
|
||||||
/CONTRIBUTING.md export-ignore
|
/CONTRIBUTING.md export-ignore
|
||||||
/INTERNAL.md export-ignore
|
/INTERNAL.md export-ignore
|
||||||
/SUPPORT.md export-ignore
|
/SUPPORT.md export-ignore
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
/Dockerfile export-ignore
|
/Dockerfile export-ignore
|
||||||
/doctum export-ignore
|
/doctum export-ignore
|
||||||
/phpunit.xml export-ignore
|
/phpunit.xml export-ignore
|
||||||
|
/static_properties.nu export-ignore
|
||||||
/t export-ignore
|
/t export-ignore
|
||||||
/test export-ignore
|
/test export-ignore
|
||||||
/tests export-ignore
|
/tests export-ignore
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
-- The tailwindcss LSP doesn't play nice with testbench due to the recursive
|
-- The tailwindcss LSP doesn't play nice with testbench due to the recursive
|
||||||
-- `vendor` symlink in `testbench-core/laravel/vendor`, so we nuke its setup method here.
|
-- `vendor` symlink in `testbench-core/laravel/vendor`, so we disable it here.
|
||||||
-- This prevents the setup() call in neovim config from starting the client (or doing anything at all).
|
vim.lsp.enable('tailwindcss', false)
|
||||||
require('lspconfig').tailwindcss.setup = function () end
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
// Provision API keys, create S3 buckets, anything you want!
|
// Provision API keys, create S3 buckets, anything you want!
|
||||||
])->send(function (Events\TenantCreated $event) {
|
])->send(function (Events\TenantCreated $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false), // `false` by default, but you likely want to make this `true` in production.
|
})->shouldBeQueued(false),
|
||||||
|
|
||||||
// Listeners\CreateTenantStorage::class,
|
// Listeners\CreateTenantStorage::class,
|
||||||
],
|
],
|
||||||
|
|
@ -80,7 +80,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Jobs\DeleteDatabase::class,
|
Jobs\DeleteDatabase::class,
|
||||||
])->send(function (Events\TenantDeleted $event) {
|
])->send(function (Events\TenantDeleted $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
})->shouldBeQueued(false),
|
||||||
],
|
],
|
||||||
|
|
||||||
Events\TenantMaintenanceModeEnabled::class => [],
|
Events\TenantMaintenanceModeEnabled::class => [],
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ return [
|
||||||
Bootstrappers\DatabaseTenancyBootstrapper::class,
|
Bootstrappers\DatabaseTenancyBootstrapper::class,
|
||||||
Bootstrappers\CacheTenancyBootstrapper::class,
|
Bootstrappers\CacheTenancyBootstrapper::class,
|
||||||
// Bootstrappers\CacheTagsBootstrapper::class, // Alternative to CacheTenancyBootstrapper
|
// Bootstrappers\CacheTagsBootstrapper::class, // Alternative to CacheTenancyBootstrapper
|
||||||
|
// Bootstrappers\DatabaseCacheBootstrapper::class, // Separates cache by DB rather than by prefix, must run after DatabaseTenancyBootstrapper
|
||||||
Bootstrappers\FilesystemTenancyBootstrapper::class,
|
Bootstrappers\FilesystemTenancyBootstrapper::class,
|
||||||
Bootstrappers\QueueTenancyBootstrapper::class,
|
Bootstrappers\QueueTenancyBootstrapper::class,
|
||||||
// Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
// Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
||||||
|
|
@ -178,9 +179,10 @@ return [
|
||||||
Bootstrappers\DatabaseSessionBootstrapper::class,
|
Bootstrappers\DatabaseSessionBootstrapper::class,
|
||||||
|
|
||||||
// Configurable bootstrappers
|
// Configurable bootstrappers
|
||||||
|
// Bootstrappers\TenantConfigBootstrapper::class,
|
||||||
// Bootstrappers\RootUrlBootstrapper::class,
|
// Bootstrappers\RootUrlBootstrapper::class,
|
||||||
// Bootstrappers\UrlGeneratorBootstrapper::class,
|
// Bootstrappers\UrlGeneratorBootstrapper::class,
|
||||||
// Bootstrappers\MailConfigBootstrapper::class, // Note: Queueing mail requires using QueueTenancyBootstrapper with $forceRefresh set to true
|
// Bootstrappers\MailConfigBootstrapper::class,
|
||||||
// Bootstrappers\BroadcastingConfigBootstrapper::class,
|
// Bootstrappers\BroadcastingConfigBootstrapper::class,
|
||||||
// Bootstrappers\BroadcastChannelPrefixBootstrapper::class,
|
// Bootstrappers\BroadcastChannelPrefixBootstrapper::class,
|
||||||
|
|
||||||
|
|
@ -419,7 +421,6 @@ return [
|
||||||
'features' => [
|
'features' => [
|
||||||
// Stancl\Tenancy\Features\UserImpersonation::class,
|
// Stancl\Tenancy\Features\UserImpersonation::class,
|
||||||
// Stancl\Tenancy\Features\TelescopeTags::class,
|
// Stancl\Tenancy\Features\TelescopeTags::class,
|
||||||
// Stancl\Tenancy\Features\TenantConfig::class,
|
|
||||||
// Stancl\Tenancy\Features\CrossDomainRedirect::class,
|
// Stancl\Tenancy\Features\CrossDomainRedirect::class,
|
||||||
// Stancl\Tenancy\Features\ViteBundler::class,
|
// Stancl\Tenancy\Features\ViteBundler::class,
|
||||||
// Stancl\Tenancy\Features\DisallowSqliteAttach::class,
|
// Stancl\Tenancy\Features\DisallowSqliteAttach::class,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
|
PHP_VERSION: ${PHP_VERSION:-8.4}
|
||||||
XDEBUG_ENABLED: ${XDEBUG_ENABLED:-false}
|
XDEBUG_ENABLED: ${XDEBUG_ENABLED:-false}
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ class CloneRoutesAsTenant
|
||||||
{
|
{
|
||||||
protected array $routesToClone = [];
|
protected array $routesToClone = [];
|
||||||
protected bool $addTenantParameter = true;
|
protected bool $addTenantParameter = true;
|
||||||
|
protected bool $tenantParameterBeforePrefix = true;
|
||||||
protected string|null $domain = null;
|
protected string|null $domain = null;
|
||||||
protected Closure|null $cloneUsing = null; // The callback should accept Route instance or the route name (string)
|
protected Closure|null $cloneUsing = null; // The callback should accept Route instance or the route name (string)
|
||||||
protected Closure|null $shouldClone = null;
|
protected Closure|null $shouldClone = null;
|
||||||
|
|
@ -177,6 +178,13 @@ class CloneRoutesAsTenant
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tenantParameterBeforePrefix(bool $tenantParameterBeforePrefix): static
|
||||||
|
{
|
||||||
|
$this->tenantParameterBeforePrefix = $tenantParameterBeforePrefix;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/** Clone an individual route. */
|
/** Clone an individual route. */
|
||||||
public function cloneRoute(Route|string $route): static
|
public function cloneRoute(Route|string $route): static
|
||||||
{
|
{
|
||||||
|
|
@ -226,7 +234,13 @@ class CloneRoutesAsTenant
|
||||||
$action->put('middleware', $middleware);
|
$action->put('middleware', $middleware);
|
||||||
|
|
||||||
if ($this->addTenantParameter) {
|
if ($this->addTenantParameter) {
|
||||||
$action->put('prefix', $prefix . '/{' . PathTenantResolver::tenantParameterName() . '}');
|
$tenantParameter = '{' . PathTenantResolver::tenantParameterName() . '}';
|
||||||
|
|
||||||
|
$newPrefix = $this->tenantParameterBeforePrefix
|
||||||
|
? $tenantParameter . '/' . $prefix
|
||||||
|
: $prefix . '/' . $tenantParameter;
|
||||||
|
|
||||||
|
$action->put('prefix', $newPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var Route $newRoute */
|
/** @var Route $newRoute */
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
|
||||||
throw new Exception('The template connection must NOT have URL defined. Specify the connection using individual parts instead of a database URL.');
|
throw new Exception('The template connection must NOT have URL defined. Specify the connection using individual parts instead of a database URL.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Better debugging, but breaks cached lookup in prod
|
// Better debugging, but breaks cached lookup, so we disable this in prod
|
||||||
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
|
if (app()->environment('local') || app()->environment('testing')) {
|
||||||
$database = $tenant->database()->getName();
|
$database = $tenant->database()->getName();
|
||||||
if (! $tenant->database()->manager()->databaseExists($database)) { // todo@samuel does this call correctly use the host connection?
|
if (! $tenant->database()->manager()->databaseExists($database)) {
|
||||||
throw new TenantDatabaseDoesNotExistException($database);
|
throw new TenantDatabaseDoesNotExistException($database);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
src/Bootstrappers/TenantConfigBootstrapper.php
Normal file
54
src/Bootstrappers/TenantConfigBootstrapper.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Bootstrappers;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
class TenantConfigBootstrapper implements TenancyBootstrapper
|
||||||
|
{
|
||||||
|
public array $originalConfig = [];
|
||||||
|
|
||||||
|
/** @var array<string, string|array> */
|
||||||
|
public static array $storageToConfigMap = [
|
||||||
|
// 'paypal_api_key' => 'services.paypal.api_key',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected Repository $config,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function bootstrap(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
foreach (static::$storageToConfigMap as $storageKey => $configKey) {
|
||||||
|
/** @var Tenant&Model $tenant */
|
||||||
|
$override = Arr::get($tenant, $storageKey);
|
||||||
|
|
||||||
|
if (! is_null($override)) {
|
||||||
|
if (is_array($configKey)) {
|
||||||
|
foreach ($configKey as $key) {
|
||||||
|
$this->originalConfig[$key] = $this->originalConfig[$key] ?? $this->config->get($key);
|
||||||
|
|
||||||
|
$this->config->set($key, $override);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->originalConfig[$configKey] = $this->originalConfig[$configKey] ?? $this->config->get($configKey);
|
||||||
|
|
||||||
|
$this->config->set($configKey, $override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revert(): void
|
||||||
|
{
|
||||||
|
foreach ($this->originalConfig as $key => $value) {
|
||||||
|
$this->config->set($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ use Illuminate\Console\Command;
|
||||||
|
|
||||||
class CreatePendingTenants extends Command
|
class CreatePendingTenants extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'tenants:pending-create {--count= : The number of pending tenants to be created}';
|
protected $signature = 'tenants:pending-create {--count= : The number of pending tenants to maintain}';
|
||||||
|
|
||||||
protected $description = 'Create pending tenants.';
|
protected $description = 'Create pending tenants.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,19 @@ class CreateUserWithRLSPolicies extends Command
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $password,
|
string $password,
|
||||||
): DatabaseConfig {
|
): DatabaseConfig {
|
||||||
|
// This is a bit of a hack. We want to use our existing createUser() logic.
|
||||||
|
// That logic needs a DatabaseConfig instance. However, we aren't really working
|
||||||
|
// with any specific tenant here. We also *don't* want to use anything tenant-specific
|
||||||
|
// here. We are creating the SHARED "RLS user". Therefore, we need a custom DatabaseConfig
|
||||||
|
// instance for this purpose. The easiest way to do that is to grab an empty Tenant model
|
||||||
|
// (we use TenantWithDatabase in RLS) and manually create the host connection, just like
|
||||||
|
// DatabaseConfig::manager() would. We don't call that method since we want to use our existing
|
||||||
|
// PermissionControlledPostgreSQLSchemaManager $manager instance, rather than the "tenant's manager".
|
||||||
|
|
||||||
/** @var TenantWithDatabase $tenantModel */
|
/** @var TenantWithDatabase $tenantModel */
|
||||||
$tenantModel = tenancy()->model();
|
$tenantModel = tenancy()->model();
|
||||||
|
|
||||||
// Use a temporary DatabaseConfig instance to set the host connection
|
|
||||||
$temporaryDbConfig = $tenantModel->database();
|
$temporaryDbConfig = $tenantModel->database();
|
||||||
|
|
||||||
$temporaryDbConfig->purgeHostConnection();
|
$temporaryDbConfig->purgeHostConnection();
|
||||||
|
|
||||||
$tenantHostConnectionName = $temporaryDbConfig->getTenantHostConnectionName();
|
$tenantHostConnectionName = $temporaryDbConfig->getTenantHostConnectionName();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Commands;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
|
||||||
class Install extends Command
|
class Install extends Command
|
||||||
{
|
{
|
||||||
|
|
@ -128,14 +129,27 @@ class Install extends Command
|
||||||
public function askForSupport(): void
|
public function askForSupport(): void
|
||||||
{
|
{
|
||||||
if ($this->components->confirm('Would you like to show your support by starring the project on GitHub?', true)) {
|
if ($this->components->confirm('Would you like to show your support by starring the project on GitHub?', true)) {
|
||||||
if (PHP_OS_FAMILY === 'Darwin') {
|
$ghVersion = Process::run('gh --version');
|
||||||
exec('open https://github.com/archtechx/tenancy');
|
$starred = false;
|
||||||
|
|
||||||
|
// Make sure the `gh` binary is the actual GitHub CLI and not an unrelated tool
|
||||||
|
if ($ghVersion->successful() && str_contains($ghVersion->output(), 'https://github.com/cli/cli')) {
|
||||||
|
$starRequest = Process::run('gh api -X PUT user/starred/archtechx/tenancy');
|
||||||
|
$starred = $starRequest->successful();
|
||||||
}
|
}
|
||||||
if (PHP_OS_FAMILY === 'Windows') {
|
|
||||||
exec('start https://github.com/archtechx/tenancy');
|
if ($starred) {
|
||||||
}
|
$this->components->success('Repository starred via gh CLI, thank you!');
|
||||||
if (PHP_OS_FAMILY === 'Linux') {
|
} else {
|
||||||
exec('xdg-open https://github.com/archtechx/tenancy');
|
if (PHP_OS_FAMILY === 'Darwin') {
|
||||||
|
exec('open https://github.com/archtechx/tenancy');
|
||||||
|
}
|
||||||
|
if (PHP_OS_FAMILY === 'Windows') {
|
||||||
|
exec('start https://github.com/archtechx/tenancy');
|
||||||
|
}
|
||||||
|
if (PHP_OS_FAMILY === 'Linux') {
|
||||||
|
exec('xdg-open https://github.com/archtechx/tenancy');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Contracts;
|
namespace Stancl\Tenancy\Contracts;
|
||||||
|
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
/** Additional features, like Telescope tags and tenant redirects. */
|
/** Additional features, like Telescope tags and tenant redirects. */
|
||||||
interface Feature
|
interface Feature
|
||||||
{
|
{
|
||||||
public function bootstrap(Tenancy $tenancy): void;
|
public function bootstrap(): void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ trait HasDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($key === $this->internalPrefix() . 'db_connection') {
|
if ($key === $this->internalPrefix() . 'db_connection') {
|
||||||
// Remove DB connection because that's not used here
|
// Remove DB connection because that's not used for the connection *contents*.
|
||||||
|
// Instead the code uses getInternal('db_connection').
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,13 @@ namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Events\CreatingPendingTenant;
|
use Stancl\Tenancy\Events\CreatingPendingTenant;
|
||||||
use Stancl\Tenancy\Events\PendingTenantCreated;
|
use Stancl\Tenancy\Events\PendingTenantCreated;
|
||||||
use Stancl\Tenancy\Events\PendingTenantPulled;
|
use Stancl\Tenancy\Events\PendingTenantPulled;
|
||||||
use Stancl\Tenancy\Events\PullingPendingTenant;
|
use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||||
|
|
||||||
// todo consider adding a method that sets pending_since to null — to flag tenants as not-pending
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property ?Carbon $pending_since
|
* @property ?Carbon $pending_since
|
||||||
*
|
*
|
||||||
|
|
@ -50,46 +49,62 @@ trait HasPending
|
||||||
*/
|
*/
|
||||||
public static function createPending(array $attributes = []): Model&Tenant
|
public static function createPending(array $attributes = []): Model&Tenant
|
||||||
{
|
{
|
||||||
$tenant = static::create($attributes);
|
try {
|
||||||
|
$tenant = static::create($attributes);
|
||||||
event(new CreatingPendingTenant($tenant));
|
event(new CreatingPendingTenant($tenant));
|
||||||
|
} finally {
|
||||||
// Update the pending_since value only after the tenant is created so it's
|
// Update the pending_since value only after the tenant is created so it's
|
||||||
// Not marked as pending until finishing running the migrations, seeders, etc.
|
// not marked as pending until after migrations, seeders, etc are run.
|
||||||
$tenant->update([
|
$tenant->update([
|
||||||
'pending_since' => now()->timestamp,
|
'pending_since' => now()->timestamp,
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
event(new PendingTenantCreated($tenant));
|
event(new PendingTenantCreated($tenant));
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pull a pending tenant. */
|
/**
|
||||||
public static function pullPending(): Model&Tenant
|
* Pull a pending tenant from the pool or create a new one if the pool is empty.
|
||||||
|
*
|
||||||
|
* @param array $attributes The attributes to set on the tenant.
|
||||||
|
*/
|
||||||
|
public static function pullPending(array $attributes = []): Model&Tenant
|
||||||
{
|
{
|
||||||
/** @var Model&Tenant $pendingTenant */
|
/** @var Model&Tenant $pendingTenant */
|
||||||
$pendingTenant = static::pullPendingFromPool(true);
|
$pendingTenant = static::pullPendingFromPool(true, $attributes);
|
||||||
|
|
||||||
return $pendingTenant;
|
return $pendingTenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Try to pull a tenant from the pool of pending tenants. */
|
/**
|
||||||
public static function pullPendingFromPool(bool $firstOrCreate = true, array $attributes = []): ?Tenant
|
* Try to pull a tenant from the pool of pending tenants.
|
||||||
|
*
|
||||||
|
* @param bool $firstOrCreate If true, a tenant will be *created* if the pool is empty. Otherwise null is returned.
|
||||||
|
* @param array $attributes The attributes to set on the tenant.
|
||||||
|
*/
|
||||||
|
public static function pullPendingFromPool(bool $firstOrCreate = false, array $attributes = []): ?Tenant
|
||||||
{
|
{
|
||||||
/** @var (Model&Tenant)|null $tenant */
|
$tenant = DB::transaction(function () use ($attributes): ?Tenant {
|
||||||
$tenant = static::onlyPending()->first();
|
/** @var (Model&Tenant)|null $tenant */
|
||||||
|
$tenant = static::onlyPending()->first();
|
||||||
|
|
||||||
|
if ($tenant !== null) {
|
||||||
|
event(new PullingPendingTenant($tenant));
|
||||||
|
$tenant->update(array_merge($attributes, [
|
||||||
|
'pending_since' => null,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tenant;
|
||||||
|
});
|
||||||
|
|
||||||
if ($tenant === null) {
|
if ($tenant === null) {
|
||||||
return $firstOrCreate ? static::create($attributes) : null;
|
return $firstOrCreate ? static::create($attributes) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
event(new PullingPendingTenant($tenant));
|
// Only triggered if a tenant that was pulled from the pool is returned
|
||||||
|
|
||||||
$tenant->update(array_merge($attributes, [
|
|
||||||
'pending_since' => null,
|
|
||||||
]));
|
|
||||||
|
|
||||||
event(new PendingTenantPulled($tenant));
|
event(new PendingTenantPulled($tenant));
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase as Tenant;
|
||||||
use Stancl\Tenancy\Database\Exceptions\DatabaseManagerNotRegisteredException;
|
use Stancl\Tenancy\Database\Exceptions\DatabaseManagerNotRegisteredException;
|
||||||
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException;
|
||||||
|
|
||||||
// todo@dbRefactor refactor host connection logic to make customizing the host connection easier
|
|
||||||
class DatabaseConfig
|
class DatabaseConfig
|
||||||
{
|
{
|
||||||
/** The tenant whose database we're dealing with. */
|
/** The tenant whose database we're dealing with. */
|
||||||
|
|
@ -115,7 +114,7 @@ class DatabaseConfig
|
||||||
{
|
{
|
||||||
$this->tenant->setInternal('db_name', $this->getName());
|
$this->tenant->setInternal('db_name', $this->getName());
|
||||||
|
|
||||||
if ($this->connectionDriverManager($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
if ($this->managerForDriver($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||||
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant, $this));
|
$this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant, $this));
|
||||||
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant, $this));
|
$this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant, $this));
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +136,9 @@ class DatabaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($template = config('tenancy.database.template_tenant_connection')) {
|
if ($template = config('tenancy.database.template_tenant_connection')) {
|
||||||
return is_array($template) ? array_merge($this->getCentralConnection(), $template) : config("database.connections.{$template}");
|
return is_array($template)
|
||||||
|
? array_merge($this->getCentralConnection(), $template)
|
||||||
|
: config("database.connections.{$template}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getCentralConnection();
|
return $this->getCentralConnection();
|
||||||
|
|
@ -176,10 +177,10 @@ class DatabaseConfig
|
||||||
$config = $this->tenantConfig;
|
$config = $this->tenantConfig;
|
||||||
$templateConnection = $this->getTemplateConnection();
|
$templateConnection = $this->getTemplateConnection();
|
||||||
|
|
||||||
if ($this->connectionDriverManager($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
if ($this->managerForDriver($this->getTemplateConnectionDriver()) instanceof Contracts\ManagesDatabaseUsers) {
|
||||||
// We're removing the username and password because user with these credentials is not created yet
|
// We remove the username and password because the user with these credentials is not yet created.
|
||||||
// If you need to provide username and password when using PermissionControlledMySQLDatabaseManager,
|
// If you need to provide a username and a password when using a permission controlled database manager,
|
||||||
// consider creating a new connection and use it as `tenancy_db_connection` tenant config key
|
// consider creating a new connection and use it as `tenancy_db_connection`.
|
||||||
unset($config['username'], $config['password']);
|
unset($config['username'], $config['password']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +192,7 @@ class DatabaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purge the previous tenant connection before opening it for another tenant.
|
* Purge the previous host connection before opening it for another tenant.
|
||||||
*/
|
*/
|
||||||
public function purgeHostConnection(): void
|
public function purgeHostConnection(): void
|
||||||
{
|
{
|
||||||
|
|
@ -199,20 +200,20 @@ class DatabaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the TenantDatabaseManager for this tenant's connection.
|
* Get the TenantDatabaseManager for this tenant's host connection.
|
||||||
*
|
*
|
||||||
* @throws NoConnectionSetException|DatabaseManagerNotRegisteredException
|
* @throws NoConnectionSetException|DatabaseManagerNotRegisteredException
|
||||||
*/
|
*/
|
||||||
public function manager(): Contracts\TenantDatabaseManager
|
public function manager(): Contracts\TenantDatabaseManager
|
||||||
{
|
{
|
||||||
// Laravel caches the previous PDO connection, so we purge it to be able to change the connection details
|
// Laravel persists the PDO connection, so we purge it to be able to change the connection details
|
||||||
$this->purgeHostConnection();
|
$this->purgeHostConnection();
|
||||||
|
|
||||||
// Create the tenant host connection config
|
// Create the tenant host connection config
|
||||||
$tenantHostConnectionName = $this->getTenantHostConnectionName();
|
$tenantHostConnectionName = $this->getTenantHostConnectionName();
|
||||||
config(["database.connections.{$tenantHostConnectionName}" => $this->hostConnection()]);
|
config(["database.connections.{$tenantHostConnectionName}" => $this->hostConnection()]);
|
||||||
|
|
||||||
$manager = $this->connectionDriverManager(config("database.connections.{$tenantHostConnectionName}.driver"));
|
$manager = $this->managerForDriver(config("database.connections.{$tenantHostConnectionName}.driver"));
|
||||||
|
|
||||||
if ($manager instanceof Contracts\StatefulTenantDatabaseManager) {
|
if ($manager instanceof Contracts\StatefulTenantDatabaseManager) {
|
||||||
$manager->setConnection($tenantHostConnectionName);
|
$manager->setConnection($tenantHostConnectionName);
|
||||||
|
|
@ -222,12 +223,11 @@ class DatabaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* todo@name come up with a better name
|
* Get the TenantDatabaseManager for a given database driver.
|
||||||
* Get database manager class from the given connection config's driver.
|
|
||||||
*
|
*
|
||||||
* @throws DatabaseManagerNotRegisteredException
|
* @throws DatabaseManagerNotRegisteredException
|
||||||
*/
|
*/
|
||||||
protected function connectionDriverManager(string $driver): Contracts\TenantDatabaseManager
|
protected function managerForDriver(string $driver): Contracts\TenantDatabaseManager
|
||||||
{
|
{
|
||||||
$databaseManagers = config('tenancy.database.managers');
|
$databaseManagers = config('tenancy.database.managers');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
|
||||||
{
|
{
|
||||||
$database = $databaseConfig->getName();
|
$database = $databaseConfig->getName();
|
||||||
$username = $databaseConfig->getUsername();
|
$username = $databaseConfig->getUsername();
|
||||||
$hostname = $databaseConfig->connection()['host'];
|
|
||||||
$password = $databaseConfig->getPassword();
|
$password = $databaseConfig->getPassword();
|
||||||
|
|
||||||
$this->connection()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'");
|
$this->connection()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'");
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,6 @@ class PermissionControlledPostgreSQLSchemaManager extends PostgreSQLSchemaManage
|
||||||
$tables = $this->connection()->select("SELECT table_name FROM information_schema.tables WHERE table_schema = '{$schema}' AND table_type = 'BASE TABLE'");
|
$tables = $this->connection()->select("SELECT table_name FROM information_schema.tables WHERE table_schema = '{$schema}' AND table_type = 'BASE TABLE'");
|
||||||
|
|
||||||
// Grant permissions to any existing tables. This is used with RLS
|
// 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) {
|
foreach ($tables as $table) {
|
||||||
$tableName = $table->table_name;
|
$tableName = $table->table_name;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\TenantDatabaseManagers;
|
namespace Stancl\Tenancy\Database\TenantDatabaseManagers;
|
||||||
|
|
||||||
use AssertionError;
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
@ -15,17 +14,12 @@ use Throwable;
|
||||||
class SQLiteDatabaseManager implements TenantDatabaseManager
|
class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* SQLite Database path without ending slash.
|
* SQLite database directory path.
|
||||||
|
*
|
||||||
|
* Defaults to database_path().
|
||||||
*/
|
*/
|
||||||
public static string|null $path = null;
|
public static string|null $path = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Should the WAL journal mode be used for newly created databases.
|
|
||||||
*
|
|
||||||
* @see https://www.sqlite.org/pragma.html#pragma_journal_mode
|
|
||||||
*/
|
|
||||||
public static bool $WAL = true;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If this isn't null, a connection to the tenant DB will be created
|
* If this isn't null, a connection to the tenant DB will be created
|
||||||
* and passed to the provided closure, for the purpose of keeping the
|
* and passed to the provided closure, for the purpose of keeping the
|
||||||
|
|
@ -89,25 +83,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return file_put_contents($this->getPath($name), '') !== false;
|
||||||
if (file_put_contents($path = $this->getPath($name), '') === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (static::$WAL) {
|
|
||||||
$pdo = new PDO('sqlite:' . $path);
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
|
|
||||||
// @phpstan-ignore-next-line method.nonObject
|
|
||||||
assert($pdo->query('pragma journal_mode = wal')->fetch(PDO::FETCH_ASSOC)['journal_mode'] === 'wal', 'Unable to set journal mode to wal.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (AssertionError $e) {
|
|
||||||
throw $e;
|
|
||||||
} catch (Throwable) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
public function deleteDatabase(TenantWithDatabase $tenant): bool
|
||||||
|
|
@ -122,8 +98,16 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$path = $this->getPath($name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return unlink($this->getPath($name));
|
unlink($path . '-journal');
|
||||||
|
unlink($path . '-wal');
|
||||||
|
unlink($path . '-shm');
|
||||||
|
} catch (Throwable) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return unlink($path);
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -150,15 +134,10 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
|
||||||
return $baseConfig;
|
return $baseConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setConnection(string $connection): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPath(string $name): string
|
public function getPath(string $name): string
|
||||||
{
|
{
|
||||||
if (static::$path) {
|
if (static::$path) {
|
||||||
return static::$path . DIRECTORY_SEPARATOR . $name;
|
return rtrim(static::$path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return database_path($name);
|
return database_path($name);
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Events;
|
namespace Stancl\Tenancy\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importantly, listeners for this event should not switch tenancy context.
|
||||||
|
*
|
||||||
|
* This event is fired from within a database transaction.
|
||||||
|
*/
|
||||||
class PullingPendingTenant extends Contracts\TenantEvent {}
|
class PullingPendingTenant extends Contracts\TenantEvent {}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,10 @@ namespace Stancl\Tenancy\Features;
|
||||||
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Stancl\Tenancy\Contracts\Feature;
|
use Stancl\Tenancy\Contracts\Feature;
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
class CrossDomainRedirect implements Feature
|
class CrossDomainRedirect implements Feature
|
||||||
{
|
{
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
public function bootstrap(): void
|
||||||
{
|
{
|
||||||
RedirectResponse::macro('domain', function (string $domain) {
|
RedirectResponse::macro('domain', function (string $domain) {
|
||||||
/** @var RedirectResponse $this */
|
/** @var RedirectResponse $this */
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,17 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Features;
|
namespace Stancl\Tenancy\Features;
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Database\Connectors\ConnectionFactory;
|
use Illuminate\Database\Connectors\ConnectionFactory;
|
||||||
use Illuminate\Database\SQLiteConnection;
|
use Illuminate\Database\SQLiteConnection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use PDO;
|
use PDO;
|
||||||
use Stancl\Tenancy\Contracts\Feature;
|
use Stancl\Tenancy\Contracts\Feature;
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
class DisallowSqliteAttach implements Feature
|
class DisallowSqliteAttach implements Feature
|
||||||
{
|
{
|
||||||
protected static bool|null $loadExtensionSupported = null;
|
|
||||||
public static string|false|null $extensionPath = null;
|
public static string|false|null $extensionPath = null;
|
||||||
|
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
public function bootstrap(): void
|
||||||
{
|
{
|
||||||
// Handle any already resolved connections
|
// Handle any already resolved connections
|
||||||
foreach (DB::getConnections() as $connection) {
|
foreach (DB::getConnections() as $connection) {
|
||||||
|
|
@ -39,31 +36,29 @@ class DisallowSqliteAttach implements Feature
|
||||||
|
|
||||||
protected function loadExtension(PDO $pdo): bool
|
protected function loadExtension(PDO $pdo): bool
|
||||||
{
|
{
|
||||||
if (static::$loadExtensionSupported === null) {
|
// todo@php85 In PHP 8.5, we can use setAuthorizer() instead of loading an extension.
|
||||||
static::$loadExtensionSupported = method_exists($pdo, 'loadExtension');
|
// However, this is currently blocked on https://github.com/phpredis/phpredis/issues/2688
|
||||||
}
|
static $loadExtensionSupported = method_exists($pdo, 'loadExtension');
|
||||||
|
|
||||||
if (static::$loadExtensionSupported === false) {
|
if ((! $loadExtensionSupported) ||
|
||||||
return false;
|
(static::$extensionPath === false) ||
|
||||||
}
|
(PHP_INT_SIZE !== 8)
|
||||||
if (static::$extensionPath === false) {
|
) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$suffix = match (PHP_OS_FAMILY) {
|
$suffix = match (PHP_OS_FAMILY) {
|
||||||
'Linux' => 'so',
|
'Linux' => 'so',
|
||||||
'Windows' => 'dll',
|
'Windows' => 'dll',
|
||||||
'Darwin' => 'dylib',
|
'Darwin' => 'dylib',
|
||||||
default => throw new Exception("The DisallowSqliteAttach feature doesn't support your operating system: " . PHP_OS_FAMILY),
|
default => 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($suffix === 'error') return false;
|
||||||
|
|
||||||
$arch = php_uname('m');
|
$arch = php_uname('m');
|
||||||
$arm = $arch === 'aarch64' || $arch === 'arm64';
|
$arm = $arch === 'aarch64' || $arch === 'arm64';
|
||||||
|
|
||||||
static::$extensionPath ??= realpath(base_path('vendor/stancl/tenancy/extensions/lib/' . ($arm ? 'arm/' : '') . 'noattach.' . $suffix));
|
static::$extensionPath ??= realpath(base_path('vendor/stancl/tenancy/extensions/lib/' . ($arm ? 'arm/' : '') . 'noattach.' . $suffix));
|
||||||
if (static::$extensionPath === false) {
|
if (static::$extensionPath === false) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->loadExtension(static::$extensionPath); // @phpstan-ignore method.notFound
|
$pdo->loadExtension(static::$extensionPath); // @phpstan-ignore method.notFound
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,10 @@ namespace Stancl\Tenancy\Features;
|
||||||
use Laravel\Telescope\IncomingEntry;
|
use Laravel\Telescope\IncomingEntry;
|
||||||
use Laravel\Telescope\Telescope;
|
use Laravel\Telescope\Telescope;
|
||||||
use Stancl\Tenancy\Contracts\Feature;
|
use Stancl\Tenancy\Contracts\Feature;
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
class TelescopeTags implements Feature
|
class TelescopeTags implements Feature
|
||||||
{
|
{
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
public function bootstrap(): void
|
||||||
{
|
{
|
||||||
if (! class_exists(Telescope::class)) {
|
if (! class_exists(Telescope::class)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ use Stancl\Tenancy\Contracts\Feature;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Events\RevertedToCentralContext;
|
use Stancl\Tenancy\Events\RevertedToCentralContext;
|
||||||
use Stancl\Tenancy\Events\TenancyBootstrapped;
|
use Stancl\Tenancy\Events\TenancyBootstrapped;
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
|
// todo@release remove this class
|
||||||
|
|
||||||
|
/** @deprecated Use the TenantConfigBootstrapper instead. */
|
||||||
class TenantConfig implements Feature
|
class TenantConfig implements Feature
|
||||||
{
|
{
|
||||||
public array $originalConfig = [];
|
public array $originalConfig = [];
|
||||||
|
|
@ -27,7 +29,7 @@ class TenantConfig implements Feature
|
||||||
protected Repository $config,
|
protected Repository $config,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
public function bootstrap(): void
|
||||||
{
|
{
|
||||||
Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) {
|
Event::listen(TenancyBootstrapped::class, function (TenancyBootstrapped $event) {
|
||||||
/** @var Tenant $tenant */
|
/** @var Tenant $tenant */
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ class UserImpersonation implements Feature
|
||||||
/** The lifespan of impersonation tokens (in seconds). */
|
/** The lifespan of impersonation tokens (in seconds). */
|
||||||
public static int $ttl = 60;
|
public static int $ttl = 60;
|
||||||
|
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
public function bootstrap(): void
|
||||||
{
|
{
|
||||||
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): Model {
|
Tenancy::macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): Model {
|
||||||
return UserImpersonation::modelClass()::create([
|
return UserImpersonation::modelClass()::create([
|
||||||
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
|
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,14 @@ namespace Stancl\Tenancy\Features;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Facades\Vite;
|
use Illuminate\Support\Facades\Vite;
|
||||||
use Stancl\Tenancy\Contracts\Feature;
|
use Stancl\Tenancy\Contracts\Feature;
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
class ViteBundler implements Feature
|
class ViteBundler implements Feature
|
||||||
{
|
{
|
||||||
/** @var Application */
|
public function __construct(
|
||||||
protected $app;
|
protected Application $app,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function __construct(Application $app)
|
public function bootstrap(): void
|
||||||
{
|
|
||||||
$this->app = $app;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
|
||||||
{
|
{
|
||||||
Vite::createAssetPathsUsing(function ($path, $secure = null) {
|
Vite::createAssetPathsUsing(function ($path, $secure = null) {
|
||||||
return global_asset($path);
|
return global_asset($path);
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,11 @@ class PreventAccessFromUnwantedDomains
|
||||||
return in_array($request->getHost(), config('tenancy.identification.central_domains'), true);
|
return in_array($request->getHost(), config('tenancy.identification.central_domains'), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo@samuel technically not an identification middleware but probably ok to keep this here
|
|
||||||
public function requestHasTenant(Request $request): bool
|
public function requestHasTenant(Request $request): bool
|
||||||
{
|
{
|
||||||
|
// This middleware is special in that it's not an identification middleware
|
||||||
|
// but still uses some logic from UsableWithEarlyIdentification, so we just
|
||||||
|
// need to implement this method here. It doesn't matter what it returns.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ class TenancyUrlGenerator extends UrlGenerator
|
||||||
*/
|
*/
|
||||||
public function route($name, $parameters = [], $absolute = true)
|
public function route($name, $parameters = [], $absolute = true)
|
||||||
{
|
{
|
||||||
if ($name instanceof BackedEnum && ! is_string($name = $name->value)) { // @phpstan-ignore function.impossibleType
|
if ($name instanceof BackedEnum && ! is_string($name = $name->value)) {
|
||||||
throw new InvalidArgumentException('Attribute [name] expects a string backed enum.');
|
throw new InvalidArgumentException('Attribute [name] expects a string backed enum.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,7 +125,7 @@ class TenancyUrlGenerator extends UrlGenerator
|
||||||
*/
|
*/
|
||||||
public function temporarySignedRoute($name, $expiration, $parameters = [], $absolute = true)
|
public function temporarySignedRoute($name, $expiration, $parameters = [], $absolute = true)
|
||||||
{
|
{
|
||||||
if ($name instanceof BackedEnum && ! is_string($name = $name->value)) { // @phpstan-ignore function.impossibleType
|
if ($name instanceof BackedEnum && ! is_string($name = $name->value)) {
|
||||||
throw new InvalidArgumentException('Attribute [name] expects a string backed enum.');
|
throw new InvalidArgumentException('Attribute [name] expects a string backed enum.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use Illuminate\Foundation\Bus\PendingDispatch;
|
||||||
use Illuminate\Support\Traits\Macroable;
|
use Illuminate\Support\Traits\Macroable;
|
||||||
use Stancl\Tenancy\Concerns\DealsWithRouteContexts;
|
use Stancl\Tenancy\Concerns\DealsWithRouteContexts;
|
||||||
use Stancl\Tenancy\Concerns\ManagesRLSPolicies;
|
use Stancl\Tenancy\Concerns\ManagesRLSPolicies;
|
||||||
|
use Stancl\Tenancy\Contracts\Feature;
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
|
||||||
|
|
@ -40,7 +41,7 @@ class Tenancy
|
||||||
public static array $findWith = [];
|
public static array $findWith = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of bootstrappers that have been initialized.
|
* List of bootstrappers that have been initialized.
|
||||||
*
|
*
|
||||||
* This is used when reverting tenancy, mainly if an exception
|
* This is used when reverting tenancy, mainly if an exception
|
||||||
* occurs during bootstrapping, to ensure we don't revert
|
* occurs during bootstrapping, to ensure we don't revert
|
||||||
|
|
@ -53,6 +54,23 @@ class Tenancy
|
||||||
*/
|
*/
|
||||||
public array $initializedBootstrappers = [];
|
public array $initializedBootstrappers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of features that have been bootstrapped.
|
||||||
|
*
|
||||||
|
* Since features may be bootstrapped multiple times during
|
||||||
|
* the request cycle (in TSP::boot() and any other times the user calls
|
||||||
|
* bootstrapFeatures()), we keep track of which features have already
|
||||||
|
* been bootstrapped so we do not bootstrap them again. Features are
|
||||||
|
* bootstrapped once and irreversible.
|
||||||
|
*
|
||||||
|
* The main point of this is that some features *need* to be bootstrapped
|
||||||
|
* very early (see #949), so we bootstrap them directly in TSP, but we
|
||||||
|
* also need the ability to *change* which features are used at runtime
|
||||||
|
* (mainly tests of this package) and bootstrap features again after making
|
||||||
|
* changes to config('tenancy.features').
|
||||||
|
*/
|
||||||
|
protected array $bootstrappedFeatures = [];
|
||||||
|
|
||||||
/** Initialize tenancy for the passed tenant. */
|
/** Initialize tenancy for the passed tenant. */
|
||||||
public function initialize(Tenant|int|string $tenant): void
|
public function initialize(Tenant|int|string $tenant): void
|
||||||
{
|
{
|
||||||
|
|
@ -136,7 +154,7 @@ class Tenancy
|
||||||
public function getBootstrappers(): array
|
public function getBootstrappers(): array
|
||||||
{
|
{
|
||||||
// If no callback for getting bootstrappers is set, we return the ones in config.
|
// If no callback for getting bootstrappers is set, we return the ones in config.
|
||||||
$resolve = $this->getBootstrappersUsing ?? function (Tenant $tenant) {
|
$resolve = $this->getBootstrappersUsing ?? function (?Tenant $tenant) {
|
||||||
return config('tenancy.bootstrappers');
|
return config('tenancy.bootstrappers');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -154,6 +172,26 @@ class Tenancy
|
||||||
return in_array($bootstrapper, static::getBootstrappers(), true);
|
return in_array($bootstrapper, static::getBootstrappers(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap configured Tenancy features.
|
||||||
|
*
|
||||||
|
* Normally, features are bootstrapped directly in TSP::boot(). However, if
|
||||||
|
* new features are enabled at runtime (e.g. during tests), this method may
|
||||||
|
* be called to bootstrap new features. It's idempotent and keeps track of
|
||||||
|
* which features have already been bootstrapped. Keep in mind that feature
|
||||||
|
* bootstrapping is irreversible.
|
||||||
|
*/
|
||||||
|
public function bootstrapFeatures(): void
|
||||||
|
{
|
||||||
|
foreach (config('tenancy.features') ?? [] as $feature) {
|
||||||
|
/** @var class-string<Feature> $feature */
|
||||||
|
if (! in_array($feature, $this->bootstrappedFeatures)) {
|
||||||
|
app($feature)->bootstrap();
|
||||||
|
$this->bootstrappedFeatures[] = $feature;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Builder<Tenant&Model>
|
* @return Builder<Tenant&Model>
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -40,15 +40,6 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
// Make sure Tenancy is stateful.
|
// Make sure Tenancy is stateful.
|
||||||
$this->app->singleton(Tenancy::class);
|
$this->app->singleton(Tenancy::class);
|
||||||
|
|
||||||
// Make sure features are bootstrapped as soon as Tenancy is instantiated.
|
|
||||||
$this->app->extend(Tenancy::class, function (Tenancy $tenancy) {
|
|
||||||
foreach ($this->app['config']['tenancy.features'] ?? [] as $feature) {
|
|
||||||
$this->app[$feature]->bootstrap($tenancy);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $tenancy;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make it possible to inject the current tenant by type hinting the Tenant contract.
|
// Make it possible to inject the current tenant by type hinting the Tenant contract.
|
||||||
$this->app->bind(Tenant::class, function ($app) {
|
$this->app->bind(Tenant::class, function ($app) {
|
||||||
return $app[Tenancy::class]->tenant;
|
return $app[Tenancy::class]->tenant;
|
||||||
|
|
@ -176,6 +167,11 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
return $instance;
|
return $instance;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bootstrap features that are already enabled in the config.
|
||||||
|
// If more features are enabled at runtime, this method may be called
|
||||||
|
// multiple times, it keeps track of which features have already been bootstrapped.
|
||||||
|
$this->app->make(Tenancy::class)->bootstrapFeatures();
|
||||||
|
|
||||||
Route::middlewareGroup('clone', []);
|
Route::middlewareGroup('clone', []);
|
||||||
Route::middlewareGroup('universal', []);
|
Route::middlewareGroup('universal', []);
|
||||||
Route::middlewareGroup('tenant', []);
|
Route::middlewareGroup('tenant', []);
|
||||||
|
|
|
||||||
103
static_properties.nu
Executable file
103
static_properties.nu
Executable file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env nu
|
||||||
|
|
||||||
|
# Utility for exporting static properties used for configuration
|
||||||
|
def main []: nothing -> string {
|
||||||
|
"See --help for subcommands"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The current number of config static properties in the codebase
|
||||||
|
def "main count" [...paths: string]: nothing -> int {
|
||||||
|
props ...$paths | length
|
||||||
|
}
|
||||||
|
|
||||||
|
# Available static properties, grouped by file, rendered as a table
|
||||||
|
def "main table" [...paths: string]: nothing -> string {
|
||||||
|
props ...$paths | table --theme rounded --expand
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plain text version of available static properties
|
||||||
|
def "main plain" [...paths: string]: nothing -> string {
|
||||||
|
props ...$paths
|
||||||
|
| each { $"// File: ($in.file)\n($in.props | str join "\n\n")"}
|
||||||
|
| str join "\n//------------------------------------------------------------\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Expressive Code formatting of available static properties, used in docs
|
||||||
|
def "main docs" [...paths: string]: nothing -> string {
|
||||||
|
(("{/* GENERATED_BEGIN */}\n" + (props ...$paths
|
||||||
|
| each { update props { each { if ($in | str ends-with "= [") {
|
||||||
|
$"($in)/* ... */];"
|
||||||
|
} else { $in }}}}
|
||||||
|
| each { $"```php /public static .*$/\n// File: ($in.file)\n($in.props | str join "\n\n")\n```"}
|
||||||
|
| str join "\n\n"))
|
||||||
|
+ "\n{/* GENERATED_END */}")
|
||||||
|
}
|
||||||
|
|
||||||
|
def props [...paths: string]: nothing -> table<file: string, props: list<string>> {
|
||||||
|
ls ...(if ($paths | length) > 0 {
|
||||||
|
($paths | each {|path|
|
||||||
|
if ($path | str contains "*") {
|
||||||
|
# already a glob expr
|
||||||
|
$path | into glob
|
||||||
|
} else if ($path | str ends-with ".php") {
|
||||||
|
# src/Foo/Bar.php
|
||||||
|
$path
|
||||||
|
} else {
|
||||||
|
# just 'src/Foo' passed
|
||||||
|
$"($path)/**/*.php" | into glob
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
[("src/**/*.php" | into glob)]
|
||||||
|
})
|
||||||
|
| each { { name: $in.name, content: (open $in.name) } }
|
||||||
|
| find -nr 'public static (?!.*function)'
|
||||||
|
| par-each {|file|
|
||||||
|
let lines = $file.content | lines
|
||||||
|
mut docblock_start = 0
|
||||||
|
mut docblock_end = 0
|
||||||
|
mut props = []
|
||||||
|
for line in ($lines | enumerate) {
|
||||||
|
if ($line.item | str contains "/**") {
|
||||||
|
$docblock_start = $line.index
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line.item | str contains "@internal") {
|
||||||
|
# Docblocks with @internal are ignored
|
||||||
|
$docblock_start = 0
|
||||||
|
$docblock_end = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line.item | str contains "*/") {
|
||||||
|
$docblock_end = $line.index
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
( # Valid (non-internal) docblock
|
||||||
|
$docblock_start != 0 and
|
||||||
|
$docblock_end != 0 and
|
||||||
|
$docblock_end == ($line.index - 1)
|
||||||
|
) or
|
||||||
|
( # No docblock
|
||||||
|
$line.index != 0 and
|
||||||
|
(($lines | get ($line.index - 1)) | str index-of "*/") == -1
|
||||||
|
)
|
||||||
|
) and
|
||||||
|
($line.item | str trim | str index-of "public static") == 0 and
|
||||||
|
($line.item | str trim | str index-of "public static function") == -1
|
||||||
|
) {
|
||||||
|
if ($docblock_start == 0) or ($docblock_end == 0) or ($docblock_end != ($line.index - 1)) {
|
||||||
|
$docblock_start = $line.index
|
||||||
|
$docblock_end = $line.index
|
||||||
|
}
|
||||||
|
$props = $props | append ($lines | slice $docblock_start..$line.index | each { str trim } | str join "\n")
|
||||||
|
$docblock_start = 0
|
||||||
|
$docblock_end = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{file: $file.name, props: $props}
|
||||||
|
}
|
||||||
|
| where ($it.props | length) > 0
|
||||||
|
}
|
||||||
2
t
2
t
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
if [[ "${CLAUDECODE}" != "1" ]]; then
|
if [[ "${CLAUDECODE}" != "1" ]]; then
|
||||||
COLOR_FLAG="--colors=always"
|
COLOR_FLAG="--colors=always"
|
||||||
|
|
|
||||||
2
test
2
test
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
if [[ "${CLAUDECODE}" != "1" ]]; then
|
if [[ "${CLAUDECODE}" != "1" ]]; then
|
||||||
COLOR_FLAG="--colors=always"
|
COLOR_FLAG="--colors=always"
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,6 @@ test('files can get fetched using the storage url', function() {
|
||||||
test('storage_path helper does not change if suffix_storage_path is off', function() {
|
test('storage_path helper does not change if suffix_storage_path is off', function() {
|
||||||
$originalStoragePath = storage_path();
|
$originalStoragePath = storage_path();
|
||||||
|
|
||||||
// todo@tests https://github.com/tenancy-for-laravel/v4/pull/44#issue-2228530362
|
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
||||||
'tenancy.filesystem.suffix_storage_path' => false,
|
'tenancy.filesystem.suffix_storage_path' => false,
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ test('the clone action can clone specific routes either using name or route inst
|
||||||
false,
|
false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('the clone action prefixes already prefixed routes correctly', function () {
|
test('the clone action prefixes already prefixed routes correctly', function (bool $tenantParameterBeforePrefix) {
|
||||||
$routes = [
|
$routes = [
|
||||||
RouteFacade::get('/home', fn () => true)
|
RouteFacade::get('/home', fn () => true)
|
||||||
->middleware(['clone'])
|
->middleware(['clone'])
|
||||||
|
|
@ -195,7 +195,12 @@ test('the clone action prefixes already prefixed routes correctly', function ()
|
||||||
->prefix('prefix/'),
|
->prefix('prefix/'),
|
||||||
];
|
];
|
||||||
|
|
||||||
app(CloneRoutesAsTenant::class)->handle();
|
$cloneAction = app(CloneRoutesAsTenant::class);
|
||||||
|
$cloneAction
|
||||||
|
->tenantParameterBeforePrefix($tenantParameterBeforePrefix)
|
||||||
|
->handle();
|
||||||
|
|
||||||
|
$expectedPrefix = $tenantParameterBeforePrefix ? '{tenant}/prefix' : 'prefix/{tenant}';
|
||||||
|
|
||||||
$clonedRoutes = [
|
$clonedRoutes = [
|
||||||
RouteFacade::getRoutes()->getByName('tenant.home'),
|
RouteFacade::getRoutes()->getByName('tenant.home'),
|
||||||
|
|
@ -206,9 +211,10 @@ test('the clone action prefixes already prefixed routes correctly', function ()
|
||||||
|
|
||||||
// The cloned route is prefixed correctly
|
// The cloned route is prefixed correctly
|
||||||
foreach ($clonedRoutes as $key => $route) {
|
foreach ($clonedRoutes as $key => $route) {
|
||||||
expect($route->getPrefix())->toBe("prefix/{tenant}");
|
expect($route->getPrefix())->toBe($expectedPrefix);
|
||||||
|
|
||||||
$clonedRouteUrl = route($route->getName(), ['tenant' => $tenant = Tenant::create()]);
|
$clonedRouteUrl = route($route->getName(), ['tenant' => $tenant = Tenant::create()]);
|
||||||
|
$expectedPrefixInUrl = $tenantParameterBeforePrefix ? "{$tenant->id}/prefix" : "prefix/{$tenant->id}";
|
||||||
|
|
||||||
expect($clonedRouteUrl)
|
expect($clonedRouteUrl)
|
||||||
// Original prefix does not occur in the cloned route's URL
|
// Original prefix does not occur in the cloned route's URL
|
||||||
|
|
@ -216,14 +222,14 @@ test('the clone action prefixes already prefixed routes correctly', function ()
|
||||||
->not()->toContain("//prefix")
|
->not()->toContain("//prefix")
|
||||||
->not()->toContain("prefix//")
|
->not()->toContain("prefix//")
|
||||||
// Instead, the route is prefixed correctly
|
// Instead, the route is prefixed correctly
|
||||||
->toBe("http://localhost/prefix/{$tenant->id}/{$routes[$key]->getName()}");
|
->toBe("http://localhost/{$expectedPrefixInUrl}/{$routes[$key]->getName()}");
|
||||||
|
|
||||||
// The cloned route is accessible
|
// The cloned route is accessible
|
||||||
pest()->get($clonedRouteUrl)->assertOk();
|
pest()->get($clonedRouteUrl)->assertOk();
|
||||||
}
|
}
|
||||||
});
|
})->with([true, false]);
|
||||||
|
|
||||||
test('clone action trims trailing slashes from prefixes given to nested route groups', function () {
|
test('clone action trims trailing slashes from prefixes given to nested route groups', function (bool $tenantParameterBeforePrefix) {
|
||||||
RouteFacade::prefix('prefix')->group(function () {
|
RouteFacade::prefix('prefix')->group(function () {
|
||||||
RouteFacade::prefix('')->group(function () {
|
RouteFacade::prefix('')->group(function () {
|
||||||
// This issue seems to only happen when there's a group with a prefix, then a group with an empty prefix, and then a / route
|
// This issue seems to only happen when there's a group with a prefix, then a group with an empty prefix, and then a / route
|
||||||
|
|
@ -237,7 +243,10 @@ test('clone action trims trailing slashes from prefixes given to nested route gr
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app(CloneRoutesAsTenant::class)->handle();
|
$cloneAction = app(CloneRoutesAsTenant::class);
|
||||||
|
$cloneAction
|
||||||
|
->tenantParameterBeforePrefix($tenantParameterBeforePrefix)
|
||||||
|
->handle();
|
||||||
|
|
||||||
$clonedLandingUrl = route('tenant.landing', ['tenant' => $tenant = Tenant::create()]);
|
$clonedLandingUrl = route('tenant.landing', ['tenant' => $tenant = Tenant::create()]);
|
||||||
$clonedHomeRouteUrl = route('tenant.home', ['tenant' => $tenant]);
|
$clonedHomeRouteUrl = route('tenant.home', ['tenant' => $tenant]);
|
||||||
|
|
@ -245,17 +254,20 @@ test('clone action trims trailing slashes from prefixes given to nested route gr
|
||||||
$landingRoute = RouteFacade::getRoutes()->getByName('tenant.landing');
|
$landingRoute = RouteFacade::getRoutes()->getByName('tenant.landing');
|
||||||
$homeRoute = RouteFacade::getRoutes()->getByName('tenant.home');
|
$homeRoute = RouteFacade::getRoutes()->getByName('tenant.home');
|
||||||
|
|
||||||
expect($landingRoute->uri())->toBe('prefix/{tenant}');
|
$expectedPrefix = $tenantParameterBeforePrefix ? '{tenant}/prefix' : 'prefix/{tenant}';
|
||||||
expect($homeRoute->uri())->toBe('prefix/{tenant}/home');
|
$expectedPrefixInUrl = $tenantParameterBeforePrefix ? "{$tenant->id}/prefix" : "prefix/{$tenant->id}";
|
||||||
|
|
||||||
|
expect($landingRoute->uri())->toBe($expectedPrefix);
|
||||||
|
expect($homeRoute->uri())->toBe("{$expectedPrefix}/home");
|
||||||
|
|
||||||
expect($clonedLandingUrl)
|
expect($clonedLandingUrl)
|
||||||
->not()->toContain("prefix//")
|
->not()->toContain("prefix//")
|
||||||
->toBe("http://localhost/prefix/{$tenant->id}");
|
->toBe("http://localhost/{$expectedPrefixInUrl}");
|
||||||
|
|
||||||
expect($clonedHomeRouteUrl)
|
expect($clonedHomeRouteUrl)
|
||||||
->not()->toContain("prefix//")
|
->not()->toContain("prefix//")
|
||||||
->toBe("http://localhost/prefix/{$tenant->id}/home");
|
->toBe("http://localhost/{$expectedPrefixInUrl}/home");
|
||||||
});
|
})->with([true, false]);
|
||||||
|
|
||||||
test('tenant routes are ignored from cloning and clone middleware in groups causes no issues', function () {
|
test('tenant routes are ignored from cloning and clone middleware in groups causes no issues', function () {
|
||||||
// Should NOT be cloned, already has tenant parameter
|
// Should NOT be cloned, already has tenant parameter
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Illuminate\Contracts\Http\Kernel;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
|
|
@ -120,7 +121,7 @@ test('early identification works with path identification', function (bool $useK
|
||||||
RouteFacade::get('/{post}/comment/{comment}/edit', [$controller, 'computePost']);
|
RouteFacade::get('/{post}/comment/{comment}/edit', [$controller, 'computePost']);
|
||||||
});
|
});
|
||||||
|
|
||||||
$tenant = Tenant::create(['tenancy_db_name' => pest()->randomString()]);
|
$tenant = Tenant::create(['tenancy_db_name' => Str::random(10)]);
|
||||||
|
|
||||||
// Migrate users and comments tables on tenant connection
|
// Migrate users and comments tables on tenant connection
|
||||||
pest()->artisan('tenants:migrate', [
|
pest()->artisan('tenants:migrate', [
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ test('sqlite ATTACH statements can be blocked', function (bool $disallow) {
|
||||||
return json_encode(DB::select(request('q2')));
|
return json_encode(DB::select(request('q2')));
|
||||||
});
|
});
|
||||||
|
|
||||||
tenancy(); // trigger features: todo@samuel remove after feature refactor
|
tenancy()->bootstrapFeatures();
|
||||||
|
|
||||||
if ($disallow) {
|
if ($disallow) {
|
||||||
expect(fn () => pest()->post('/central-sqli', [
|
expect(fn () => pest()->post('/central-sqli', [
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ test('tenant redirect macro replaces only the hostname', function () {
|
||||||
'tenancy.features' => [CrossDomainRedirect::class],
|
'tenancy.features' => [CrossDomainRedirect::class],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
tenancy()->bootstrapFeatures();
|
||||||
|
|
||||||
Route::get('/foobar', function () {
|
Route::get('/foobar', function () {
|
||||||
return 'Foo';
|
return 'Foo';
|
||||||
})->name('home');
|
})->name('home');
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,27 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Event;
|
use Stancl\Tenancy\Bootstrappers\TenantConfigBootstrapper;
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
|
||||||
use Stancl\Tenancy\Features\TenantConfig;
|
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
use function Stancl\Tenancy\Tests\pest;
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use function Stancl\Tenancy\Tests\withBootstrapping;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [TenantConfigBootstrapper::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
withBootstrapping();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
TenantConfig::$storageToConfigMap = [];
|
TenantConfigBootstrapper::$storageToConfigMap = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
test('nested tenant values are merged', function () {
|
test('nested tenant values are merged', function () {
|
||||||
expect(config('whitelabel.theme'))->toBeNull();
|
expect(config('whitelabel.theme'))->toBeNull();
|
||||||
config([
|
|
||||||
'tenancy.features' => [TenantConfig::class],
|
|
||||||
'tenancy.bootstrappers' => [],
|
|
||||||
]);
|
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
|
||||||
|
|
||||||
TenantConfig::$storageToConfigMap = [
|
TenantConfigBootstrapper::$storageToConfigMap = [
|
||||||
'whitelabel.config.theme' => 'whitelabel.theme',
|
'whitelabel.config.theme' => 'whitelabel.theme',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -39,14 +37,8 @@ test('nested tenant values are merged', function () {
|
||||||
|
|
||||||
test('config is merged and removed', function () {
|
test('config is merged and removed', function () {
|
||||||
expect(config('services.paypal'))->toBe(null);
|
expect(config('services.paypal'))->toBe(null);
|
||||||
config([
|
|
||||||
'tenancy.features' => [TenantConfig::class],
|
|
||||||
'tenancy.bootstrappers' => [],
|
|
||||||
]);
|
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
|
||||||
|
|
||||||
TenantConfig::$storageToConfigMap = [
|
TenantConfigBootstrapper::$storageToConfigMap = [
|
||||||
'paypal_api_public' => 'services.paypal.public',
|
'paypal_api_public' => 'services.paypal.public',
|
||||||
'paypal_api_private' => 'services.paypal.private',
|
'paypal_api_private' => 'services.paypal.private',
|
||||||
];
|
];
|
||||||
|
|
@ -68,14 +60,8 @@ test('config is merged and removed', function () {
|
||||||
|
|
||||||
test('the value can be set to multiple config keys', function () {
|
test('the value can be set to multiple config keys', function () {
|
||||||
expect(config('services.paypal'))->toBe(null);
|
expect(config('services.paypal'))->toBe(null);
|
||||||
config([
|
|
||||||
'tenancy.features' => [TenantConfig::class],
|
|
||||||
'tenancy.bootstrappers' => [],
|
|
||||||
]);
|
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
|
||||||
|
|
||||||
TenantConfig::$storageToConfigMap = [
|
TenantConfigBootstrapper::$storageToConfigMap = [
|
||||||
'paypal_api_public' => [
|
'paypal_api_public' => [
|
||||||
'services.paypal.public1',
|
'services.paypal.public1',
|
||||||
'services.paypal.public2',
|
'services.paypal.public2',
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ beforeEach(function () {
|
||||||
|
|
||||||
test('vite bundler ensures vite assets use global_asset when asset_helper_override is enabled', function () {
|
test('vite bundler ensures vite assets use global_asset when asset_helper_override is enabled', function () {
|
||||||
config(['tenancy.features' => [ViteBundler::class]]);
|
config(['tenancy.features' => [ViteBundler::class]]);
|
||||||
|
tenancy()->bootstrapFeatures();
|
||||||
|
|
||||||
withBootstrapping();
|
withBootstrapping();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
|
@ -44,7 +45,7 @@ test('asset can be accessed using the url returned by the tenant asset helper',
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
$filename = 'testfile' . pest()->randomString(10);
|
$filename = 'testfile' . Str::random(8);
|
||||||
Storage::disk('public')->put($filename, 'bar');
|
Storage::disk('public')->put($filename, 'bar');
|
||||||
$path = storage_path("app/public/$filename");
|
$path = storage_path("app/public/$filename");
|
||||||
|
|
||||||
|
|
@ -136,7 +137,7 @@ test('TenantAssetController headers are configurable', function () {
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
$tenant->createDomain('foo.localhost');
|
$tenant->createDomain('foo.localhost');
|
||||||
|
|
||||||
$filename = 'testfile' . pest()->randomString(10);
|
$filename = 'testfile' . Str::random(10);
|
||||||
Storage::disk('public')->put($filename, 'bar');
|
Storage::disk('public')->put($filename, 'bar');
|
||||||
|
|
||||||
$this->withoutExceptionHandling();
|
$this->withoutExceptionHandling();
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQ
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
|
||||||
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
|
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
|
||||||
use function Stancl\Tenancy\Tests\pest;
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
|
use function Stancl\Tenancy\Tests\withBootstrapping;
|
||||||
|
use function Stancl\Tenancy\Tests\withTenantDatabases;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
SQLiteDatabaseManager::$path = null;
|
SQLiteDatabaseManager::$path = null;
|
||||||
|
|
@ -43,7 +45,7 @@ test('databases can be created and deleted', function ($driver, $databaseManager
|
||||||
"tenancy.database.managers.$driver" => $databaseManager,
|
"tenancy.database.managers.$driver" => $databaseManager,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$name = 'db' . pest()->randomString();
|
$name = 'db' . Str::random(10);
|
||||||
|
|
||||||
$manager = app($databaseManager);
|
$manager = app($databaseManager);
|
||||||
|
|
||||||
|
|
@ -70,7 +72,7 @@ test('dbs can be created when another driver is used for the central db', functi
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->toListener());
|
})->toListener());
|
||||||
|
|
||||||
$database = 'db' . pest()->randomString();
|
$database = 'db' . Str::random(10);
|
||||||
|
|
||||||
$mysqlmanager = app(MySQLDatabaseManager::class);
|
$mysqlmanager = app(MySQLDatabaseManager::class);
|
||||||
$mysqlmanager->setConnection('mysql');
|
$mysqlmanager->setConnection('mysql');
|
||||||
|
|
@ -86,7 +88,7 @@ test('dbs can be created when another driver is used for the central db', functi
|
||||||
$postgresManager = app(PostgreSQLDatabaseManager::class);
|
$postgresManager = app(PostgreSQLDatabaseManager::class);
|
||||||
$postgresManager->setConnection('pgsql');
|
$postgresManager->setConnection('pgsql');
|
||||||
|
|
||||||
$database = 'db' . pest()->randomString();
|
$database = 'db' . Str::random(10);
|
||||||
expect($postgresManager->databaseExists($database))->toBeFalse();
|
expect($postgresManager->databaseExists($database))->toBeFalse();
|
||||||
|
|
||||||
Tenant::create([
|
Tenant::create([
|
||||||
|
|
@ -146,18 +148,15 @@ test('db name is prefixed with db path when sqlite is used', function () {
|
||||||
expect(database_path('foodb'))->toBe(config('database.connections.tenant.database'));
|
expect(database_path('foodb'))->toBe(config('database.connections.tenant.database'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sqlite databases use the WAL journal mode by default', function (bool|null $wal) {
|
test('sqlite databases respect the template journal_mode config', function (string $journal_mode) {
|
||||||
$expected = $wal ? 'wal' : 'delete';
|
withTenantDatabases();
|
||||||
if ($wal !== null) {
|
withBootstrapping();
|
||||||
SQLiteDatabaseManager::$WAL = $wal;
|
config([
|
||||||
} else {
|
'database.connections.sqlite.journal_mode' => $journal_mode,
|
||||||
// default behavior
|
'tenancy.bootstrappers' => [
|
||||||
$expected = 'wal';
|
DatabaseTenancyBootstrapper::class,
|
||||||
}
|
],
|
||||||
|
]);
|
||||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
|
||||||
return $event->tenant;
|
|
||||||
})->toListener());
|
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenancy_db_connection' => 'sqlite',
|
'tenancy_db_connection' => 'sqlite',
|
||||||
|
|
@ -170,11 +169,18 @@ test('sqlite databases use the WAL journal mode by default', function (bool|null
|
||||||
$db = new PDO('sqlite:' . $dbPath);
|
$db = new PDO('sqlite:' . $dbPath);
|
||||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe($expected);
|
// Before we connect to the DB using Laravel, it will be in default delete mode
|
||||||
|
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe('delete');
|
||||||
|
|
||||||
// cleanup
|
// This will trigger the logic in Laravel's SQLiteConnector
|
||||||
SQLiteDatabaseManager::$WAL = true;
|
$tenant->run(fn () => DB::select('select 1'));
|
||||||
})->with([true, false, null]);
|
|
||||||
|
$db = new PDO('sqlite:' . $dbPath);
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
// Once we connect to the DB, it will be in the configured journal mode
|
||||||
|
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe($journal_mode);
|
||||||
|
})->with(['delete', 'wal']);
|
||||||
|
|
||||||
test('schema manager uses schema to separate tenant dbs', function () {
|
test('schema manager uses schema to separate tenant dbs', function () {
|
||||||
config([
|
config([
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ beforeEach(function () {
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
tenancy()->bootstrapFeatures();
|
||||||
|
|
||||||
Event::listen(
|
Event::listen(
|
||||||
TenantCreated::class,
|
TenantCreated::class,
|
||||||
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
use function Stancl\Tenancy\Tests\pest;
|
use function Stancl\Tenancy\Tests\pest;
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\DatabaseCacheBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\TenantConfigBootstrapper;
|
||||||
|
|
||||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
{
|
{
|
||||||
|
|
@ -193,6 +194,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
$app->singleton(RootUrlBootstrapper::class);
|
$app->singleton(RootUrlBootstrapper::class);
|
||||||
$app->singleton(UrlGeneratorBootstrapper::class);
|
$app->singleton(UrlGeneratorBootstrapper::class);
|
||||||
$app->singleton(FilesystemTenancyBootstrapper::class);
|
$app->singleton(FilesystemTenancyBootstrapper::class);
|
||||||
|
$app->singleton(TenantConfigBootstrapper::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getPackageProviders($app)
|
protected function getPackageProviders($app)
|
||||||
|
|
@ -236,11 +238,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
$app->singleton('Illuminate\Contracts\Console\Kernel', Etc\Console\ConsoleKernel::class);
|
$app->singleton('Illuminate\Contracts\Console\Kernel', Etc\Console\ConsoleKernel::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function randomString(int $length = 10)
|
|
||||||
{
|
|
||||||
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', (int) (ceil($length / strlen($x))))), 1, $length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function assertArrayIsSubset($subset, $array, string $message = ''): void
|
public function assertArrayIsSubset($subset, $array, string $message = ''): void
|
||||||
{
|
{
|
||||||
parent::assertTrue(array_intersect($subset, $array) == $subset, $message);
|
parent::assertTrue(array_intersect($subset, $array) == $subset, $message);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue