PHPackages                             rentpost/doctrine-multi-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. rentpost/doctrine-multi-tenancy

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

rentpost/doctrine-multi-tenancy
===============================

Advanced Doctrine2 multi-tenancy extension

1.3.0(2mo ago)212.3k2[1 issues](https://github.com/rentpost/doctrine-multi-tenancy/issues)MITPHPPHP &gt;=8.2CI passing

Since Jan 14Pushed 1w ago1 watchersCompare

[ Source](https://github.com/rentpost/doctrine-multi-tenancy)[ Packagist](https://packagist.org/packages/rentpost/doctrine-multi-tenancy)[ Docs](https://github.com/rentpost/doctrine-multi-tenancy-extension)[ RSS](/packages/rentpost-doctrine-multi-tenancy/feed)WikiDiscussions master Synced 3w ago

READMEChangelog (5)Dependencies (4)Versions (9)Used By (0)

Doctrine MultiTenancy
=====================

[](#doctrine-multitenancy)

Doctrine 3 extension providing advanced multi-tenancy support. The purpose of this extension is to allow flexibility in how multi-tenancy is defined on a per entity basis, as well as within contexts.

Why?
----

[](#why)

Often times multi-tenancy is handled differently depending on a number of different business concerns. Maybe each user has different roles, or is part of multiple organizations, etc.

Now, generally speaking, you could handle much of these concerns within repositories, and if your business logic allows for such organization, you should consider this approach instead. However, this is not always possible or ideal in many scenarios, especially when accessing relational entities and even more-so when exposing your entities and relationships over something like a GraphQL API where relationships can be traversed in an end-user defined manner.

This advanced approach to multi-tenancy aims to address these concerns, providing flexibility to define how multi-tenancy is handled across *contexts* on a per-entity basis.

Getting Started
---------------

[](#getting-started)

Use the following instructions to get started with this Doctrine extension.

### Prerequisites

[](#prerequisites)

This extension is compatible with [Doctrine 3](https://github.com/doctrine/orm) and PHP &gt;= 8.2.

*If you're looking for PHP &gt;= 7.4 support, please use `1.0.3`, the last version to support it*

### Installation

[](#installation)

```
composer require rentpost/doctrine-multi-tenancy
```

### Setup

[](#setup)

In order for this extension to work, you will need to register it with Doctrine's `EntityManager` and `EventManager`. To do so, you'll want to add the following to your configuration and setup for Doctrine. How this is done will depend on your implementation. See [Doctrine's installation and configuration documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/configuration.html) for further details.

```
use Doctrine\ORM\Configuration;
use Doctrine\DBAL\Connection;
use App\Adapter\Doctrine\ORM\MultiTenancy\ContextProvider; // Your namespace for ContextProviders
use App\Adapter\Doctrine\ORM\MultiTenancy\ValueHolder; // Your namespace for ValueHolders
use Rentpost\Doctrine\MultiTenancy\Listener as MultiTenancyListener;

$connection = Connection($dbalParams, new MySQLDriver())
$config = new Configuration();
...

$eventManager = $connection->getEventManager();

// Instantiate the MultiTenancy\Listener
$multiTenancyListener = new MultiTenancyListener();
// Now add any ValueHolders you wish to use
$multiTenancyListener->addValueHolder(new ValueHolder\Company());
$multiTenancyListener->addValueHolder(new ValueHolder\User());
$multiTenancyListener->addValueHolder(new ValueHolder\Role());
// And any contexts you may wish to use
$multiTenancyListener->addContextProvider(new ContextProvider\Admin();
$multiTenancyListener->addContextProvider(new ContextProvider\Manager());
$multiTenancyListener->addContextProvider(new ContextProvider\Guest();
// Subscribe the listener to the EventManager now
$eventManager->addEventSubscriber($multiTenancyListener);

// Add the filter to the EntityManager config
$config->addFilter('multi-tenancy', 'Rentpost\Doctrine\MultiTenancy\Filter');

$entityManager = EntityManager::create($connection, $config, $eventManager);

// Lastly, you need to be sure you've enabled the filter
$entityManager->getFilters()->enable('multi-tenancy');
```

Now, let's break this down, if you're not familiar with Doctrine's configuration/setup. Depending on how your application is configured, the above may vary. We won't go into the particulars of Doctrine's configuration here.

The first part you need to be concerned with here is subscribing the listener to the `EventManager`. If, for whatever reason, you do not wish to have any `ValueHolders` or `ContextProviders`, you can actually skip this step entirely, and only add the filter. Let's assume that you want to use both though.

#### What is a ValueHolder?

[](#what-is-a-valueholder)

A `ValueHolder` is a class that `implements Rentpost\Doctrine\MultiTenancy\ValueHolderInterface`. The primary purpose of a `ValueHolder` is to define a value for a given "identifier".

In the configuration above, we've added `ValueHolder`s for `Company`, `User`, and `Role`. These are going to provide parameters and values you'll want to use within an SQL query. The `ValueHolderInterface` defines 2 methods:

```
public function getIdentifier(): string;
```

```
public function getValue(): ?string;
```

The example, `User`, above might return `userId` as the "identifier" and the id of that User, represented as a string. It's effectively acting as a key/value store that's lazily loaded, such that, the value can mutate state.

The purpose of this will be more clear when viewing the example attributes below.

#### What is a ContextProvider?

[](#what-is-a-contextprovider)

A `ContextProvider` is a class that `implements Rentpost\Doctrine\MultiTenancy\ContextProviderInterface`. The primary purpose of a `ContextProvider` is to define "contexts" with a way to validate if that context is currently within context, or "contextual".

A "context" might, for example, be the "roles" for Users, or, it could be an authorization level, or any other use you may find to be fitting for your business logic. It's intended to be flexible, so as to accommodate any number of use cases.

In the configuration example above, we added `ContextProvider`s for `Admin`, `Manager`, and `Guest`. Each of these `ContextProvider`s will expose a "context".

The `ContextProviderInterface` defines 2 methods:

```
public function getIdentifier(): string;
```

```
public function isContextual(): bool;
```

Using the `Admin` example above, we might return `admin` as an "identifier". The `isContextual` method is responsible for determining if this particular `admin` identifier is consider to be within context, or contextual. In this situation, you might construct this class with a `User` object that has a method called `isAdmin`.

As with the `ValueHolder`, this will all be more clear when viewing the example attributes below.

Usage
-----

[](#usage)

After you've gotten everything setup, the hard part is out of the way. Taking the time to properly evaludate how you'll setup your `ValueHolder` and `ContextProvider` classes will go a long way in making the usage clean and simple.

### Examples

[](#examples)

There are a couple things to note first.

- `$this` represents the alias for the current table, as defined by Doctrine.
- "Identifiers" of a `ValueHolder` are enclosed in filters with curly brackets, `{myIdentifier}`.
- Multiple filters can be applied. Adding multiple fitlers will execute all that are "in context".
- Filters without an explicitly defined context, even if you have added `ContextProvider`s, will be applied for all contexts. Basically, it will always be executed.
- Multiple "contexts" can be defined for a filter. If any context defined is "contextual", the filter will be applied.

#### Simple example without any context

[](#simple-example-without-any-context)

In this example, it's assumed that the `Product` table has a column called `company_id`, which is used for multi-tenancy to associate products with a given company. The `{companyId}` parameter here is defined in our `ValueHolder\Company` in the example configuration above. `companyId` would be the "identifier" and the value would be the id, of the current company.

```
use Doctrine\ORM\Mapping as ORM;
use Rentpost\Doctrine\MultiTenancy\Attribute\MultiTenancy;

#[ORM\Entity]
#[MultiTenancy(filters: [
    new MultiTenancy\Filter(where: '$this.company_id = {companyId}'),
])]

class Product
{
  // Whatever
}
```

#### Another example with multiple filters and context

[](#another-example-with-multiple-filters-and-context)

In this example, we've added multiple filters. The first filter would always be applied. The second filter, with the "manager" context, would only be applied if the "identifier", `manager`, as defined in the respective `ContextProvider` is considered to be "contextual", via the `isContextual()` method. If so, it would be applied as well.

In the second filter, the `product` table doesn't have access to the necessary information we need to properly apply multi-tenancy filtering. Therefore, we execute a sub-select query. This allows for us to perform queries on relational tables. In this case, we're effectively saying that a `manager` context only has access to a `Product` that's in a `product_group` with a status that is "published". If isn't true, the `Product` wouldn't be returned.

```
use Doctrine\ORM\Mapping as ORM;
use Rentpost\Doctrine\MultiTenancy\Attribute\MultiTenancy;

#[ORM\Entity]
#[MultiTenancy(filters: [
    new MuiltiTenancy\Filter(where: '$this.company_id = {companyId}'),
    new MultiTenancy\Filter(
        context: ['visitor'],
        where: '$this.id IN(
            SELECT product_id
            FROM product_group
            WHERE status = 'published'
        )'
    ),
])]
class Product
{
  // Whatever
}
```

### Filter Strategies

[](#filter-strategies)

When multiple filters match the current context, the `FilterStrategy` determines how they combine:

- **`FilterStrategy::AnyMatch`** (default) — All matching filters are AND'd together.
- **`FilterStrategy::FirstMatch`** — Only the first matching filter is applied; subsequent filters are skipped.

**Keep in mind, if you do not provide a context, it's assumed to be in context, and that filter will be applied, meaning any subsequent filters will never be evaluated (under `FirstMatch`).**

```
use Doctrine\ORM\Mapping as ORM;
use Rentpost\Doctrine\MultiTenancy\Attribute\MultiTenancy;

#[ORM\Entity]
#[MultiTenancy(
    strategy: MultiTenancy\FilterStrategy::FirstMatch,
    filters: [
        new MultiTenancy\Filter(
            context: ['admin'],
            where: '$this.company_id = {companyId}',
        ),
        new MultiTenancy\Filter(
            context: ['other'],
            ignore: true,
        ),
        new MultiTenancy\Filter(
            context: ['visitor'],
            where: '$this.id IN(
                SELECT product_id
                FROM product_group
                WHERE status = \'published\'
            )',
        ),
    ],
)]
class Product
{
  // Whatever
}
```

The `ignore` parameter above allows you to specify a context where no filter conditions will be applied. This can be especially useful in combination with `FilterStrategy::FirstMatch`, allowing you to entirely, or selectively, ignore all multi-tenancy for an entity — for a given context.

### Strict Mode

[](#strict-mode)

Setting `strict: true` flips to a "deny by default" model. Filters are still processed per the chosen `FilterStrategy`, but after processing, if any active `ContextProvider` is not covered by any filter's `context:` array, `1 = 0` is appended — denying all results.

This eliminates the need to explicitly deny every uncovered context. Only contexts that are declared in a filter (even with `ignore: true`) are permitted.

Note: `strict` is orthogonal to `FilterStrategy` — it works with either `AnyMatch` or `FirstMatch`.

#### Context-free filters

[](#context-free-filters)

A filter with no `context:` array applies to all contexts (as documented earlier), and consistent with that semantic, it also **covers** all contexts for the strict check. So mixing a context-free filter with `strict: true` effectively disables the coverage enforcement — every active context is implicitly covered.

For strict mode to be meaningful, declare a `context:` array on every filter so coverage is explicit. The following example scopes access contextually: managers get company-wide access, admins are permitted without extra conditions (`ignore: true`), and anyone else is denied.

```
use Doctrine\ORM\Mapping as ORM;
use Rentpost\Doctrine\MultiTenancy\Attribute\MultiTenancy;

#[ORM\Entity]
#[MultiTenancy(
    strict: true,
    filters: [
        new MultiTenancy\Filter(
            context: ['manager'],
            where: '$this.company_id = {companyId}',
        ),
        new MultiTenancy\Filter(
            context: ['admin'],
            ignore: true,
        ),
    ],
)]
class Product
{
  // Whatever
}
```

In this example:

- A `manager` sees products scoped to their company
- An `admin` sees everything (ignored filter, but the context is acknowledged as covered)
- Any other active context (e.g. `guest`) is automatically denied — no need to add explicit deny filters

#### Ambient Context Providers

[](#ambient-context-providers)

Some context providers represent ambient environmental state rather than primary access contexts — for example, "is any role active?" or "is a user logged in?". These are always active and don't represent discrete access levels.

When using `strict: true`, ambient contexts would cause universal denial since they're never listed in a filter's `context:` array. To prevent this, implement `AmbientContextProviderInterface` (a marker interface extending `ContextProviderInterface`) on those providers. Strict mode will skip them during its coverage check.

```
use Rentpost\Doctrine\MultiTenancy\AmbientContextProviderInterface;

class UserContextProvider implements AmbientContextProviderInterface
{
    // ...
}
```

Using ConditionResolver for Raw SQL Queries
-------------------------------------------

[](#using-conditionresolver-for-raw-sql-queries)

The `Filter` class (Doctrine's `SQLFilter`) only applies to DQL queries. For raw SQL queries in repositories, use `ConditionResolver` directly to get the same multi-tenancy conditions:

```
use Rentpost\Doctrine\MultiTenancy\ConditionResolver;

// The Listener is the same one registered with the EventManager during setup
$resolver = new ConditionResolver($listener);

// Resolve conditions for a given entity class and table alias
$condition = $resolver->resolve(Product::class, 'p');
// Returns e.g.: "p.company_id = 42 AND p.id IN(SELECT product_id FROM ...)"

// Use in a raw SQL query
$sql = "SELECT p.* FROM product p WHERE {$condition} AND p.status = 'active'";
```

The `resolve()` method reads the `#[MultiTenancy]` attribute from the entity class, evaluates the current context via `ContextProviders`, substitutes values from `ValueHolders`, and returns the composed WHERE clause fragment. It uses the same logic as the `Filter` — just without requiring Doctrine's DQL layer.

Development
-----------

[](#development)

### Running Tests

[](#running-tests)

```
make test
```

Or directly via PHPUnit:

```
vendor/bin/phpunit
```

### Setup

[](#setup-1)

```
make init
```

This installs all Composer dependencies, including PHPUnit for running the test suite.

Issues / Bugs / Questions
-------------------------

[](#issues--bugs--questions)

Please feel free to raise an issue against this repository if you have any questions or problems.

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

[](#contributing)

New contributors to this project are welcome. If you are interested in contributing please send a courtesy email to .

Authors and Maintainers
-----------------------

[](#authors-and-maintainers)

Jacob Thomason

License
-------

[](#license)

This library is released under the [MIT license](LICENSE).

###  Health Score

56

—

FairBetter than 97% of packages

Maintenance91

Actively maintained with recent releases

Popularity29

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity75

Established project with proven stability

 Bus Factor1

Top contributor holds 98.5% 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 ~380 days

Recently: every ~517 days

Total

7

Last Release

71d ago

PHP version history (3 changes)1.0PHP &gt;=7.2.0

1.0.2PHP &gt;=7.4

1.3.0PHP &gt;=8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/0b137ef566e79f531991d98195e811da3324a38baba94eac14760736e1fcddfa?d=identicon)[rentpost](/maintainers/rentpost)

---

Top Contributors

[![oojacoboo](https://avatars.githubusercontent.com/u/764664?v=4)](https://github.com/oojacoboo "oojacoboo (64 commits)")[![nclsHart](https://avatars.githubusercontent.com/u/833625?v=4)](https://github.com/nclsHart "nclsHart (1 commits)")

---

Tags

doctrinedoctrine-extensionmulti-tenancy

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/rentpost-doctrine-multi-tenancy/health.svg)

```
[![Health](https://phpackages.com/badges/rentpost-doctrine-multi-tenancy/health.svg)](https://phpackages.com/packages/rentpost-doctrine-multi-tenancy)
```

###  Alternatives

[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

1155.2k](/packages/rcsofttech-audit-trail-bundle)[sonata-project/entity-audit-bundle

Audit for Doctrine Entities

6421.0M1](/packages/sonata-project-entity-audit-bundle)[kimai/kimai

Kimai - Time Tracking

4.7k8.7k1](/packages/kimai-kimai)[doctrineencryptbundle/doctrine-encrypt-bundle

Encrypted symfony entity's by verified and standardized libraries

32477.7k](/packages/doctrineencryptbundle-doctrine-encrypt-bundle)

PHPackages © 2026

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