Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Neo4J Php Client Laravel Package

laudis/neo4j-php-client

PHP client and driver for Neo4j, giving Laravel and PHP apps a clean, intuitive API to the graph database. Choose Bolt or auto-routed drivers, configure easily, and rely on full psalm typing, extensibility, and official Neo4j testkit validation.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm the host can actually run it. The driver needs PHP 8.0+ (v3.x), plus ext-bcmath, ext-json, and ext-sockets. That last one is the usual surprise in slim Docker images and Alpine builds — add docker-php-ext-install sockets bcmath before you debug anything else.

composer require laudis/neo4j-php-client

2. Put connection config in config/neo4j.php. There is no first-party Laravel package, so you own the wiring. Keep URIs in .env, not in code:

// config/neo4j.php
return [
    'default' => env('NEO4J_DRIVER', 'neo4j'),
    'drivers' => [
        'neo4j' => [
            'uri'  => env('NEO4J_URI', 'neo4j://localhost:7687'),
            'user' => env('NEO4J_USER', 'neo4j'),
            'pass' => env('NEO4J_PASSWORD'),
            'database' => env('NEO4J_DATABASE'), // null = server default
        ],
    ],
];

3. Build one client in a service provider and bind it as a singleton.

// app/Providers/Neo4jServiceProvider.php
public function register(): void
{
    $this->app->singleton(Client::class, function ($app) {
        $cfg = $app['config']['neo4j'];
        $builder = ClientBuilder::create();

        foreach ($cfg['drivers'] as $alias => $d) {
            $builder->withDriver(
                $alias,
                $d['uri'],
                Authenticate::basic($d['user'], $d['pass']),
            );
        }

        return $builder->withDefaultDriver($cfg['default'])->build();
    });
}

Authenticate::basic() is the workhorse; Authenticate::oidc($token) is what you switch to for Aura / SSO, and Authenticate::none() for local dev.

4. First use case — the one you should copy. Skip run() for your first feature and go straight to a transaction function, because it is what the driver team considers the default and it will survive a replica failover on day one:

use Laudis\Neo4j\Contracts\TransactionInterface;

$email = '[email protected]';

$user = $client->writeTransaction(static function (TransactionInterface $tsx) use ($email) {
    return $tsx->run(
        'MERGE (u:User {email: $email}) SET u.updatedAt = datetime() RETURN u',
        ['email' => $email],
    )->first()->get('u');
});

echo $user->getProperty('email');

That's it: MERGE in, a typed Node out. Rows are CypherMap, the result set is CypherList, and Node/Relationship/Path are real classes with getProperty(), getLabels(), getType(), etc. — not arrays.

Where to look first: the "Concepts" section of the README (Client → Driver → Session → Transaction), then the three execution modes. That mental model explains nearly every oddity later. The Laravel realworld reference implementation (neo4j-examples/php-laravel-neo4j-realworld-example) is a working example of exactly the provider above.


Implementation Patterns

Pick the execution mode deliberately

Mode Use when
run() / runStatement() Fire-and-forget reads, health checks, seed scripts, ad-hoc reporting. One round trip, auto-committed, no retry.
readTransaction() / writeTransaction() Everything in application code. Retried on transient errors, routed to follower/leader correctly, auto commit/rollback.
beginTransaction() Multi-step logic needing manual commit points, or streaming with explicit rollback on user abort.

The read/write split is not cosmetic — with a neo4j:// (auto-routed) driver against a cluster or Aura, a MERGE inside readTransaction() will be rejected by the follower. If you see "Writing in read access mode not allowed", you called the wrong method, not a permissions problem.

Wrap the client in one domain-facing service

Don't inject Client into fifteen controllers. One repository/gateway class per bounded context gives you a single seam for faking in tests, one place to enforce LIMIT, and one place to translate driver types into your DTOs.

final class UserGraph
{
    public function __construct(private Client $client) {}

    public function find(string $email): ?UserDto
    {
        $row = $this->client->readTransaction(
            fn (TransactionInterface $tsx) => $tsx->run(
                'MATCH (u:User {email: $email}) RETURN u, u.id AS id',
                ['email' => $email],
            )->first()
        );

        if ($row === null) {
            return null;
        }

        return new UserDto(
            id: $row->get('id'),
            email: $row->get('u')->getProperty('email'),
        );
    }
}

Bind the interface to the fake in the test container; the driver never needs to know.

Use Statement objects for multi-statement work

beginTransaction() takes an initial list and commit() takes a trailing list. Both accept Statement::create($cypher, $params). This is also the cleanest way to batch a "create node + create edge" pair without a second round trip:

$tsx = $client->beginTransaction([
    Statement::create('MERGE (a:Account {id: $id})', ['id' => $id]),
]);
$tsx->run('MATCH (a:Account {id: $id}) MERGE (a)-[:OWNS]->(:Doc {ref: $ref})', compact('id', 'ref'));
$tsx->commit();

Note the pitfall in the docs: beginTransaction() returns only the transaction — the results of the initial statements are discarded. Don't expect them back.

Parameterize, and use ParameterHelper for the array ambiguity

PHP's single array type can't distinguish Cypher list from Cypher map, and an empty array is the one case that actually bites:

use Laudis\Neo4j\ParameterHelper;

// ambiguous but resolves to an empty list
$client->run('... WHERE x.slug IN $v', ['v' => []]);

// explicit, survives refactors
$client->run('... WHERE x.slug IN $v', ['v' => ParameterHelper::asList([])]);
$client->run('... WHERE x.tags = $v',   ['v' => ParameterHelper::asMap([])]);

Never string-interpolate a value into Cypher, even for an integer ID from a route. $client->run("MATCH (n {id: {$id}}) ...") is a Cypher injection and also defeats the query plan cache.

Read the summary for free observability

The default formatter returns a SummarizedResult; getResult() gives you the same rows. The summary carries counters, notifications, plan, timing and server info — invaluable in a queue job when you want to log "created 412 nodes" or catch a PlannerNotification about a missing index before it becomes an incident.

$res = $client->run('EXPLAIN MATCH (u:User {email: $e}) RETURN u', ['e' => $email]);
$summary = $res->getSummary();
$summary->getNotifications(); // surface cartesian-product / label warnings in CI

Keep transaction functions idempotent

The driver retries on transient errors. Anything outside the transaction that mutates state must move after it:

// wrong — double counts on retry
$client->writeTransaction(function (TransactionInterface $tsx) use ($counter) {
    $counter->increment();               // may run twice
    $tsx->run('CREATE (:Event)');
});

// right — generate the idempotency key first
$eventId = Uuid::v4();
$client->writeTransaction(fn (TransactionInterface $tsx) =>
    $tsx->run('MERGE (:Event {id: $id})', ['id' => $eventId]));
$counter->increment();

Same rule for dispatching events, sending mail, and incrementing cache counters inside the closure — capture everything you need via use and do side effects after.


Gotchas and Tips

HTTP drivers are gone. Anything ≥ 3.3 dropped http:///https:// URIs. If you're porting an older project and see a "no driver for scheme" error, rewrite to bolt://, bolt+s://, neo4j://, or neo4j+s://. Use neo4j:// for Aura and clusters (auto-routing), bolt:// for a single instance.

+s vs +ssc. +s requires a CA-trusted cert; +ssc is self-signed. Aura wants neo4j+s:// with no port. Local Docker usually wants bolt://localhost:7687 with no TLS at all. Getting this wrong produces TLS handshake errors that look like auth failures.

Unbounded results are eager and in memory. Results are fully materialized into CypherList/CypherMap objects, and each Node is a PHP object with properties. MATCH (n) RETURN n on a million nodes will OOM your worker long before Neo4j breaks a sweat. Always LIMIT in application queries; use EXPLAIN/PROFILE through the summary if you're unsure.

Integers beyond PHP_INT_MAX and ext-bcmath. Large Cypher integers and floats flow through bcmath; forgetting the extension produces fatal errors or silent precision loss. If you store timestamps in millis or snowflake IDs, verify round-tripping in a test.

DateTimeInterface params become DateTimeZoneId. That's usually what you want, but it's not symmetric with the Date/LocalDateTime result types. Convert deliberately (new \Laudis\Neo4j\Types\LocalDateTime(...)) rather than assuming a DateTimeImmutable comes back out.

Vectors decode but don't encode. Embeddings come back as Vector; you cannot send a Vector as a query parameter. Pass the raw float[] list instead when writing embedding properties.

Name your drivers with intent. withDriver('bolt', ...)->withDriver('neo4j', ...)->withDefaultDriver('bolt') is a common dev setup: cheap single-instance default, routed driver for the code paths you explicitly target with $client->run($cypher, $params, 'neo4j'). The alias is the third argument everywhere — use it for read replicas and reporting queries so you don't accidentally hammer the leader.

Singleton scope under Octane / long-lived workers. A singleton binding is right for FPM. Under Octane, a worker lives for thousands of requests and holds driver connection state across them; if you see stale-connection errors after an idle period, rebind per-request in a RequestReceived listener or resolve the client from the container scoped rather than singleton. Classic FPM is unaffected because the process dies.

Database selection is per-driver, not per-query. neo4j://host?database=analytics sets the default database for that alias. For a one-off query against a different database, open a session explicitly rather than rebuilding the client. Multi-database users are a frequent source of "why am I seeing empty results" confusion — the query ran fine, just against the wrong DB.

Testing. Two viable paths: (a) bind a fake of your own gateway interface and test the domain logic without Neo4j at all; (b) run a disposable neo4j:5 container in CI and exercise the real driver, which is the only way to catch Cypher syntax errors, missing constraints, and type mapping bugs. Do (a) for volume, (b) for the repository layer. Assert on the Summary counters (nodesCreated, relationshipsCreated) to verify your MERGE is actually a merge and not a create.

Debugging failed queries. The driver throws a hierarchy under Neo4jException; catch the specific transient/Neo4jException subtypes rather than \Throwable. For plan issues, PROFILE from the summary shows db hits per operator — a Cartesian product is usually a missing MATCH join condition, and it is visible in getPlan() before it ever becomes a slow-query alert.

Version matrix matters. v3.x requires PHP 8+ and Neo4j 4.x/5.x; v2.8 supports PHP 7.4 and Neo4j 3.5/4.x. Pin accordingly if you're on an older Aura or a legacy 3.5 server — CREATE CONSTRAINT ... IF NOT EXISTS style Cypher differs enough between 4 and 5 that upgrading the driver without upgrading the server (or vice versa) will fail at runtime, not at install time.

Extension points, if you outgrow the defaults. The client builder accepts a custom formatter (swap SummarizedResultFormatter for the plain one if the summary overhead shows up in profiling), and the driver layer is pluggable via driver factories — useful for injecting a decorator that logs every Cypher statement and its duration in non-production environments.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings