PHPackages                             houlokmah/laravel-tenancy-batch-fix - 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. houlokmah/laravel-tenancy-batch-fix

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

houlokmah/laravel-tenancy-batch-fix
===================================

Fixes the DatabaseBatchRepository stale connection singleton problem in multi-tenant Laravel apps using stancl/tenancy

01PHP

Since Mar 17Pushed 3mo agoCompare

[ Source](https://github.com/houlokmah/tenancy-batch-fix)[ Packagist](https://packagist.org/packages/houlokmah/laravel-tenancy-batch-fix)[ RSS](/packages/houlokmah-laravel-tenancy-batch-fix/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Laravel Tenancy Batch Fix
=========================

[](#laravel-tenancy-batch-fix)

[![Latest Version on Packagist](https://camo.githubusercontent.com/fad8446bde9ee9814e6bbfca46de0f58de4129be54e9dabeebe548430d62c499/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f686f756c6f6b6d61682f6c61726176656c2d74656e616e63792d62617463682d6669782e737667)](https://packagist.org/packages/houlokmah/laravel-tenancy-batch-fix)[![License](https://camo.githubusercontent.com/c40627de4be9885aa486827782d20ff3f33ab97f434f8b37fd7b9cf50a9f2dc3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f686f756c6f6b6d61682f6c61726176656c2d74656e616e63792d62617463682d6669782e737667)](https://packagist.org/packages/houlokmah/laravel-tenancy-batch-fix)

**Zero-config fix** for the `DatabaseBatchRepository` stale connection problem in multi-tenant Laravel apps using [stancl/tenancy](https://tenancyforlaravel.com/).

If you've ever seen this error in your queue workers:

```
Call to a member function prepare() on null

```

...on `DatabaseBatchRepository` after the first job succeeds but subsequent jobs fail — this package fixes it.

The Problem
-----------

[](#the-problem)

There are **two layers** to this bug:

### Layer 1: Connection purging

[](#layer-1-connection-purging)

`QueueTenancyBootstrapper` calls `DB::purge('tenant')` after each job completes. This destroys the PDO instance on the cached connection object, setting it to `null`.

### Layer 2: Deferred singleton override

[](#layer-2-deferred-singleton-override)

Laravel's `BusServiceProvider` implements `DeferrableProvider`. It doesn't load until `BatchRepository` is first resolved. When it does load, it registers `BatchRepository` and `DatabaseBatchRepository` as **singletons** — overriding any `bind()` you may have set up in your own service provider.

### The combined effect

[](#the-combined-effect)

```
Job 1 starts
  → Tenancy bootstraps, connects to tenant DB
  → BatchRepository singleton created (holds live connection) ✅
  → Job completes
  → QueueTenancyBootstrapper calls DB::purge() → PDO set to null

Job 2 starts
  → Tenancy bootstraps new tenant DB
  → BatchRepository resolves → returns SAME singleton (stale connection, null PDO)
  → prepare() on null 💥

```

The same issue occurs in the scheduler when using `runForMultiple()` to iterate through tenants.

The Solution
------------

[](#the-solution)

This package applies a **three-pronged fix**:

1. **`register()`** — Binds `BatchRepository` and `DatabaseBatchRepository` using `bind()` instead of `singleton()`, creating a fresh instance per resolution.
2. **`TenancyBootstrapped` listener** — Re-applies the `bind()` after each tenant switch, clearing any cached singleton that the deferred `BusServiceProvider` may have created.
3. **`Queue::before()` listener** — Safety net that re-binds before every queued job, covering central-connection jobs where `TenancyBootstrapped` doesn't fire.

### Why this works

[](#why-this-works)

- `Container::bind()` calls `dropStaleInstances()`, which removes any cached singleton
- The closure inside `bind()` resolves `DB::connection()` at **resolution time** (not registration time), so it always gets the current live tenant connection
- `Queue::before()` fires after `QueueTenancyBootstrapper` initializes the tenant (because the bootstrapper registers its listener during the `register` phase, while our `Queue::before` registers in `boot`)

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

[](#installation)

```
composer require houlokmah/laravel-tenancy-batch-fix
```

Then restart your queue workers:

```
php artisan queue:restart
```

That's it. The package uses Laravel's auto-discovery — no manual service provider registration needed.

Configuration (Optional)
------------------------

[](#configuration-optional)

For most apps, zero configuration is needed. If you need to customize the batch table name or database connection:

```
php artisan vendor:publish --tag=tenancy-batch-fix-config
```

This publishes `config/tenancy-batch-fix.php`:

```
return [
    // Override batch table name (default: falls through to queue.batching.table)
    'table' => null,

    // Override database connection (default: falls through to queue.batching.database)
    'connection' => null,
];
```

Execution Flow
--------------

[](#execution-flow)

### Before (broken)

[](#before-broken)

```
Worker boots → AppServiceProvider bind()
            → Job 1 resolves BatchRepository
            → BusServiceProvider loads (deferred) → overrides with singleton()
            → Job 1 succeeds
            → DB::purge() → PDO = null
            → Job 2 resolves BatchRepository → gets stale singleton → 💥

```

### After (fixed)

[](#after-fixed)

```
Worker boots → TenancyBatchFixServiceProvider bind()
            → Job 1 resolves BatchRepository → fresh instance ✅
            → BusServiceProvider loads (deferred) → overrides with singleton()
            → Job 1 succeeds
            → DB::purge() → PDO = null
            → Queue::before fires → re-binds (clears stale singleton)
            → TenancyBootstrapped fires → re-binds again (belt + suspenders)
            → Job 2 resolves BatchRepository → fresh instance ✅

```

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

[](#requirements)

- PHP 8.0+
- Laravel 10, 11, or 12
- [stancl/tenancy](https://tenancyforlaravel.com/) v3.x

How It Works (Deep Dive)
------------------------

[](#how-it-works-deep-dive)

### Why `bind()` clears the singleton

[](#why-bind-clears-the-singleton)

Laravel's `Container::bind()` method calls `dropStaleInstances($abstract)`, which removes the key from both `$this->instances` (where singletons are cached) and `$this->aliases`. This is the mechanism that clears the stale `DatabaseBatchRepository` singleton.

### Why `BusServiceProvider` overrides our binding

[](#why-busserviceprovider-overrides-our-binding)

`BusServiceProvider` implements `DeferrableProvider` and declares `BatchRepository::class` in its `provides()` method. Laravel's deferred provider mechanism means the provider only loads when one of its provided abstracts is first resolved. At that point, its `register()` method runs and calls `singleton()`, which **replaces** any `bind()` we set up earlier.

### Why `Queue::before` fires after tenancy initialization

[](#why-queuebefore-fires-after-tenancy-initialization)

`QueueTenancyBootstrapper::__constructStatic()` registers its `JobProcessing` listener during the service provider `register` phase (via `Event::listen`). Our `Queue::before()` registers during `boot()`, which runs later. Since Laravel dispatches listeners in registration order, the tenancy bootstrapper's listener fires first (initializing the tenant connection), and our listener fires second (re-binding with the now-live connection).

### Why we bind both abstracts

[](#why-we-bind-both-abstracts)

`BusServiceProvider::registerBatchServices()` registers singletons for **both** `BatchRepository::class` and `DatabaseBatchRepository::class`. If we only override `BatchRepository`, the deferred provider's cached singleton for `DatabaseBatchRepository` could still surface. Overriding both ensures no stale instances remain.

Testing
-------

[](#testing)

```
composer test
```

Or directly:

```
vendor/bin/phpunit
```

License
-------

[](#license)

The MIT License (MIT). See [LICENSE](LICENSE) for details.

###  Health Score

18

—

LowBetter than 8% of packages

Maintenance54

Moderate activity, may be stable

Popularity1

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity12

Early-stage or recently created project

 Bus Factor1

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

### Community

Maintainers

![](https://www.gravatar.com/avatar/2eae8f92b9d786519c053bcab0adb8a7ca540eec16ab71414532f86fedbece24?d=identicon)[houlokmah](/maintainers/houlokmah)

---

Top Contributors

[![renoschubert](https://avatars.githubusercontent.com/u/46114615?v=4)](https://github.com/renoschubert "renoschubert (1 commits)")

### Embed Badge

![Health badge](/badges/houlokmah-laravel-tenancy-batch-fix/health.svg)

```
[![Health](https://phpackages.com/badges/houlokmah-laravel-tenancy-batch-fix/health.svg)](https://phpackages.com/packages/houlokmah-laravel-tenancy-batch-fix)
```

###  Alternatives

[jdorn/sql-formatter

a PHP SQL highlighting library

3.9k116.5M113](/packages/jdorn-sql-formatter)[propel/propel1

Propel is an open-source Object-Relational Mapping (ORM) for PHP5.

8351.6M87](/packages/propel-propel1)

PHPackages © 2026

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