Download the PHP package glueful/tenancy without Composer

On this page you can find all versions of the php package glueful/tenancy. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package tenancy

glueful/tenancy

Shared-database, row-level multi-tenancy for the Glueful framework.

What it is

glueful/tenancy gives a single application the ability to serve many tenants out of one shared database, isolating their data at the row level: tenant-owned tables carry a tenant_uuid column, and every read/write against those tables is automatically scoped to the active tenant.

The data model is many-tenants-per-user: a (global) user is granted a role inside a tenant through a tenant_memberships bridge row, so one user can belong to several tenants and one tenant can have many members. This is logical isolation, not hard isolation — see Security posture.

Concern Mechanism
Resolve the active tenant from a request tenant middleware + resolver chain
Scope reads / stamp writes on tenant tables BelongsToTenant trait (ORM global scope + create/update hooks)
Backstop for raw (non-ORM) SQL Connection table hook + TenantQueryGuard interceptor
Step outside the scope deliberately Tenancy::runAsTenant / runAsSystem / forAnyTenant
Carry the tenant into jobs / CLI / scheduler PropagatesTenant, RunsInTenantContext, ForEachTenant
Operate tenants tenant:create|list|activate|suspend|diagnose

Install / enable

Tenancy uses two providers with intentionally different lifecycles:

Provider Registration Responsibility
TenancyControlPlaneProvider Always present in config/serviceproviders.php Migrations, tenant provisioning, lifecycle/domain administration, context-running services, and tenancy commands
TenancyServiceProvider Enabled/disabled through config/extensions.php Request resolution, middleware, table scoping, write stamping, and query enforcement

Add the control-plane provider to the application's static provider list:

Then run the migrations. The control-plane provider registers the migrations that create the central tenant, membership, domain, and released-host tables.

When the application is ready to enforce tenancy, enable the extension provider:

Enabling adds Glueful\Extensions\Tenancy\TenancyServiceProvider to config/extensions.php. Its presence means enforcement is active: it registers the resolver, middleware, scoping hooks, stamper, and query guard. Disabling the extension removes that provider and therefore removes enforcement on the next application boot. The control plane remains available while enforcement is disabled.

extensions:enable and extensions:disable manage only the manifest provider. They cannot add or remove TenancyControlPlaneProvider in config/serviceproviders.php.

Upgrading from the single-provider release

Before deploying the provider-split release:

  1. Add Glueful\Extensions\Tenancy\TenancyControlPlaneProvider to config/serviceproviders.php.
  2. Keep Glueful\Extensions\Tenancy\TenancyServiceProvider in config/extensions.php if tenancy enforcement is currently active; remove it if enforcement should remain off.
  3. Deploy the package update, rebuild the extension/container cache as required by the host, and run php glueful migrate:run.
  4. Verify tenant administration and resolution before accepting traffic.

Do not upgrade without the control-plane registration. The enforcement provider no longer owns migrations, default configuration, commands, or administration bindings, so loading it alone is an invalid deployment.

The old config('tenancy.enabled') switch no longer suppresses enforcement while TenancyServiceProvider is loaded. Applications that previously kept the provider enabled and set tenancy.enabled=false must instead remove/disable the enforcement provider. Provider presence is the engine-level enforcement switch; a host application may maintain a separate persisted lifecycle state for transition orchestration.

Custom membership-role authority

The control plane validates membership roles through MembershipRoleAuthority. By default, ConfigRoleAuthority preserves the static tenancy.membership.roles allow-list. A host that owns per-tenant role definitions may configure an implementation class:

Register that concrete class in the host container. The engine factory resolves it while retaining the config allow-list as a safe default. Membership assignment and role changes validate inside a transaction while holding canonical per-tenant role advisory locks; concurrent conflicts surface as MembershipRoleConflictException rather than proceeding with an unprotected role.

Framework requirement: glueful/framework ^1.67.0. The extension relies on the chainable Connection::addTableHook() / QueryExecutor::addQueryInterceptor() seams (so tenancy hooks compose with host interceptors instead of replacing them) and the Connection::class container binding. Earlier framework versions are not supported.

The data model

Two registry tables ship with the extension (both are central / never tenant-scoped):

tenants — the tenant directory.

Column Type Notes
id bigint, PK, auto-increment
uuid string(12), unique stable public principal id used across the system
slug string(255), unique human-facing key
name string(255) display name
status string(32), default active active resolves; anything else (e.g. suspended) does not
settings text, nullable per-tenant JSON blob
created_at / updated_at timestamp DB CURRENT_TIMESTAMP defaults
deleted_at timestamp, nullable soft delete

tenant_memberships — grants a global user a role in a tenant.

Column Type Notes
id bigint, PK, auto-increment
uuid string(12), unique
tenant_uuid string(12) FK → tenants(uuid), cascade on delete
user_uuid string(12) indexed only — external principal id, no FK (the user store is a separate package)
role string(64), default member one of owner, admin, member, viewer (configurable)
status string(32), default active
unique(tenant_uuid, user_uuid) — one membership per user per tenant

Glueful\Extensions\Tenancy\Models\Tenant and Glueful\Extensions\Tenancy\Models\TenantMembership model these. Their consumer-side counterpart is your own tenant-owned model — any model that opts in via BelongsToTenant.

Making a consumer table tenant-owned

Add a tenant_uuid column to the table and the trait to the model:

Also list every tenant-owned table in config/tenancy.php under tables — that list is the authoritative registry the raw-query backstop reads at boot (the trait also self-registers, but the config list is what protects a table before its model is booted):

⚠ The composite-unique pitfall

A per-tenant-unique business key (a slug, an order number, an email-within-tenant) must be a composite unique on (tenant_uuid, key) — never a global unique on the key alone:

A global unique('slug') leaks across the tenant boundary: as soon as tenant A creates a project with slug flagship, tenant B is permanently blocked from using it — and the failure surfaces as a confusing database constraint violation, not a tenancy error. Always scope uniqueness by tenant_uuid.

Request flow

Register the tenant middleware on tenant-scoped routes. It must run after authentication (it reads auth.user.uuid to check membership):

On each request the middleware: resolve the tenant candidate (resolver chain) → validate it exists, is active, and that the user is a member (or holds a bypass permission) → set the request tenant context → run the handler → clear the context in a finally (state never leaks to a later request, even on success).

Responses:

Situation Status
Tenant unknown or inactive (suspended/soft-deleted) 404 — the two are never distinguished, so existence is not leaked
Tenant known, authenticated user is not a member 403
Above 403, with hide_existence enabled collapsed to 404 so membership cannot be probed

Resolver chain

Resolvers run in the configured order; the first non-null candidate wins. Configure order and each resolver's input in config/tenancy.php:

Resolver Reads
subdomain left-most subdomain label of the host (requires subdomain.base_domain)
path leading /<segment>/<tenant>/... path segment
header the configured request header (header.name)
query the configured query parameter (query.name)
jwt the configured claim from the jwt.claims request attribute
active_session the tenancy.active_tenant request attribute

Trim the list to only the resolvers you use; unknown names are skipped rather than erroring. Keep client-controlled resolvers (header, query, path) after trusted resolvers when more than one source may be present; resolver order is security-relevant because the first candidate wins.

Automatic scoping

Adding BelongsToTenant to a model wires three behaviors against the model's request-scoped tenant context:

Raw / non-ORM access

The ORM scope only covers Model-path queries. For hand-written query-builder code, two backstops apply:

  1. Auto-injection table hook — a Connection table hook injects where tenant_uuid = <current> into any query against a registered tenant-owned table while a tenant is active and no bypass is set.
  2. Pre-execution guard — TenantQueryGuard inspects the final SQL just before execution and, if it sees unscoped access to a tenant-owned table, throws in dev/test (guard.dev = throw) or emits a metric/log in prod (guard.prod = metric). It is conservative (prefers letting a query through over a false-positive outage) and is a no-op outside a tenant request or under a bypass.

For deliberate raw access, use the provided helper, which asserts the table is registered and returns the already-scoped builder:

Rule of thumb: use models or TenantQuery; the guard catches the rest.

Bypass APIs (noisy on purpose)

Glueful\Extensions\Tenancy\Bypass\Tenancy is the only sanctioned way to step outside the per-request scope. The names are intentionally explicit — there is no generic withoutScope() — so a bypass is always obvious in a diff and in a security review. Each method saves, sets, and restores tenancy state in a finally, so they nest and unwind cleanly even on exception.

Method When to use
runAsTenant(Tenant\|string $tenant, callable $fn) act as one specific tenant
runAsSystem(callable $fn) trusted system / no-tenant maintenance
forAnyTenant(callable $fn, bool $requirePermission = true, ?TenantAccess $access = null) cross-tenant read
registerTable(string $table) register a table as tenant-owned (delegates to the registry)

forAnyTenant is permission-gated on request paths. By default it checks whether the current user holds any of config('tenancy.bypass_permissions') (default tenancy.access_any, tenancy.manage) and throws TenantAccessDeniedException if not — failing closed when authorization cannot be evaluated. Trusted CLI / system callers pass $requirePermission = false to skip the check.

The check honors your app's active permission provider first (PermissionManager::can() — the same authority the rest of the app uses), then falls back to the framework Gate's voters when no provider is active. So an RBAC extension like glueful/aegis governs bypass directly: grant a role tenancy.access_any / tenancy.manage in aegis and it unlocks cross-tenant access. With no provider installed, a configured super_roles user (or a config/permissions.php policy) grants it via the Gate.

Context propagation

The tenant middleware only sets the tenant for HTTP requests. Outside the request lifecycle, propagate the tenant explicitly:

Jobs — opt in per job with PropagatesTenant. Capture the tenant at dispatch (it is stored in the job's serialization-surviving payload) and restore it on the worker (where it is re-loaded and re-validated as active; a missing/inactive tenant throws rather than running unscoped):

No captured tenant ⇒ the job runs system-scoped (no tenant, no implicit bypass).

CLI — add the --tenant switch with RunsInTenantContext:

--tenant=<uuid|slug> resolves + active-validates a single tenant; no flag = trusted system context (no tenant, enforcement suspended).

Scheduler — fan a scheduled callback out over every active tenant with ForEachTenant:

One tenant failure does not abort the remaining tenants. Inspect $result->failed and $result->errors after the run to decide whether the scheduler job should alert or retry.

Console commands

tenant:diagnose reports three sections:

It is a report, not a gate — it always exits success, but renders warnings prominently. Run it after adding tenant-owned tables.

Security posture

This is logical isolation, not hard isolation. All tenants share one database; isolation is enforced in the application layer by three cooperating mechanisms — the ORM scope, the create-time force-stamp, and the raw-query guard.

Threat model — be honest about what it does and does not cover:

Fail-closed defaults:

Recommendations:

Configuration reference

config/tenancy.php (merged from the extension; override per app):

Key Default Env Purpose
resolvers ['subdomain','path','header','query','jwt','active_session'] resolver precedence (first non-null wins)
subdomain.base_domain null TENANCY_BASE_DOMAIN base host for subdomain resolution
path.segment 't' leading path segment
header.name 'X-Tenant-Id' tenant header
query.name 'tenant_id' tenant query param
jwt.claim 'tenant_id' JWT claim name
tables [] authoritative list of tenant-owned tables
enforcement.required_by_default true BelongsToTenant fails closed with no tenant
enforcement.require_authenticated true tenant selection requires auth.user.uuid before membership/bypass checks
enforcement.hide_existence false collapse the membership 403 → 404
enforcement.guard.dev 'throw' dev/test guard action
enforcement.guard.prod 'metric' prod guard action — metric | log | off
bypass_permissions ['tenancy.access_any','tenancy.manage'] permissions that satisfy forAnyTenant
membership.roles ['owner','admin','member','viewer'] allowed membership roles
membership.role_authority ConfigRoleAuthority::class host implementation of tenant-aware membership-role assignment policy

Enforcement activation is intentionally not a configuration key. It is controlled by whether Glueful\Extensions\Tenancy\TenancyServiceProvider is present in the application's enabled-extension list. TenancyControlPlaneProvider remains statically registered regardless of that state.


All versions of tenancy with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
glueful/extension-contracts Version ^1.3.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package glueful/tenancy contains the following files

Loading the files please wait ...