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(3w ago)00MITPHPPHP ^8.4

Since May 16Pushed 3w 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 1w 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

40

—

FairBetter than 86% of packages

Maintenance95

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

24d 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.1M1.7k](/packages/cakephp-cakephp)[eliashaeussler/typo3-solver

Solver - Extends TYPO3's exception handling with AI generated solutions. Problems can also be solved from command line. Several OpenAI parameters are configurable and prompts and solution providers can be customized as desired.

292.1k](/packages/eliashaeussler-typo3-solver)

PHPackages © 2026

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