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

Neoeloquent Laravel Package

vinelab/neoeloquent

NeoEloquent brings Eloquent-style models to Neo4j in Laravel. Define graph nodes and relationships as expressive PHP classes, use familiar query building and eager loading, and work with Cypher-backed graph data without leaving Laravel's ORM conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

NeoEloquent is an Eloquent driver for Neo4j — you get Model::find(), where(), save() and relationships, but they compile to Cypher over Neo4j's HTTP endpoint instead of SQL.

1. Install and wire up

composer require vinelab/neoeloquent
php artisan vendor:publish --provider="Vinelab\NeoEloquent\NeoEloquentServiceProvider"

Register the provider (and optionally the NeoEloquent facade) in config/app.php if you're on a Laravel version without package auto-discovery, and confirm the package's Laravel constraint against your composer.json before you commit to it.

2. Configure the connection

config/neoeloquent.php holds named connections. Point them at env vars so staging/production differ:

'default' => env('NEO4J_CONNECTION', 'neo4j'),

'connections' => [
    'neo4j' => [
        'driver'   => 'neo4j',
        'host'     => env('NEO4J_HOST', 'localhost'),
        'port'     => env('NEO4J_PORT', 7474),
        'username' => env('NEO4J_USERNAME', 'neo4j'),
        'password' => env('NEO4J_PASSWORD'),
        'database' => env('NEO4J_DATABASE', 'neo4j'), // added in 1.8.2
    ],
],

3. First model

use Vinelab\NeoEloquent\Eloquent\Model;

class User extends Model
{
    protected $label = 'User';
    protected $fillable = ['name', 'email'];
    protected $dates = ['created_at', 'updated_at'];

    public function follows()
    {
        return $this->hasMany('App\User', 'FOLLOWS');
    }
}

$user = User::create(['name' => 'Ada', 'email' => '[email protected]']);
$user->follows()->attach($other->id, ['since' => now()->toDateString()]);

Where to look first: config/neoeloquent.php (connection shape), then the package's Model class — it's the contract you're extending, and it documents the label/edge/timestamp conventions you'll otherwise discover by hitting errors.

First use case: anything where the interesting question is "how is X connected to Y" — social graphs, recommendations, permissions/inheritance trees, tagging. If your queries are all single-table lookups, Neo4j is the wrong store and this package will just add latency.

Implementation Patterns

Labels, not tables. $label for a single label, $labels = ['User', 'Admin'] for multiple. Treat labels as your "tables + indexes" and keep them few and stable — every distinct label is a query-planning hint in Neo4j. If you're tempted to make a label per tenant, use the database selector instead.

Selecting a database (1.8.2+). On Neo4j 4.x the connection targets the database key from config. Two patterns worth knowing:

  • Per-environment separation: separate NEO4J_DATABASE for staging/prod so a misconfigured deploy can't touch live data.
  • Per-tenant graphs: define connections dynamically at runtime via config()->set("neoeloquent.connections.tenant_{$id}", [...]) and call Model::on('tenant_'.$id), which keeps tenant isolation at the server rather than in where clauses. Cache the resolved connection per request; don't rebuild config in a loop.

Relationship verbs carry the edge type. The second argument is the Cypher relationship type, not an attribute. Name it loudly (FOLLOWS, WORKS_AT, PURCHASED) and give edges their own properties — attach($id, ['role' => 'owner']) — rather than stuffing metadata onto nodes. Edge properties are what makes graph queries cheap later.

Eager loading works, but it's still N queries. ->with('follows.follows') is fine for bounded depth; unbounded expansion is a foot-gun. For anything more than two hops, drop to raw Cypher and hydrate what you need:

$rows = NeoEloquent::connection('neo4j')->statement(
    'MATCH (u:User {uuid: $uuid})-[:FOLLOWS*1..3]->(f:User) RETURN DISTINCT f',
    ['uuid' => $user->uuid]
);

Put traversal logic behind a repository. Graph access patterns change faster than schema does; a Graph/ namespace with one class per question ("who should we recommend", "what can this user see") keeps Cypher out of controllers and makes it testable against a fixture graph.

Migrations are not schema. Don't look for create_users_table. Create indexes and constraints with Cypher in a deploy step or a seeding command:

CREATE CONSTRAINT ON (u:User) ASSERT u.uuid IS UNIQUE
CREATE INDEX ON :User(email)

Seeding and tests. Build a small fixture graph in a seeder and point tests at a dedicated database (or a throwaway Neo4j in CI). Between tests you need a wipe — MATCH (n) DETACH DELETE n — because there's no RefreshDatabase equivalent here.

Gotchas and Tips

The 1.8.2 mass-assignment fix changes what you catch. Previously, filling a non-fillable attribute blew up with a "class does not exist" error for Vinelab\NeoEloquent\Eloquent\MassAssignmentException; it now behaves correctly. If you had a workaround — a try/catch on that phantom class, or defensive array_intersect_key before fill() — remove it, and update any catch blocks to Illuminate\Database\Eloquent\MassAssignmentException so your handling still fires.

delete() and edges. Neo4j refuses to delete a node that still has relationships. Prefer a soft-delete flag (deleted_at property) on business nodes, and reserve hard deletes for DETACH DELETE run explicitly. A delete() that works in isolation and fails in production is usually a node that acquired an edge since you last tested.

Reserved property names. id, label, type, created_at, updated_at are handled specially — an id you define yourself will collide with the internal node id, and setting type on a model that also uses edge typing produces confusing results. Namespace anything ambiguous (external_id, user_id).

Timestamps are strings. Neo4j has no date type here, so created_at/updated_at are written as strings. Always list them in $dates so they come back as Carbon; otherwise comparisons and ->diffForHumans() silently misbehave.

It's the HTTP REST API, not Bolt. Port 7474, JSON round-trips, per-query overhead. Wrap multi-step writes in a transaction where the connection supports it, and never put a graph query inside a loop over a few hundred records — batch it. Latency, not correctness, is the usual production complaint.

Debug with the Cypher, not the model. The query builder can emit the statement it would run — log or dd() that instead of guessing why a where matched nothing. Property filters are case- and type-sensitive: '20' will not match 20, and a missing property is not the same as an empty string.

No joins means whereHas-style filtering is expensive. Subqueries that would be a single EXISTS in SQL become MATCH patterns. For list screens with counts, run a separate aggregate query and merge in PHP rather than loading every node.

Package age is a real risk. Pin your Laravel version deliberately, read the changelog before minor bumps, and keep a thin escape hatch (raw Cypher through the connection) so a future migration to laudis/neo4j-php-client is a swap of the repository layer, not the whole domain model.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core