graphaware/neo4j-bolt
A PHP library for communicating with Neo4j over the Bolt protocol, providing a low-level client for sending Cypher queries and handling results in PHP applications. It underpins GraphAware's Neo4j PHP integrations and supports efficient binary communication with Neo4j servers.
graphaware/neo4j-bolt is a raw PHP driver for Neo4j's Bolt binary protocol — not a Laravel package. There is no service provider, facade, config file, or auto-discovery. You install it, then wire everything yourself. Its practical role day-to-day is to be the thin pipe between your Laravel app and a Neo4j 3.x server, wrapped inside a repository class so nothing else in the app knows it exists.
composer require graphaware/neo4j-bolt
First query:
use GraphAware\Bolt\GraphDatabase;
$driver = GraphDatabase::createDriver('bolt://neo4j:secret@localhost:7687');
$session = $driver->session();
$result = $session->run(
'MATCH (u:User {id: {id}}) RETURN u.name AS name',
['id' => 42]
);
foreach ($result->records() as $record) {
echo $record->get('name');
}
$session->close();
$driver->close();
Order of exploration:
GraphAware\Bolt\GraphDatabase::createDriver() — the only static entry point; opens the socket immediately.GraphAware\Bolt\Driver — session(), close().GraphAware\Bolt\Session — run($cypher, array $params, $tag), close().GraphAware\Bolt\Result\Result / Record — records(), get($key), values(), summarize().GraphAware\Bolt\Result\Type\{Node,Relationship,Path} — the hydrated graph objects.Your first real use case should be one read-only query that MySQL is bad at (friends-of-friends, "users who follow brand X via any 2-hop path", fraud ring detection) hidden behind a single repository method called from one controller. Do not attempt a full domain model in Cypher on day one.
Bind the driver once, as scoped(), never per query. createDriver() opens a TCP socket on construction, so a singleton-per-request is the baseline pattern:
// AppServiceProvider::register()
$this->app->scoped('neo4j.driver', fn ($app) => GraphDatabase::createDriver(
config('neo4j.uri'),
['timeout' => config('neo4j.timeout', 5)],
$app->make(\Psr\Log\LoggerInterface::class)
));
Prefer scoped() over singleton() — under Octane and long-running queue workers a singleton keeps a stale socket alive across requests and you get intermittent ConnectionExceptions.
Sessions are cheap, wrap them in try/finally. The universal pattern:
class Neo4jUserRepository
{
public function __construct(private Driver $driver) {}
public function friendsOfFriends(int $id): array
{
$session = $this->driver->session();
try {
$result = $session->run(
'MATCH (u:User {id:{id}})-[:FOLLOWS*2]->(f) RETURN DISTINCT f.id AS id',
['id' => $id]
);
return array_map(fn ($r) => $r->get('id'), $result->records()->toArray());
} finally {
$session->close();
}
}
}
An abandoned session mid-result-stream leaves protocol state hanging; finally is non-negotiable.
Define a contract and never leak Bolt types past the repository. Records return GraphAware\Bolt\Result\Type\Node, not arrays. Map to DTOs / Eloquent models at the boundary via $node->getProperties() and $node->labels(). A Neo4jClient interface also makes the inevitable migration to laudis/neo4j-php-client a one-class change rather than an app-wide sweep.
Split reads and writes by execution context.
Cache::remember("neo4j:u:{$id}:fof", 300, fn () => $repo->friendsOfFriends($id)). Neo4j is fast, not free.Transactions. Some 1.x releases expose $session->transaction(); check your pinned version. If it's missing you only get single-statement atomicity, which is a strong argument for upgrading to the maintained client rather than writing compensating Cypher.
Testing. Because the factory is static and hits a socket, bind a fake implementing your Neo4jClient interface in TestCase. Reserve real integration tests for a Dockerized neo4j:3.5 so you test against the same Bolt version you ship.
composer require may warn or fail on modern PHP; --ignore-platform-reqs is a stopgap, not a fix. From day one, open a migration ticket for laudis/neo4j-php-client (the maintained successor, same conceptual API).{name}, not $name. Always pass values through the second argument to run(). String interpolation into Cypher is Cypher injection — identical risk profile to SQL injection, and more likely because users are used to query builders.DateTime, enums, Carbon, and Eloquent models must be converted (ISO-8601 strings / epoch ints / arrays) before passing. Unsupported objects throw a ProtocolException with a vague message.null vs 0 in optional MATCHes.ConnectionException (socket down, auth failure) vs ProtocolException (unsupported type, Bolt version mismatch) vs client exceptions (Cypher syntax/semantic errors). Map them in your repository so callers can see ConnectionException → retryable, others → 500. Never blindly retry writes; you'll duplicate edges.$result->records() returns a collection that may hold unread messages on the wire. Call summarize() or iterate fully before closing the session, or you can get protocol desync on the next query on the same session.bolt://user:pass@host:7687 covers basic auth. TLS requires bolt+tls:// or explicit config flags depending on version — verify against your Neo4j server, not the docs, which trail the archived code.php artisan neo4j:ping command that does RETURN 1 and wire it into your readiness probe. Add a short timeout in the config array (see ['timeout' => 5]) and a circuit breaker around any call from a request path.composer.lock and in your Docker image, and add a CI job that runs one query against the exact server version you deploy.Psr\Log\LoggerInterface and a config array — that's the whole surface. Query-level metrics, span logging, and slow-query detection must be built in your repository wrapper (time $session->run(), log $cypher with param keys but redacted values, emit to your metrics backend).How can I help you explore Laravel packages today?