Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ha Bundle Laravel Package

kibatic/ha-bundle

A Symfony bundle from Kibatic that brings high availability to Doctrine connections. It transparently fails over to a standby database server when the primary goes down, keeping your application online with minimal configuration and no changes to your existing queries.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the framework reality check. kibatic/ha-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/database.php integration point. If you are on Laravel, the only thing you can realistically take from it is the pattern — read the connection wrapper source, understand how it distinguishes a "primary is unreachable" exception from a generic query error, and reimplement that small piece against Laravel's DatabaseManager. Everything below describes day-to-day use on the stack it actually targets (Symfony + Doctrine DBAL), with notes on the Laravel equivalent.

Registration (Symfony).

  • composer require kibatic/ha-bundle — then immediately inspect composer.json. The version constraints on symfony/framework-bundle, doctrine/dbal, and php are the single most important thing you will read. A bundle that pins DBAL ^2.x will not install on a modern Symfony app at all, and that decision happens before you write a line of config.
  • Register the bundle in config/bundles.php (Symfony 4+) or AppKernel::registerBundles() (Symfony 2/3).
  • Configure per connection under doctrine.dbal.connections.<name>. The bundle works by swapping Doctrine's wrapper_class (or equivalent connection factory) for its own wrapper, so your existing connection config — host, dbname, user, password, charset — stays intact. Expect a shape like:
doctrine:
  dbal:
    connections:
      default:
        wrapper_class: Kibatic\HABundle\...\Connection
        host: '%primary_host%'
        # bundle-specific block: replicas/slaves, health-check timeout,
        # retry count, retry backoff, fallback-to-replica flag

Where to look first in the source (in this order):

  1. The DI extension class — it tells you exactly which config keys exist and which are required vs. defaulted.
  2. The connection wrapper — the failover decision logic lives here. Look for connect(), __construct, and any catch around DriverException.
  3. Any health-check or "is the master alive" service — this determines whether failover is proactive (polling) or reactive (exception-driven). That distinction drives your whole latency budget.

First use case to validate. Provision a scratch MySQL primary + one replica (docker-compose is fine). Run a read query, confirm it hits the replica. Then docker stop the primary and re-run. What you want to observe: reads still succeed from the replica, writes throw a specific, catchable exception class rather than a raw driver error. If writes hang for 30 seconds on a TCP timeout before failing, you have found your first thing to tune.

Implementation Patterns

Reactive failover around the first query of the request. The bundle's value is concentrated in a very short window: the moment a request opens a connection and discovers the primary is gone. Pattern it explicitly — wrap your read path so a driver exception triggers one reconnect attempt against the replica, and cache the resulting "primary is down" verdict in APCu or Redis with a short TTL (10–30s). Without that cache, every PHP-FPM worker independently rediscovers the outage, and the primary's recovery is greeted by a reconnect stampede.

Read/write split as a load lever, not an HA mechanism. If the realistic goal is absorbing read-heavy traffic (feeds, reporting, search-adjacent queries), don't put this on the failover path at all — use doctrine.dbal.connections.default.replica (DBAL 3 does this natively) or Laravel's read/write keys in config/database.php. Static splitting is predictable; dynamic failover is the part that needs tests.

Circuit breaker above the connection. Have the wrapper surface a distinguishable exception (e.g. a ReadOnlyException or a custom HAConnectionException) and catch it in a middleware / exception handler. Map it to a degraded-mode response: a banner on read pages, a 503 with a clear message on the write endpoints (checkout, signup, profile edits). This is the pattern that makes the bundle worth anything — the failover itself is a few dozen lines; the policy around it is where you'll spend the design time.

Pin CLI and migration commands to the primary. doctrine:migrations:migrate, doctrine:schema:update, and queue workers doing writes must never be routed to a replica. Add an explicit override in the console environment — a separate connections.default_cli with replicas disabled, or a Kernel::CONSOLE guard. This is the single most common way application-level failover silently corrupts a schema.

Laravel-equivalent pattern (what you'd actually build). Extend Illuminate\Database\MySqlConnection (or wrap it), override reconnect() and run() so a QueryException whose driver code matches "unknown server host" / "connection refused" (MySQL 2002/2003, SQLSTATE HY000 with the right message) flips a per-connection degraded flag, and the read path retries against the replica host. Register via a custom connector in a service provider. Scope it: it's ~200 lines and a handful of tests, and you own it. That is the honest trade against adopting an unmaintained dependency — the TPM assessment is right that this is the cheaper path.

Observability hook. Subclass the bundle's wrapper and log every routing decision (primary / replica / failover-attempt / degraded) plus a counter metric on failover events. Without this you cannot tell an outage from a slow query, and the bundle by default will degrade silently.

Testing. Integration-test the three scenarios that matter: primary down (reads succeed, writes fail cleanly), primary recovered (traffic returns, no connection storm), replica lagging (you must not serve stale data to a read-after-write flow). Do this in CI with a real MySQL replica, not a mock — the bug class lives in driver exception shapes.

Gotchas and Tips

The version matrix will reject you before the code does. Symfony bundles of this vintage typically pin doctrine/dbal to ^2.x and break on DBAL 3, which is where PrimaryReadReplicaConnection replaced the old master/slave model. Check composer.json first; if it doesn't install cleanly, stop — do not start patching constraints to force it.

wrapper_class is an exclusive seam. Replacing the connection wrapper means any wrapper you already had (query logging, multi-tenancy, read/write hints) is gone. You either subclass the bundle's wrapper and chain your behaviour back in, or you don't use the bundle. There is no composition point offered.

Transactions do not survive failover. If a transaction is open when the primary disappears, the bundle cannot resume it — you get a rollback mid-request. Any code that assumes "the write either happened or the request failed before it started" is now wrong. Wrap multi-step writes so a connection failure leaves the database in a state your retry logic can reason about, and never blindly retry on the app side.

Replica lag + read-after-write is the silent-corruption path. A user saves a record, the confirmation page reads from a replica that hasn't caught up, and they see their edit missing — or worse, resubmit it. Guard by routing reads immediately after a write to the primary for the duration of the request (a "sticky primary" flag), and don't rely on the bundle to know this.

Health checks on the request path cost latency. If the config exposes a poll interval, set it generously and rely on reactive exception handling for the actual detection. A synchronous health-check per request is a per-request timeout you've just added to your p99.

Persistent PDO connections (PHP-FPM) hide outages. A worker with a live primary connection won't notice the primary is gone until the socket dies, and the failover decision may be made against stale state. If you use PDO::ATTR_PERSISTENT, test the failure window explicitly.

Two sources of truth if the infrastructure also fails over. If the database sits behind RDS Multi-AZ, a proxy, or Patroni, this bundle competes with it — the app may believe the primary is down while the orchestrator is already promoting a replica. Split-brain is the failure mode here. Do not run application-level failover on top of infrastructure-level failover.

Config quirks to verify in the extension class: whether replicas are a single host or a list, whether retry/backoff is configurable per connection or globally, and what the default fallback behaviour is when both primary and replica are down (fail closed with a clear exception, or a generic driver error you can't distinguish). Also check whether config is validated at container compile time — a typo'd key that silently defaults is a nasty surprise during an incident.

Debug in dev. Set the logging level for the wrapper's namespace to debug in dev and warning in prod; you want to see routing decisions locally and only incidents in production. If the bundle doesn't log, subclass and add it — this is the cheapest extension point available.

Treat it as vendored code. With zero dependents and no upstream, fork it into your repo (repositories with a VCS entry, or copy the one class you actually need). Pin the commit. Write a test suite around the failover path before it touches production, and record an expiry date for the whole arrangement against your managed-HA migration.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums