quvel/tenant
Composer 安装命令:
composer require quvel/tenant
包简介
Multi-tenant support for the Quvel framework
README 文档
README
Multi-tenant support for Laravel applications with flexible tenant resolution, database isolation, and service scoping.
Installation
Step 1: Install the Package
Install from Packagist:
composer require quvel/tenant
Step 2: Publish Configuration
Publish the configuration file to config/tenant.php:
php artisan vendor:publish --tag=tenant-config
Step 3: Publish and Run Migrations
Publish the migrations:
php artisan vendor:publish --tag=tenant-migrations
This publishes:
2024_01_01_000000_create_tenants_table- Main tenants table2024_01_02_000000_add_tenant_id_to_tables- Adds tenant_id to existing tables
Run the migrations:
php artisan migrate
Step 4: Configure Environment Variables
Add to your .env file:
# Tenant Resolver TENANT_RESOLVER=Quvel\Tenant\Resolvers\DomainResolver TENANT_RESOLVER_ENABLE_CACHE=false TENANT_RESOLVER_CACHE_TTL=300 # Tenant Configuration TENANT_AUTO_MIDDLEWARE=true TENANT_DEFAULT_PIPELINE=config,database,mail,filesystem # Database per Tenant (optional) # Set to true if each tenant has its own database TENANT_SEPARATE_DATABASES=false # Admin UI (optional) TENANT_ADMIN_ENABLED=false
Step 5: Update User Model
Add the tenant relationship to your User model:
use Quvel\Tenant\Concerns\BelongsToTenant; class User extends Authenticatable { use BelongsToTenant; // Your existing code... }
Step 6: Verify Installation
Check that the package is discovered:
php artisan package:discover
You should see quvel/tenant in the list.
Test tenant functionality:
php artisan tinker >>> Quvel\Tenant\Models\Tenant::count() >>> Quvel\Tenant\Facades\TenantContext::isActive()
Optional: Publish Additional Assets
# Publish language files
php artisan vendor:publish --tag=tenant-lang
Advanced: Custom Route Files
By default, routes are autoloaded from the package. For advanced customization, you can publish and customize route files:
# Publish tenant config API routes (tenant-info endpoints) php artisan vendor:publish --tag=tenant-config-routes # Publish tenant admin routes php artisan vendor:publish --tag=tenant-admin-routes
To use your published routes instead of the package defaults:
- Publish the route file(s) you want to customize (commands above)
- Disable auto-loading in
config/tenant.php:// For tenant config API routes 'api' => [ 'enabled' => false, // Disable autoloading // ... other settings ], // For tenant admin routes 'admin' => [ 'enabled' => false, // Disable autoloading // ... other settings ],
- Customize your published route files in
routes/tenant-info.phporroutes/tenant-admin.php
Note: Most users don't need to publish routes. Use config settings to customize prefixes, middleware, and route names.
Troubleshooting
Package isn't discovered:
- Clear bootstrap cache:
rm bootstrap/cache/*.php - Run:
php artisan package:discover
Migration conflicts:
If you get "table already exists" errors, the tables may have been created by another package (like quvel/core). Mark them as migrated:
php artisan tinker DB::table('migrations')->insert([ ['migration' => '2024_01_01_000000_create_platform_settings_table', 'batch' => 2], ['migration' => '2024_01_01_000001_create_user_devices_table', 'batch' => 2], ]);
Tenant not resolving:
- Check
TENANT_RESOLVERclass is correct - Verify middleware is enabled:
TENANT_AUTO_MIDDLEWARE=true - Check domain configuration in tenants table
Configuration
Tenant Model
'model' => \Quvel\Tenant\Models\Tenant::class,
Configures which model class to use for tenant operations. This allows you to extend the base tenant model with custom functionality while keeping the package code model-agnostic.
How it works:
- Used by
tenant_model()helper function to create new tenant instances - Used by
TenantFactoryfor testing and seeding - Used by resolvers when querying tenants from the database
- Enables model customization without modifying package code
Usage example:
// Custom tenant model class CustomTenant extends \Quvel\Tenant\Models\Tenant { public function customMethod() { // Your custom logic } } // In config/tenant.php 'model' => \App\Models\CustomTenant::class,
The tenant_model() helper function returns a new instance of the configured model:
$tenant = tenant_model(); // Returns new instance of configured model class $tenant->name = 'New Tenant'; $tenant->save();
Middleware Configuration
'middleware' => [ 'auto_register' => env('TENANT_AUTO_MIDDLEWARE', true), 'internal_request' => 'tenant.is-internal', ],
auto_register: Controls automatic tenant middleware registration on ALL HTTP requests.
true(default):TenantMiddlewareis automatically prepended to the global middleware stack, running on every requestfalse: Manual middleware registration required - add'tenant'middleware to specific routes/groups
How it works:
// When auto_register = true // Middleware runs automatically on all requests // When auto_register = false, manually add middleware: Route::middleware(['tenant'])->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); });
internal_request: Middleware alias for protecting internal/admin endpoints.
- Default value:
'tenant.is-internal' - Maps to
RequireInternalTenant::classmiddleware - Used by admin routes to ensure only internal tenants can access admin functionality
- Applied automatically to tenant admin UI routes when enabled
Usage in the package:
// Admin routes automatically protected with internal_request middleware Route::middleware(config('tenant.middleware.internal_request')) ->group(__DIR__.'/../routes/tenant-admin.php');
Tenant Resolver Configuration
'resolver' => [ 'class' => env('TENANT_RESOLVER', \Quvel\Tenant\Resolvers\DomainResolver::class), 'config' => [ 'cache_enabled' => env('TENANT_RESOLVER_ENABLE_CACHE', false), 'cache_ttl' => env('TENANT_RESOLVER_CACHE_TTL', 300), ], ],
class: The resolver class that determines how tenants are identified from HTTP requests.
- Default:
DomainResolver- resolves tenants by domain name (tenant.example.com) - Must implement
TenantResolverinterface - Receives config array in constructor
How it works:
// DomainResolver extracts host from request $identifier = $request->getHost(); // e.g., "tenant.example.com" $tenant = TenantModel::findByIdentifier($identifier);
config.cache_enabled: Controls whether resolved tenants are cached.
false(default): No caching - resolver called on every requesttrue: Enables caching using resolver'sgetCacheKey()method
config.cache_ttl: Cache time-to-live in seconds.
300(default): Cache resolved tenants for 5 minutes0: Cache forever (until manually cleared)- Only used when
cache_enabled = true
Caching behavior:
// When cache_enabled = true Cache::remember("tenant.{$cacheKey}", $cacheTtl, function() { return $resolver->resolve($request); });
Available Resolvers:
DomainResolver: Resolve by full domain (tenant.example.com)
Custom Resolver Example:
class SubdomainResolver implements TenantResolver { public function resolve(Request $request) { $host = $request->getHost(); $subdomain = explode('.', $host)[0]; return TenantModel::findByIdentifier($subdomain); } public function getCacheKey(Request $request): ?string { return explode('.', $request->getHost())[0]; } }
Tenant Not Found Handling
'not_found' => [ 'strategy' => env('TENANT_NOT_FOUND_STRATEGY', 'abort'), 'config' => [ // For 'redirect' strategy 'redirect_url' => env('TENANT_NOT_FOUND_REDIRECT', '/'), // For 'custom' strategy 'handler' => null, // Invokable class name ], ],
strategy: Defines what happens when no tenant is found for a request.
'abort'(default): ThrowsNotFoundHttpException(returns 404 Not Found)'redirect': Redirects to a specified URL'custom': Calls a custom handler class or callable
How it works in TenantMiddleware:
protected function handleTenantNotFound(Request $request): never { $strategy = config('tenant.not_found.strategy'); $config = config('tenant.not_found.config', []); match ($strategy) { 'redirect' => redirect($config['redirect_url'] ?? '/')->send(), 'custom' => $this->callCustomHandler($config['handler'] ?? null, $request), default => throw new NotFoundHttpException('Tenant not found'), }; }
config.redirect_url: URL for redirect strategy.
- Default:
'/'- redirects to homepage - Environment:
TENANT_NOT_FOUND_REDIRECT - Only used when
strategy = 'redirect'
config.handler: Custom handler for 'custom' strategy.
- Must be an invokable class name or callable
- Receives the
Requestobject - Example:
\App\Handlers\TenantNotFoundHandler::class
Custom Handler Example:
class TenantNotFoundHandler { public function __invoke(Request $request) { // Custom logic - log, redirect, show special page, etc. return response()->view('tenant-not-found', [], 404); } } // In config 'not_found' => [ 'strategy' => 'custom', 'config' => [ 'handler' => \App\Handlers\TenantNotFoundHandler::class, ], ],
Tenant Config API
'api' => [ 'allow_public_config' => env('TENANT_ALLOW_PUBLIC_CONFIG', false), 'allow_protected_config' => env('TENANT_ALLOW_PROTECTED_CONFIG', false), ],
Controls access to tenant configuration API endpoints. These endpoints expose tenant config for frontend applications and SSR.
allow_public_config: Enables the public config API endpoint.
false(default): Public config API disabled - throws 404true: Enables/tenant-info/publicendpoint- Returns only config marked with
ConfigVisibility::PUBLIC - No authentication required
allow_protected_config: Enables the protected config API endpoint.
false(default): Protected config API disabled - throws 404true: Enables/tenant-info/protectedendpoint- Returns config marked with
ConfigVisibility::PUBLICandConfigVisibility::PROTECTED - Protected by
internal_requestmiddleware
Available API Endpoints:
// Public config (when allow_public_config = true) GET /tenant-info/public // Returns: { data: { config: { /* public config only */ } } } // Protected config (when allow_protected_config = true) GET /tenant-info/protected // Returns: { data: { config: { /* public + protected config */ } } } // Cache endpoint (always requires internal middleware) GET /tenant-info/cache // Returns: Collection of all tenant configs for SSR
How it works:
// In TenantPublicConfig action public function __invoke($tenant): TenantConfigResource { $allowPublicConfig = config('tenant.api.allow_public_config', false); if ($allowPublicConfig !== true) { throw new NotFoundHttpException('API not enabled for this tenant'); } return new TenantConfigResource($tenant)->setVisibilityLevel('public'); }
Config Visibility Levels:
ConfigVisibility::PRIVATE: Never exposed via APIConfigVisibility::PROTECTED: Exposed in protected endpoint onlyConfigVisibility::PUBLIC: Exposed in both public and protected endpoints
Use Cases:
allow_public_config: Frontend needs app name, theme colors, public settingsallow_protected_config: SSR needs database config, mail settings, API keys
Configuration Pipes
'pipes' => [ \Quvel\Tenant\Pipes\CoreConfigPipe::class, \Quvel\Tenant\Pipes\BroadcastingConfigPipe::class, \Quvel\Tenant\Pipes\CacheConfigPipe::class, \Quvel\Tenant\Pipes\DatabaseConfigPipe::class, \Quvel\Tenant\Pipes\FilesystemConfigPipe::class, \Quvel\Tenant\Pipes\LoggingConfigPipe::class, \Quvel\Tenant\Pipes\MailConfigPipe::class, \Quvel\Tenant\Pipes\QueueConfigPipe::class, \Quvel\Tenant\Pipes\RedisConfigPipe::class, \Quvel\Tenant\Pipes\SessionConfigPipe::class, \Quvel\Tenant\Pipes\ServicesConfigPipe::class, \Quvel\Tenant\Pipes\QuvelCoreConfigPipe::class, // Add your custom pipes here ],
Configuration pipes apply tenant config to Laravel's runtime configuration. Pipes are executed in array order during tenant resolution.
How it works:
// In TenantMiddleware, after tenant is resolved: $this->configPipeline->apply($tenant, config()); // ConfigurationPipeManager loads pipes from config and executes them: foreach ($this->pipes as $pipe) { $pipe->handle($tenant, $config); }
Available Pipes:
- CoreConfigPipe: App settings (
app.name,app.url,app.timezone,frontend.url, CORS) - BroadcastingConfigPipe: Pusher, Reverb, broadcasting drivers and credentials
- CacheConfigPipe: Cache drivers, Redis configuration, prefixes
- DatabaseConfigPipe: Database connections, credentials, isolated databases
- FilesystemConfigPipe: Storage disks, S3 configuration, CDN settings
- LoggingConfigPipe: Log channels, Sentry configuration, log levels
- MailConfigPipe: SMTP settings, mail drivers, from addresses
- QueueConfigPipe: Queue drivers (database, Redis, SQS), worker configuration
- RedisConfigPipe: Redis connections with tenant prefixing
- SessionConfigPipe: Session drivers and tenant isolation
- ServicesConfigPipe: Third-party APIs (Stripe, PayPal, payment gateways)
- QuvelCoreConfigPipe: Enable tenant scoping for the Quvel Core package
Pipe Implementation:
class CoreConfigPipe extends BasePipe { public function apply(): void { $this->setMany([ 'app.name', // Direct 1:1 mapping 'app.url', 'frontend.url', ]); } }
Custom Pipe Example:
class CustomConfigPipe extends BasePipe { public function apply(): void { // Set config if tenant has the value $this->setIfExists('custom.api_key', 'services.custom.key'); // Set multiple configs at once $this->setMany([ 'custom.endpoint', // Direct mapping 'tenant_theme' => 'app.theme', // Custom mapping ]); } }
Execution Context:
- Executed on every request after tenant resolution
- Receives current tenant and Laravel config repository
- Can modify any Laravel configuration at runtime
- Order matters - later pipes can override earlier ones
Tenant Tables Configuration
'tables' => [ // Users table with proper tenant isolation 'users' => [ 'after' => 'id', 'cascade_delete' => true, 'drop_uniques' => [['email']], 'tenant_unique_constraints' => [['email']] ], 'posts' => true, // Simple registration with defaults // 'orders' => \App\Tenant\Tables\OrdersTableConfig::class, ],
Defines which application tables should have tenant_id columns added. These tables get automatically modified by TenantTableManager to support tenant isolation.
How it works:
// Process configured tables to add tenant_id columns $manager = app(\Quvel\Tenant\Managers\TenantTableManager::class); $results = $manager->processTables(); // Result: ['users' => 'processed', 'posts' => 'skipped_exists']
Configuration Options:
Simple Registration:
'table_name' => true, // Uses default settings
Advanced Configuration:
'users' => [ 'after' => 'id', // Insert tenant_id after this column 'cascade_delete' => true, // Cascade delete when tenant is deleted 'drop_uniques' => [['email']], // Drop these unique constraints 'tenant_unique_constraints' => [['email']] // Create tenant-scoped unique constraints ],
Custom Configuration Class:
'orders' => \App\Tenant\Tables\OrdersTableConfig::class, // OrdersTableConfig.php class OrdersTableConfig { public function getConfig(): TenantTableConfig { return new TenantTableConfig( after: 'id', cascadeDelete: true, dropUniques: [['order_number']], tenantUniqueConstraints: [['order_number']] ); } }
What gets added to tables:
tenant_idforeign key column referencingtenants.id- Index on
tenant_idfor query performance - Optional cascade delete constraint
- Tenant-scoped unique constraints (e.g.,
tenant_id + emailunique)
Automatic Service Table Registration: The manager automatically adds system tables when service isolation is enabled:
// When 'queue.auto_tenant_id' = true 'jobs', 'failed_jobs', 'job_batches' // When 'sessions.auto_tenant_id' = true 'sessions' // When 'cache.auto_tenant_id' = true 'cache', 'cache_locks' // When 'password_reset_tokens.auto_tenant_id' = true 'password_reset_tokens'
Processing Results:
'processed': Table successfully updated with tenant_id'skipped_exists': Table already has tenant_id column'skipped_missing': Table doesn't exist in database'error: message': Processing failed with error details
Usage Example:
// Process all configured tables $manager = app(\Quvel\Tenant\Managers\TenantTableManager::class); $results = $manager->processTables(); // Process specific tables only $results = $manager->processTables(['users', 'posts']); // Remove tenant support $results = $manager->removeTenantSupport(['old_table']);
Scoped Models Configuration
'scoped_models' => [ // Add your application models here // \App\Models\Post::class, // \App\Models\Order::class, // Laravel's built-in models // \Illuminate\Notifications\DatabaseNotification::class, // Sanctum tokens (for API authentication per tenant) // \Laravel\Sanctum\PersonalAccessToken::class, // Spatie permissions (enable if roles should be tenant-scoped) // \Spatie\Permission\Models\Role::class, // \Spatie\Permission\Models\Permission::class, ],
Models that should have tenant scoping automatically applied. The package adds global scopes and model events to enforce tenant isolation without requiring code changes.
How it works:
// In TenantServiceProvider::bootExternalModelScoping() foreach ($models as $modelClass) { // Add global scope for automatic WHERE tenant_id filtering $modelClass::addGlobalScope(new Scopes\TenantScope()); // Add model events for automatic tenant_id assignment and validation $modelClass::creating(function ($model) { if (!isset($model->tenant_id) && !tenant_bypassed()) { $model->tenant_id = tenant_id(); } }); $modelClass::updating(function ($model) { $this->validateTenantMatch($model); // Prevent cross-tenant updates }); $modelClass::deleting(function ($model) { $this->validateTenantMatch($model); // Prevent cross-tenant deletes }); }
Automatic Scoping Behavior:
Query Filtering:
// Automatically scoped to current tenant $posts = Post::all(); // WHERE tenant_id = current_tenant_id // Available query macros $posts = Post::forAllTenants()->get(); // Remove tenant filtering $posts = Post::withoutTenantScope()->get(); // Same as forAllTenants() $posts = Post::forTenant(123)->get(); // Filter for specific tenant ID
Model Creation:
// tenant_id automatically assigned $post = Post::create(['title' => 'New Post']); // Results in: INSERT ... (title, tenant_id) VALUES ('New Post', current_tenant_id)
Cross-Tenant Protection:
// Prevents updating/deleting models from different tenants $otherTenantPost = Post::forAllTenants()->where('tenant_id', 999)->first(); $otherTenantPost->update(['title' => 'Hacked']); // Throws TenantMismatchException
Isolated Database Behavior: For tenants using isolated databases, tenant_id scoping is automatically disabled since database-level isolation provides the separation.
No-Tenant Handling: When no tenant context exists:
throw_no_tenant_exception = true: ThrowsNoTenantExceptionthrow_no_tenant_exception = false: Returns empty results (WHERE 1 = 0)
Bypass Mode:
// Admin operations can bypass tenant scoping without_tenant(function () { return Post::all(); // Returns all posts across all tenants }); // Or using TenantContext TenantContext::bypass(); $allPosts = Post::all();
Events Dispatched:
TenantScopeApplied: When scope is applied to a queryTenantScopeNoTenantFound: When no tenant context existsTenantMismatchDetected: When cross-tenant operation is blocked
Scoping Behavior Configuration
'scoping' => [ // Whether to throw NoTenantException when no tenant is found // When false, returns empty results instead of throwing 'throw_no_tenant_exception' => env('TENANT_THROW_NO_TENANT_EXCEPTION', true), // Whether to automatically add tenant_id to model $fillable arrays 'auto_fillable' => env('TENANT_AUTO_FILLABLE', true), // Whether to automatically add tenant_id to model $hidden arrays 'auto_hidden' => env('TENANT_AUTO_HIDDEN', true), // Skip tenant_id scoping for tenants using isolated databases // When true, skip tenant_id scoping for tenants using isolated databases. // When false, always use tenant_id scoping for consistency. 'skip_tenant_id_in_isolated_databases' => env('TENANT_SKIP_WHEN_ISOLATED', false), ],
Configures how tenant scoping behaves across the application.
throw_no_tenant_exception: Controls behavior when no tenant context exists.
true(default): ThrowsNoTenantExceptionwhen trying to query tenant-scoped models without tenant contextfalse: Returns empty results by applyingWHERE 1 = 0condition
How it works:
// In TenantScope::apply() if (!$tenant) { if (config('tenant.scoping.throw_no_tenant_exception', true)) { throw new NoTenantException('No tenant context found for model...'); } // Return empty results $builder->whereRaw('1 = 0'); }
auto_fillable: Automatically adds tenant_id to model $fillable arrays.
true(default): Models usingTenantScopedtrait gettenant_idadded to$fillablefalse: Manual fillable management required
auto_hidden: Automatically adds tenant_id to model $hidden arrays.
true(default): Models usingTenantScopedtrait gettenant_idadded to$hidden(excludes from JSON serialization)false: Manual hidden management required
How it works:
// In TenantScoped::initializeTenantScoped() if (config('tenant.scoping.auto_fillable', true) && !in_array('tenant_id', $this->getFillable(), true)) { $this->fillable[] = 'tenant_id'; } if (config('tenant.scoping.auto_hidden', true) && !in_array('tenant_id', $this->getHidden(), true)) { $this->hidden[] = 'tenant_id'; }
skip_tenant_id_in_isolated_databases: Controls tenant_id scoping for isolated database tenants.
false(default): Always use tenant_id scoping for consistency across all isolation strategiestrue: Skip tenant_id scoping for tenants using isolated databases since database-level isolation provides separation
How it works:
// In TenantScope and model events if ($this->tenantUsesIsolatedDatabase($tenant)) { return; // Skip tenant_id logic - database isolation handles it } // Otherwise apply tenant_id scoping $builder->where('tenant_id', $tenant->id);
Configuration Trade-offs:
throw_no_tenant_exception = false:
- Pros: Prevents exceptions during development/testing
- Cons: Can hide bugs where tenant context is missing
skip_tenant_id_in_isolated_databases = true:
- Pros: Better performance, cleaner database schema for isolated tenants
- Cons: Inconsistent behavior between isolation strategies
auto_fillable/auto_hidden = false:
- Pros: Full control over model configuration
- Cons: Manual configuration required for every tenant-scoped model
Context Preservation
'preserve_context' => env('TENANT_PRESERVE_CONTEXT', true),
Enables automatic tenant context preservation across async operations using Laravel's Context feature. This ensures tenant context is maintained in queued jobs, HTTP requests, and other async operations.
How it works:
// In TenantServiceProvider::registerContextPreservation() Context::dehydrating(static function ($context): void { $tenant = TenantContextFacade::current(); if ($tenant) { $context->addHidden('tenant', $tenant); } }); Context::hydrated(function ($context): void { if ($context->hasHidden('tenant')) { $tenant = $context->getHidden('tenant'); TenantContextFacade::setCurrent($tenant); app(ConfigurationPipeManager::class)->apply( $tenant, $this->app->make(ConfigRepository::class) ); } });
When enabled (true, default):
- Tenant context is automatically serialized when Laravel dehydrates context (queued jobs, etc.)
- Tenant context is restored when Laravel hydrates context in the worker/async process
- Configuration pipes are re-applied to restore tenant-specific settings
- Works across queue drivers (database, Redis, SQS, etc.)
When disabled (false):
- No automatic context preservation
- Async operations lose tenant context
- Manual tenant context management required for jobs
Use Cases:
Queued Jobs:
// With preserve_context = true dispatch(new ProcessOrderJob($order)); // Job automatically has tenant context and config // With preserve_context = false dispatch(new ProcessOrderJob($order, tenant_id())); // Must manually pass tenant_id and restore context
Event Listeners:
// With preserve_context = true event(new OrderCreated($order)); // Event listeners have tenant context // With preserve_context = false // Listeners lose tenant context if queued
HTTP Client Requests:
// With preserve_context = true Http::post('https://api.service.com', $data); // Outbound requests can include tenant context via middleware // With preserve_context = false // Manual context management for external requests
Performance Considerations:
- Minimal overhead - tenant object is serialized/deserialized
- Context data travels with async operations
- Configuration pipes re-run on context restoration
Requirements:
- Laravel 11+ Context feature
- Works with all queue drivers that support Laravel Context
- No additional setup required
Queue Configuration
'queue' => [ // Enable tenant-aware database queue with automatic tenant_id column management 'auto_tenant_id' => env('TENANT_QUEUE_AUTO_TENANT_ID', false), ],
Enables tenant-aware queue behavior for Laravel's database queue driver. When enabled, this automatically adds tenant_id columns to queue tables and scopes queue operations per tenant.
How it works:
// In TenantServiceProvider if (config('tenant.queue.auto_tenant_id', true)) { // Override database queue connector $manager->addConnector('database', function () { return new TenantDatabaseConnector($this->app['db']); }); // Override failed job provider return new TenantDatabaseUuidFailedJobProvider($app['db'], ...); // Override batch repository return new TenantDatabaseBatchRepository(...); }
What gets modified:
Queue Tables (jobs):
// In TenantDatabaseQueue::buildDatabaseRecord() $record = [ 'queue' => $queue, 'payload' => $payload, 'tenant_id' => TenantContext::current()?->id, // Added automatically ];
Failed Jobs Table (failed_jobs):
// TenantDatabaseUuidFailedJobProvider adds tenant_id when jobs fail $failedJob['tenant_id'] = TenantContext::current()?->id;
Job Batches Table (job_batches):
// TenantDatabaseBatchRepository adds tenant_id to batches $batch['tenant_id'] = TenantContext::current()?->id;
Automatic Table Registration:
When enabled, these tables are automatically added to the tables config for TenantTableManager:
// Automatically added tables when auto_tenant_id = true 'jobs' => [ 'after' => 'id', 'cascade_delete' => true, ], 'failed_jobs' => [ 'after' => 'id', 'cascade_delete' => true, ], 'job_batches' => [ 'after' => 'id', 'cascade_delete' => true, ],
Usage Example:
// Jobs automatically get tenant_id when dispatched dispatch(new ProcessOrderJob($order)); // Failed jobs include tenant_id for isolation // Batches are tenant-scoped automatically // Process tables to add tenant_id columns $manager = app(\Quvel\Tenant\Managers\TenantTableManager::class); $results = $manager->processTables(['jobs', 'failed_jobs', 'job_batches']);
Requirements:
- Database queue driver must be configured in
config/queue.php - Queue tables must exist (run
queue:tableand migrate) - Only affects the
databasequeue driver - Other drivers (Redis, SQS) rely on context preservation
Tenant Context in Jobs:
- Jobs automatically retain tenant context via
preserve_context - Queue isolation works alongside context preservation
- Worker processes restore tenant context and apply configuration pipes
Benefits:
- Complete job isolation per tenant
- Failed job isolation and debugging
- Batch operation isolation
- Database-level queue security
Admin Interface Configuration
'admin' => [ 'enable' => env('TENANT_ADMIN_ENABLE', false), ],
Controls the optional admin interface for tenant management. When enabled, provides a web UI and API endpoints for creating and managing tenants using preset configurations.
enable: Enables/disables the admin interface.
false(default): Admin interface disabled for securitytrue: Enables admin routes, views, and API endpoints
How it works:
// In TenantServiceProvider if (config('tenant.admin.enable', false)) { // Register admin routes with internal middleware protection Route::middleware(config('tenant.middleware.internal_request')) ->group(__DIR__.'/../routes/tenant-admin.php'); // Register view namespace for Blade templates $this->loadViewsFrom(__DIR__.'/../resources/views', 'tenant'); }
Available Routes (when enabled):
// UI Routes GET /admin/tenants/ui - Tenant management interface // API Routes (protected by internal middleware) GET /admin/tenants/presets - Get available presets GET /admin/tenants/presets/{preset}/fields - Get form fields for preset GET /admin/tenants - List all tenants POST /admin/tenants - Create new tenant
Admin Interface Features:
Tenant Creation with Presets:
- Basic Preset: Shared database with minimal configuration
- Isolated Database Preset: Dedicated database with connection settings
- Dynamic form generation based on preset requirements
- Real-time field validation and error handling
Tenant Listing:
- View all existing tenants
- Display tenant names, identifiers, and creation dates
- Sortable and filterable interface
API Integration:
// Get available presets GET /admin/tenants/presets // Response: {"presets": {"basic": {...}, "isolated_database": {...}}} // Get form fields for a preset GET /admin/tenants/presets/basic/fields // Response: {"preset": "basic", "fields": [...]} // Create tenant POST /admin/tenants { "name": "Acme Corp", "identifier": "acme.example.com", "preset": "basic", "config": { "app.name": "Acme Application", "frontend.url": "https://acme.example.com" } }
Security Considerations:
Protected by Internal Middleware:
- Admin routes require
internal_requestmiddleware - Only internal/system tenants can access admin functionality
- Prevents external access to tenant management
Default Disabled:
- Admin interface is disabled by default
- Must be explicitly enabled via environment variable
- Recommended to disable in production environments
Environment Configuration:
// Enable admin interface (development/staging only) TENANT_ADMIN_ENABLE=true // Disable admin interface (production - default) TENANT_ADMIN_ENABLE=false
File Structure:
/resources/views/admin/ui.blade.php - Main admin interface
/routes/tenant-admin.php - Admin route definitions
/Http/Controllers/TenantController.php - Admin API endpoints
Use Cases:
- Development/staging tenant setup
- Internal admin panels
- Automated tenant provisioning
- Tenant configuration management
Production Recommendations:
- Keep
enable = falsein production - Use programmatic tenant creation instead
- Implement custom admin interfaces with proper authentication
quvel/tenant 适用场景与选型建议
quvel/tenant 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 8 次下载、GitHub Stars 达 0, 最近一次更新时间为 2025 年 11 月 02 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「laravel」 「saas」 「multi-tenant」 「tenancy」 「quvel」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 quvel/tenant 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 quvel/tenant 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 quvel/tenant 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Laravel Plans is a package for SaaS apps that need management over plans, features, subscriptions, events for plans or limited, countable features.
Asaas.com PHP API v3 Wrapper
Adaptaçao Asaas.com PHP API Wrapper para PHP 5.6
Laravel Tenant Multi-Self-Hosted-DB connections with Migration tools package for Laravel 5.1+
Alfabank REST API integration
This bundle provides tools to build multi-tenant architecture for PHP applications.
统计信息
- 总下载量: 8
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 24
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-11-02