Download the PHP package mroosz/php-cassandra without Composer
On this page you can find all versions of the php package mroosz/php-cassandra. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package php-cassandra
php-cassandra: A modern Cassandra client for PHP
php-cassandra is a pure-PHP client for Apache Cassandra and ScyllaDB with support for CQL binary protocol v3, v4 and v5 (Cassandra 2.1+ incl. 3.xβ5.x; ScyllaDB 6.2, 2025.x, 2026.x), synchronous and asynchronous APIs, prepared statements, batches, result iterators, object mapping, SSL/TLS, and LZ4 compression.
Packagist: mroosz/php-cassandra
Repository: GitHub β MichaelRoosz/php-cassandra
Table of contents
- php-cassandra: A modern Cassandra client for PHP
- Table of contents
- Introduction
- Why choose php-cassandra?
- Key Features
- Requirements
- System Requirements
- PHP Extensions
- Data Type Compatibility
- Installation
- Using Composer (Recommended)
- Without Composer
- Quick start
- Basic Connection and Query
- Prepared statements
- Async Operations Example
- Error Handling Example
- SSL/TLS Connection Example
- Connecting
- Consistency levels
- Queries
- Prepared statements
- Batches
- Results and fetching
- Object mapping
- Data types
- Type definition syntax for complex values
- Collection updates
- Lightweight transactions (LWT)
- JSON support
- Events
- Tracing and custom payloads (advanced)
- Asynchronous API
- Compression
- Error handling
- Exception Hierarchy
- Error Handling Patterns
- Basic Error Handling
- Specific Server Error Handling
- Retry Logic with Exponential Backoff
- Timeout Handling
- Error Information Access
- Configuration Reference
- Connection Configuration
- Node Configuration
- Connection Options
- Request Options
- Query Options
- Execute Options
- Prepare Options
- Batch Options
- Advanced Configuration
- Value Encoding Configuration
- Event Listeners
- Notes
- Frequently Asked Questions (FAQ)
- General Questions
- Installation and Setup
- Data Types and Modeling
- Migration Guide
- From DataStax PHP Driver
- Connection Setup
- Query Execution
- Prepared Statements
- Data Types
- Async Operations
- Migration Checklist
- Connection tuning examples
- Configuring value encoding
- Warnings listener
- Event processing patterns
- v5 keyspace per request
- Tracing notes
- Performance tips
- Benchmarks
- Version support
- Server compatibility and required settings
- Server-side features that are off by default
- API reference (essentials)
- Changelog
- License
- Contributing
- Development Setup
- Contribution Guidelines
- Code Standards
- Contributors
- Supporting the Project
Introduction
php-cassandra is a modern PHP client for Apache Cassandra that prioritizes correctness, performance, and developer experience. This library aims to provide full protocol coverage and advanced features while maintaining simplicity.
Why choose php-cassandra?
π Modern Architecture
- Pure PHP implementation with no external dependencies
- Support for latest Cassandra protocol versions (v3/v4/v5)
- Built for PHP 8.1+ with modern language features
β‘ High Performance
- Asynchronous request pipelining for maximum throughput
- LZ4 compression support for reduced bandwidth
- Prepared statement caching and reuse
π― Developer Friendly
- Complete data type coverage including complex nested structures
- Rich configuration options with sensible defaults
- Object mapping with customizable row classes
Key Features
- Protocol Support: v3/v4/v5 with automatic negotiation
- Transports: Sockets and PHP streams (including SSL/TLS)
- Request Types: Synchronous, Asynchronous
- Statements: Prepared statements with positional/named binding, auto-prepare
- Data Types: Full coverage including collections, tuples, UDTs, custom types, vectors
- CQL Coverage: Incremental collection updates, lightweight transactions, JSON, TTL/timestamps
- Results: Iterators, multiple fetch styles, object mapping
- Events: Schema/status/topology change notifications
- Advanced: LZ4 compression, server overload signaling, tracing support
Requirements
System Requirements
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| PHP Version | 8.1.0 | 8.3+ | Latest stable version recommended |
| Architecture | 32-bit/64-bit | 64-bit | 64-bit required for Bigint/Counter/Date/Duration/Time/Timestamp types and defaultTimestamp request option (unsupported on 32-bit) |
PHP Extensions
No PHP extension is required; the library runs on a minimal PHP build. The following are optional and only enhance functionality:
| Extension | Required | Purpose | Notes |
|---|---|---|---|
| sockets | Optional | Socket transport | Required for connections configured with SocketNodeConfig; StreamNodeConfig connections need no extension |
| openssl | Optional | TLS/SSL encrypted connections | Required for tls:// connections configured with StreamNodeConfig |
| lz4 | Optional | Native LZ4 (de)compression | Used automatically when present for much faster compressed connections; a pure-PHP implementation is the transparent fallback |
| gmp or bcmath | Optional | Faster large integer math | Speeds up the Varint and Decimal types; gmp is preferred when both are present, and a pure-PHP calculator is the fallback |
Data Type Compatibility
Some Cassandra data types require 64-bit PHP and are unsupported on 32-bit:
| Type | 32-bit PHP | 64-bit PHP | Notes |
|---|---|---|---|
Bigint |
β οΈ Partial | β Full | Supported if the value is within 32-bit range |
Counter |
β οΈ Partial | β Full | Supported if the value is within 32-bit range |
Date |
β Unsupported | β Full | Requires 64-bit PHP |
Duration |
β Unsupported | β Full | Requires 64-bit PHP |
Time |
β Unsupported | β Full | Requires 64-bit PHP |
Timestamp |
β Unsupported | β Full | Requires 64-bit PHP |
Additionally, the defaultTimestamp request option (in QueryOptions and BatchOptions) requires 64-bit PHP and is unsupported on 32-bit.
Installation
Using Composer (Recommended)
Then include Composer's autoloader in your application entrypoint (if not already):
Without Composer
If you can't use Composer, you can load the library's own autoloader:
Quick start
Basic Connection and Query
Prepared statements
Async Operations Example
Error Handling Example
SSL/TLS Connection Example
Connecting
Create NodeConfig instances and pass them to Connection:
Connection options are provided via ConnectionOptions:
enableCompression= use LZ4 if enabled on serverthrowOnOverload= true to ask server to throw on overload (v4+)nodeSelectionStrategy=Random(default) orRoundRobinpreparedResultCacheSize= cache size for prepared metadata (default 100)
Keyspace selection β the keyspace given to the constructor, or set later with $conn->setKeyspace('ks'), applies to every request sent on the connection, but how it gets there depends on the negotiated protocol version:
- v3/v4: a
USEis sent, so the keyspace is a property of the node's session and holds for every request on the connection. A keyspace that does not exist fails the call that set it, and leaves the connection on the keyspace it was already on. Switching also empties the prepared-statement cache, since aPREPAREcannot say which keyspace it was prepared in before v5. Moving off a keyspace βsetKeyspace('')on a connected v3/v4 connection β is refused: there is no CQL that un-sets a session's keyspace, so open a new connection without one instead. - v5: the keyspace travels with each request (
USEis deprecated from v5), so it is filled into every request on its way to the wire. Override it per request with thekeyspaceoption on Query/Execute/Prepare/Batch options (see below). A keyspace that does not exist fails the next request rather thansetKeyspace(). - A request you build yourself and pass to
syncRequest()/asyncRequest()gets the keyspace too. Set thekeyspaceoption on it to point that one statement elsewhere, or qualify the table name to ignore the keyspace entirely. A request sent more than once takes whatever the connection is on at each send; only akeyspaceoption you set yourself survives a latersetKeyspace(). - The name is taken exactly as given, on every protocol version: a keyspace created as
MyKsis reached by that spelling, not bymyks. $conn->getKeyspace()reads back what the connection is on.
Consistency levels
Use the Consistency enum:
ALL,ANY,EACH_QUORUM,LOCAL_ONE,LOCAL_QUORUM,LOCAL_SERIAL,ONE,QUORUM,SERIAL,THREE,TWO
Apply per call or as default via setConsistency().
Queries
Synchronous:
Asynchronous:
Query options (QueryOptions):
autoPrepare(bool, default true): transparently prepare+execute when neededpageSize(positive int up to 2,147,483,647;nulluses the server default)pagingState(string)serialConsistency(SerialConsistency::SERIALorSerialConsistency::LOCAL_SERIAL)defaultTimestamp(microseconds since epoch)- Requires 64-bit PHP; unsupported on 32-bit
namesForValues(bool): true to use associative binds; if not explicitly set, it is auto-detected for queries and executeskeyspace(string; protocol v5 only)nowInSeconds(int; protocol v5 only)
Notes:
- If you supply non-
Value\*PHP values withQueryOptions(autoPrepare: true), the driver auto-prepares + executes for correct typing. - Always use fully-qualified table names (including keyspace) for
PREPAREstatements to avoid ambiguity, e.g.SELECT ... FROM ks.users WHERE ....
Fetch all pages helpers:
Prepared statements
Pagination with prepared statements:
Execute all pages helper:
Additional notes:
- For
PREPAREandEXECUTE,namesForValuesis auto-detected if not set explicitly based on the array keys (associative vs indexed). - Always use fully-qualified table names in prepared statements.
Batches
Batch notes:
- BATCH does not support names for values at the protocol level for simple queries; use positional values for
appendQuery. For prepared entries, provide values consistent with the prepared statement (associative for named markers). BatchOptions:serialConsistency,defaultTimestamp(64-bit PHP only),keyspace(v5),nowInSeconds(v5).
Results and fetching
query()/execute() return a Result; call asRowsResult() for row-returning queries. Supported RowsResult methods:
fetch(FetchType::ASSOC|NUM|BOTH)returns next row or falsefetchAll(FetchType)returns all remaining rowsfetchColumn(int $index)/fetchAllColumns(int $index)fetchKeyPair(int $keyIndex, int $valueIndex)/fetchAllKeyPairs(...)getIterator()returns aResultIteratorso you canforeach ($rowsResult as $row)
Example:
Advanced fetching examples:
Pagination example:
Object mapping
You can fetch rows into objects by implementing RowClassInterface or by using the default RowClass:
Data types
All native Cassandra types are supported via classes in Cassandra\Value\*. You may pass either:
- A concrete
Value\...instance, or - A PHP scalar/array matching the type; the driver will convert it when metadata is available (prepared statements, or
query()with the defaultautoPrepare).
Without that metadata (e.g. query(..., autoPrepare: false) or Batch::appendQuery() simple-query values) a bare PHP int is encoded as 32-bit int, and a bare DateTime has no unambiguous encoding β wrap large integers in Value\Bigint and temporal values in Value\Timestamp/Date/Time (or use a prepared statement). The driver throws rather than silently sending a wrong value.
Examples:
UUID / Timeuuid input forms
Cassandra\Value\Uuid and Cassandra\Value\Timeuuid accept any of three forms, distinguished by length:
- the canonical 36-character string
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx(case-insensitive); - the compact 32-character undashed hex string
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; - the raw 16-byte binary form (for example a value read with
UuidEncodeOption::AS_BINARY, which can be re-bound directly without a formatβparse round-trip).
The value is stored raw internally, so getBinary() is a no-op and getValue() always returns the canonical lowercase string. Any string that is none of these forms is rejected with a Cassandra\Exception\ValueException.
Type definition syntax for complex values
For complex types, the driver needs a type definition to encode PHP values. Wherever you see a parameter like \Cassandra\Type|(array{ type: \Cassandra\Type }&array<mixed>), you can either pass a scalar Type::... (for simple elements) or a definition array with nested types for complex structures. The common shapes are:
- List:
['type' => Type::LIST, 'valueType' => <elementType>, 'isFrozen' => bool] - Set:
['type' => Type::SET, 'valueType' => <elementType>, 'isFrozen' => bool] - Map:
['type' => Type::MAP, 'keyType' => <keyType>, 'valueType' => <valueType>, 'isFrozen' => bool] - Tuple:
['type' => Type::TUPLE, 'valueTypes' => [<t1>, <t2>, ...]] - UDT:
['type' => Type::UDT, 'valueTypes' => ['field' => <type>, ...], 'isFrozen' => bool, 'keyspace' => 'ks', 'name' => 'udt_name'] - Vector:
['type' => Type::VECTOR, 'valueType' => <elementType>, 'dimensions' => int]
Examples
Nested complex example (Set
Collection updates
On non-frozen set, list, and map columns you can add or remove individual elements instead of rewriting the whole collection. This is plain CQL (DataStax docs) β php-cassandra adds no special API: you write the UPDATE yourself and bind only the delta (the elements to add or remove) as ?.
| Operation | set |
list |
map |
|---|---|---|---|
| Replace whole collection | INSERT, or SET col = ? |
INSERT, or SET col = ? |
INSERT, or SET col = ? |
| Add | SET col = col + ? (merges members) |
SET col = col + ? (append)SET col = ? + col (prepend) |
SET col = col + ? (merges entries) |
| Remove | SET col = col - ? (removes those members) |
SET col = col - ? (removes by value β every occurrence) |
SET col = col - ? (removes keys; pass a set/list of keys) |
| Single element | β | SET col[?] = ? (by index)DELETE col[?] FROM β¦ (by index) |
SET col[?] = ? (by key)DELETE col[?] FROM β¦ (by key) |
| Clear | SET col = {} or DELETE col FROM β¦ |
SET col = ? with [], or DELETE col FROM β¦ |
SET col = {} or DELETE col FROM β¦ |
Notes:
- Use
?for the whole collection operand in prepared queries.{ ? }/[ ? ]inside the query string is not valid CQL (curly braces and square brackets are literal syntax only). Indexes and keys incol[?]are bindable. - List
-matches by value, not by position, and removes every occurrence. To remove by position useDELETE col[i] FROM β¦, which requires an internal read and is unsafe under concurrent writes β prefer removal by value. - Assigning
nullto a map key (SET col[?] = ?withnull) deletes that entry. - An empty collection is stored as
null; reading it back yieldsnull, not[]. - Frozen collections (
frozen<set<...>>, etc.) cannot use+/-β the server rejects it with anInvalidException. Assign the full value instead. - Incremental updates combine with
USING TTLand with conditions (IF β¦). Because a non-frozen collection is multi-cell,TTL(col)returns one entry per element. - With default
autoPrepare, plain PHP arrays work as binds (the driver prepares the statement to learn the types β see Data types); passing an explicitSetCollection/MapCollection/ListCollectionvalue skips that step and always works.
Each example below is shown twice: first with explicit Value\...::fromValue() objects (always works, no prepare step), then with plain PHP values (relies on the default autoPrepare, which prepares the statement to learn the column types).
Set β add and remove members:
Map β merge entries, remove keys, or address a single key:
List β append, prepend, address a position, or remove by value:
Nested collections (map<int, frozen<list<text>>>, set<frozen<list<int>>>, list<frozen<udt>>) support the same +/- operations on the outer collection; the frozen inner value is always replaced as a whole.
Counter columns use the same + pattern with Counter::fromValue():
Special values:
new \Cassandra\Value\NotSet()encodes a bind variable as NOT SET (distinct from NULL); this requires protocol v4 or newer
Lightweight transactions (LWT)
INSERT, UPDATE and DELETE accept an IF clause for compare-and-set semantics (see the DataStax docs). No special API is needed β a conditional statement simply returns a rows result whose first column is [applied]:
The same applies to conditional updates and deletes:
Use serialConsistency to choose the Paxos consistency level, and read back with Consistency::SERIAL / Consistency::LOCAL_SERIAL to observe in-progress transactions:
Conditional statements also work inside a batch (all statements must target the same partition):
Notes:
- LWT costs roughly four extra round-trips compared to a normal write β use it only where you need it.
USING TIMESTAMPis not allowed together withIF NOT EXISTS; the timestamp comes from the transaction itself.
JSON support
CQL can read and write rows as JSON documents (see the DataStax docs). The driver transports the document as a plain varchar, so no dedicated API is required:
By default a column that is absent from the document is written as null. Append DEFAULT UNSET to leave such columns untouched instead:
Events
Register a listener and subscribe for events on the connection:
Non-blocking event polling:
Tracing and custom payloads (advanced)
You can enable tracing on a request. Custom payloads require protocol v4 or
newer and are supported by Query, Prepare, Execute, and Batch requests.
The driver rejects other version/opcode combinations before sending them.
Asynchronous API
The async API lets you pipeline multiple requests without blocking. Each async method returns a Cassandra\Statement handle that you can resolve later.
You now have both blocking and non-blocking control:
- Blocking per statement:
getResult()/getRowsResult()/waitForResponse() - Blocking for sets:
waitForStatements(array $statements)andwaitForAllPendingStatements() - Non-blocking/polling:
drainAvailableResponses(int $max = PHP_INT_MAX): intβ processes up tomaxresponses if availabletryResolveStatement(Statement $statement): boolβ resolves a specific statement if possibletryResolveStatements(array $statements, int $max = PHP_INT_MAX): intβ resolves from a set without blockingwaitForAnyStatement(array $statements): Statementβ blocks until any of the given statements completes
Basics:
Waiting for all responses:
Non-blocking draining and polling:
Prepared + async:
Advanced waiting:
What $timeoutInSeconds means on the waits
Every wait takes the same four values β 0 makes one non-blocking read attempt
and returns, n waits at most that many seconds, INF waits until something
arrives. Only the default, null, differs between them, because each has a
different thing to fall back on:
| Wait | null means |
|---|---|
waitForStatements(), waitForAnyStatement(), waitForAllPendingStatements() |
let the statements' own request timeouts bound it |
waitForNextResponse() |
use the connection's requestTimeoutInSeconds β there is no statement to go by |
waitForNextEvent() |
wait for as long as it takes; an event can arrive at any time |
Whichever it is, every request in flight keeps its own budget while a wait runs,
so one going overdue is noticed there rather than only when its own caller next
asks about it. A wait that ends up unbounded β no bound of the caller's and no
bounded request in flight β leans entirely on the heartbeat to tell a dead
connection from a quiet one; with heartbeatIntervalInSeconds set to null as
well, the transport's stall window is all that is left and its elapsing fails
the connection.
Compression
Enable LZ4 compression (if supported by the server) via ConnectionOptions:
Notes:
- Compression is negotiated during STARTUP. When enabled, the client accepts server-compressed frames and transparently decompresses them.
- The client may still send some frames uncompressed depending on size/heuristics; this is allowed by the protocol.
- LZ4 works out of the box with a pure-PHP implementation. If the native
lz4PHP extension is installed it is detected and used automatically for substantially faster (de)compression β no configuration or code change required.
Error handling
php-cassandra provides comprehensive error handling with a well-structured exception hierarchy. Understanding these exceptions helps you build robust applications with proper error recovery.
Exception Hierarchy
Which timeout is which
Three different exceptions report a timeout, and they mean quite different things:
| Exception | Raised by | Meaning | Node health |
|---|---|---|---|
ServerException\ReadTimeoutException / WriteTimeoutException |
the coordinator | the server hit its own read_request_timeout / write_request_timeout and said so |
fine β it answered |
RequestTimeoutException |
the client | the server never answered within requestTimeoutInSeconds |
not blamed; the connection stays open and only the request that ran out is finished |
SocketException / StreamException (with a SOCKET_TIMEOUT_DURING_* / STREAM_TIMEOUT_DURING_* code) |
the transport | the connection made no progress within the stall timeout | suspect β counted as a node failure |
A ConnectionException naming the heartbeat is the fourth possibility: the connection went quiet and did not answer its OPTIONS probe, so it is treated as dead even though a request may still be outstanding.
Error Handling Patterns
Basic Error Handling
Specific Server Error Handling
Retry Logic with Exponential Backoff
Timeout Handling
Error Information Access
Most server exceptions provide additional context:
Configuration Reference
Connection Configuration
Node Configuration
StreamNodeConfig (supports SSL/TLS)
SocketNodeConfig (requires ext-sockets)
Both timeout components are honoured, so sub-second timeouts work (['sec' => 0, 'usec' => 500000] is half a second). Setting both to 0 disables the timeout, matching the meaning of the socket option itself; request timeouts, wait bounds and the heartbeat are handed to the read itself and so fire on schedule regardless β see Choosing timeout values for what disabling the receive timeout does give up. connectTimeoutInSeconds is separate and must stay positive, so an unreachable host can never wedge the client indefinitely.
Choosing timeout values
There are two independent layers, and they answer different questions.
Transport timeouts (SO_RCVTIMEO/SO_SNDTIMEO, timeoutInSeconds) are stall timeouts: they bound how long the connection makes no progress at all, not how long a request takes end to end. Sending a large batch or reading a large result set over a slow link therefore does not trip them, and there is no implicit ceiling on payload size.
The request timeout (ConnectionOptions::$requestTimeoutInSeconds, default 30s) is what governs a slow query. A server that is simply thinking sends nothing, which looks exactly like a stalled connection, so the transport timeout alone cannot tell the two apart β it keeps waiting, and only the request timeout decides when to give up, with a RequestTimeoutException. Adjust it per situation:
requestTimeoutInSeconds is available on QueryOptions, ExecuteOptions, BatchOptions and PrepareOptions, and as the last argument of every call that sends a request β query(), queryAll(), queryAsync(), execute(), executeAll(), executeAsync(), batch(), batchAsync(), prepare(), prepareAsync(), syncRequest() and asyncRequest(). Precedence runs from the most specific to the least: the explicit argument, then the request's own options, then setRequestTimeout(), then ConnectionOptions.
The explicit argument bounds each request the call sends, not the call as a whole β when the driver has to prepare or reprepare the statement first, the PREPARE and the request it precedes each get the full budget, and on the paged helpers (queryAll(), executeAll()) each page request does:
The default of 30s is chosen to sit above Cassandra's own coordinator timeouts, so that the server answers with a proper error before the client gives up. Raise it for anything the server itself allows more time for, or that is bounded by data volume rather than by a server-side limit:
| Operation | Suggested | Why |
|---|---|---|
| ordinary reads and writes | 30s (default) | above read_request_timeout (5s), write_request_timeout (2s) and range_request_timeout (10s) |
TRUNCATE |
90s | truncate_request_timeout is 60s server-side |
| DDL / schema changes | 120s | waits for schema agreement across all nodes |
| full scans, aggregates, large batches | 300s or more | bounded by how much data is walked, not by a server-side timeout |
Raising the request timeout is safe: a connection that has actually died is still caught within about 35s by the heartbeat, rather than being left to the request timeout to notice.
Request budgets vs. wait bounds
Two different things are being bounded, and the API keeps them apart by name:
| Set by | Means | On expiry | |
|---|---|---|---|
Request budget β requestTimeoutInSeconds |
ConnectionOptions, setRequestTimeout(), request options, the $requestTimeoutInSeconds argument |
how long the server may take to answer a request | the request is given up on: RequestTimeoutException |
Wait bound β timeoutInSeconds |
the waitForβ¦() methods |
how long this call may block | the call returns empty-handed; nothing is given up on |
Every waitForβ¦() method takes the wait bound the same way:
| Value | Meaning |
|---|---|
null |
that method's default, see below |
0 |
do not wait: make one non-blocking read attempt and return |
n |
wait at most n seconds |
INF |
wait for as long as it takes |
The default (null) is whatever makes sense for that wait:
| Method | null means |
|---|---|
waitForNextEvent() |
wait for as long as it takes β an event can arrive at any time |
waitForNextResponse() |
the connection's request timeout. Not "as long as it takes": with nothing in flight no response can ever arrive, so such a wait would never end |
waitForStatements(), waitForAnyStatement(), waitForAllPendingStatements() |
let the statements' own budgets bound the wait |
Note that 0 is the shortest wait, not an unlimited one β the opposite of what it means for the transport timeouts, where SO_RCVTIMEO => ['sec' => 0, 'usec' => 0] disables the timeout because that is what the socket option itself means. Even 0 still costs one read β a non-blocking one, so it does not wait on the transport either; for a look that never bounds itself by a deadline, use tryReadNextEvent() / tryReadNextResponse(), or tryResolveStatement() / tryResolveStatements() when waiting on statements.
Every wait is bounded by whichever comes first, and a request that runs out of budget is given up on from any wait β so it cannot quietly outlive its budget while you are, say, listening for events. Which wait reports it is a separate matter: only a call that was asked about that request raises it. waitForNextEvent() and waitForNextResponse() were asked for the next event or response, not about any request in particular, so they run their course; the caller finds out when they next touch the statement, which then throws RequestTimeoutException.
When several requests run out at the same moment they are all finished in one pass and reported as a single failure, which carries the statements themselves:
A parked stream id is only released when its late answer arrives, so a node that keeps leaving requests unanswered would tie up more and more of them. Past maxOrphanedStreams the connection is closed and started over, and that is raised β as a ConnectionException β whatever the caller was waiting for: their connection is gone and the requests still in flight on it went with it.
Cassandra's own coordinator defaults are 5s for reads, 2s for writes, 10s for range requests and the generic request timeout, and 60s for TRUNCATE. Keep the request timeout above whichever of those apply, so the server gets to answer with a proper error β which this driver surfaces as ReadTimeoutException / WriteTimeoutException β instead of the client giving up first.
A RequestTimeoutException leaves the connection open and does not count the node as failed, so one expensive query neither drops your prepared statements nor pushes a healthy node out of rotation. Only the request that ran out of time is finished.
Deadlines are handed to the read itself rather than merely consulted before one starts, so the transport timeout does not bound when a deadline is noticed: a request timeout of half a second fires after half a second even against a completely silent server whose receive timeout is 15s. A deadline that has already passed never buys a blocking read at all, so a request that ran out while you were busy elsewhere is reported at once, and a connection busy answering other requests cannot defer it either.
That makes the two settings genuinely independent, and disabling the receive timeout safe: SO_RCVTIMEO => ['sec' => 0, 'usec' => 0] and timeoutInSeconds: 0 mean "no stall window", not "no deadlines" β request timeouts, wait bounds and the heartbeat all still fire on schedule. What you give up is the transport's own judgement that a connection making no progress at all has failed. That judgement is what ends a wait carrying no deadline of its own on a connection with the heartbeat turned off β a stall window elapsing on such a read is raised as the transport failure it is (SocketException / StreamException, both NodeExceptions) rather than waited out, since nothing else is left to notice a connection that died. Turn both of those off as well and nothing bounds the wait at all.
The heartbeat (heartbeatIntervalInSeconds, default 30s) is what distinguishes a dead connection from a quiet one β at the socket, a coordinator that is still thinking and a node that vanished look identical. Whenever the connection has been silent for the interval, an OPTIONS frame is sent; because stream ids are multiplexed, it is answered on its own stream while a slow request is still being computed. A broken connection therefore surfaces after heartbeatInterval + heartbeatTimeout (~35s by default) as a ConnectionException, no matter how high the request timeout is β which is what makes generous request timeouts safe to set. Reads are bounded by when the probe is next due, so that holds during a wait with no deadline of its own and whatever the transport timeouts are set to.
The probe is the driver's own request and stays out of your way entirely: it is held to heartbeatTimeoutInSeconds rather than to your request timeout, it does not keep waitForAllPendingStatements() waiting, and its answer is resolved out of sight rather than handed back by waitForNextResponse() / tryReadNextResponse() β so a loop pumping the connection for responses never sees a Supported frame it did not ask for.
Note that these three values are independent: the request timeout follows your cluster's coordinator timeouts, while the heartbeat interval should stay below the idle timeout of any NAT, firewall or load balancer between you and the node. They both default to 30s by coincidence, not because they are related.
Timeouts and async statements
The budget of an async statement runs from the moment its request was written to the node, not from the moment you get around to waiting for it β so a statement gets the same total allowance whether you wait immediately or after other work:
Each statement carries the timeout its own request asked for, so a mixed batch is not forced onto one number:
When several statements are waited on together, the wait ends as soon as the first of them exhausts its own budget.
The polling methods (tryResolveStatement(), tryReadNextResponse(), β¦) never wait, so how long your loop runs is yours to bound β but they do keep the same books a wait does. Request budgets are still enforced, so a statement you only ever poll runs out of time and releases its stream id rather than staying pending for good; and the heartbeat is still sent, so a connection that died quietly is noticed by an application that never blocks. Statement::tryGetResult() and its siblings therefore raise RequestTimeoutException once the budget is gone, exactly as the blocking calls do. To look at a statement without any of that, use Statement::peekResponse(), which only reads what has already arrived.
When an async statement times out, only that statement is finished β the connection stays open and every other statement in flight on it keeps waiting:
What makes that safe is that the timed-out statement's stream id is not returned to the pool β the server may still answer on it, and reusing it would let that late answer resolve a different request. The id is parked until the late answer arrives, then released. A connection whose requests keep timing out would tie up more and more ids, so past maxOrphanedStreams (default 24) the connection is closed instead.
Synchronous requests take their stream ids from the same pool, so a sync timeout is handled identically and also leaves the connection open β which matters because closing it would clear the prepared statement cache and force every prepared statement to be prepared again.
Closing a connection invalidates every statement still in flight on it β their stream ids meant something only on that connection. Those are marked abandoned, and touching one fails at once with a StatementException rather than waiting out another request timeout:
The host may be a hostname, an IPv4 literal, or an IPv6 literal in either bare (::1) or bracketed ([::1]) form. It is resolved with getaddrinfo(), so IPv4-only, IPv6-only and dual-stack hosts all work; when a name resolves to several addresses, each is tried in turn until one connects. URL schemes (tcp://, tls://) are not accepted here β use StreamNodeConfig for TLS.
Connection Options
Request Options
Query Options
Execute Options
Prepare Options
Batch Options
Advanced Configuration
Value Encoding Configuration
Map decoding defaults to MapEncodeOption::AUTO. Maps with losslessly
representable scalar keys are returned as native PHP arrays, preserving the
existing result shape. A map whose configured keys are objects or composites
β for example a timestamp key decoded as DateTimeImmutable, or a tuple key β
is returned as Cassandra\Value\MapCollection; inspect its ordered
MapEntry values with getEntries(). The decision is based only on the
declared key type and the active ValueEncodeConfig, never on the entries in a
particular value. Given the same key type and configuration, the result type is
stable across rows: empty and non-empty maps have the same PHP representation.
For example, map<text, β¦> is an array in AUTO mode, while
map<timestamp, β¦> is a MapCollection when timestamps are configured as
DateTimeImmutable (and an array when configured as strings or integers). Use
AS_MAP_COLLECTION to request MapCollection for every map, or AS_ARRAY to
require an array and receive a ValueException when conversion would be lossy.
Handling both results in AUTO mode:
Force a stable representation for every map returned by the connection:
Create a map with timestamp object keys through the entry API:
Tuple keys use the same API:
Event Listeners
Notes
pageSizeis sent unchanged when it is between 1 and 2,147,483,647. Zero, negative values and larger values are rejected; usenullfor the server default.- If you supply non-
Value\*PHP values withQueryOptions(autoPrepare: true), the driver auto-prepares + executes for correct typing. - On
UNPREPAREDserver errors, the driver transparently re-prepares and retries. This coversexecute()/executeAsync()and batches: a node answersUNPREPAREDfor one statement at a time, so a batch is re-prepared one statement per round and re-sent carrying the new statement id, with a budget ofMAX_REPREPARATIONSplus one round per distinct prepared statement it holds. - Always use fully-qualified table names in
PREPAREstatements.
Frequently Asked Questions (FAQ)
General Questions
Q: What's the difference between this library and the DataStax PHP Driver?
A: The main differences are:
- Pure PHP: No C extensions required, easier deployment
- Protocol v5 Support: Full support for latest Cassandra protocol features
- Active Development: Actively maintained with regular updates
- Modern PHP: Built for PHP 8.1+ with modern language features
Q: Can I use this with older versions of Cassandra?
A: Yes! The library supports protocol versions v3, v4, and v5:
- Cassandra 2.1+: Protocol v3
- Cassandra 2.2+: Protocol v4 (recommended)
- Cassandra 4.0+: Protocol v5 (recommended for new deployments)
Installation and Setup
Q: Do I need any PHP extensions?
A: No. The library requires no PHP extension and works on a minimal PHP build, but some extensions enhance functionality:
ext-sockets: Required forSocketNodeConfig(alternative:StreamNodeConfig, which needs no extension)ext-openssl: Required fortls://connections configured withStreamNodeConfigext-lz4: Much faster native LZ4 (de)compression; a pure-PHP implementation is used otherwiseext-gmporext-bcmath: Faster large integer math for theVarintandDecimaltypes; a pure-PHP calculator is used otherwise
Q: Can I run this on 32-bit PHP?
A: Yes, with limited support. The following features are unsupported on 32-bit PHP: value types Bigint, Counter, Date, Duration, Time, Timestamp, and the defaultTimestamp request option. Use 64-bit PHP for full compatibility.
Data Types and Modeling
Q: How do I handle complex data structures?
A: Use collections and UDTs:
Q: How do I add or remove items from a set, list, or map?
A: Use CQL UPDATE with + / - and bind the delta as ? (not { ? }). See Collection updates for set/map/list examples and a syntax table.
Q: How do I work with timestamps?
A: Use the Timestamp value class:
Migration Guide
From DataStax PHP Driver
If you're migrating from the DataStax PHP Driver, here are the key differences and migration steps:
Connection Setup
Query Execution
Prepared Statements
Data Types
Async Operations
Migration Checklist
- [ ] Update connection setup - Replace cluster builder with Connection and NodeConfig
- [ ] Update query methods - Replace execute() with query() and asRowsResult()
- [ ] Update data types - Replace Cassandra* types with Cassandra\Value* types
- [ ] Update prepared statements - Use new prepare/execute pattern
- [ ] Update async operations - Replace futures with statement handles
- [ ] Update error handling - Use new exception hierarchy
- [ ] Update batch operations - Use new Batch class
- [ ] Test thoroughly - Verify all functionality works as expected
Connection tuning examples
Configuring value encoding
Warnings listener
Event processing patterns
v5 keyspace per request
- When the server negotiates protocol v5, you can set
keyspaceonQueryOptions,ExecuteOptions, andPrepareOptions. - If you also call
setKeyspace(), the per-request option takes precedence for that request.
Tracing notes
- Use tracing sparingly in production; it adds overhead.
- Read the trace id from the result to correlate with server logs (if enabled).
Performance tips
- Prefer prepared statements for hot paths; the driver caches prepared metadata.
- Iterate results instead of materializing large arrays.
Benchmarks
The following results were produced by the benchmarking suite in benchmarks/ (Dockerized), comparing this library against the legacy DataStax PHP driver and the ScyllaDB PHP driver. Full setup and reproduction steps are documented in benchmarks/README.md; raw outputs are stored under benchmarks/results/.
Notes:
- The DataStax driver runs on PHP 7.1; ScyllaDB and php-cassandra ran on PHP 8.5.
- Environment details and exact commands are in
benchmarks/README.md.
Version support
- Protocol versions v3, v4, and v5 are supported. Features like per-request
keyspace/now_in_secondsrequire protocol v5. - Cassandra 3.0, 3.11, 4.x, 5.x and ScyllaDB 6.2, 2025.1, 2025.2, 2025.3 are part of the regular test matrix.
Server compatibility and required settings
| Server version | Supported? | Protocol(s) | Notes / required settings |
|---|---|---|---|
| Apache Cassandra 2.1.x | β With manual configuration | v3 | Cassandra 2.1 only supports protocol v3 and does not support protocol negotiation. You must set the initial protocol explicitly: new ConnectionOptions(initialProtocolVersion: ProtocolVersion::V3). Optionally, also restrict allowedProtocolVersions to [ProtocolVersion::V3]. |
| Apache Cassandra 2.2.x / 3.x | β (3.0 / 3.11 tested) | v4 | These versions speak protocol v4 but do not support protocol negotiation. The default initialProtocolVersion is ProtocolVersion::V4, so no special configuration is required. |
| Apache Cassandra 4.x | β (tested) | v4 / v5 | Protocol negotiation is supported from 4.x; the driver will automatically negotiate the highest mutually supported protocol (v5, then v4, then v3) using the default ConnectionOptions. |
| Apache Cassandra 5.x | β (tested) | v5 | Fully supported with protocol negotiation. No special configuration is required; v5 will be negotiated when available. |
| ScyllaDB 6.2, 2025.1, 2025.2, 2025.3 | β (tested) | v4 | ScyllaDB currently supports protocol v4 and does not support protocol negotiation. The default initialProtocolVersion is ProtocolVersion::V4, so no special configuration is required. |
For example, to connect to a Cassandra 2.1 cluster:
Server-side features that are off by default
A few CQL features need to be enabled on the server before any driver can use them. They are pure server configuration β nothing changes on the client side.
| Feature | Apache Cassandra | ScyllaDB |
|---|---|---|
| User-defined functions / aggregates | user_defined_functions_enabled: true in cassandra.yaml (called enable_user_defined_functions in 4.0) |
--experimental-features=udf and --enable-user-defined-functions=true; bodies are written in Lua, not Java |
| Materialized views | materialized_views_enabled: true in cassandra.yaml (called enable_materialized_views in 4.0); every CREATE MATERIALIZED VIEW returns a warning that the feature is experimental |
Enabled by default |
SASI indexes (CREATE CUSTOM INDEX β¦ USING 'org.apache.cassandra.index.sasi.SASIIndex', LIKE) |
sasi_indexes_enabled: true in cassandra.yaml (called enable_sasi_indexes in 4.0) |
Not supported |
API reference (essentials)
-
Cassandra\Connectionconnect(),disconnect(),isConnected(),getProtocolVersion()setConsistency(Consistency),withConsistency(Consistency)setKeyspace(string),withKeyspace(string),getKeyspace(),supportsKeyspaceRequestOption(),supportsNowInSecondsRequestOption()query(string, array = [], ?Consistency, QueryOptions)/queryAsync(...)/queryAll(...)prepare(string, PrepareOptions)/prepareAsync(...)execute(Result $previous, array = [], ?Consistency, ExecuteOptions)/executeAsync(...)/executeAll(...)batch(Batch)/batchAsync(Batch)syncRequest(Request, ?float $requestTimeoutInSeconds)/asyncRequest(Request, ?float $requestTimeoutInSeconds)waitForStatements(array $statements, ?float $timeoutInSeconds)/waitForAllPendingStatements(?float $timeoutInSeconds)/waitForAnyStatement(array $statements, ?float $timeoutInSeconds): ?StatementregisterEventListener(EventListener)/unregisterEventListener(EventListener)/waitForNextEvent(?float $timeoutInSeconds): ?EventregisterWarningsListener(WarningsListener)/unregisterWarningsListener(WarningsListener)waitForNextResponse(?float $timeoutInSeconds): ?Responseβ both waits return null when the timeout elapses with nothing to reportsetRequestTimeout(?float)- Non-blocking helpers:
drainAvailableResponses(),tryResolveStatement(),tryResolveStatements(),tryReadNextResponse(),tryReadNextEvent()
-
Results
RowsResult(iterable):fetch(),fetchAll(),fetchColumn(),fetchAllColumns(),fetchKeyPair(),fetchAllKeyPairs(),configureFetchObject(),fetchObject(),fetchAllObjects(),getRowsMetadata(),hasMorePages()PreparedResult(for execute)SchemaChangeResult,SetKeyspaceResult,VoidResult
- Types
Cassandra\Consistency(enum)Cassandra\SerialConsistency(enum)Cassandra\Type(enum) andCassandra\Value\*classes (Ascii, Bigint, Blob, Boolean, Counter, Date, Decimal, Double, Duration, Float32, Inet, Int32, ListCollection, MapCollection, NotSet, SetCollection, Smallint, Time, Timestamp, Timeuuid, Tinyint, Tuple, UDT, Uuid, Varchar, Varint, Vector, ...)
Changelog
See CHANGELOG.md for release notes and upgrade considerations.
License
This library is released under the MIT License. See LICENSE for details.
Contributing
Contributions are welcome! Here's how to get started:
Development Setup
-
Fork and Clone
-
Install Dependencies
-
Start Development Environment
- Run Tests
Docker quickstart for integration tests:
Contribution Guidelines
Code Standards
- PHP 8.1+: Use modern PHP features and syntax
- PSR-12: Follow PHP-FIG coding standards
- Type Hints: Use strict typing everywhere possible
- Documentation: Document all public methods and classes
- Tests: Include tests for all new functionality
Contributors
- Michael Roosz - Current maintainer and lead developer
- Shen Zhenyu - Original driver development
- Evseev Nikolay - Foundation and early development
Special thanks to all contributors who have helped make this library better.
Supporting the Project
If you find this library useful, consider:
- β Starring the repository on GitHub
- π Reporting bugs and suggesting features
- π Contributing code or documentation
- π¬ Sharing your experience with the community
- π Writing tutorials or blog posts
Your support helps keep this project active and improving!