anyitsolutions/neo4j-bolt
A Laravel/PHP package that connects applications to Neo4j using the Bolt protocol, providing a convenient client for running Cypher queries and integrating graph database operations into Laravel projects with simple, configuration-driven connections.
anyitsolutions/neo4j-bolt is a low-level PHP implementation of Neo4j's Bolt binary protocol (PackStream serialization + Bolt message framing). It is not an ORM, not a Laravel package, and not a high-level client — it gives you the wire protocol and you build the rest. With 1 star, 0 dependents, and no release since August 2020, treat it as a small, readable reference implementation you own once installed, not as a maintained dependency.
Install and orient yourself:
composer require anyitsolutions/neo4j-bolt
First places to look:
README.md — the only usage example you'll get; copy it verbatim before adapting.src/Connection/ — the socket/stream connection classes (host/port, handshake, message read/write loop).src/Protocol/V1/ (and any other version folders) — the Bolt version this library actually negotiates. This is the single most important folder to check, because it tells you the newest Neo4j server you can talk to.src/PackStream/ — value (de)serialization; you'll touch this when types round-trip wrong.Minimum viable flow (names are illustrative — confirm against src/ on install, since there is no stable API contract):
use AnyIT\Bolt\Connection\StreamSocket;
use AnyIT\Bolt\Protocol\V1\Session;
use AnyIT\Bolt\Protocol\V1\Message\BasicAuth;
$conn = new StreamSocket('127.0.0.1', 7687);
$session = new Session($conn);
$session->init(new BasicAuth('neo4j', getenv('NEO4J_PASSWORD')));
$session->run(
'MATCH (u:User {email: $email})-[:FOLLOWS]->(f) RETURN f.name AS name LIMIT $limit',
['email' => '[email protected]', 'limit' => 10]
);
foreach ($session->pullAll() as $record) {
$name = $record->get('name'); // or $record['name'] — check the Record class
}
$session->close();
First use case: a read-only endpoint or queued job that runs one parameterized Cypher query and maps records to arrays. That is the sweet spot for this library. Anything resembling "an app data layer" should sit behind your own abstraction (see below), because you will almost certainly outgrow or replace this driver.
Sanity checks before committing:
Neo.ClientError.Security.CredentialsExpired). Change it via cypher-shell first.SUCCESS/RECORD/FAILURE/IGNORED message states — if it doesn't expose failures cleanly, you'll want a thin wrapper (see Gotchas).1. Bind it as a singleton in Laravel, but keep it lazy and disposable.
// config/neo4j.php
return [
'host' => env('NEO4J_HOST', '127.0.0.1'),
'port' => env('NEO4J_PORT', 7687),
'username' => env('NEO4J_USER', 'neo4j'),
'password' => env('NEO4J_PASSWORD'),
'timeout' => env('NEO4J_TIMEOUT', 5),
];
$this->app->singleton(Neo4jConnection::class, fn () => new Neo4jConnection(config('neo4j')));
Do not inject the raw Session everywhere. Inject a narrow interface (GraphReader, GraphWriter) so swapping in laudis/neo4j-php-client or graphaware/neo4j-php-client later is a one-line container change.
2. One session per unit of work, never per request globally. Bolt connections are stateful and single-threaded: one outstanding query at a time. In Octane/Swoole/RoadRunner, a shared connection will interleave frames and corrupt results. Open in the job/action, close in finally.
$session = $this->connect();
try {
$session->run($cypher, $params);
return $this->map($session->pullAll());
} finally {
$session->close();
}
3. Parameterize everything. PackStream handles int, float, string, bool, null, list, and map. Use $param placeholders for values; only interpolate labels and relationship types (which Bolt/PackStream cannot bind anyway), and validate those against an allow-list:
$label = in_array($label, self::ALLOWED_LABELS, true) ? $label : throw new InvalidArgumentException();
$cypher = "MATCH (n:{$label}) WHERE n.id = \$id RETURN n";
4. Explicit transactions for multi-statement writes. Bolt's BEGIN / COMMIT / ROLLBACK are separate messages; wrap them in a helper that guarantees rollback:
public function transaction(callable $fn)
{
$this->session->begin();
try {
$result = $fn($this->session);
$this->session->commit();
return $result;
} catch (\Throwable $e) {
$this->session->rollback();
throw $e;
}
}
5. Batch large results. Prefer LIMIT and pagination in Cypher over pullAll(). If the driver exposes pull($n), loop until SUCCESS to avoid unbounded memory growth from a 100k-row MATCH.
6. Discard writes you don't consume. For CREATE/MERGE/SET statements with no return values, use DISCARD rather than PULL (or just don't iterate) so the stream stays in sync. A half-consumed stream poisons the connection for the next query.
7. Hydrate graph structures centrally. Bolt returns Node, Relationship, and Path structure types — not plain arrays. Build a mapper once:
private function nodeToArray($node): array
{
return [
'id' => $node->getId(), // check actual accessor names in src/
'labels' => $node->getLabels(),
'props' => $node->getProperties(),
];
}
8. Keep reads and writes in different jobs. There is no routing, bookmark, or leader-awareness here. If you ever move to a causal cluster, this driver cannot do read-after-write consistency — your GraphReader abstraction is the seam where that changes.
9. Integration-test against a real container. Unit-test your Cypher strings and mappers; integration-test the driver against neo4j:3.5 in Docker. Add a FakeGraphRepository for controller tests so CI doesn't need a server.
Neo4j version compatibility is the headline risk. A 2020-vintage Bolt driver almost certainly negotiates Bolt 1 (and possibly 2), i.e. Neo4j 3.x. Against Neo4j 4/5 you will get a handshake failure or ignored messages. Verify the version strings in src/Protocol/ and pin your server image accordingly (neo4j:3.5).
FAILURE poisons the stream. After a server error, Bolt requires a RESET before the connection is usable — otherwise every subsequent query returns IGNORED. Wrap your session:
try {
$session->run($cypher, $params);
} catch (BoltFailureException $e) {
$session->reset(); // or discard the connection entirely
throw $e;
}
If the library doesn't send RESET, close and reconnect — cheapest correct behavior.
No connection pooling. Builds are cheap but a new TCP handshake per query is not. A tiny pool keyed by process (checked out/returned, with health check) is worth writing for queue workloads.
PackStream numeric edge cases. PHP has no unsigned 64-bit int; large int64 values may arrive as floats or strings, and float round-trips lose precision. Test with your real IDs/timestamps. Booleans vs. 0/1 and empty maps vs. empty lists are also common serializer bugs in small drivers — write a round-trip test for every PHP type you store.
Nulls in maps. Cypher null inside a map is legal but some PackStream implementations silently drop it; verify before relying on it.
Chunking on large payloads. Bolt splits messages into 64 KB chunks. If you push a multi-megabyte property (a blob, a serialized document), watch for truncated writes or reads. Keep properties small; store big blobs elsewhere.
TLS is likely absent. Check for bolt+s / stream_socket_enable_crypto support before pointing this at anything outside localhost. If missing, run it over a private network or a TLS-terminating proxy — do not assume the socket is encrypted by default.
Timeouts are your responsibility. Set stream_set_timeout (or the constructor's timeout) explicitly; a blocked read with no timeout will hang a queue worker until the process is killed. Combine with retry_after / timeout in your job config.
Fresh-install password trap. Neo.ClientError.Security.CredentialsExpired on first connect is expected on Neo4j 3.1+; change the password before your first app deploy, not in code.
Debugging the wire. Enable server-side query logging (dbms.logs.query.enabled), and for framing bugs put a TCP proxy (mitmproxy in TCP mode, or socat -v) between client and server and diff the hex against the Bolt spec. The spec's message frames are simple enough to decode by eye.
Treat it as vendored source. 1 star and no commits since 2020 mean no security patches. If you rely on it, fork it into your org, pin the exact commit, and be prepared to patch Connection/ and Protocol/ yourself. The upside: it's small, so the extension points are just plain classes — subclass the session to add logging/metrics, swap the socket class for a pooled one, or extend PackStream for a custom structure type.
Read composer.json requirements. Check the php constraint and any ext-* (sockets/openssl) before assuming it runs on PHP 8.3. Old drivers often declare ^7.x only; a fork with a relaxed constraint and a modern PHP test pass is usually a 10-minute job.
How can I help you explore Laravel packages today?