PHPackages                             mhpdigital/cross-tenant-security-bundle - 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. mhpdigital/cross-tenant-security-bundle

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

mhpdigital/cross-tenant-security-bundle
=======================================

Symfony bundle for role-based, row-level tenant isolation in Doctrine repositories

v1.0.3(2mo ago)1315[1 PRs](https://github.com/mhpdigital/cross-tenant-security-bundle/pulls)MITPHPPHP &gt;=8.1

Since Mar 31Pushed 3w agoCompare

[ Source](https://github.com/mhpdigital/cross-tenant-security-bundle)[ Packagist](https://packagist.org/packages/mhpdigital/cross-tenant-security-bundle)[ RSS](/packages/mhpdigital-cross-tenant-security-bundle/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (12)Versions (11)Used By (0)

Cross-Tenant Security Bundle
============================

[](#cross-tenant-security-bundle)

Role-based, row-level tenant isolation for Doctrine repositories in Symfony.

Pick one of three traits per repository to declare its access policy. Filtering is applied automatically in `createQueryBuilder()` (and `find()`, `findBy()`, `findOneBy()`, `findAll()`), so controllers and services never pass the current user around — the security context is read from the token.

Install &amp; register
----------------------

[](#install--register)

```
composer require mhpdigital/cross-tenant-security-bundle
```

Wire the factory as Doctrine's repository factory:

```
# config/packages/doctrine.yaml
doctrine:
    orm:
        repository_factory: 'Mhpdigital\CrossTenantSecurity\Repository\CrossTenantRepositoryFactory'
```

The factory injects `token_storage`, `role_hierarchy` and `request_stack` into every repository that uses one of the traits.

Access matrix
-------------

[](#access-matrix)

Traitconsole / workerauthenticated webtoken-less web`CrossTenantRepository`**all rows**your tenant filter (override)**none** (`1=0`)`AdminOnlyAccessRepository`**all rows**`ROLE_SUPER_ADMIN` → all, else none**none**`OpenAccessRepository`all rowsall rows**all rows** (public)"console / worker" = any process with no HTTP request in flight (a console command, a Messenger/queue worker, a cron run). These are trusted local processes and get full access automatically — see [Console context](#console--worker-context).

Examples
--------

[](#examples)

### 1. Tenant-scoped — `CrossTenantRepository`

[](#1-tenant-scoped--crosstenantrepository)

Each user sees only their own rows; `ROLE_SUPER_ADMIN` sees all. Use the **trait-alias pattern**: call the library's secured builder first (you inherit the unauthenticated `1=0` *and* the console-context bypass), then add your own filter.

```
use Mhpdigital\CrossTenantSecurity\Repository\CrossTenantRepository;

class PostRepository extends ServiceEntityRepository
{
    use CrossTenantRepository {
        CrossTenantRepository::createQueryBuilder as secureQueryBuilder;
    }

    public function createQueryBuilder($alias, $indexBy = null): QueryBuilder
    {
        $qb = $this->secureQueryBuilder($alias, $indexBy);

        // Authenticated non-super-admins are scoped to their own rows.
        // (No current user ⇒ console context ⇒ full access, so this is skipped.)
        if ($this->getCurrentUser() !== null && $this->getHighestRole() !== 'ROLE_SUPER_ADMIN') {
            $qb->andWhere("$alias.author = :_author")
               ->setParameter('_author', $this->getUserId());
        }

        return $qb;
    }
}
```

### 2. Public lookup — `OpenAccessRepository`

[](#2-public-lookup--openaccessrepository)

Genuinely public reference data — **everyone, including unauthenticated requests**, sees all rows. Use for `sex`, `country`, `currency`, `status`, `category`, `tag`. No override needed.

```
use Mhpdigital\CrossTenantSecurity\Repository\OpenAccessRepository;

class SexRepository extends ServiceEntityRepository
{
    use OpenAccessRepository;
}
```

> If a lookup must require login, do **not** use this trait — use `CrossTenantRepository`(authenticated → all rows, token-less web → none).

### 3. Admin-only — `AdminOnlyAccessRepository`

[](#3-admin-only--adminonlyaccessrepository)

Only `ROLE_SUPER_ADMIN` (and trusted console/worker runs) see rows; everyone else sees none.

```
use Mhpdigital\CrossTenantSecurity\Repository\AdminOnlyAccessRepository;

class AuditLogRepository extends ServiceEntityRepository
{
    use AdminOnlyAccessRepository;
}
```

### 4. Content filter + access gate (custom `createQueryBuilder`)

[](#4-content-filter--access-gate-custom-createquerybuilder)

When a repository has an **always-on content filter** (e.g. hide soft-deleted or unpublished rows) *as well as* an access gate, reimplement `createQueryBuilder()` and place `isConsoleContext()` **between** them — the content filter must apply in every context, but the access gate is bypassed for console/worker:

```
public function createQueryBuilder($alias, $indexBy = null): QueryBuilder
{
    $qb = $em->createQueryBuilder()->select($alias)->from(/* … */);

    $qb->andWhere("$alias.deleted IS NULL");   // (1) content filter — ALWAYS applies

    if ($this->isConsoleContext()) {           // CLI/worker bypasses ONLY the gate below
        return $qb;
    }

    // (2) access gate — bypassed in console
    if (!\in_array($this->getHighestRole(), self::READER_ROLES, true)) {
        $qb->andWhere('1=0');
    }

    return $qb;
}
```

A console index build then sees all **non-deleted** rows without `createUnrestrictedQueryBuilder()`, while deleted rows stay hidden everywhere.

Console / worker context
------------------------

[](#console--worker-context)

A console command, Messenger/queue worker or cron run has **no HTTP request** and carries no security token. The bundle detects this (`isConsoleContext()` — request-presence, so functional tests that issue a sub-request keep full web semantics) and grants **full access** through the secured `createQueryBuilder()`. CLI/background code no longer needs `createUnrestrictedQueryBuilder()`.

If `request_stack` is not wired, the bundle assumes a web context and fails **closed**.

Explicit, context-independent bypass
------------------------------------

[](#explicit-context-independent-bypass)

```
$qb = $repo->createUnrestrictedQueryBuilder('e'); // raw Doctrine builder, no filtering at all
```

Use only when you must bypass filtering regardless of context (e.g. an authenticated admin maintenance action). For ordinary CLI/background work the console-context detection already grants full access, so you should rarely need this.

See also
--------

[](#see-also)

The `example/` app is a runnable Symfony project exercising every trait — `example/src/Repository/*` and `example/tests/Integration/BundleIntegrationTest.php` are the executable documentation, including the console-context tests.

###  Health Score

44

—

FairBetter than 91% of packages

Maintenance91

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 73.7% 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 ~4 days

Total

5

Last Release

73d ago

Major Versions

v0.0.9 → v1.0.02026-03-31

### Community

Maintainers

![](https://www.gravatar.com/avatar/6c94c72b97d2849fe5f2c58045e8818bf2fc109db9748ab5031b2529e99181b3?d=identicon)[mhpdigital](/maintainers/mhpdigital)

---

Top Contributors

[![jochendaum](https://avatars.githubusercontent.com/u/2119862?v=4)](https://github.com/jochendaum "jochendaum (14 commits)")[![mhpdigital](https://avatars.githubusercontent.com/u/272352319?v=4)](https://github.com/mhpdigital "mhpdigital (5 commits)")

---

Tags

symfonybundlesecuritydoctrinerepositorysaasmulti-tenanttenancyrow-level-security

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/mhpdigital-cross-tenant-security-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/mhpdigital-cross-tenant-security-bundle/health.svg)](https://phpackages.com/packages/mhpdigital-cross-tenant-security-bundle)
```

###  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.

1175.2k](/packages/rcsofttech-audit-trail-bundle)[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M378](/packages/easycorp-easyadmin-bundle)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1615.6k12](/packages/2lenet-crudit-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M196](/packages/sulu-sulu)[kimai/kimai

Kimai - Time Tracking

4.8k8.7k1](/packages/kimai-kimai)[ahmed-bhs/doctrine-doctor

Runtime analysis tool for Doctrine ORM integrated into Symfony Web Profiler. Unlike static linters, it analyzes actual query execution at runtime to detect performance bottlenecks, security vulnerabilities, and best practice violations during development with real execution context and data.

939.0k](/packages/ahmed-bhs-doctrine-doctor)

PHPackages © 2026

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