PHPackages                             our-edu/multi-tenant - 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. [Framework](/categories/framework)
4. /
5. our-edu/multi-tenant

ActiveLibrary[Framework](/categories/framework)

our-edu/multi-tenant
====================

Shared multi-tenant infrastructure for OurEdu Laravel services (tenant context, global scope, traits, middleware).

1.1.9(2mo ago)0968↑266.7%MITPHPPHP &gt;=8.1CI passing

Since Jan 18Pushed 2mo agoCompare

[ Source](https://github.com/our-edu/multi-tenant-package)[ Packagist](https://packagist.org/packages/our-edu/multi-tenant)[ Docs](https://github.com/ouredu/multi-tenant)[ RSS](/packages/our-edu-multi-tenant/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (14)Versions (15)Used By (0)

Laravel Multi-Tenant
====================

[](#laravel-multi-tenant)

[![Packagist Version](https://camo.githubusercontent.com/73257b4431a51e99d7343dae273114cd5b77e2a9c186e514b093d2417061d20a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f75722d6564752f6d756c74692d74656e616e742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/our-edu/multi-tenant)[![License](https://camo.githubusercontent.com/c3ee9b5ee3368181920b8153852d246bc6918ed59e8a7ae8c94cff83783f3c19/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6f75722d6564752f6d756c74692d74656e616e742e7376673f7374796c653d666c61742d737175617265)](LICENSE)[![PHP Version](https://camo.githubusercontent.com/11cf3564a1ad6e4407a27c3c07bda7ba14a771a20fcdaf360777f6226496cab7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6f75722d6564752f6d756c74692d74656e616e742e7376673f7374796c653d666c61742d737175617265)](composer.json)[![Laravel Version](https://camo.githubusercontent.com/573a3f023b934f7d13f1003871ccbe3b484154f8dae7632fa3505681f10fc73d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d392e7825323025374325323031302e7825323025374325323031312e782d7265642e7376673f7374796c653d666c61742d737175617265)](composer.json)

A Laravel package for building multi-tenant applications. This package provides tenant context management, automatic query scoping, and model traits for seamless multi-tenancy support.

Features
--------

[](#features)

- **Tenant Context** - Centralized tenant state management across requests, jobs, and commands
- **Automatic Query Scoping** - All queries automatically filtered by tenant
- **Model Trait** - Simple `HasTenant` trait for tenant-aware models
- **Built-in Resolvers** - Session and Header resolvers included
- **Flexible Resolution** - Implement your own tenant resolution strategy
- **Middleware Support** - HTTP middleware for tenant resolution with excluded routes
- **Exception Handling** - Throws exception when tenant cannot be resolved (translatable messages)
- **Auto-assignment** - Automatically sets tenant ID on model creation/update
- **Zero Configuration** - Works out of the box with sensible defaults
- **Customizable** - Override tenant column names per model
- **Queue Support** - Maintain tenant context in queued jobs
- **Command Support** - Run commands for specific tenants
- **Laravel Octane Compatible** - Uses scoped bindings for request isolation

Requirements
------------

[](#requirements)

- PHP 8.1 or higher
- Laravel 9.x, 10.x, or 11.x

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

[](#installation)

Install the package via Composer:

```
composer require our-edu/multi-tenant
```

The package will auto-register its service provider and automatically publish the configuration file.

Quick Start
-----------

[](#quick-start)

### 1. Configure (Optional)

[](#1-configure-optional)

The package uses `ChainTenantResolver` by default, which tries resolvers in order:

1. `UserSessionTenantResolver` - Gets `tenant_id` from `getSession()` helper
2. `DomainTenantResolver` - Gets `tenant_id` by querying tenant table by domain

Configure the session helper in `config/multi-tenant.php`:

```
'session' => [
    'helper' => 'getSession',      // Your helper function name
    'tenant_column' => 'tenant_id', // Column on session object
],
```

### 2. Add Trait to Models

[](#2-add-trait-to-models)

**Option A: Use HasTenant Trait Manually**

Add the `HasTenant` trait to models that should be tenant-scoped:

```
use Illuminate\Database\Eloquent\Model;
use Ouredu\MultiTenant\Traits\HasTenant;

class Project extends Model
{
    use HasTenant;
}
```

**Option B: Use Artisan Command (Recommended)**

Configure your tables and run the command to automatically add the trait:

```
// config/multi-tenant.php
'tables' => [
    'projects' => \App\Models\Project::class,
    'invoices' => \App\Models\Invoice::class,
    'orders' => \App\Models\Order::class,
],
```

```
# Add HasTenant trait to all configured table models
php artisan tenant:add-trait

# Preview changes without modifying files
php artisan tenant:add-trait --dry-run

# Add trait to specific tables only
php artisan tenant:add-trait --table=projects --table=invoices
```

That's it! All queries on configured models will now be automatically scoped to the current tenant.

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

[](#configuration)

The configuration file is automatically published to `config/multi-tenant.php`:

```
return [
    // Your tenant model class (used by DomainTenantResolver)
    'tenant_model' => App\Models\Tenant::class,

    // Default tenant column name
    'tenant_column' => 'tenant_id',

    // Session configuration (for UserSessionTenantResolver)
    'session' => [
        'helper' => 'getSession',     // Helper function name
        'tenant_column' => 'tenant_id',
    ],

    // Header configuration (for HeaderTenantResolver)
    'header' => [
        'name' => 'X-Tenant-ID',      // Header name containing tenant ID
        'routes' => [                  // Routes where header resolution is allowed
            // 'api.external.*',
            // 'api/v1/external/*',
        ],
    ],

    // Excluded routes (bypass tenant resolution in middleware)
    'excluded_routes' => [
        // 'health',
        // 'login',
        // 'password/*',
    ],

    // Domain configuration (for DomainTenantResolver)
    'domain' => [
        'column' => 'domain',
    ],

    // Tables mapped to models (for migration, trait command, and query listener)
    'tables' => [
        // 'users' => \App\Models\User::class,
        // 'orders' => \App\Models\Order::class,
    ],

    // Query listener (logs queries without tenant_id filter)
    'query_listener' => [
        'enabled' => true,
        'log_channel' => null,  // null = default channel
    ],
];
```

Database Migration
------------------

[](#database-migration)

Add `tenant_id` column to your configured tables:

```
# Add tenant_id to all configured tables
php artisan tenant:migrate

# Add tenant_id to specific tables
php artisan tenant:migrate --table=users --table=orders

# Remove tenant_id from tables (rollback)
php artisan tenant:migrate --rollback
```

Query Listener
--------------

[](#query-listener)

The package includes a database query listener that logs errors when queries are executed on tenant tables without a `tenant_id` filter.

### Configuration

[](#configuration-1)

```
'tables' => [
    'users' => \App\Models\User::class,
    'orders' => \App\Models\Order::class,
],

'query_listener' => [
    'enabled' => env('MULTI_TENANT_QUERY_LISTENER_ENABLED', true),
    'log_channel' => env('MULTI_TENANT_QUERY_LISTENER_CHANNEL'),
    'primary_keys' => ['id', 'uuid'],  // Primary key columns to skip
],
```

### Smart Detection

[](#smart-detection)

The query listener is smart about detecting safe queries:

- **Primary Key Operations**: UPDATE/DELETE by `id` or `uuid` are considered safe (model was already loaded with tenant scope)
- **Excluded Models**: Models with `$withoutTenantScope = true` are skipped
- **Configurable Primary Keys**: Add custom primary key columns to `primary_keys` config

### Log Output

[](#log-output)

When a query without tenant filter is detected:

```
{
    "message": "Query executed without tenant_id filter",
    "context": {
        "table": "orders",
        "sql": "SELECT * FROM orders WHERE status = ?",
        "bindings": ["pending"],
        "tenant_id": 1,
        "file": "/app/Http/Controllers/OrderController.php",
        "line": 45
    }
}
```

Usage
-----

[](#usage)

### Tenant Context

[](#tenant-context)

Access the current tenant ID anywhere in your application:

```
use Ouredu\MultiTenant\Tenancy\TenantContext;

$context = app(TenantContext::class);

// Get current tenant ID
$tenantId = $context->getTenantId();

// Check if tenant exists
if ($context->hasTenant()) {
    // ...
}

// Manually set tenant ID (for testing, jobs, commands)
$context->setTenantId($tenantId);

// Run code in tenant context
$context->runForTenant($tenantId, function () {
    // All queries scoped to this tenant
});
```

### Model Trait

[](#model-trait)

```
use Ouredu\MultiTenant\Traits\HasTenant;

class Invoice extends Model
{
    use HasTenant;

    // Optional: custom tenant column
    public function getTenantColumn(): string
    {
        return 'organization_id';
    }
}
```

The trait provides:

- Automatic global scope for tenant filtering
- Automatic tenant ID assignment on create/update
- `tenant()` relationship method
- `scopeForTenant($query, $tenantId)` scope

### Middleware

[](#middleware)

Register and use the tenant middleware:

```
// In bootstrap/app.php or Kernel.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'tenant' => \Ouredu\MultiTenant\Middleware\TenantMiddleware::class,
    ]);
})

// In routes
Route::middleware('tenant')->group(function () {
    Route::resource('projects', ProjectController::class);
});
```

#### Excluded Routes

[](#excluded-routes)

Configure routes that should bypass tenant resolution:

```
// config/multi-tenant.php
'excluded_routes' => [
    'health',           // Exact match
    'api/health',       // Exact path
    'password/*',       // Wildcard pattern
    'auth.*',           // Route name pattern
],
```

#### Exception Handling

[](#exception-handling)

When no resolver can determine the tenant ID, a `TenantNotResolvedException` is thrown. This ensures all non-excluded routes have a valid tenant context.

```
use Ouredu\MultiTenant\Exceptions\TenantNotResolvedException;

// Handle in your exception handler
public function render($request, Throwable $e)
{
    if ($e instanceof TenantNotResolvedException) {
        return response()->json(['error' => 'Tenant not found'], 404);
    }

    return parent::render($request, $e);
}
```

### Header Tenant Resolver

[](#header-tenant-resolver)

For API routes where the tenant ID is passed as a header (e.g., external integrations, webhooks):

```
// config/multi-tenant.php
'header' => [
    'name' => 'X-Tenant-ID',      // Header name
    'routes' => [
        'api.external.*',          // Route name pattern
        'api/v1/webhook/*',        // URI pattern
    ],
],
```

Then send requests with the header:

```
curl -H "X-Tenant-ID: 123" https://api.example.com/api/v1/webhook/process
```

### Queued Jobs

[](#queued-jobs)

For jobs that need tenant context, set the tenant ID in the job:

```
class ProcessInvoice implements ShouldQueue
{
    public ?int $tenantId = null;

    public function __construct(public Invoice $invoice)
    {
        $this->tenantId = app(TenantContext::class)->getTenantId();
    }

    public function handle(): void
    {
        // Restore tenant context
        if ($this->tenantId) {
            app(TenantContext::class)->setTenantId($this->tenantId);
        }

        // Process invoice...
    }
}
```

### Event/Message Listeners

[](#eventmessage-listeners)

For listeners that receive messages with tenant context, use the `SetsTenantFromPayload` trait:

```
use Ouredu\MultiTenant\Traits\SetsTenantFromPayload;

class PaymentCreatedListener
{
    use SetsTenantFromPayload;

    public function handle(PaymentCreatedEvent $event): void
    {
        // Set tenant from message payload
        // Throws TenantNotFoundException if tenant_id not found and fallback disabled
        $this->setTenantFromPayload($event->payload);

        // Now all queries will be tenant-scoped
        $order = Order::find($event->orderId);
    }
}
```

**Automatically Add Trait to Listeners**

Use the artisan command to add `SetsTenantFromPayload` trait to all listeners in a config file:

```
# From a config file (by name, e.g., sqs_events.php in config directory)
php artisan tenant:add-listener-trait --config=sqs_events

# Preview changes without modifying files
php artisan tenant:add-listener-trait --config=sqs_events --dry-run

# From multi-tenant.php config
php artisan tenant:add-listener-trait

# Add trait to specific listener class
php artisan tenant:add-listener-trait --listener="App\Listeners\PaymentCreatedListener"
```

The command supports various config file formats:

- SQS events style: `'event.type' => ListenerClass::class`
- EventServiceProvider style: `['Event' => [ListenerClass::class]]`
- Simple array: `[ListenerClass::class, ...]`

Configure the listener fallback behavior in `config/multi-tenant.php`:

```
'listener' => [
    // Fallback to database when tenant_id not in payload (queries where is_active = true)
    'fallback_to_database' => env('MULTI_TENANT_LISTENER_FALLBACK_DB', false),
],
```

The trait works with both array and object payloads:

- First checks if `tenant_id` exists in the payload
- If not found and `fallback_to_database` is true, queries tenant table where `is_active = true`
- If fallback is disabled or no active tenant found, throws `TenantNotFoundException`

### Artisan Commands

[](#artisan-commands)

Run commands for specific tenants:

```
class GenerateReports extends Command
{
    protected $signature = 'reports:generate {--tenant= : Tenant ID}';

    public function handle(): int
    {
        $tenantId = $this->option('tenant');

        if ($tenantId) {
            app(TenantContext::class)->setTenantId((int) $tenantId);
        }

        // Generate reports...

        return self::SUCCESS;
    }
}
```

API Reference
-------------

[](#api-reference)

### TenantContext

[](#tenantcontext)

MethodDescription`getTenantId(): ?int`Get the current tenant ID`hasTenant(): bool`Check if a tenant is set`setTenantId(?int $tenantId): void`Manually set the tenant ID`clear(): void`Clear the tenant context`runForTenant(int $tenantId, callable $callback): mixed`Run callback with specific tenant### HasTenant Trait

[](#hastenant-trait)

MethodDescription`tenant(): BelongsTo`Relationship to tenant model`scopeForTenant($query, int $id): Builder`Scope to specific tenant`getTenantColumn(): string`Get tenant column name (override)Translations
------------

[](#translations)

The package supports translatable exception messages. Language files are **automatically published** when the package is installed.

**Supported languages:** English (en), Arabic (ar)

To manually re-publish or update the language files:

```
php artisan vendor:publish --tag=multi-tenant-lang --force
```

Published files location: `lang/vendor/multi-tenant/`

```
// lang/vendor/multi-tenant/en/exceptions.php
return [
    'tenant_not_resolved' => 'Unable to resolve tenant. No resolver returned a valid tenant ID.',
];

// lang/vendor/multi-tenant/ar/exceptions.php
return [
    'tenant_not_resolved' => 'غير قادر على تحديد المستأجر. لم يُرجع أي محلل معرف مستأجر صالح.',
];
```

### Adding More Languages

[](#adding-more-languages)

Create additional language files in `lang/vendor/multi-tenant/{locale}/exceptions.php`:

```
// lang/vendor/multi-tenant/fr/exceptions.php
return [
    'tenant_not_resolved' => 'Impossible de résoudre le locataire. Aucun résolveur n\'a retourné un ID de locataire valide.',
];
```

### SetsTenantFromPayload Trait

[](#setstenantfrompayload-trait)

MethodDescription`setTenantFromPayload(array|object $payload): void`Set tenant context from payload, throws TenantNotFoundException if not foundTesting
-------

[](#testing)

```
# Run tests
composer test

# Run with coverage
composer test:coverage
```

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.

Changelog
---------

[](#changelog)

Please see [CHANGELOG.md](CHANGELOG.md) for version history.

License
-------

[](#license)

The MIT License (MIT). Please see [LICENSE](LICENSE) for more information.

Credits
-------

[](#credits)

- [OurEdu](https://github.com/ouredu)

###  Health Score

44

—

FairBetter than 92% of packages

Maintenance86

Actively maintained with recent releases

Popularity20

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 95.6% 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

Every ~5 days

Recently: every ~11 days

Total

11

Last Release

67d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d85c4fda0cfea81414f36c09f5950ec1764db34e52e2162b6b16de73ca2f127a?d=identicon)[ouredu](/maintainers/ouredu)

---

Top Contributors

[![She3bo](https://avatars.githubusercontent.com/u/17051391?v=4)](https://github.com/She3bo "She3bo (130 commits)")[![YoussefAshraf397](https://avatars.githubusercontent.com/u/40191968?v=4)](https://github.com/YoussefAshraf397 "YoussefAshraf397 (4 commits)")[![elwafa](https://avatars.githubusercontent.com/u/9096983?v=4)](https://github.com/elwafa "elwafa (2 commits)")

---

Tags

laravelsaastenantmulti-tenantmultitenancyouredu

###  Code Quality

TestsPHPUnit

Code StylePHP CS Fixer

### Embed Badge

![Health badge](/badges/our-edu-multi-tenant/health.svg)

```
[![Health](https://phpackages.com/badges/our-edu-multi-tenant/health.svg)](https://phpackages.com/packages/our-edu-multi-tenant)
```

###  Alternatives

[laravel/passport

Laravel Passport provides OAuth2 server support to Laravel.

3.4k85.0M532](/packages/laravel-passport)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k25.9M107](/packages/laravel-cashier)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k49.4M479](/packages/laravel-scout)[laravel/socialite

Laravel wrapper around OAuth 1 &amp; OAuth 2 libraries.

5.7k96.9M674](/packages/laravel-socialite)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k12.1M99](/packages/laravel-pulse)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

71510.9M66](/packages/laravel-mcp)

PHPackages © 2026

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