basho/riak
Official PHP client for Riak, Basho's distributed NoSQL database. Provides a simple API for storing, retrieving, and querying data across a Riak cluster, with support for buckets, keys, links, MapReduce, and connection handling.
Install and orient yourself in 5 minutes
composer require basho/riak
The client speaks Riak KV's HTTP interface (default port 8098), not the Protocol Buffers port (8087). You need a reachable Riak 2.x node — in practice the basho/riak-kv Docker image or an existing cluster.
use Basho\Riak;
use Basho\Riak\Node;
$node = (new Node\Builder())
->atHost('127.0.0.1')
->onPort(8098)
->build();
$riak = new Riak($node);
$ping = (new Riak\Command\Builder\Ping($riak))->build()->execute();
var_dump($ping->isSuccess()); // sanity check the connection before anything else
Where to look first. The package is archived and Basho's documentation site is gone, so the source is the documentation. Read these in order:
vendor/basho/riak/README.md — the last surviving tutorial.vendor/basho/riak/src/Basho/Riak/Command/Builder/ — one class per operation; grep here before guessing a method name.vendor/basho/riak/src/Basho/Riak/Command/Response/ — tells you what isSuccess() / isNotFound() / getObject() return.First real use case. A key/value store behind a cache or session layer — one bucket, one object per key, no queries:
$store = (new Riak\Command\Builder\StoreObject($riak))
->buildLocation('user:42', 'app_sessions', 'app_bucket_type')
->withData(json_encode(['cart' => [1, 2, 3]]))
->withContentType('application/json')
->build()
->execute();
$fetch = (new Riak\Command\Builder\FetchObject($riak))
->buildLocation('user:42', 'app_sessions', 'app_bucket_type')
->build()
->execute();
if ($fetch->isNotFound()) {
// cold cache
} elseif ($fetch->isSuccess()) {
$data = $fetch->getObject()->getData(); // already json_decode()'d
}
The three-argument buildLocation($key, $bucketName, $bucketType) is the pattern you'll type a hundred times. Riak 2.x is bucket-type first; there is no "just a bucket" any more.
Register one client per node in the container. Basho\Riak holds a Node and an HTTP API instance; constructing it per request is wasteful but harmless, while constructing it per call is a code smell. Bind it as a singleton and inject it. If you run a multi-node cluster, either put a load balancer in front and point at that, or keep an array of Node\Builder results and pick one per request.
Builder → Command → Response is the universal shape. Every operation is:
$response = (new Riak\Command\Builder\FetchObject($riak))
->buildLocation($key, $bucket, $type)
->build() // returns a Command
->execute(); // returns a Response
Keep the two-step build/execute in mind when you wrap operations in repository classes: build the command in one method, execute in another, and your unit tests can assert on the command's parameters without a live node.
Use CRDTs instead of read-modify-write. This is the single biggest architectural win available with this client. A naive fetch → mutate → store loop loses writes under concurrency unless you carry the vclock back:
$fetch = (new Riak\Command\Builder\FetchObject($riak))
->buildLocation($key, $bucket, $type)->build()->execute();
$store = (new Riak\Command\Builder\StoreObject($riak))
->buildLocation($key, $bucket, $type)
->withData($newData)
->withVClock($fetch->getVClock()) // CAS guard
->build()->execute();
For counters, sets, and maps, skip that entirely:
$response = (new Riak\Command\Builder\UpdateCounter($riak))
->buildBucket('page_views', 'counters')
->increment(1)
->build()->execute();
$response->getCounter(); // new value
$response->getPreviousCounter(); // value before the update
Same builder family exists for UpdateSet / FetchSet and UpdateMap / FetchMap. Offload anything monotonically increasing or set-like to these and you delete an entire class of race conditions.
Model your access patterns before your data. Riak has no joins and key listing is expensive. Decide up front whether a lookup goes through:
FetchKeys/ListKeys command with an index and value; the index field must be registered (index on the bucket type).Command\Builder\Search\FetchObjects with withIndexName() / withQuery() for range or full-text predicates.If none of those fit, Riak is the wrong store for that entity.
Push aggregation to the server with MapReduce — sparingly. Command\Builder\MapReduce lets you chain JS map/reduce phases over a bucket. It's genuinely useful for backfills and one-off reporting, but MapReduce is deprecated in Riak 2.x and removed from the surrounding ecosystem; don't build product features on it. Keep phases in resources/ as .js files, load them with file_get_contents(), and version them alongside your code.
Handle siblings explicitly if you enable allow_mult. With multi-value buckets, concurrent writes leave siblings; getObject()->getData() gives you one and hides the rest. For anything where losing a write matters, inspect the sibling list from the response and merge (last-write-wins by timestamp, or a set union) before writing back with the combined vclock. Decide this at bucket-type creation time — flipping allow_mult later is a cluster operation.
Test against a real node, cheaply. There's no useful in-memory fake. Run docker run -d -p 8098:8098 basho/riak-kv in CI, wait for /ping to return OK, create your bucket types in a fixture, and truncate by deleting/recreating the bucket between tests. For unit tests, the Api layer is swappable — inject a stub HTTP client so you can assert on the generated request (method, path, headers) without a socket.
Health and readiness endpoints. Ping for liveness, Command\Builder\Stats for a richer readiness probe. Cache the result for a few seconds; hitting the node on every request is noise.
The project is dead — budget for that. Basho Technologies shut down in 2017; this repo is archived and its last release is from April 2017. There will be no security fixes, no TLS/library updates, and no PHP-version work. Its practical lifespan is the lifespan of the Riak cluster you already own. If you're greenfielding, this package is a reason to pick a different store, not a reason to pick Riak.
Composer on modern PHP is the first fight. The package predates PHP 7 type declarations and pins an old Guzzle range. On PHP 8 you'll get deprecation notices (implicit nullable parameters, dynamic properties) and Guzzle 5 will not run at all — force guzzlehttp/guzzle:^6 or ^7 in your root composer.json and let the conflict resolver tell you the truth. If you must run it long-term, fork it into your own VCS namespace and patch it there; don't rely on composer update ever touching it again.
Bucket types must pre-exist. riak-admin bucket-type create app_bucket_type '{"props":{"allow_mult":true}}' then riak-admin bucket-type activate app_bucket_type. The default type always exists, which is why tutorials "just work" and your code then fails in production. Treat bucket-type creation as a migration step, checked into your deploy scripts.
JSON is encoded and decoded for you — sometimes twice. With Content-Type: application/json, getData() returns an array, not a string. If you json_decode() it again you'll get null. Conversely, if you setData() an array with a non-JSON content type, the client will stringify it in ways you didn't intend. Pick one convention (store JSON, always) and assert on getObject()->getContentType() in tests.
isSuccess() is not enough. Distinguish isNotFound() from a 500 from a connection failure. A node that's down throws from Guzzle inside execute(); a node that's up but has no surviving primaries returns 503. Wrap execute() in a try/catch and check status — retries belong in a Guzzle middleware, not in a loop around the builder.
Never list keys to paginate a UI. ListKeys on a large bucket is O(n) over the whole keyspace and will eventually take down your node's HTTP listener. If you need paging, you needed 2i or Solr in the design phase. There is no LIMIT/OFFSET.
Counters can't be set. UpdateCounter only increments or decrements. There's no "reset to zero" other than writing a new counter object at the same key (which resets and can produce siblings). If your feature needs "set the counter," it's a map field, not a counter.
MapReduce is deprecated. It still works on most 2.x releases but is on the way out and has a reputation for timing out on large buckets. Cap it, run it from a queue worker, and don't let a web request block on it.
Extension points worth knowing. Node\Builder exposes TLS configuration (certificate, CA file, verify-peer) and auth, which is how you go from localhost to a secured cluster. Api\Http accepts an injected Guzzle client — that's your hook for request logging, tracing headers, and retry/backoff middleware. For raw operations the builders don't cover, drop to Command\Builder\FetchObject-style commands with a custom path, or implement Api yourself against the PB protocol if HTTP overhead becomes the bottleneck.
Debugging tip. Set a Guzzle middleware that dumps X-Riak-Vclock and X-Riak-Sibling-Count on every response. Nine out of ten "the data disappeared" reports are sibling resolution or a lost vclock, and those two headers tell you which within seconds.
How can I help you explore Laravel packages today?