stefanak-michal/bolt
Low-level PHP library for communicating with graph databases over TCP using the Bolt protocol. Works with Neo4j, Memgraph, Amazon Neptune and others, supports Bolt up to version 6, requires PHP 8.1+ with mbstring, and needs sockets or openssl only for certain connection classes.
Install the package:
composer require stefanak-michal/bolt
Note (v7.5.0): The internal
autoload.phpwas removed. Composer's autoloader is the only supported path going forward — ensurevendor/autoload.phpis loaded if you're not on a framework.
Basic connection:
use Bolt\Bolt;
use Bolt\connection\Socket;
$connection = new Socket('localhost', 7687); // Default Neo4j Bolt port
$bolt = new Bolt($connection);
$protocol = $bolt->build(); // Auto-selects latest supported version (4.3-6.1)
First query:
$protocol
->hello()
->logon(['scheme' => 'basic', 'principal' => 'neo4j', 'credentials' => 'password'])
->run('MATCH (n) RETURN n LIMIT 10')
->pullAll();
Bolt class: Factory for protocol versions (default: 4.3-6.1). Prefer this entry point — internal constructor signatures changed in v7.5.0.Socket/StreamSocket: Connection classes (use Socket for plain TCP, StreamSocket for SSL).Client helper: Simplified CRUD operations (see Implementation Patterns).Connection & Handshake:
$protocol = (new Bolt(new Socket('host', 7687)))
->setProtocolVersions(5.4) // Optional: Force specific version
->build();
$protocol->hello()->getResponse();
Query Execution:
// Pipeline pattern (recommended for performance)
$protocol
->run('MATCH (n) WHERE n.name = $name RETURN n', ['name' => 'Alice'])
->pullAll(); // Fetch all records at once
Transactions:
$protocol
->begin()
->run('CREATE (n:Test {id: $id})', ['id' => 1])
->commit();
Bookmarks (new in v7.5.0):
// Capture a bookmark from a committed transaction and pass it forward
$bookmark = $protocol->commit()->getResponse()->getBookmark();
$second = (new Bolt(new Socket('host2', 7687)))->build();
$second->hello()->logon([...]);
$second->run('MATCH (n) RETURN n', [], ['bookmarks' => [$bookmark]]);
Use bookmarks to enforce causal consistency across multiple connections (e.g. read-your-writes across a cluster).
Laravel Service Provider:
// config/bolt.php
'connections' => [
'neo4j' => [
'host' => env('NEO4J_HOST', 'localhost'),
'port' => env('NEO4J_PORT', 7687),
'user' => env('NEO4J_USER', 'neo4j'),
'pass' => env('NEO4J_PASS', 'password'),
'version' => '5.4',
],
];
// app/Providers/BoltServiceProvider.php
public function register()
{
$this->app->singleton('bolt', function ($app) {
$config = config('bolt.connections.neo4j');
$connection = new Socket($config['host'], $config['port']);
$bolt = (new Bolt($connection))->setProtocolVersions($config['version']);
// v7.5.0: attach a PSR-3 logger for structured debugging
$bolt->setLogger($app['log']);
return $bolt->build();
});
}
Query Builder Helper:
// app/Services/Neo4jQueryBuilder.php
class Neo4jQueryBuilder {
protected $protocol;
public function __construct($protocol) {
$this->protocol = $protocol;
}
public function findNodes(string $label, array $conditions = []) {
$query = "MATCH (n:$label)";
if (!empty($conditions)) {
$query .= ' WHERE ' . $this->buildConditions($conditions);
}
$query .= ' RETURN n';
return $this->protocol
->run($query, $conditions)
->pullAll();
}
}
Event Listeners:
// Listen for model events (e.g., Eloquent)
public function handle(NodeCreated $event) {
$bolt = app('bolt');
$bolt->run('CREATE (n:User {email: $email})', ['email' => $event->user->email]);
}
Batching:
// Process large datasets in chunks
$protocol->run('MATCH (n) RETURN n SKIP $skip LIMIT 100', ['skip' => 0]);
while (true) {
$records = $protocol->pull()->getResponse();
if (empty($records)) break;
// Process records...
$protocol->discard(); // Free server memory
$protocol->run('... SKIP $skip LIMIT 100', ['skip' => $skip += 100]);
}
Parameterized Queries:
// Avoid string concatenation for security/performance
$protocol->run(
'MATCH (n:User {email: $email}) RETURN n',
['email' => $user->email]
);
Protocol Version Mismatch:
Bolt 6.1 against Neo4j 4.x will fail during the handshake.$bolt->setProtocolVersions(4.4, 5.0); // Only allow these
setPackStreamVersion() is deprecated (v7.5.0):
$bolt->setPackStreamVersion(...) calls from your bootstrap; let the auto-negotiated Bolt version decide the PackStream version.Custom AProtocol constructors will break:
AProtocol. This is not a breaking change for normal consumers who get a protocol instance from Bolt::build(), but it is for anyone extending AProtocol directly.AProtocol for actual protocol extensions; retrieve instances via Bolt::build() in app code.Connection Timeouts:
Socket class or use StreamSocket with custom timeout:
$socket = new StreamSocket('ssl://host:7687', [
'timeout' => 30, // 30 seconds
]);
Memory Leaks with Large Results:
pullAll() loads all records into memory.pull() in a loop with discard():
$protocol->run('MATCH (n) RETURN n');
while (true) {
$records = $protocol->pull()->getResponse();
if (empty($records)) break;
// Process records...
$protocol->discard();
}
Authentication Errors:
logon scheme/credentials.basic, bearer) and inspect the response:
$response = $protocol
->logon(['scheme' => 'basic', 'principal' => 'neo4j', 'credentials' => 'password'])
->getResponse();
Unsupported Type handling (v7.5.0):
UnsupportedType structure means unknown Bolt types no longer crash pack/unpack — they surface as UnsupportedType envelopes.UnsupportedType in a record, check the Bolt version being negotiated — it usually means the server understands a type your protocol version doesn't.Cypher Parameter Types:
array instead of object for dictionaries).Bytes class for binary data or implement IPackListGenerator for custom types.Bolt::$debug toggle):
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('bolt');
$log->pushHandler(new StreamHandler('php://stdout', Logger::DEBUG));
$bolt->setLogger($log); // or inject via Laravel's `$app['log']`
$bolt->build();
$response = $protocol->getResponse();
if ($response->isSuccess()) {
// Handle success
} else {
throw new \RuntimeException($response->getFailureMessage());
}
DEBUG level. Enable in neo4j.conf:
dbms.logs.query.debug=true
Custom Connection Classes:
AbstractConnection to support custom protocols (e.g., WebSocket):
class WebSocketConnection extends AbstractConnection {
public function connect() { /* ... */ }
}
Protocol Extensions:
AbstractProtocol. Be aware that the parent constructor signature changed in v7.5.0 — mirror the current signature when overriding __construct:
class CustomProtocol extends AbstractProtocol {
public function customMessage(array $extra = []) {
return $this->sendMessage('+Custom', $extra);
}
}
Response Decorators:
Response objects to transform data:
$response = $protocol->run('...')->pull()->getResponse();
$nodes = collect($response->getRecords())->map(function ($record) {
return (object) $record['n'];
});
setPackStreamVersion(). The Bolt version determines the PackStream version (Bolt 6.1 pairs with the newer PackStream). Do not pin PackStream independently any more.$extra/bookmark parameter on run() and read them via getBookmark() on the commit() / run() response.PipelineTooLarge.openssl extension is enabled for StreamSocket with SSL:
$socket = new StreamSocket('ssl://host:7687', [
'ssl' => [
'verify_peer' => false, // Disable for self-signed certs (not recommended)
],
]);
commit()/rollback() frequently.// Correct
$protocol->run('MATCH (n) WHERE n.id = $id', ['id' => 1]);
// Incorrect (syntax error)
$protocol
How can I help you explore Laravel packages today?