nuelcyoung/tenantable 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

nuelcyoung/tenantable

最新稳定版本:v1.3.0

Composer 安装命令:

composer require nuelcyoung/tenantable

包简介

Multi tenancy package for CodeIgniter 4 with multiple identification strategies, bootstrap system, and lifecycle events

README 文档

README

A robust multitenant package for CodeIgniter 4 that provides flexible tenant identification and automatic tenant isolation.

Features

  • Flexible Tenant Identification — Subdomain, domain, path, or request data
  • Multiple Isolation Strategies — Row-level, table prefix, or database-per-tenant
  • Automatic Provisioning — Database auto-created and migrated on tenant creation
  • Third-Party Migration Support — Run Shield (or any package) migrations per-tenant automatically
  • Automatic Tenant Context — Models automatically respect tenant boundaries
  • Superadmin Bypass — Built-in support for platform admins
  • CLI Support — Create tenants, scaffold migrations/models, fan-out commands

Requirements

  • PHP 8.1+
  • CodeIgniter 4.0+

Architecture Options

This package supports 3 isolation strategies:

Strategy How It Works Pros Cons
tenant_id Shared tables with tenant_id column Simple to implement Risk of leakage
Table Prefix Separate tables per tenant (tenant_1_students) No leakage possible More complex setup
Separate DB Different database per tenant Complete isolation Most complex

Installation

composer require nuelcyoung/tenantable

Tenant Identification

Tenantable identifies which tenant a request belongs to using filters. You choose your strategy by applying the corresponding filter to your routes.

Available Strategies

Filter Alias Class Identifies by Example
tenant / tenant_subdomain SubdomainFilter URL subdomain acme.example.com
tenant_domain DomainFilter Custom domain (stored in tenants.domain) acme.com
tenant_domain_or_subdomain DomainOrSubdomainFilter Domain first, falls back to subdomain acme.com or acme.example.com
tenant_path PathFilter First URL path segment /acme/dashboard
tenant_request RequestDataFilter Header, query param, or body field X-Tenant-ID: acme

How to Configure

Register your chosen filter in app/Config/Filters.php:

// Option A: Apply globally
public array $globals = [
    'before' => [
        'tenant_subdomain' => ['except' => ['health', 'api/*']],
    ],
];

// Option B: Apply per route group (you can mix strategies)
// In Routes.php:
$routes->group('app', ['filter' => 'tenant_subdomain'], function ($routes) {
    // Web routes identified by subdomain
});
$routes->group('api', ['filter' => 'tenant_request'], function ($routes) {
    // API routes identified by header/query param
});

All filters are auto-registered by the package. The default tenant alias maps to SubdomainFilter.

Strategy 1: Table Prefix (Recommended)

Best for: Most applications. No tenant_id leakage risks.

How It Works

students table → tenant_1_students, tenant_2_students, ...
classes table → tenant_1_classes, tenant_2_classes, ...

Setup

  1. Run Migration
php spark migrate -g tenantable
  1. Configure Filters
// app/Config/Filters.php
public array $globals = [
    'before' => [
        'tenant_subdomain' => ['except' => ['health', 'api/*']],
    ],
];
  1. Use the Model
use nuelcyoung\tenantable\Traits\TenantTablePrefixModel;

class StudentModel extends TenantTablePrefixModel
{
    protected $table = 'students';
}

// Automatic: queries tenant_1_students when tenant_id = 1
$students = $studentModel->findAll();

Configuration

// app/Config/Tenantable.php
public $prefixFormat = 'tenant_{id}_{table}'; // Default format
public $baseDomain = 'example.com'; // Your production domain (or 'myapp.test' for local dev)

Strategy 2: Shared Database with tenant_id

Best for: Simple applications, few tenants.

Setup

  1. Add tenant_id to tables
php spark make:migration add_tenant_id
  1. Use the Trait
use nuelcyoung\tenantable\Traits\TenantableTrait;

class StudentModel extends Model
{
    use TenantableTrait;
    protected $table = 'students';
}

Warning: Leakage Risks

Using tenant_id has security concerns:

  • Forgetting to add trait to a model
  • Raw SQL queries bypassing trait
  • Joins missing tenant_id
  • IDOR attacks

Use TenantTablePrefixTrait instead to eliminate these risks.

Strategy 3: Separate Database Per Tenant

Best for: Enterprise, strict compliance needs.

Configuration

// app/Config/Tenantable.php
public bool   $separateDatabasePerTenant = true;
public ?string $isolationMode            = 'database';

// Point to your tenant-specific migrations
public ?string $tenantMigrationsNamespace = 'App\Database\Migrations\Tenant';

// Auto-provisioning (both true by default)
public bool $autoCreateDatabase = true;   // CREATE DATABASE on tenant insert
public bool $autoMigrateTenant  = true;   // Run migrations after creation

// Optional: custom database naming convention (default: tenant_{id})
public $databaseNameGenerator = null;

// Include third-party package migrations per-tenant (e.g. Shield)
public array $tenantMigrationsNamespaces = [
    'CodeIgniter\Shield',    // Shield's auth tables in every tenant DB
];

How It Works

The database name is derived dynamically — it is never stored in the tenants table. By default the convention is tenant_{id} (e.g. tenant_1, tenant_5).

When you create a tenant:

php spark tenants:create acme "Acme Corp"

Or programmatically:

$tenantModel->insert(['name' => 'Acme Corp', 'subdomain' => 'acme']);

The package automatically:

  1. Inserts the row into the tenants table
  2. Creates the database: CREATE DATABASE IF NOT EXISTS tenant_1
  3. Runs your tenant migrations (from $tenantMigrationsNamespace)
  4. Runs third-party migrations (from $tenantMigrationsNamespaces — e.g. Shield)
  5. Fires the tenantCreated event

On each request, the filter identifies the tenant and swaps Config\Database::$default to point at the tenant's database. All models transparently query the correct DB.

DB Credentials

Connection credentials (host, user, password, port) come from your .env / Config\Database::$default. Only the database name changes per tenant. Your DB user must have CREATE privileges.

Credentials are never stored in the tenants table — storing secrets inside the database they unlock is a security foot-gun.

Custom Naming

// Default: tenant_1, tenant_2, ...
public $databaseNameGenerator = null;

// Custom: myapp_acme, myapp_globex, ...
public $databaseNameGenerator = fn(array $tenant) => 'myapp_' . $tenant['subdomain'];

Usage

// Automatically switches to tenant's database
$school = tenant(); // Connects to tenant_1
$students = $studentModel->findAll(); // Queries tenant_1.students

Usage Examples

Helper Functions

// Get current tenant ID
$tenantId = tenant_id();

// Get tenant data
$tenant = tenant();

// Check if tenant context exists
if (has_tenant()) {
    // Safe to query
}

// Generate tenant URL
$url = tenant_url('dashboard');

// Check if admin can bypass
if (can_bypass_tenant()) {
    // Access all tenants
}

Manual Tenant Setting

use nuelcyoung\tenantable\Services\TenantManager;
use nuelcyoung\tenantable\Services\TenantTableManager;

TenantManager::getInstance()->setTenantById(1);
TenantTableManager::getInstance()->setTenant(1, 'school1');

Bypassing (Superadmin)

// Temporarily bypass for specific query
Model::withoutTenant(function() {
    return Model::findAll(); // All tenants
});

// Or
Model::enableTenantBypass();
// queries...
Model::disableTenantBypass();

CLI Commands

Command Purpose
tenants:setup Provision the central tenants table
tenants:create <subdomain> <name> Create a tenant (auto-provisions DB in database mode)
tenants:list List tenants (--active, --inactive)
tenants:run <command> Run a Spark command per tenant (auto-targets tenant DB in database mode)
tenants:make-model <name> Scaffold a tenant or global model
tenants:make-migration <name> Scaffold a tenant migration file

Examples

# Initial setup
php spark tenants:setup

# Create tenants
php spark tenants:create foodblog "Food Blog"
php spark tenants:create acme "Acme Corp" --domain=acme.com

# Scaffold migrations
php spark tenants:make-migration CreatePostsTable --table=posts
php spark tenants:make-migration CreateCategoriesTable

# Scaffold models
php spark tenants:make-model Post
php spark tenants:make-model Post --prefix --table=posts

# Run migrations on all tenant databases (database mode)
php spark tenants:run migrate

# Rollback specific tenants
php spark tenants:run migrate:rollback --tenants=1,3

Package Structure

src/
├── Config/
│   └── Tenantable.php
├── Database/
│   └── Migrations/
│       └── CreateTenantsTable.php
├── Exceptions/
│   ├── TenantInactiveException.php
│   └── TenantNotFoundException.php
├── Filters/
│   ├── BaseTenantFilter.php
│   ├── SubdomainFilter.php
│   ├── DomainFilter.php
│   ├── DomainOrSubdomainFilter.php
│   ├── PathFilter.php
│   ├── RequestDataFilter.php
│   └── TenantFilter.php
├── Helpers/
│   └── tenantable_helper.php
├── Middleware/
│   └── TenantSecurityMiddleware.php
├── Models/
│   ├── GlobalModel.php
│   ├── TenantModel.php
│   └── TenantableModel.php
├── Services/
│   ├── TenantManager.php
│   ├── TenantDatabaseManager.php
│   └── TenantTableManager.php
└── Traits/
    ├── TenantTablePrefixTrait.php
    └── TenantableTrait.php

Database Schema

The tenants table:

Field Type Description
id INT Primary key (auto-increment)
subdomain VARCHAR(50) Unique subdomain for SubdomainFilter
domain VARCHAR(255) Custom domain for DomainFilter
name VARCHAR(255) Display name
is_active BOOLEAN Tenant status
settings JSON Custom key-value settings
created_at DATETIME Created timestamp
updated_at DATETIME Updated timestamp

Note: The database name is not stored in the table. It is derived at runtime via Config\Tenantable::$databaseNameGenerator (default: tenant_{id}).

Migration for Existing Apps

Option A: Table Prefix (Recommended)

  1. Create tenants in tenants table
  2. Create new prefixed tables for each tenant:
    • tenant_1_students (copy of students)
    • tenant_2_students
  3. Delete old shared tables
  4. Update models to use TenantTablePrefixModel

Option B: Add tenant_id

  1. Add tenant_id column to all tables
  2. Backfill with correct tenant IDs
  3. Use TenantableTrait in models

Security Features

  • TenantContext Middleware - Enforces tenant on all requests
  • IDOR Protection - Validates tenant_id in requests
  • Global Table Protection - Mark tables as exempt from prefixing
  • Audit Logging - Log bypass attempts

Local Development

When developing locally with Laravel Herd, Valet, or similar tools that serve sites under .test / .local TLDs, subdomain-based tenancy works out of the box — just set baseDomain to your local domain:

// app/Config/Tenantable.php
public string $baseDomain = 'myapp.test';

Or via environment variable:

TENANT_BASE_DOMAIN = myapp.test

Then access tenants at acme.myapp.test, demo.myapp.test, etc.

DNS for Subdomains

Platform Wildcard DNS Setup
macOS (Herd/Valet) ✅ Built-in No extra setup needed
Windows (Herd) ❌ Not built-in Add entries to hosts file, or install Acrylic DNS Proxy for *.myapp.test wildcard
Linux ❌ Not built-in Use dnsmasq with address=/.myapp.test/127.0.0.1

See SETUP.md — Local Development for detailed instructions.

License

MIT

统计信息

  • 总下载量: 10
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-02-23

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固