m6web/cassandra-bundle
Symfony bundle for integrating Cassandra into your app, with configuration, service wiring, and profiling tools to manage connections and inspect Cassandra queries from within Symfony.
Read this first: it is not a Laravel package. m6web/cassandra-bundle is a Symfony 2/3-era bundle that wires datastax/php-driver (the ext-cassandra PHP extension) into the container. You will not composer require it into a Laravel app and get working services — its M6WebCassandraBundle, DependencyInjection/Configuration.php, and service definitions assume Symfony's kernel and DI conventions. For a Laravel developer the realistic "getting started" is: read it as a reference design, port the ~200 lines of wiring it does, and write your own service provider. Treat it as documentation, not a dependency.
Step 1 — get the extension working before you touch any PHP glue. Everything hinges on ext-cassandra, which wraps the DataStax C++ driver:
pecl install cassandra # needs libuv, openssl, and the C++ driver toolchain
php -m | grep cassandra
If this fails, none of the bundle's code (or any port of it) matters. The most common first-day failure is a container/boot error like Class "Cassandra\Cluster" not found because the extension isn't loaded.
Step 2 — look at exactly two files in the repo:
DependencyInjection/Configuration.php — the authoritative list of every cluster option the bundle supports: contactpoints, port, keyspace, timeout, connect_timeout, consistency, retry_policy, load_balancing, protocol_version, username/password, default_page_size. This is your checklist for what a real Cassandra config needs.Cassandra/Connection.php (and the session factory around it) — how the bundle wraps a Cassandra\Session and exposes query() / prepared statements. This is the interface you'll re-implement.Step 3 — mirror it in Laravel. Create config/cassandra.php (one array per cluster, matching the bundle's YAML shape) and a CassandraServiceProvider that binds a lazily-created singleton per cluster:
// config/cassandra.php
return [
'default' => 'default',
'clusters' => [
'default' => [
'contactpoints' => explode(',', env('CASSANDRA_HOSTS', '127.0.0.1')),
'port' => (int) env('CASSANDRA_PORT', 9042),
'keyspace' => env('CASSANDRA_KEYSPACE', 'app'),
'timeout' => 5,
'connect_timeout' => 5,
'consistency' => 'quorum', // LOCAL_QUORUM in prod multi-DC
'protocol_version'=> 4,
'username' => env('CASSANDRA_USER'),
'password' => env('CASSANDRA_PASS'),
],
],
];
// app/Providers/CassandraServiceProvider.php
public function register(): void
{
$this->app->singleton(CassandraManager::class, fn ($app) =>
new CassandraManager($app['config']->get('cassandra'))
);
$this->app->bind(CassandraConnection::class, fn ($app) =>
$app->make(CassandraManager::class)->connection()
);
}
First use case — one write, one read, through a repository:
final class UserRepository
{
public function __construct(private CassandraConnection $cassandra) {}
public function touch(string $userId): void
{
$this->cassandra->execute(
'INSERT INTO users (id, last_seen) VALUES (?, toTimestamp(now()))',
[$userId]
);
}
public function find(string $userId): ?array
{
$rows = $this->cassandra->execute(
'SELECT id, last_seen FROM users WHERE id = ?',
[$userId]
);
return $rows[0] ?? null; // map to a DTO, don't leak Cassandra\Row
}
}
The keyspace must already exist — neither the bundle nor your port creates schema. Do it with cqlsh or a dedicated artisan command before the app connects.
Repository wrapper, not raw session in controllers. The single most important pattern the bundle implies (but doesn't enforce) is: keep all CQL in a per-table-family repository and inject a small connection wrapper. The Symfony coupling stays in one service definition; in Laravel the equivalent is one binding in a service provider and repositories that accept an interface. This is what survives the underlying driver going EOL — your CQL surface is small and swappable.
Port the bundle's cluster/session model, not its YAML. Recreate the shape: a CassandraManager that holds cluster definitions and lazily builds a Cassandra\Session per cluster name, caching it. The bundle's value is exactly this lifecycle — build session on first use, keep it for the request lifecycle, configure consistency/retry at the cluster level. Do not create a Session per query.
final class CassandraManager
{
private array $sessions = [];
public function __construct(private array $config) {}
public function connection(?string $cluster = null): CassandraConnection
{
$name = $cluster ?? $this->config['default'];
if (!isset($this->sessions[$name])) {
$cfg = $this->config['clusters'][$name];
$cluster = Cassandra::cluster()
->withContactPoints(...$cfg['contactpoints'])
->withPort($cfg['port'])
->withDefaultConsistency(constant('Cassandra::CONSISTENCY_' . strtoupper($cfg['consistency'])))
->withConnectTimeout($cfg['connect_timeout'])
->build();
$this->sessions[$name] = new CassandraConnection(
$cluster->connect($cfg['keyspace'])
);
}
return $this->sessions[$name];
}
}
Reuse prepared statements; use ExecutionOptions for per-query overrides. The bundle's config sets defaults on the session, but real workloads need per-query consistency (e.g. ONE for hot reads, LOCAL_QUORUM for writes). That means prepared statements plus an options object — this is where the bundle's defaults stop and your application takes over:
$stmt = $session->prepare('SELECT * FROM events WHERE user_id = ? AND ts > ?');
$opts = new Cassandra\ExecutionOptions([
'arguments' => [$userId, $since],
'consistency' => Cassandra::CONSISTENCY_ONE,
'page_size' => 500,
]);
$rows = $session->execute($stmt, $opts);
Multiple clusters → inject the manager, not a connection. The bundle supports multiple lazily-created sessions; mirror that exactly. Have CassandraManager::connection('analytics') and bind a named alias CassandraConnection::class to the default, plus analytics via a tagged or when() binding. Do not autowire different connection types by name — Laravel has no equivalent to Symfony's service IDs, so use interfaces (AnalyticsConnection) or explicit calls in the repository constructor.
Mirror the bundle's profiler with a query listener or Telescope. The bundle ships a Symfony data collector to observe Cassandra queries — that's the one "nice to have" you'll actually miss. In Laravel, wrap CassandraConnection::execute() to fire a log entry (query, bound args count, duration, consistency) and optionally a Telescope-like listener. Never log raw bound values in prod (PII).
Testing strategy. Because the extension is heavy, define CassandraConnectionInterface with execute(string $cql, array $args): array and bind a fake for unit tests. Integration-test against a real single-node cluster (docker cassandra:3.11 or scylladb) — mocking Cassandra's query semantics is more trouble than it's worth.
Schema as code. Since neither the bundle nor the driver manages DDL, add an artisan command cassandra:migrate that runs versioned .cql files from database/cassandra/. This is the piece every Cassandra integration needs and no framework bundles for you.
Port a legacy Symfony app by wrapping, not rewriting. If you're migrating a Symfony app that uses this bundle, the safe move is to keep the bundle's Connection semantics (same method names, same config keys) behind a Laravel CassandraConnection class. Repositories then compile against either side unchanged.
ext-cassandra is a native extension, not a Composer package. It can't be installed on shared hosting or many serverless platforms (Laravel Vapor included). CI needs libuv/openssl and the C++ driver. Build it into your base Docker image and version-pin it.cassandra.<cluster>_session) vary between releases, which is why you bin/console debug:container cassandra on the Symfony side. In Laravel, always bind explicit interfaces; never rely on container naming conventions for Cassandra.Configuration.php is the only reliable reference for accepted keys. consistency is a string like quorum; in code you'll need Cassandra::CONSISTENCY_QUORUM constants. protocol_version must match your cluster (4 for Cassandra 3.x, 5 for 4.x) — a mismatch causes opaque handshake failures, not a helpful error.Cassandra\Exception\TimeoutException, UnavailableException, InvalidQueryException, AlreadyExistsException, etc. Map them in your wrapper — at minimum translate TimeoutException/UnavailableException into a retryable RuntimeException and let InvalidQueryException bubble up untouched (it means a bug). Silently swallowing them is how you get mystery data loss.Cassandra\Session is long-lived and internally pooled. Creating one per query will exhaust connections under load; creating one per request loses warmup but is safe. Bind as a singleton in the container and never new Cassandra\Cluster() in a controller.Keyspace not found error deep in the connect call — check cqlsh -e "DESCRIBE KEYSPACES" before debugging anything else.ExecutionOptions. Teams routinely set quorum globally and then wonder why one read is slow — that read probably should be ONE or LOCAL_ONE. Bake consistency into repository methods, not into config.paginate() helper. You must thread Cassandra\ExecutionOptions::$pagingState across requests (store it in the session or the client). The bundle's default_page_size only sets a default; it doesn't paginate for you.Cassandra\Row / Cassandra\Collections into domain code. Convert to plain arrays or DTOs inside the repository. This is what lets you replace the underlying driver later — which, given the EOL status, is a when, not an if.ALLOW FILTERING will bite you. It works in dev and destroys production. The bundle gives you a query surface with zero guardrails; add a static analysis rule or code-review checklist banning it outside known-partition queries.app.debug. Per-query logging on a high-write Cassandra workload is measurable.retry_policy in the bundle is passed to the cluster builder, not the session; changing it requires rebuilding the session. Since sessions are singletons, a config change means restart, not a request-scope tweak.vendor/ or a packages/ path, pin the PHP version, assign an owner, and file a removal ticket. Zero dependents and archived status mean no community answers when something breaks — you own every failure.php -m | grep cassandra (extension), cqlsh -u ... (cluster reachability and auth), DESCRIBE KEYSPACES (keyspace exists), protocol_version match, then your code. Almost every "bundle is broken" incident is one of the first three.How can I help you explore Laravel packages today?