neo4j/neo4j-bundle
A Symfony bundle that integrates Neo4j graph databases into PHP applications. It configures the Neo4j client, wires services for dependency injection, and provides convenient access to graph database queries and transactions from Symfony projects.
This is not a Laravel package — it's a Symfony Bundle (neo4j/neo4j-bundle, source at neo4j-php/neo4j-symfony) that wires the laudis/neo4j-php-client into Symfony's DI container. Laravel's package discovery will not find it, and it cannot be registered in config/app.php because it declares Symfony\Component\HttpKernel\Bundle\Bundle. The realistic Laravel path is to depend on the underlying client and reuse the bundle's patterns.
On a Symfony project (the intended target):
composer require neo4j/neo4j-bundleNeo4j\Neo4jBundle\Neo4jBundle::class => ['all' => true] in config/bundles.php.config/packages/neo4j.yaml (keys have shifted across releases — check the installed version's README):neo4j:
default_connection: default
connections:
default:
dsn: '%env(NEO4J_DSN)%' # bolt://neo4j:password@localhost:7687
Neo4j\Client\ClientInterface (or the DriverInterface) into a service and run a first statement.On a Laravel project (the pragmatic route):
composer require laudis/neo4j-php-client — same driver the bundle wraps.NEO4J_DSN=bolt://neo4j:secret@localhost:7687 to .env and config/neo4j.php returning ['dsn' => env('NEO4J_DSN')].Neo4jServiceProvider:$this->app->singleton(ClientInterface::class, function () {
return ClientBuilder::create()
->withDriver('default', config('neo4j.dsn'))
->withDefaultDriver('default')
->build();
});
Neo4j facade over the singleton so you can write Neo4j::run('MATCH (n) RETURN count(n)').First use case: a graph sidecar next to Eloquent — user follows, product recommendations, permission hierarchies, or audit lineage. Keep relational data in MySQL/Postgres and treat Neo4j as an index you rebuild from domain events; that's the lowest-risk first integration.
Connection and client access. The bundle exposes the client through Symfony's autowiring aliases on ClientInterface and DriverInterface. In Laravel, mirror that: bind both interfaces, and always resolve the client from the container rather than constructing it in a controller — the driver owns a connection pool that must outlive a single request.
Session-per-unit-of-work. Do not run queries directly on the client unless you're comfortable with ephemeral sessions. The pattern the bundle encourages and that translates cleanly to Laravel:
$session = Neo4j::driver()->createSession();
try {
$session->run('CREATE (u:User {id: $id})', ['id' => $user->id]);
} finally {
$session->close();
}
Wire this into a small Neo4jRepository base class or a Neo4j::transaction(fn ($tx) => ...) helper so session lifecycle is never duplicated in controllers.
Syncing Eloquent to the graph. The highest-value pattern is write-through via observers or domain events dispatched to a queue:
User::created(fn ($user) => SyncUserToGraph::dispatch($user->id));
Inside the job, use MERGE (not CREATE) so re-delivery from a queue is idempotent: MERGE (u:User {id: $id}) SET u.name = $name. Never let a graph write failure roll back the relational transaction — the graph is a derived view, and queue retries will heal it.
Schema as code. There is no migration system. Keep a database/neo4j/schema.cypher (or an Artisan command neo4j:schema) with idempotent statements:
CREATE CONSTRAINT user_id IF NOT EXISTS FOR (u:User) REQUIRE u.id IS UNIQUE;
CREATE INDEX user_email IF NOT EXISTS FOR (u:User) ON (u.email);
Run this on deploy. Without a uniqueness constraint on your sync key, MERGE silently produces duplicates under concurrency.
Reads that earn their keep. Don't use the graph for straightforward lookups — Eloquent is better. Use Cypher for variable-depth traversal, shortest path, and recommendation queries where the join count would explode in SQL. Return flat maps (.name, .id) rather than whole nodes so result hydration stays cheap.
Instrumentation belongs at session/transaction level. Since 1.2.0 the bundle routes sessions and transactions through the same stopwatch, logger and event bus it already used for the client, so Symfony's profiler shows per-session and per-transaction spans rather than one opaque client call. That's a useful blueprint: in Laravel, decorate your bound ClientInterface (or your Neo4j facade/Neo4jRepository base class) so that session creation, transaction commit and each run() are timed and dispatched as Laravel events. If you only instrument the client you will see one number for an entire request, which is rarely the query that hurt you.
Testing. Spin up Neo4j via Testcontainers (testcontainers/testcontainers-php) or a fixed test database, and wrap each test in an explicit transaction you roll back. Never point the test suite at NEO4J_DSN from .env — add a separate NEO4J_TEST_DSN and fail loudly if they match.
neo4j/neo4j-bundle is the Composer name; the repo is neo4j-php/neo4j-symfony. It's Symfony-only. Budget for writing roughly 40 lines of Laravel glue rather than expecting a drop-in.@, /, : or % must be URL-encoded, or auth fails with a confusing "unauthorized" that looks like a server problem. Prefer the builder's withBasicAuth() over string DSNs when credentials come from a secret manager.run(), not at boot — so add a neo4j:ping Artisan/console command and call it in your health check and post-deploy smoke test.Result yields records as you iterate. If you run a second query on the same session before consuming the first result, you can deadlock or truncate. Consume (or ->toArray() on small results) before issuing the next statement.beginTransaction() for multi-statement writes. Long-lived transactions hold locks and can starve the pool — keep them under a few hundred milliseconds and never wrap an HTTP call inside one.maxConnectionPoolSize and make sure you're not also lazily creating drivers in a helper.IF NOT EXISTS, never inline at request time.neo4j database is often wrong in clustered or Aura deployments. Specify the database when creating the session, or you'll write to an unexpected store.drivers/connections/dsn placement) have changed between major releases. Pin the version and read that tag's README rather than the blog posts — 1.2.0 is additive over 1.1.x (extra stopwatch/logging/event-bus coverage for sessions and transactions), so config written for 1.1.x still applies, but that is not a guarantee across majors.ClientBuilder::withDefaultLogger(); there's no event bus to subscribe to, so if you want per-query events you own the decorator. That decorator is also the cheapest place to add retry/backoff for transient Neo4jExceptions.withDefaultLogger($psrLogger)) and watch slow-query logs on the server side (dbms.logs.query.enabled=true, dbms.logs.query.threshold=100ms). On Symfony, 1.2.0 means the profiler panel now breaks timings down by session and transaction — read that before reaching for server-side logging. In Laravel you have no profiler panel, so log at minimum: query text hash, parameters count (not values), duration, and whether it ran in a transaction. Most "the graph is slow" complaints are missing indexes, not the driver.How can I help you explore Laravel packages today?