PHPackages                             monkeyscloud/monkeyslegion-tenancy - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. monkeyscloud/monkeyslegion-tenancy

ActiveLibrary[Database &amp; ORM](/categories/database)

monkeyscloud/monkeyslegion-tenancy
==================================

Enterprise multi-tenant patterns for MonkeysLegion: single-DB, schema-per-tenant, database-per-tenant isolation with domain resolution and tenant-aware infrastructure

1.0.0(2mo ago)00MITPHPPHP ^8.4

Since May 16Pushed 2mo agoCompare

[ Source](https://github.com/MonkeysCloud/MonkeysLegion-Tenancy)[ Packagist](https://packagist.org/packages/monkeyscloud/monkeyslegion-tenancy)[ Docs](https://monkeyslegion.com/docs/packages/tenancy)[ RSS](/packages/monkeyscloud-monkeyslegion-tenancy/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (12)Versions (2)Used By (0)

MonkeysLegion Tenancy v2
========================

[](#monkeyslegion-tenancy-v2)

Enterprise multi-tenant patterns for MonkeysLegion: single-DB with `tenant_id` scoping, schema-per-tenant, and database-per-tenant. Domain/subdomain identification, tenant-aware cache/queue/storage. Essential for B2B SaaS. Ground-up build for PHP 8.4 with property hooks, backed enums, and zero magic.

Features
--------

[](#features)

FeatureStatus**Three Isolation Modes**Single-DB (tenant\_id scoping), Schema-per-Tenant, Database-per-Tenant**Five Resolution Strategies**Domain, Subdomain, HTTP Header, URL Path, Query Parameter + Chain**Tenant Context**Static per-request holder with scoped `run()` execution**Entity Scoping**`#[BelongsToTenant]` attribute, automatic WHERE injection, cross-tenant protection**PSR-15 Middleware**Auto-resolution → status check → driver activation → cleanup**Lifecycle Management**Create, suspend, activate, delete with schema/DB provisioning**Tenant-Aware Cache**Transparent key prefixing: `tenant:{id}:`**Tenant-Aware Queue**Per-tenant queue names, payload enrichment, context restoration**Tenant-Aware Storage**Path scoping: `tenants/{key}/` with traversal protection**Tenant-Aware Session**Session key prefixing for shared infrastructure**Migration Orchestration**Per-tenant migrations, auto-generated central `tenants` table**Event System**7 lifecycle + resolution events for audit/telemetry**PHP 8.4 Native**Property hooks, backed enums, asymmetric visibilityRequirements
------------

[](#requirements)

- **PHP 8.4** or higher
- `monkeyscloud/monkeyslegion-database` (ConnectionManager)
- `monkeyscloud/monkeyslegion-events` (Event dispatching)
- `psr/http-message` ^2.0
- `psr/http-server-middleware` ^1.0

Installation
------------

[](#installation)

```
composer require monkeyscloud/monkeyslegion-tenancy
```

Architecture
------------

[](#architecture)

```
┌───────────────────────────────────────────────────────────┐
│                     HTTP Request                           │
└─────────────────────────┬─────────────────────────────────┘
                          ▼
┌───────────────────────────────────────────────────────────┐
│            TenantResolverMiddleware (PSR-15)               │
│  ChainResolver: Domain → Subdomain → Header → Path → QP  │
└─────────────────────────┬─────────────────────────────────┘
                          ▼
┌──────────────┐  ┌────────────────┐  ┌─────────────────┐
│ TenantContext │  │ TenancyDriver  │  │  TenantScope    │
│  ::set()      │  │  ::connect()   │  │  WHERE tenant_id│
└──────────────┘  └────────────────┘  └─────────────────┘
                          ▼
┌───────────────────────────────────────────────────────────┐
│                   Application Layer                        │
│  ┌────────┐ ┌────────┐ ┌─────────┐ ┌─────────┐           │
│  │ Cache  │ │ Queue  │ │ Storage │ │ Session │           │
│  │Adapter │ │Adapter │ │ Adapter │ │ Adapter │           │
│  └────────┘ └────────┘ └─────────┘ └─────────┘           │
└───────────────────────────────────────────────────────────┘

```

The package is organized into clear namespaces:

- `Attribute/`: Entity attributes (`#[BelongsToTenant]`)
- `Context/`: Per-request tenant holder (`TenantContext`)
- `Contracts/`: Core interfaces (`TenantInterface`, `TenancyDriverInterface`, `TenantResolverInterface`)
- `Driver/`: Isolation implementations (`SingleDatabaseDriver`, `SchemaDatabaseDriver`, `SeparateDatabaseDriver`)
- `Entity/`: Default `Tenant` entity with property hooks and lifecycle methods
- `Enum/`: Backed enums (`TenancyMode`, `TenantStatus`, `ResolutionStrategy`)
- `Event/`: Lifecycle and resolution events (7 total)
- `Infrastructure/`: Tenant-aware adapters for cache, queue, storage, session
- `Lifecycle/`: Provisioning and management (`TenantManager`)
- `Middleware/`: PSR-15 middleware for automatic resolution
- `Migration/`: Central table creation and per-tenant migration runner
- `Resolver/`: Resolution strategies (Domain, Subdomain, Header, Path, QueryParam, Chain)
- `Scope/`: Automatic query scoping and entity lifecycle hooks

Configuration
-------------

[](#configuration)

Copy the example config to your application's config directory:

```
cp vendor/monkeyscloud/monkeyslegion-tenancy/config/tenancy.mlc config/tenancy.mlc
```

```
tenancy {
    # Isolation mode: single_db, schema, database
    mode = ${TENANCY_MODE:-single_db}

    # Resolution strategies (comma-separated, tried in order)
    resolvers = ${TENANCY_RESOLVERS:-subdomain,header}

    # Base domain for subdomain resolution
    base_domain = ${APP_DOMAIN:-localhost}

    # HTTP header for header-based resolution
    header_name = ${TENANCY_HEADER:-X-Tenant-ID}

    # Tenant prefix for schema/database naming
    tenant_prefix = ${TENANCY_PREFIX:-tenant_}

    # Per-tenant queue isolation
    queue {
        per_tenant = ${TENANCY_QUEUE_PER_TENANT:-true}
        name_template = ${TENANCY_QUEUE_TEMPLATE:-tenant_{tenant_key}}
    }

    # Cache key prefixing
    cache {
        prefix_template = ${TENANCY_CACHE_PREFIX:-tenant:{tenant_id}:}
    }

    # Storage path scoping
    storage {
        path_template = ${TENANCY_STORAGE_PATH:-tenants/{tenant_key}/}
    }

    # Lifecycle automation
    lifecycle {
        auto_migrate = ${TENANCY_AUTO_MIGRATE:-true}
        auto_seed = ${TENANCY_AUTO_SEED:-true}
        backup_on_delete = ${TENANCY_BACKUP_ON_DELETE:-true}
    }
}

```

Isolation Modes
---------------

[](#isolation-modes)

### Single Database (`single_db`)

[](#single-database-single_db)

All tenants share one database. Isolation is achieved via automatic `WHERE tenant_id = :current` clauses injected by `TenantScope`.

Best for: SaaS startups, low-to-medium tenant counts, cost-sensitive deployments.

```
// Mark entities as tenant-scoped
#[Entity(table: 'invoices')]
#[BelongsToTenant]
class Invoice
{
    #[Id]
    public private(set) int $id;

    #[Field(type: 'string')]
    public string $title;

    // tenant_id column is auto-managed — you don't touch it
}
```

### Schema per Tenant (`schema`)

[](#schema-per-tenant-schema)

Each tenant gets a dedicated PostgreSQL schema (or MySQL database). The `SchemaDatabaseDriver` switches `search_path` / `USE` per request.

Best for: Mid-size SaaS, compliance-sensitive industries, moderate isolation needs.

```
// Automatically switches to tenant schema on each request
// PostgreSQL: SET search_path TO "tenant_acme", public
// MySQL:      USE `tenant_acme`
```

### Database per Tenant (`database`)

[](#database-per-tenant-database)

Each tenant gets a fully separate database. The `SeparateDatabaseDriver` routes to a dedicated `ConnectionInterface` per tenant.

Best for: Enterprise SaaS, maximum isolation, regulated industries (HIPAA, SOC2).

Tenant Resolution
-----------------

[](#tenant-resolution)

The middleware resolves tenants by trying resolvers in the order configured:

```
// 1. Subdomain: "acme.example.com" → tenant key "acme"
// 2. HTTP Header: X-Tenant-ID: acme
// 3. URL Path: /t/acme/dashboard
// 4. Full Domain: custom-domain.com → tenants.domain match
// 5. Query Parameter: ?tenant=acme
```

### Example: Subdomain Resolution

[](#example-subdomain-resolution)

With `base_domain = "example.com"`:

Request HostResolved Tenant Key`acme.example.com``acme``globex.example.com``globex``example.com``null` (central context)`nested.sub.example.com``null` (nested not supported)Tenant Context
--------------

[](#tenant-context)

The `TenantContext` is the central access point for the current tenant:

```
use MonkeysLegion\Tenancy\Context\TenantContext;

// Set by middleware automatically, but can be used manually
TenantContext::set($tenant);

// Quick access
$tenant = TenantContext::get();        // ?TenantInterface
$id     = TenantContext::id();         // int|string|null
$key    = TenantContext::key();        // ?string
$tenant = TenantContext::require();    // throws if not resolved

// Check
if (TenantContext::isResolved()) {
    // Inside a tenant context
}

// Scoped execution — restores previous context after callback
TenantContext::run($otherTenant, function () {
    // All operations here are scoped to $otherTenant
    $invoices = $repo->findAll(); // WHERE tenant_id = $otherTenant->getId()
});
// Previous tenant context restored here
```

Entity Scoping
--------------

[](#entity-scoping)

### Automatic WHERE Injection

[](#automatic-where-injection)

```
use MonkeysLegion\Tenancy\Scope\TenantScope;

// Before your query, apply tenant scoping
$result = TenantScope::apply(
    "SELECT * FROM invoices WHERE status = :status",
    ['status' => 'paid'],
);
// Result: "SELECT * FROM invoices WHERE tenant_id = :__tenant_scope_id AND status = :status"
// Params: ['status' => 'paid', '__tenant_scope_id' => 42]

$stmt = $conn->query($result['sql'], $result['params']);
```

### Automatic Insert Data

[](#automatic-insert-data)

```
$data = TenantScope::insertData();
// Returns: ['tenant_id' => 42]
// Merge into your INSERT data to auto-set the tenant column
```

### Cross-Tenant Validation

[](#cross-tenant-validation)

```
// Validates a row belongs to the current tenant — throws on mismatch
TenantScope::validate($row);
```

### Entity Lifecycle Listener

[](#entity-lifecycle-listener)

```
use MonkeysLegion\Tenancy\Scope\TenantScopeListener;

// Auto-inject tenant_id on INSERT
$data = TenantScopeListener::beforeInsert($entity, $data);

// Validate before UPDATE/DELETE
TenantScopeListener::beforeMutation($entity, $data);
```

Lifecycle Management
--------------------

[](#lifecycle-management)

The `TenantManager` provides a complete provisioning pipeline:

```
use MonkeysLegion\Tenancy\Lifecycle\TenantManager;
use MonkeysLegion\Tenancy\Enum\TenancyMode;

$manager = $container->get(TenantManager::class);

// Create a new tenant (auto-provisions schema, runs migrations, activates)
$tenant = $manager->create(
    key: 'acme',
    name: 'Acme Corporation',
    mode: TenancyMode::Schema,
    plan: 'enterprise',
    domain: 'acme.example.com',
    migrationSqls: [
        'CREATE TABLE invoices (...)',
        'CREATE TABLE projects (...)',
    ],
);

// Suspend (e.g., payment overdue)
$manager->suspend($tenant, reason: 'Payment overdue — invoice #1234');

// Reactivate
$manager->activate($tenant);

// Soft delete (sets status = 'deleted')
$manager->delete($tenant);

// Hard delete (drops schema/DB, removes row)
$manager->delete($tenant, hard: true);

// List all active tenants
$tenants = $manager->all();

// Find by key
$tenant = $manager->findByKey('acme');
```

### Auto-Generated Central Table

[](#auto-generated-central-table)

The `tenants` table is created automatically by the package:

```
$migration = $container->get(TenantMigrationRunner::class);
$migration->ensureCentralTable();
```

This creates:

```
CREATE TABLE tenants (
    id            INTEGER PRIMARY KEY AUTO_INCREMENT,
    `key`         VARCHAR(64) NOT NULL UNIQUE,
    name          VARCHAR(255) NOT NULL,
    domain        VARCHAR(255) DEFAULT NULL,
    database_name VARCHAR(128) DEFAULT NULL,
    schema_name   VARCHAR(128) DEFAULT NULL,
    plan          VARCHAR(64) NOT NULL DEFAULT 'free',
    status        VARCHAR(32) NOT NULL DEFAULT 'pending',
    mode          VARCHAR(32) NOT NULL DEFAULT 'single_db',
    metadata      JSON DEFAULT NULL,
    created_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```

Tenant-Aware Infrastructure
---------------------------

[](#tenant-aware-infrastructure)

### Cache

[](#cache)

```
use MonkeysLegion\Tenancy\Infrastructure\TenantCacheAdapter;

$adapter = $container->get(TenantCacheAdapter::class);

$cacheKey = $adapter->key('user:42');
// → "tenant:5:user:42"

$tag = $adapter->tag('reports');
// → "tenant_5:reports"

$pattern = $adapter->flushPattern();
// → "tenant:5:*"
```

### Queue (Per-Tenant Isolation)

[](#queue-per-tenant-isolation)

```
use MonkeysLegion\Tenancy\Infrastructure\TenantQueueAdapter;

$adapter = $container->get(TenantQueueAdapter::class);

$queueName = $adapter->queueName();
// → "tenant_acme"

// Enrich job payload with tenant metadata
$payload = $adapter->enrichPayload(['job' => 'SendInvoice', 'invoice_id' => 99]);
// → ['job' => 'SendInvoice', 'invoice_id' => 99, '__tenant_id' => 5, '__tenant_key' => 'acme']

// Restore tenant context in the worker
$info = $adapter->extractTenantFromPayload($payload);
// → ['tenant_id' => 5, 'tenant_key' => 'acme']
```

### Storage (Path Scoping)

[](#storage-path-scoping)

```
use MonkeysLegion\Tenancy\Infrastructure\TenantStorageAdapter;

$adapter = $container->get(TenantStorageAdapter::class);

$path = $adapter->path('uploads/logo.png');
// → "/app/storage/tenants/acme/uploads/logo.png"

$root = $adapter->tenantRoot();
// → "/app/storage/tenants/acme/"

// Path traversal protection — this throws:
$adapter->path('../../etc/passwd'); // RuntimeException!
```

### Session

[](#session)

```
use MonkeysLegion\Tenancy\Infrastructure\TenantSessionAdapter;

$key = TenantSessionAdapter::key('cart_items');
// → "t5:cart_items"

$name = TenantSessionAdapter::sessionName();
// → "MLSESSID_t5"
```

Events
------

[](#events)

All lifecycle and resolution events extend `MonkeysLegion\Events\Event`:

EventDispatched When`TenantResolved`Tenant identified from request (includes resolver class)`TenantNotFound`Resolution failed (includes host + path)`TenantSwitched`Context changed from one tenant to another`TenantCreated`New tenant provisioned`TenantSuspended`Tenant suspended (includes reason)`TenantActivated`Tenant reactivated`TenantDeleted`Tenant deleted (includes ID + key)```
// Listen to tenant events for audit/telemetry
$dispatcher->listen(TenantResolved::class, function (TenantResolved $event) {
    $logger->info("Tenant resolved: {$event->tenant->getKey()} via {$event->resolverClass}");
});

$dispatcher->listen(TenantSuspended::class, function (TenantSuspended $event) {
    $notifier->alertAdmin("Tenant {$event->tenant->getName()} suspended: {$event->reason}");
});
```

Middleware Setup
----------------

[](#middleware-setup)

Register the middleware in your HTTP pipeline:

```
// In your middleware configuration
$pipeline->pipe(TenantResolverMiddleware::class);

// The middleware automatically:
// 1. Resolves tenant via ChainResolver
// 2. Returns 404 if no tenant found
// 3. Returns 503 if tenant is suspended
// 4. Sets TenantContext
// 5. Activates the tenancy driver (connect)
// 6. Adds tenant to request attributes
// 7. Cleans up after response (disconnect + context reset)
```

Tenant Entity
-------------

[](#tenant-entity)

The default `Tenant` entity uses PHP 8.4 property hooks:

```
use MonkeysLegion\Tenancy\Entity\Tenant;
use MonkeysLegion\Tenancy\Enum\TenancyMode;

// Factory creation
$tenant = Tenant::create('acme', 'Acme Corp', TenancyMode::Schema, 'enterprise');

// Property hooks — computed on access
$tenant->isActive;     // bool (delegates to status->isOperational())
$tenant->isSuspended;  // bool
$tenant->status;       // TenantStatus enum
$tenant->mode;         // TenancyMode enum
$tenant->displayName;  // name or key fallback

// Lifecycle actions
$tenant->activate();
$tenant->suspend();
$tenant->archive();
$tenant->markDeleted();

// Metadata
$tenant->setMetadata('max_users', 50);
$tenant->getMetadataValue('max_users'); // 50
```

Security Posture
----------------

[](#security-posture)

- **Cross-tenant protection** — `TenantScope::validate()` prevents accessing rows from other tenants
- **Path traversal prevention** — `TenantStorageAdapter` rejects `../` paths
- **Suspended tenant blocking** — middleware returns 503 for inactive tenants
- **Automatic cleanup** — `try/finally` ensures driver disconnect on all code paths
- **Scoped execution** — `TenantContext::run()` guarantees context restoration

Testing
-------

[](#testing)

```
composer test
composer phpstan
```

License
-------

[](#license)

MIT © [MonkeysCloud](https://monkeys.cloud)

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance86

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

70d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/2913369?v=4)[Jorge Peraza](/maintainers/yorchperaza)[@yorchperaza](https://github.com/yorchperaza)

---

Top Contributors

[![yorchperaza](https://avatars.githubusercontent.com/u/2913369?v=4)](https://github.com/yorchperaza "yorchperaza (1 commits)")

---

Tags

isolationb2bsaasmulti-tenanttenancymonkeyslegiondatabase-per-tenantschema-per-tenant

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/monkeyscloud-monkeyslegion-tenancy/health.svg)

```
[![Health](https://phpackages.com/badges/monkeyscloud-monkeyslegion-tenancy/health.svg)](https://phpackages.com/packages/monkeyscloud-monkeyslegion-tenancy)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.8k19.5M1.8k](/packages/cakephp-cakephp)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[cakephp/authentication

Authentication plugin for CakePHP

1214.1M107](/packages/cakephp-authentication)[sunrise/http-router

A powerful solution as the foundation of your project.

17451.8k11](/packages/sunrise-http-router)[typo3/cms-adminpanel

TYPO3 CMS Admin Panel - The Admin Panel displays information about your site in the frontend and contains a range of metrics including debug and caching information.

115.7M68](/packages/typo3-cms-adminpanel)[pagemachine/searchable

TYPO3 extension to index and search content with Elasticsearch

1139.9k](/packages/pagemachine-searchable)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
