1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-05-06 19:44:03 +00:00
Commit graph

1425 commits

Author SHA1 Message Date
lukinovec
5299cfb6d4
Merge 0c463ead28 into 23b18c93a0 2026-05-04 08:47:55 +02:00
Thomas
23b18c93a0
Skip DB deletion when create_database=false, add ignoreFailures (#1394)
Database deletion is now skipped by default if the tenant has the
`create_database` internal attribute set to false, meaning it was likely
created without a database. This skip can be opted out of by changing a
static property.

It also adds an opt-in static property for ignoring any other failures
during database deletion, to allow continuing execution of the delete
pipeline.

---------

Co-authored-by: Samuel Štancl <samuel@archte.ch>
2026-05-01 21:57:19 +02:00
41701aff5f
phpstan fix: Model covariants in Scope generics
Builds on changes in recent commit:
Commit ID: c32f52ce7c
Change ID: qsnosyvyulxzrnzorpxqwqqztmqorsmk
2026-05-01 16:09:52 +02:00
53f44762ca
docker: change mssql env yaml syntax 2026-05-01 15:55:06 +02:00
lukinovec
0c463ead28 Resolve docs to-do
Resolved in https://github.com/archtechx/tenancy-docs/pull/5
2026-04-29 14:21:12 +02:00
lukinovec
4764b99c8f Merge pending_since into the other attributes during pending tenant creation
Ensures that createPending always creates tenants with pending_since set to the current timestamp.
2026-04-28 12:50:31 +02:00
lukinovec
e81e6ac651 Add regression test for jobs recognizing pending tenants
Reverting the HasPending changes (in createPending(), create tenant without pending_since, and only after that, update the tenant to give it pending_since) makes the test fail.
2026-04-28 12:47:42 +02:00
lukinovec
e3673f5557 Dispatch pending tenant creating/created events in the appropriate hooks 2026-04-28 12:28:31 +02:00
lukinovec
0bb112dbdf Cover invalid --with-pending values in tests
Include --with-pending=foo as the fasly option in the falsy --with-pending test.
2026-04-28 12:19:09 +02:00
lukinovec
f309dcc65c Test pending tenant creation with job pipelines
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 10:59:04 +02:00
lukinovec
66114e6835 Default to including pending tenants in MigrateDatabase and SeedDatabase
Now that tenants are created with pending_since right away, the migrate and seed database jobs need to include pending tenants for creating pending tenants to work fully. The jobs can still exclude pending tenants if $includePending is set to false.

Co-authored-by: Copilot <copilot@github.com>
2026-04-28 10:55:42 +02:00
lukinovec
b592d3dad4 Give pending_since to pending tenants during creation
Instead of creating pending tenants without pending_since, letting job pipelines (e.g. TenantCreated) run, and only after that, setting the tenant's pending_since, set it while creating. This allows checking tenant's pending status accurately e.g. in the CreateDatabase job pipeline.
2026-04-28 10:51:15 +02:00
lukinovec
6b4d22bb92 Update --with-pending tests
Cover that --with-pending can now accept values, and that the option takes precedence over include_in_queries config. Also simplify/improve existing assertions.
2026-04-28 10:39:42 +02:00
lukinovec
950ff0fbfd Make --with-pending override include_in_queries when passed
Pasing --with-pending, --with-pending=1 or --with-pending=true now makes tenant commands include pending tenants regardless of the include_in_queries config, and passing --with-pending=false, --with-pending=0 or --with-pending=foo makes the commands exclude pending tenants.
2026-04-28 10:35:54 +02:00
lukinovec
984911946a
Change tenant storage listeners into jobs (#1446)
The `CreateTenantStorage` and `DeleteTenantStorage` listeners were used
alongside JobPipelines. When the `TenantCreated` JobPipeline had
`shouldBeQueued(true)` and the `Listeners\CreateTenantStorage` was
uncommented, the listener would throw an exception
(`Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException
Database tenantX.sqlite does not exist.`) because at the time of
executing the listener, the tenant DB wasn't created yet.

The same issue could likely also occur in the `DeleteTenantStorage`
listener as it uses `tenancy()->run()` to resolve the tenant's storage
path which wouldn't work if the tenant's database (or other resources)
was already deleted, making initialization impossible.

This PR changes `DeleteTenantStorage` into a job and puts it (commented)
into the job pipeline, so that it can be queued with the rest of the
jobs. It also removes `CreateTenantStorage` because it should be
redundant with the FilesystemTenancyBootstrapper creating the same paths
automatically when storage path is suffixed.

The old classes are kept but deprecated for backwards compatibility.

We've also added some edge case hardening to `DeleteTenantStorage` to
make sure it never deletes the central storage path directory, which
previously could in theory occur due to a misconfiguration if a user
enabled this job/listener but disabled storage path suffixing.

Co-authored-by: Samuel Štancl <samuel@archte.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-22 16:45:54 +02:00
lukinovec
ab2a4d8438
Fix chaining withoutPending() with where() (#1457)
At the moment, `where()` cannot be used correctly while using
`withoutPending()`. For example, if we have a single non-pending tenant
in our DB (with ID 'foo'), queries like
`Tenant::withoutPending()->where('id', 'nonexistent')->first()`will
incorrectly return the non-pending tenant ('foo').

This is because `withoutPending()` does
`$builder->whereNull('data->pending_since')->orWhereNull('data')`. These
two aren't grouped, so `withoutPending()->where('id', 'nonexistent')`
basically translates to "WHERE data->pending_since IS NULL **OR (data IS
NULL AND id = 'nonexistent')**". So the query will include all tenants
whose `pending_since` is null (= all non-pending tenants).

Grouping `->whereNull('data->pending_since')->orWhereNull('data')` in a
closure passed to a separate `where()` fixes this issue.
2026-04-22 14:32:53 +02:00
c32f52ce7c
phpstan fix: Scope generics
phpstan started failing with '... implements generic interface
Illuminate\Database\Eloquent\Scope but does not specify its types:
TModel'. We solve this by adding an implements docblock to the scopes
implementing that interface. They're fairly generic - we just use the
Model type itself in the code - so we use Model for the type parameter.
2026-04-15 11:23:12 +02:00
e31249dd09
Prevent mkdir() race conditions in FilesystemTenancyBootstrapper (#1453)
This prevents race conditions that may occur if there are two concurrent
processes trying to create the storage path for the tenant. The
storagePath() method runs during bootstrap() which can easily happen
in two places at once. The race condition specifically occurs in between
the is_dir() check and the mkdir() call, the latter producing an
exception if the dir already exist. We simply ignore any error coming
out of mkdir() and then check for success separately.

We could omit that success check since failure is unlikely and would
only occur due to a server misconfiguration that would manifest itself
in other ways as well, but this way the simple TOC/TOU race condition
is prevented while other errors are still reported.

We apply the same change to the mkdir() in scopeSessions() as the logic
is similar.

Resolves #1452
2026-04-13 23:57:59 +02:00
60dd5226c4
[4.x] Add Tenancy::reinitialize() method (#1449)
Some bootstrappers read attributes of the tenant during bootstrap() but
don't respond to changes made to the tenant afterwards.

Therefore, when making changes to the tenant that'd affect the behavior
of a bootstrapper, it's necessary to reinitialize tenancy (if it matters
that changes are reflected immediately). This adds a convenience helper
for that purpose.
2026-04-08 19:21:43 +02:00
Samuel Mwangi
fb654e7a6b
[4.x] Update Pest to v4 (#1430) 2026-03-30 09:44:53 +02:00
c4960b76cb
[4.x] Laravel 13 support (#1443)
- Update ci.yml and composer.json
- Wrap single database tenancy trait scopes in whenBooted()
- Update SessionSeparationTest to use laravel-cache- prefix in L13
  and laravel_cache_ in <=L12. Our own prefix remains tenant_%tenant%_
  (as configured in tenancy.cache.prefix). We could update this to be
  tenant-%tenant%- from now on for consistency with Laravel's prefixes
  (changed in https://github.com/laravel/framework/pull/56172) but I'm
  not sure yet. _ seems to read a bit better but perhaps consistency
  is more important. We may change this later and it can be adjusted
  in userland easily (since it's just a config option).
2026-03-18 19:17:28 +01:00
8f3ea6297f
phpstan: change InputOption syntax 2026-03-09 02:13:37 +01:00
lukinovec
16861d2599
[4.x] Make URL::temporarySignedRoute() respect the bypass parameter (#1438)
Using `URL::temporarySignedRoute()` in tenant context with
`UrlGeneratorBootstrapper` enabled doesn't work the same as `route()`.
The bypass parameter doesn't actually bypass the route name prefixing.

`route()` is called in the `parent::temporarySignedRoute()` call, and
because the bypass parameter is removed before calling
`parent::temporarySignedRoute()`, the underlying `route()` call doesn't
get the bypass parameter and it ends up attempting to generate URL for a
route with the name prefixed with 'tenant.'.

This PR adds the bypass parameter back after `prepareRouteInputs()`, so
that `parent::temporarySignedRoute()` receives it, and the underlying
`route()` call respects it. Also added basic tests for the
`URL::temporarySignedRoute()` behavior (the new bypass parameter test
works as a regression test).
2026-03-09 02:07:02 +01:00
lukinovec
37b2a91aa9
[4.x] Fix URL override example in TenancyServiceProvider stub (#1426)
This PR fixes the URL override example in TenancyServiceProvider stub
(the commented `overrideUrlInTenantContext()` segment). If the tenant
doesn't have any domain, set the root URL back to the original one.
2026-01-14 11:18:15 +01:00
Punyapal Shah
e3701f1cc1
[4.x] Add more relation type annotations (#1424)
This pull request adds improved PHPDoc type annotations to several
Eloquent relationship methods, enhancing static analysis and developer
experience. These changes clarify the expected return types for
relationships, making the codebase easier to understand and work with.

Relationship method type annotations:

* Added a detailed return type annotation to the `tenant` method in the
`BelongsToTenant` trait, specifying the related model and the current
class.
* Added a detailed return type annotation to the `domains` method in the
`HasDomains` trait, specifying the related model and the current class.
* Added a detailed return type annotation to the `tenants` method in the
`ResourceSyncing` class, specifying the related model and the current
class.
2025-12-28 23:20:05 +01:00
Victor R
3c0e21b726
[4.x] Filesystem bootstrapper: scoped disk support (#1402)
Fixes #1401

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: lukinovec <lukinovec@gmail.com>
Co-authored-by: Samuel Stancl <samuel@archte.ch>
2025-12-16 23:17:11 +01:00
7955aae6d5
TSP stub: remove unnecessary imports
Also update PHP 8.5 steps in CONTRIBUTING.md since PHP 8.5 is released
now.
2025-12-12 20:20:29 +01:00
a778e17686
Merge pull request #1411 from archtechx/resource-syncing-refactor
[4.x] Improve resource syncing (refactor + mapping cleanup + morph maps)
2025-12-12 04:02:42 +01:00
lukinovec
159e600a9b Syncing: support morph maps in TriggerSyncingEvents 2025-12-12 03:43:52 +01:00
04a20ca930
[MINOR BC BREAK] Syncing: PivotWithRelation -> PivotWithCentralResource
The old names of the class and method were misleading. We don't
actually need any relation. And we don't even need a model instance
as we were returning previously -- the only use of that method was
in TriggerSyncingEvents which would immediately use ::class on the
returned value. Therefore, all we are asking for in this interface
is just the central resource class.
2025-11-26 05:52:55 +01:00
072fcc6326
Syncing: move global ID generation logic to an overridable method
Also make all resource syncing-related listener closures static.

Also correct return type for getGlobalIdentifierKey to string|int.
(We intentionally do not support returning null like many other
"get x key" methods would since such a case might break resource
syncing logic. This is also why we use inline getAttribute() in the
creating listener instead of calling the method.)
2025-11-26 05:52:55 +01:00
lukinovec
e079803025 Syncing: Add DeleteAllTenantMappings listener 2025-11-26 05:52:55 +01:00
lukinovec
44e8ec8abf Syncing: SyncedResourceDeleted event and DeleteResourceMapping listener
Also move pivot record deletion to that listener and improve tests

The 'tenant pivot records are deleted along with the tenants to which
they belong to' test is failing in this commit -- the listener
for deleting mappings when a *tenant* is deleted is only implemented
in the next commit. The only change done here is to re-add FKs
(necessary for passing *in this commit* in that specific dataset
variant) that were removed from the default test migration as we now
have the DeleteResourceMapping listener that's enabled by default.
2025-11-26 05:52:48 +01:00
45cf7029af
globalUrl: useAssetOrigin() instead of setAssetRoot()
This change was prompted by a phpstan failure after a recent update.
While making this change, I noticed we don't need the macro anymore
as useAssetOrigin() was added to the UrlGenerator earlier this year,
simplifying our implementation.
2025-11-14 10:59:31 +01:00
0cd0bc44b1
config: ignore port in default central_domains value
Recent Laravel installations often have http://localhost:8000 as
APP_URL, so we make sure to strip any port suffix from the default
central domain derived from APP_URL.
2025-11-11 02:06:13 +01:00
38aab013a4
Merge pull request #1416 from archtechx/cloning-improvements
[4.x] Route cloning improvements
2025-11-10 22:04:02 +01:00
2aca784c0b
Cloning: remove comments in TSP stub in favor of referencing class docs 2025-11-10 17:31:37 +01:00
6ef4b91744
Cloning: improve type annotations, add cloneRoutes() for convenience 2025-11-10 02:16:57 +01:00
197513dd84
Cloning: addTenantMiddleware() for specifying ID MW for cloned route
Previously, tenant identification middleware was typically specified
for the cloned route by "inheriting" it from the central route, which
necessarily meant that the central route had to also be marked as
universal so it could continue working in the central context --
despite presumably not being usable in the tenant context, thus being
universal for no proper reason. In such cases, universal routes were
used mainly as a mechanism for specifying the tenant identification
middleware to use on the cloned tenant route.

Given that recent refactors of the cloning feature have made it more
customizable and a bit nicer to use "multiple times", i.e. run handle()
with a few different configurations of the action, letting the
developer specify the used tenant middleware using a method like this
only makes sense.

The feature also becomes more independently usable and not just a
"hack for universal routes with path identification".
2025-11-09 00:27:14 +01:00
97c5afd2cf
Cloning: clarify case where neither paths nor domains differ
In such a case, the cloned route will actually *override* the original
route, rather than being unused as the original docblock claimed.

Also adds a static make() function for convenience.
2025-11-08 20:38:01 +01:00
69bf768424
Cloning: remove route context middleware flags during cloning
Previously, if a universal route was cloned without a
cloneRoutesWithMiddleware(['universal']) call, i.e. it had both
'clone' and 'universal' flags, with only the former triggering cloning,
the 'universal' flag would be included in the middleware of the cloned
route.

Now, we make sure to remove all context flags -- central, tenant,
universal -- in the first step of processing middleware, before adding
just 'tenant'.
2025-11-08 01:17:15 +01:00
Hayatunnabi Nabil
947894fa1d
[4.x] Fix dropRLSPolicies() (#1413)
`?` parameters are not supported in these statements, so we have to use
string interpolation like in other related code.

---------

Co-authored-by: Samuel Štancl <samuel@archte.ch>
2025-11-08 00:52:08 +01:00
8bdacf566b
Merge pull request #1412 from archtechx/push-xwrxrozxnsks
[4.x] Config: scope_sessions = true only with supported drivers, always throw
2025-11-05 15:08:18 +01:00
510358b9be
Config: scope_sessions = true only with supported drivers, always throw
With the previous implementation, many users would use the default
config that enables scope_sessions. They would then deploy the app
to production and get the exception there since they use the
`database` session driver which is scoped by a different mechanism.

The idea behind throwing the exception only in prod was to make it
easy to use different setups locally without getting annoying
exceptions, while notifying users that a security feature they enabled
isn't running in production.

However, a better way of doing this is to just throw the exception
consistently in all setups and use a sane default for enabling the
scope_sessions setting based on the SESSION_DRIVER env var.

Users are always encouraged to read the session scoping docs to make
sure their session scoping configuration makes sense for their specific
setup, but this is a good balance for providing solid security out of
the box for most setups without requiring users to configure things
manually.
2025-11-05 14:53:07 +01:00
cab8ecebec
Create tenant storage directories in FilesystemTenancyBootstrapper (#1410)
This is because the CreateTenantStorage listener only runs when
a tenant is created, but in multi-server setups the directory may
need to be created each time a tenant is *used*, not just created.

Also changed the listeners to use TenantEvent instead of specific
events, to make it possible to use them with other events, such as
TenancyBootstrapped.

Also update permission bits in a few mkdir() calls to better scope
data to the current OS user.

Also fix a typo in CacheTenancyBootstrapper (exception message).
2025-11-04 21:16:39 +01:00
0ef4dfd230
DB cache bootstrapper: setConnection() instead of purge() (#1408)
By purging stores, we "detach" existing cache stores from the
CacheManager, making them impossible to adjust in the future.

We also unnecessarily recreate them on every tenancy bootstrap/revert.

A simpler case where this causes problems is defining a RateLimiter in
a service provider. That injects a single cache store into the
rate limiter singleton, which then becomes a completely independent
object after tenancy is initialized due to the purge. This in turn
means the central and tenant contexts share the rate limiter cache
instead of using separate caches as one would expect.
2025-11-04 15:47:15 +01:00
b967d1647a
Add UUIDv7Generator
Also correct docblock for ULIDGenerator and add missing @see
annotations in the config file.
2025-11-04 15:45:48 +01:00
36153a949b
docblocks: change TenantConfig references to TenantConfigBootstrapper 2025-10-30 02:32:17 +01:00
d274d8c902
pending tenants: minor cleanup 2025-10-29 22:54:53 +01:00
lukinovec
0dc187510b
[4.x] Clean up expired impersonation tokens instead of just aborting, add command for cleaning up expired tokens (#1387)
This PR makes the expired/invalid tenant impersonation tokens get
deleted instead of just aborting with 403.

The PR also adds a command (ClearExpiredImpersonationTokens) used like
`php artisan tenants:purge-impersonation-tokens`. As the name suggests,
it clears all expired impersonation tokens (= tokens older than
`UserImpersonation::$ttl`).

Resolves #1348

---------

Co-authored-by: Samuel Štancl <samuel@archte.ch>
2025-10-28 14:14:52 +01:00