Full v4 changelog
Version 4 brings many new features, with the most important ones being:
- PostgreSQL RLS
- Jetstream/Fortify support
- Early identification and a general overhaul of routing logic
- Resource syncing refactor
- New tenancy bootstrappers
- Pending tenants
- Parallel migrations and other commands
If you’re upgrading from v3, the main changes that will affect you are:
- Higher requirements: PHP 8.4+, Laravel 12+, and MySQL 8+ if you use MySQL
- Namespace changes (mostly database-related things being moved to a dedicated namespace). These are easily resolved with any modern IDE
- New config structure. You will have to delete and republish it (and then re-apply your changes, if any). It’s recommended to also do this with the
TenancyServiceProvider - Several static properties being removed and replaced with config keys
- Changes to routing logic (if you’re using universal routes)
- The
TenantConfigfeature is deprecated in favor ofTenantConfigBootstrapper
Diff: d0de09a...master
New features
Section titled “New features”- Tenant schema dump (
tenants:dump) command7d98ebb#807a1a976c#985cb7567a#1015ea19117#1019f851288 - Add Microsoft SQL Server support
cc6d4fe#715 - Add
ScoutPrefixBootstrapperf83504a#936 - Set default value for
tenantURL parameters when a tenant is identified by pathf2c6408#925 - Make
tenants:runhandle stdin prompts from subcommands409190f#9230e223e0 Add optionalDeleteTenantStoragelistenerab5fa7a#938aa6bfb4, both it andCreateTenantStoragenow accept anyTenantEvent#1410- Later deprecated in favor of the
Jobs\DeleteTenantStoragejob #1446, since a job pipeline is a better fit for this logic than a listener, andCreateTenantStoragebecame redundant onceFilesystemTenancyBootstrapperstarted creating the directories
- Later deprecated in favor of the
Add(later deprecatedBatchTenancyBootstrapperb78320b#874eff4155and removedd8af9b4)- Add
Storage::url()support7bacc50#909a7ad828222bbe48cd15db - Add tenant-specific maintenance mode
121370e#761,e1b8658(fixes bug #1404), add events3f60c4a#979 - Add
current()andcurrentOrFail()tenant methods42dab29#970 - Add option for dropping tenant databases on
migrate:fresh080b271#9719520cbc#1007 - Add cookie option on InitializeTenancyByRequestData
05f1b2d#98015d12e2 - Add pending tenants
198f34f#8697d3298c#10256b7458985bdbd5- Refactored in
6b0066c(BC break:pullPendingFromPool()now defaults to$firstOrCreate = false,pullPending()is a direct alias for it with$firstOrCreate = true, both accept$attributes, andcreatePending()setspending_sinceeven if an exception is thrown) - Add
getPendingAttributes()for providing default values for custom non-nullable columns whencreatePending()is called without them (e.g. a dummyslugthat gets overwritten when the tenant is pulled)6523f24 PendingScopecleanupd274d8c- Fix chaining
withoutPending()withwhere()(the two pending conditions weren’t grouped) #1457 - Pending tenants are now created with
pending_sincealready set (minor BC break, they used to be marked as pending only after the creation pipeline finished, so e.g.DatabaseCreatedlisteners saw them as non-pending;CreatingPendingTenant/PendingTenantCreatedare now fired from the model’screating/createdhooks),MigrateDatabase/SeedDatabasepass--with-pending(opt out using their$includePendingproperty) and--with-pendingnow accepts a value which overrides thetenancy.pending.include_in_queriesconfig #1458 - Fix a remaining race condition when two processes pull at the same time (
pending_sinceis now cleared using a conditional update whose affected row count decides who claimed the tenant, retrying with the next candidate otherwise) — minor BC break: clearingpending_sinceno longer fires model events, andPullingPendingTenantnow fires before the claim (outside the transaction), so it may fire more than once and no longer guarantees the tenant will be pulled; usePendingTenantPulledfor that #1463
- Refactored in
- Add
DatabaseSessionBootstrapper(adds support fordatabasesession driver)5849089 - Add session state when impersonating tenant
f42f08c#1029, later changed in #1437 - Add a dedicated feature for tenant-specific mail credentials
0f892f1#989ea5a746 - Add
skip-failingoption totenants:migratecommand342c67f#945 - Allow defining the tenant connection template using array syntax
087733d#1040 - Add
RootUrlBootstrapper(changes APP_URL in CLI context, used e.g. when sending emails in queued jobs that should have links to the tenant’s domain), alsoUrlGeneratorBootstrapper617e9a7#1044fbdb13f#1068719b1be#10792c0f6155d3b3d3cecf07a- Fix
URL::temporarySignedRoute()not respecting the bypass parameter #1438 - Override
toRoute()too (needed for Livewire v4, which uses it instead of theroute()helper) and remove the now-redundanttemporarySignedRoute()override #1439 - Make
TenancyUrlGeneratorinherit the originalUrlGenerator’s scheme sohttpsapp URLs no longer becomehttpin tenant context #1390
- Fix
- Add
BroadcastingConfigBootstrapper(makes multi-tenant broadcasting/websockets possible using tenant-specific keys)d7a4982#1027, Reverb:590360b, rewritten in #1448 - Add
BroadcastChannelPrefixBootstrapper(makes broadcasting/websockets tenant-aware by prefixing channel names)c34952fca400b500a00a2, Reverb:252e337, helper change:a37dc9b - Add cache prefix mode for separating tenant caches (introduces tenancy support for any cache driver, no longer only Redis)
bd9bbe8#1014 - Support defining domains for tenants using a
domainstring column instead of a HasMany relatione25e7b78b0bdd4 - Add
InitializeTenancyByOriginHeader9e4f33e56dd41191295f0 - Add permission-controlled MSSQL database manager
cf3d06c - Add the option to set headers and middleware in TenantAssetController
a39da04cc2d555fdd401f - Add more tenant key generators
dc4306655b1c5325360f68d87ee9b967d16 - Make tenant column used by path identification parameters configurable, general resolver refactor
0c11f29 - Add
tenant:tinkercommand6f4b9da489fbb9 - Add
Tenancy::$findWithac5948d - PostgreSQL RLS
7317d2605b602e588d1fc2057e1e, support table constraints (+ refactor for clarity and performance using dynamic programming) #1354, fixdropRLSPolicies()#1413,tenants:rlsfixes (--forceis now a plain flag,$forceRls = falseun-forces tables when their policy is recreated, RLS is disabled on tables that stopped being scoped and have no policies, policy lookups are limited to the current schema) #1494 - Parallel migrations and other commands
1b0e7d039bcbdaf017b65075789e303b52dc6ba62b - SQLite: support named in-memory DBs
48b916e99b79a5aba7a50, stricter in-memory database detection #1459 - SQLite: add DisallowSqliteAttach feature
a88a42f, it requires a 64-bit system (we don’t build 32-bit extensions) and silently does nothing on unsupported platforms instead of throwing13a2209a0a9b85, on PHP 8.5+ it uses the nativePDO::setAuthorizer()instead of loading a compiled extensionfadf100 - Impersonation tokens: support custom models
95dd906 - Support
databasecache store tenancyecc3374 - Add
--forceoption totenants:migrate-fresh#1391 - Use
--databaseintenants:migrateas the template connection #1386 - Add
tenants:purge-impersonation-tokenscommand (deletes impersonation tokens older thanUserImpersonation::$ttl) #1387 (resolves #1348) - Support Laravel’s
scopeddisks inFilesystemTenancyBootstrapper#1402 (resolves #1401) - Add
Tenancy::reinitialize()#1449 - Add a
--skip-tenantsoption to all tenant commands (opposite of--tenants) #1436 - Add
DatabaseTenancyBootstrapper::$harden(opt-in, throws aRuntimeExceptionif a tenant’s connection ends up pointing at the central database or another tenant’s database for whatever reason) #1459 - Add
LogChannelBootstrapper#1381 - Early identification (old version:
ff46bcf, new version:1d0ca27), related:4953c69e4df597df9324bc31215680b11834e51cdb#1362 #1371, remove a redundant universal route check fromPreventAccessFromUnwantedDomains#1427 - Add route cloning (
1d0ca27, see above):- Change the URI prefix order and move cloning to
app->booted()b789f5c - Trim trailing
/from route prefixes0f7cd2e - Refactor — add
cloneRoutesWithMiddleware()and support cloning routes by name #1353 - Add
addTenantParameter(bool)anddomain(string|null)b2f2669#1374 - Add the tenant parameter before any existing route prefix (configurable using
tenantParameterBeforePrefix()) #1393 - Add
addTenantMiddleware()for specifying the identification middleware used on cloned routes (previously it had to be “inherited” from the central route, which meant marking that route as universal even when it wasn’t meant to be usable in tenant context), remove all route context middleware flags (central,tenant,universal) from cloned routes, addcloneRoutes()and a staticmake()#1416
- Change the URI prefix order and move cloning to
Changes
Section titled “Changes”- Respect user-defined
$onFailin the Universal Routes feature233a122#679 - Change how
tenants:runis used with argumentsa45aa8e#912 #686 - Stronger typing, minor changes to the
Tenantinterface55d0a9a8af354c87212e5d463e2da94227af98a901942d79c - Improved namespacing, all database-related logic was moved to namespaces under
Stancl\Tenancy\Database40bf28c - Refactor TenantDatabaseManagers
d2e1ce00fc1054 - Fix/improve Ignition solutions
55d0a9a3542b3f - Reverse order of tenancy bootstrappers when reverting to central context
62d19c5#932 - Make impersonation tokens require stateful guards
3bf2c39#935 - Remove configurable static property from
BelongsToTenanttrait (deprecated in PHP 8.1), replace it with a config key24146b2e5bc8dd - Refactor more static properties to config keys
ccaba05 - Improve CLI command outputs using Laravel 9+ components
e4f5b92#9685d688e668de360#1030 - Change
TenantDatabaseManager-related interfaces, add publicdatabase(): Connectionmethodfe0a322 - Make
tenants:migratedefault to configured schema patha1a976c#985 - Use a dedicated DB connection for creating/deleting tenant databases
bf504f4#946 (fixes bug #515) - Improve resource syncing logic
77c5ae1#915 (fixes bug #658)22d1b20#993ea3e445#992,758fbc8#997 (adds polymorphic table for mapping resources to tenants)678468577226cf, add theSyncedResourceDeletedevent (now also fired for tenant resources, not justSyncMasters, and ondeletedrather thandeleting) and theDeleteResourceMappinglistener which deletes pivot records when a synced resource is deleted, add an optionalDeleteAllTenantMappingslistener for cleaning up pivot records of deleted tenants (intended for pivot tables without cascading foreign keys), support morph maps in pivots, renamePivotWithRelationtoPivotWithCentralResource(minor BC break, the interface now only returns the central resource class), move global ID generation to an overridablegenerateGlobalIdentifierKey()method #1411, fix attach listener event #1469 - Centralize config used by
BelongsToTenantandHasDomains, replace alltenant_idliteralsdd0f03f2a39b05(fix #998),82fa6cb - Convert publishable migrations to anonymous classes
fb2369d#1001 - Manual mode improvements (use correct event type, add new listeners)
73c5655#1013 - Add
rememberbool column toImpersonationToken228c267#1101 - Minor
migrate:rollbackfixesb503dbf - Fix
PathTenantResolverwhen resolving tenant from cache8db27a3, relatedaa1437f - Require MySQL 8
fd6070f - Disable
asset_helper_tenancy/asset_helper_overrideby default, make the controller more configurablea39da04, related (rename + misc changes):657e165 - Simplify
Tenantcontract, moverun()logic toTenancy, add genericsaf3b693 - Simplify
TenantWithDatabasecontract, movetenantConfig()logice8c3c7529d1469 - Drop Laravel 10 support
ce8e74e - Add
$ignoreExistingstatic property toCreateDatabasebf1ba69 - Filesystem logic refactor
a41ad69 - Cache prefixing logic rewrite, session scoping improvements
eecf6f2, alsoScopeSessionsimprovementdc90e60,CacheTenancyBootstrappernow always throws whentenancy.cache.scope_sessionsis enabled but the session driver isn’t cache-driven (redis,memcached,dynamodb,apc) and the config default is derived fromSESSION_DRIVERso it’s only enabled with supported drivers74634df6049adeaba7a50#1412 - Support non-interactive
tenancy:install8b41ea3, star the repository using theghCLI when it’s available3846fe8 - SQLite: use
.sqlitesuffix if no suffix is specified836a258 SQLite: use WAL mode for newly created DBs by default (configurable)(later reverted9ee1d6313a2209— the$WALstatic property is gone, Laravel’s ownjournal_modeconfig is respected instead)- Consume
PendingDispatchreturn values inTenancy::run()(see #1277)3c183e4b9cc63fffad2db - Queue logic refactor
8f958d5#1289:QueueTenancyBootstrappernow always initializes tenancy for the job being processed and reverts to the previous context afterwards, so the$forceRefreshstatic property is gone — changes to a tenant’s state are always picked up by the next job. The previous behavior, staying in the tenant’s context after the job, is available in the newPersistentQueueTenancyBootstrapper; it’s needed when aJobProcessedlistener has to run in the tenant context (e.g. Telescope’sJobWatcher, which unserializes the job and would fail on models bound to thetenantconnection), but requires more careful use, which is why it isn’t the default anymore runForMultiple()now doesn’t execute when passed an empty array30ee4e9- URL generation, request data identification improvements (both added in v4, see previous points) #1357
- Only revert initialized bootstrappers #1385
- Use
globalCacheinCachedTenantResolver3984d64 - Fix
ViteBundlernot affectingVitestatic calls #1389 - Features refactor: features are now always bootstrapped (in
TenancyServiceProvider::boot(), rather than whenTenancyis resolved from the container),Feature::bootstrap()no longer takes aTenancyinstance, addedTenancy::bootstrapFeatures()for bootstrapping features enabled at runtime4578c9e(fixes bug #949) - SQLite: delete
-journal,-waland-shmfiles along with the tenant database, handle a trailing directory separator inSQLiteDatabaseManager::$path13a2209a0a9b85,SQLiteDatabaseManager::makeConnectionConfig()now properly respects the static$pathproperty #1459 - Replace the
TenantConfigfeature withTenantConfigBootstrapper(bootstrappers have better error handling and safe reverting; the feature was effectively a bootstrapper listening to theTenancyBootstrapped/RevertedToCentralContextevents), the feature is now deprecatedb320f8f36153a9 DatabaseConfigrefactor: clarify the role of the tenant host connection, renameconnectionDriverManager()tomanagerForDriver()a0a9b85- Delete expired or invalid impersonation tokens instead of only aborting with a 403 #1387
- Create the tenant’s
framework/cachedirectory inFilesystemTenancyBootstrapper(theCreateTenantStoragelistener only runs when a tenant is created, but in multi-server setups the directory may need to be created whenever a tenant is used, so the listener is generally no longer needed), also use0750instead of0777/0755permissions for created storage directories #1410 - Skip tenant database deletion when the tenant’s
create_databaseinternal attribute isfalse(it was likely created without a database; opt out usingDeleteDatabase::$skipWhenCreateDatabaseIsFalse), addDeleteDatabase::$ignoreFailuresfor continuing the delete pipeline when deletion fails —DatabaseDeletedis now only fired when the database was actually deleted #1394 - Fix
DomainTenantResolver::isSubdomain()treating e.g.tenantsite.comas a subdomain of the central domainsite.com(it now requires an actual dot-separated prefix) #1425 - Validate parameters in tenant database manager statements — bindings are used where possible, otherwise the value is checked against an allowlist (
validateParameter()/validatePassword()) — and only include charset/collation in the MySQLCREATE DATABASEstatement when they aren’t null (nullpreviously caused aQueryException) #1459 - Show the underlying
tenants:migrateoutput intenants:migrate-freshwhen it’s run verbosely (-v), it was previously always silenced #1464
Package changes
Section titled “Package changes”- Tests now use Pest
- We now have a special queue test suite
archtechx/tenancy-queue-testerf84b22eabe1f1f - Custom Dockerfile, CI no longer uses
docker-compose - We now use phpstan
- Added API docs: api.tenancyforlaravel.com