graphaware/neo4j-php-client
Official PHP client for Neo4j, the graph database. Connect over Bolt or HTTP, run Cypher queries, and map results to objects with a clean, extensible API supporting Neo4j 3.x and 4.x.
Install with a pinned version and be explicit about the PHP constraint (v4 targets PHP 7.1+; the archived 4.x line predates PHP 8.2+ hygiene):
composer require graphaware/neo4j-php-client:^4.0
There is no Laravel service provider in the package. Create one and bind a singleton — the client is expensive to build and should be reused:
// app/Providers/Neo4jServiceProvider.php
$this->app->singleton(Client::class, fn () => ClientBuilder::create()
->addConnection('default', config('neo4j.dsn')) // bolt://neo4j:secret@localhost:7687
->build());
Register it in config/app.php providers and publish a small config/neo4j.php with dsn and per-connection options.
Your first use case should be a single read: fetch one node by an indexed property and unwrap the value.
$result = app(Client::class)->run(
'MATCH (u:User {id: $id}) RETURN u.name AS name',
['id' => $userId]
);
$name = $result->firstRecord()->get('name')->value();
Where to look first in the source: ClientBuilder (connection/auth options), Session/Transaction (execution and routing), Record + CypherValue (result unwrapping — this is where most confusion lives), and the Formatter\* classes if you need custom hydration.
Because the package is archived (MIT, no maintenance, no security patches), treat this as a green-light-only-if-you-must decision. For new work prefer laudis/neo4j-php-client, the maintained successor with a near-identical API.
Keep the client out of controllers. Bind it in a Neo4jRepository base class or dedicated services. Controllers should only ever see arrays/DTOs.
Use sessions explicitly for read/write routing. $client->run() uses a default session; for anything non-trivial, obtain a session and close it:
$session = $client->createSession();
try {
$result = $session->run('MATCH (n:Product) WHERE n.sku IN $skus RETURN n', ['skus' => $skus]);
} finally {
$session->close();
}
Transactions for anything that writes more than one node. The MERGE-heavy import path is where this client earns its keep:
$tx = $client->transaction();
try {
foreach (array_chunk($rows, 1000) as $chunk) {
$tx->run(
'UNWIND $rows AS row
MERGE (u:User {id: row.id})
SET u.name = row.name',
['rows' => $chunk]
);
}
$tx->commit();
} catch (\Throwable $e) {
$tx->rollback();
throw $e;
}
Always bind parameters; never interpolate. UNWIND $rows AS row over large arrays is dramatically faster than N queries, and it keeps the query plan cached.
Shape results before they leave the repository. Map Record objects to plain arrays for Blade and API resources:
$rows = array_map(fn ($r) => [
'id' => $r->get('id')->value(),
'name' => $r->get('name')->value(),
], $result->records());
For a repeated shape, drop in a custom formatter (registered on the client) and reuse it across repositories.
Schema as code. Run CREATE CONSTRAINT ... IF NOT EXISTS from an Artisan command or a migration-style step, invoked on deploy. Don't rely on manual browser console setup.
Coexistence with Eloquent. Use Neo4j for traversal/recommendation queries and keep the canonical record in MySQL. Store the relational ID as a node property; hydrate Eloquent models from the ID list the graph returns.
Testing. Point config/neo4j.php at a disposable Neo4j container and truncate between tests with MATCH (n) DETACH DELETE n in a RefreshDatabase-style trait.
laudis/neo4j-php-client before shipping, not after.CypherValue wrapping trips everyone up the first time. $record->get('u') returns a wrapper; you need ->value() for scalars, and ->values() / property accessors for Node and Relationship objects. Print get_class() the first time and stop guessing.http:// against a Bolt-only expectation fails cryptically.try/rollback/throw; long-running queue workers will otherwise exhaust connections silently.ClientBuilder per request or per model. It's not cheap and you lose connection reuse. Singleton, always.withAuth(); mixing them produces confusing 401s. Pick one and document it in config/neo4j.php.query.log on the server side is usually more informative than the PHP stack trace for syntax/plan errors.Driver for logging/tracing, client-level event listeners, and the Formatter classes for custom serialization. These are the clean seams if you must stay on the package.How can I help you explore Laravel packages today?