graphaware/neo4j-common
GraphAware Neo4j Common provides shared PHP utilities for Neo4j integration, including Cypher statement and result abstractions, common interfaces, and helpers used across GraphAware’s Neo4j libraries. A lightweight foundation for building Neo4j-backed PHP applications.
1. Know what you're actually installing. graphaware/neo4j-common is not a Neo4j client. It ships the shared contracts and value objects — Cypher statements, results/records, graph entities, auth tokens, exceptions — that the GraphAware driver and OGM implement against. There is no connection, no HTTP/Bolt transport, and no Laravel service provider in here. In practice you get it transitively:
composer why graphaware/neo4j-common # usually required by graphaware/neo4j-php-client
If you install it alone, the first thing to do is add a driver that consumes these interfaces:
composer require graphaware/neo4j-php-client
(For new projects prefer the maintained fork, laudis/neo4j-php-client — see Gotchas.)
2. Where to look first. Read the src/ tree in this order:
| Directory | What it gives you |
|---|---|
Cypher/ |
Statement, StatementInterface, StatementCollection — build queries as objects |
Result/ |
Result, Record, RecordView, ResultSummary — read rows without knowing the driver |
Graph/ + Type/ |
Node, Relationship, Path interfaces used when a row returns graph entities |
Driver/ |
DriverInterface / SessionInterface — the seam your code should depend on |
Exception/ |
Neo4jException with Neo4j status code + message accessors |
Auth/ |
AuthToken::basic($user, $pass) |
3. First use case — build a statement, run it, read a record. This is the 90% path: a Cypher string plus bound parameters, wrapped as an immutable object, then consumed through the RecordView interface rather than a raw array.
use GraphAware\Common\Cypher\Statement;
$statement = Statement::create(
'MATCH (u:User {id: $id}) RETURN u',
['id' => $userId],
'find_user' // tag, useful for logs/profiling
);
$result = $session->runStatement($statement);
foreach ($result->records() as $record) {
$node = $record->get('u'); // NodeInterface
$props = $node->getProperties(); // array
}
That loop is the whole point of the package: your app code never touches a transport-specific class.
Inject the session, depend on the interfaces. Bind a single session in a service provider and type-hint GraphAware\Common\Driver\SessionInterface in your services. Nothing in neo4j-common is static or global, so it containerizes cleanly — you just have to write the wiring yourself.
// app/Providers/Neo4jServiceProvider.php
$this->app->singleton(SessionInterface::class, function () {
$driver = \GraphAware\Neo4j\Client\ClientBuilder::create()
->addConnection('default', config('neo4j.dsn')) // e.g. bolt://neo4j:secret@host:7687
->build();
return $driver->session();
});
Query objects per use case. Return a Statement from a small class instead of scattering Cypher strings through controllers. It gives you one place to change an index hint, one place to add a parameter, and a natural unit-test target (assert on getText() / parameters() with no DB at all).
final class RecentOrdersForUser
{
public function __invoke(string $userId, int $limit = 25): StatementInterface
{
return Statement::create(
'MATCH (u:User {id:$id})-[:PLACED]->(o:Order)
RETURN o ORDER BY o.createdAt DESC LIMIT $limit',
['id' => $userId, 'limit' => $limit],
'recent_orders'
);
}
}
Repository returns your domain, not Records. Record/Node objects leaking into Blade views and Eloquent models is the fastest way to couple the app to the driver. Map at the boundary:
return collect($result->records())
->map(fn ($r) => UserDto::fromProperties($r->get('u')->getProperties()))
->all();
Bulk conversion ($record->values(), $record->keys()) is cheaper than repeated get() calls when you only need scalars.
Transactions via StatementCollection. Batch related statements and push them through a transaction so you can retry as a unit:
$tx = $session->transaction();
$tx->push(Statement::create('CREATE (u:User {id:$id})', ['id' => $id], 'create_user'));
$tx->push(Statement::create('MATCH (u:User {id:$id}) SET u.verified = true', ['id' => $id], 'verify'));
$tx->commit();
Queue jobs should carry the statement text + scalar parameters (both are serializable), then rebuild the callable-side work in the job; don't serialize a SessionInterface.
Test with fakes, not a live database. Because RecordView and Result are interfaces, you can hand-write a tiny in-memory double or implement AbstractRecord for a fake row. That gets you 90% of your mapping/formatting tests on CI without Neo4j running.
Exception translation at the edge. Catch Neo4jException in a middleware or the job handler and translate known codes: Neo.ClientError.Schema.ConstraintValidationFailed → 422, deadlock/transient codes → retry with backoff, everything else → 500 + log the Neo4j code and status.
composer require left you with only GraphAware\Common\*, you are missing the driver.composer.json, vendor a copy if you must, and don't plan on upstream fixes. Answers to obscure questions won't exist on Stack Overflow.laudis/neo4j-php-client is the actively maintained continuation and bundles its own commons classes. Pulling both this package and a driver that bundles a newer commons gives you duplicate/conflicting GraphAware\Common\* interfaces — check composer why and composer show before adding.NOASSERTION on GitHub. GraphAware's libraries were MIT, but GitHub can't detect it — open LICENSE in the package root and get a one-line sign-off from whoever owns legal before shipping. Don't assume from the README.Record::get() behaviour on a missing alias differs by version (null vs. exception). Don't rely on your memory; use hasValue($alias) first, or pin a version and write a test that asserts the behaviour.Statement is immutable. withParameter() / withParameters() return new instances. $statement->withParameter('id', 1); as a bare statement silently does nothing — this bug pattern is common in refactors.$params array. This package does no escaping for you.GraphAware\Common\Graph\Node objects while Bolt returned plain maps. Normalise to arrays in your repository so a driver swap doesn't ripple through your code.$tx work in try/catch and rollBack() explicitly, or a failed statement leaves the transaction open and the connection wedged.orders.create, orders.verify).getText() and the parameter keys only (values often contain PII), and pull timings from ResultSummary rather than wrapping every call in microtime().DriverInterface/Result, keep it in app/, and you stay upgradable if a maintained fork lands.How can I help you explore Laravel packages today?