laudis/neo4j-php-client
Typed Neo4j PHP client/driver with Bolt and Neo4j (auto-routed) support. Intuitive, extensible API with easy configuration, built with input from the official driver team and validated via Neo4j Testkit for reliability.
Installation
composer require laudis/neo4j-php-client
Ensure PHP ≥ 7.4 and extensions bcmath, json, and sockets are enabled.
Basic Client Setup Configure a Bolt driver (recommended for production) or Neo4j auto-routed driver:
use Laudis\Neo4j\ClientBuilder;
$client = ClientBuilder::create()
->withDriver('bolt', 'bolt+s://neo4j:password@localhost:7687')
->withDefaultDriver('bolt')
->build();
First Query Run a simple Cypher query:
$result = $client->run('CREATE (n:Test {name: $name}) RETURN n', ['name' => 'Laravel']);
$node = $result->first()->get('n');
ClientBuilder, TransactionInterface, and Statement classes.Use writeTransaction/readTransaction for atomic operations with automatic retries:
$userId = Uuid::v4();
$client->writeTransaction(function (TransactionInterface $tx) use ($userId) {
$tx->run('MERGE (u:User {id: $id}) SET u.name = $name', [
'id' => $userId,
'name' => 'John Doe'
]);
});
MERGE instead of CREATE).For fine-grained control (e.g., custom rollback logic):
$tx = $client->beginTransaction();
try {
$tx->run('CREATE (n:Order {id: $id})', ['id' => 'order-123']);
$tx->commit();
} catch (\Exception $e) {
$tx->rollback();
throw $e;
}
Use ParameterHelper to disambiguate empty arrays:
use Laudis\Neo4j\ParameterHelper;
$client->run('MATCH (n) WHERE n.tags CONTAINS ALL $tags RETURN n', [
'tags' => ParameterHelper::asList(['laravel', 'neo4j'])
]);
Execute multiple statements in a single transaction:
$client->writeTransaction(function (TransactionInterface $tx) {
$tx->runStatements([
Statement::create('CREATE (n1:Node1 {id: $id})', ['id' => 1]),
Statement::create('CREATE (n2:Node2 {id: $id})', ['id' => 2])
]);
});
Bind the client to Laravel’s container:
// config/neo4j.php
return [
'connection' => 'bolt+s://user:pass@localhost:7687',
];
// app/Providers/Neo4jServiceProvider.php
public function register()
{
$this->app->singleton(Neo4jClient::class, function ($app) {
return ClientBuilder::create()
->withDriver('bolt', config('neo4j.connection'))
->withDefaultDriver('bolt')
->build();
});
}
Create a query builder facade:
// app/Facades/Neo4j.php
public static function run(string $query, array $params = []): CypherList
{
return app(Neo4jClient::class)->run($query, $params);
}
// Usage in controllers:
$users = Neo4j::run('MATCH (u:User) RETURN u', ['limit' => 10]);
Hook into Laravel’s lifecycle to sync with Neo4j:
// app/Models/User.php
protected static function booted()
{
static::created(function ($user) {
app(Neo4jClient::class)->run(
'MERGE (u:User {email: $email}) SET u.name = $name',
['email' => $user->email, 'name' => $user->name]
);
});
}
Convert results to Laravel collections or DTOs:
use Laudis\Neo4j\Types\Node;
$results = $client->run('MATCH (u:User) RETURN u');
$users = collect($results)->map(function ($row) {
$node = $row->get('u');
return (object) [
'id' => $node->getProperty('id'),
'name' => $node->getProperty('name')
];
});
Access query metadata:
$summarized = $client->run('MATCH (n) RETURN n', [], 'default', SummarizedResultFormatter::class);
$summary = $summarized->getSummary();
$plan = $summary->getPlan(); // Query execution plan
Non-Idempotent Transactions
// ❌ Bad: External counter modified inside transaction
$client->writeTransaction(function (TransactionInterface $tx) use (&$counter) {
$tx->run('CREATE (n)');
$counter++; // ❌ Not idempotent!
});
// ✅ Good: Idempotent + side effect outside
$client->writeTransaction(function (TransactionInterface $tx) {
$tx->run('MERGE (n:Counter {id: "total"}) SET n.value = $value', ['value' => 1]);
});
$counter = $client->run('MATCH (n:Counter) RETURN n.value AS value')->first()->get('value');
Parameter Type Ambiguity
CypherList. Use ParameterHelper for clarity.
// ❌ Ambiguous: Is this a list or map?
$client->run('UNWIND $tags AS tag RETURN tag', ['tags' => []]);
// ✅ Explicit
$client->run('UNWIND $tags AS tag RETURN tag', ['tags' => ParameterHelper::asList([])]);
Connection Timeouts
->withDriver('bolt', 'bolt+s://user:pass@localhost:7687?connection_timeout=5s')
Result Caching
LIMIT:
$results = $client->run('MATCH (n) RETURN n SKIP $offset LIMIT $limit', [
'offset' => 0,
'limit' => 100
]);
Enable Logging Configure the driver to log queries and errors:
$client = ClientBuilder::create()
->withDriver('bolt', 'bolt://user:pass@localhost')
->withLogging(true)
->build();
Query Profiling
Use PROFILE to analyze slow queries:
$result = $client->run('PROFILE MATCH (n) WHERE n.property = $value RETURN n', ['value' => 'test']);
$plan = $result->getSummary()->getPlan();
Bookmarks for Consistency Use bookmarks to ensure read-your-writes consistency in clusters:
$client->run('MATCH (n) RETURN n', [], null, null, ['bookmark' => 'bm_123']);
use Laudis\Neo4j\Formatters\ResultFormatterInterface;
class JsonResultFormatter implements ResultFormatterInterface {
public function format(array $data): string {
return json_encode($data);
}
}
$client->run('MATCH (n) RETURN n', [],
How can I help you explore Laravel packages today?