ulobby/neoeloquent
NeoEloquent brings Laravel's Eloquent ORM to Neo4j graph databases. Define graph-backed models, traverse relationships, and run Cypher queries using familiar Eloquent conventions, making it easier to integrate graph data into Laravel applications.
Upgrade check first (v9.9.0 is a hard break for older Laravel). This release requires Laravel 12 or 13 and PHP 8.2+. Before anything else:
composer show ulobby/neoeloquent # what you're actually on
composer why-not ulobby/neoeloquent 9.9.0 # why the upgrade is blocked
If your constraint is ^9.0, Composer will silently keep Laravel 10/11 apps on 9.8.x — that's intended, not broken. If you wrote ^9.9 on a Laravel 11 app, resolution will simply fail. Confirm your laravel/framework constraint before touching the package version.
Where to look first
config/database.php — the neo4j connection (driver, host, port 7687, credentials). This is the only required config for day-to-day work.vendor/ulobby/neoeloquent rather than copying an old snippet.vendor/ulobby/neoeloquent/tests/ — the suite is now deterministic and runs on PHP 8.2–8.5 × Laravel 12–13 × Neo4j 5.26. Read these tests for authoritative relationship behaviour, especially hasOne/belongsTo edge cases.First use case. Pick one graph-shaped question you can't answer cheaply in SQL — recommendations, follower/following, org hierarchy, shared-tag similarity. Model it in a dedicated Neo4j connection, leave the rest of the app on your relational database, and prove the traversal in a read-only controller before you commit to migrations.
Hybrid persistence is the default, not a compromise. Keep money, auth, and anything needing multi-row transactions in Postgres/MySQL. Put edges in Neo4j. Do not attempt a cross-store transaction — there isn't one. Mirror changes via model events onto a queue and make the graph consumer idempotent (MERGE, not CREATE).
Graph-backed models. Extend the package's base model, point $connection at your neo4j connection, and set the node label explicitly. Because there is no auto-increment key, use UUIDs or ULIDs as your id and never persist Neo4j's internal node id — it's reused after deletion.
Name your relationship types. hasOne() / belongsTo() without an explicit relationship type no longer fail on Neo4j 5 (fixed in this release), but relying on inferred type names means your Cypher, your Browser queries, and your code all have to agree on a string that was never written down. Declare it — $user->hasOne(Profile::class, 'HAS_PROFILE') — so the edge label in migrations and raw Cypher matches.
Eager load deliberately; the N+1 tax is worse over Bolt. Each lazy relation is a network round trip. Model::with('follows') for shallow graphs; for anything deeper than two hops, drop to raw Cypher. Eloquent can't express variable-length paths ([:FOLLOWS*1..3]), and pretending otherwise produces a cascade of queries.
Raw Cypher for reads, models for writes. Run the traversal through the connection, then hydrate results into model instances so your controllers and resources stay unchanged:
$rows = DB::connection('neo4j')->run($cypher, $params)->toArray();
$users = User::hydrate($rows);
Bind parameters — never interpolate user input into Cypher.
Constraints and indexes belong in migrations, treated as opt-in. Neo4j won't stop you from scanning every node with a given label. Every property you MATCH on in a hot path needs an index; every logical identifier needs a uniqueness constraint. Make the migration idempotent (IF NOT EXISTS) because the migration table behaviour here is less forgiving than Laravel's SQL migrations.
Soft deletes on Laravel 13. This is the fix most apps will actually feel: SoftDeletes, onlyTrashed(), and counts now work on Laravel 13. If you're upgrading to Laravel 13 and carrying a local workaround (a deleted_at property check in a scope, a manual filter in every query), delete it and re-run your tests — the workaround now double-filters or masks the fixed behaviour. On Laravel 12 and below, nothing changes.
Testing. Spin up a real Neo4j 5.26 service container in CI — there is no in-memory equivalent, and mocking the driver hides the query-shape bugs that matter. Match the maintainers' matrix where you can: PHP 8.2–8.5, Laravel 12–13, Neo4j 5.26. Wipe labels between tests or wrap each in a transaction you roll back.
9.8.x install looks identical to a working upgrade. Check composer show after upgrading, not just the absence of errors. And note that Laravel 11 apps get none of the fixes below — including the soft-delete fix, which is Laravel 13 only anyway.delete() on a node with edges leaves orphans or fails depending on how you query it — reach for DETACH DELETE in raw Cypher, and clean edges explicitly in application code. Verify your soft-delete expectations specifically: a "trashed" node is still in the graph, still matched by raw Cypher, and still counted by anything that doesn't go through the model.count() over a relationship is a graph traversal, not an indexed lookup. It's also an area that has moved recently (the release notes call out counts alongside soft deletes) — pin behaviour with a test before you depend on it in a dashboard.withCount are partly implemented. Verify each aggregate you use against the package tests rather than assuming Eloquent parity; unsupported ones fail confusingly rather than loudly.PROFILE. Nine times out of ten the fix is a missing index or an accidental Cartesian product, not the ORM.with() on a high-degree node can pull tens of thousands of rows. Paginate, cap depth, or return projections instead of full nodes.hasOne/belongsTo bug was itself a Neo4j 5 regression, so treat server upgrades as code changes and run your suite against the target image.How can I help you explore Laravel packages today?