Neo4j Graph Eloquent Driver for Laravel 4
- Installation
- Configuration
- Models
- Relationships
- Edges
- Only in Neo
- Aggregates
- Migration
- Schema
- Things To Avoid
Add the package to your composer.json
and run composer update
.
{
"require": {
"vinelab/neoeloquent": "*"
}
}
Add the service provider in app/config/app.php
:
'Vinelab\NeoEloquent\NeoEloquentServiceProvider',
The service provider will register all the required classes for this package and will also alias
the Model
class to NeoEloquent
so you can simply extend NeoEloquent
in your models.
####Migration Support
Additionally NeoEloquent have migration support with Neo4j schema feature.
To use it first you need to update composer.json
file by adding additional line to autoload class map:
"app/database/labels",
So your composer.json
would look like this:
"autoload": {
"classmap": [
"app/commands",
"app/controllers",
"app/models",
"app/database/labels",
"app/database/migrations",
"app/database/seeds",
"app/tests/TestCase.php"
]
},
And run:
composer dumpautoload
Second step requires registering additional alias in app/config/app.php
:
'Neo4jSchema' => 'Vinelab\NeoEloquent\Facade\Neo4jSchema'
in app/config/database.php
or in case of an environment-based configuration app/config/[env]/database.php
make neo4j
your default connection:
'default' => 'neo4j',
Add the connection defaults:
'connections' => [
'neo4j' => [
'driver' => 'neo4j',
'host' => 'localhost',
'port' => '7474'
]
]
If you want to use migration feature with custom node label you can do that by adding to database.php
configuration file:
'migrations_node' => 'YourMigrationNodeLabel',
class User extends NeoEloquent {}
As simple as it is, NeoEloquent will generate the default node label from the class name,
in this case it will be :User
. Read about node labels here
When you use namespaces with your models the label will consider the full namespace.
namespace Vinelab\Cms;
class Admin extends NeoEloquent { }
The generated label from that relationship will be VinelabCmsAdmin
, this is necessary to make sure
that labels do not clash in cases where we introduce another Admin
instance like
Vinelab\Blog\Admin
then things gets messy with :Admin
in the database.
You may specify the label(s) you wish to be used instead of the default generated, they are also case sensitive so they will be stored as put here.
class User extends NeoEloquent {
protected $label = 'User'; // or array('User', 'Fan')
protected $fillable = ['name', 'email'];
}
$user = User::create(['name' => 'Some Name', 'email' => '[email protected]']);
NeoEloquent has a fallback support for the $table
variable that will be used if found and there was no $label
defined on the model.
class User extends NeoEloquent {
protected $table = 'User';
}
Do not worry about the labels formatting, You may specify them as array('Label1', 'Label2')
or separate them by a column :
and prepending them with a :
is optional.
To enable soft deleting you'll need to use Vinelab\NeoEloquent\Eloquent\SoftDeletingTrait
instead of Illuminate\Database\Eloquent\SoftDeletingTrait
and just like Eloquent you'll need the $dates
in your models as follows:
use Vinelab\NeoEloquent\Eloquent\SoftDeletingTrait;
class User extends NeoEloquent {
use SoftDeletingTrait;
protected $dates = ['deleted_at'];
}
Let's go through some examples of relationships between Nodes.
class User extends NeoEloquent {
public function phone()
{
return $this->hasOne('Phone');
}
This represents an OUTGOING
relationship direction from the :User
node to a :Phone
.
$phone = new Phone(['code' => 961, 'number' => '98765432'])
$relation = $user->phone()->save($phone);
The Cypher performed by this statement will be as follows:
MATCH (user:`User`)
WHERE id(user) = 1
CREATE (user)-[:PHONE]->(phone:`Phone` {code: 961, number: '98765432', created_at: 7543788, updated_at: 7543788})
RETURN phone;
class Phone extends NeoEloquent {
public function user()
{
return $this->belongsTo('User');
}
}
This represents an INCOMING
relationship direction from
the :User
node to this :Phone
node.
Due to the fact that we do not deal with foreign keys, in our case it is much more than just setting the foreign key attribute on the parent model. In Neo4j (and Graph in general) a relationship is an entity itself that can also have attributes of its own, hence the introduction of Edges
Note: Associated models does not persist relations automatically when calling
associate()
.
$account = Account::find(1986);
// $relation will be Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn
$relation = $user->account()->associate($account);
// Save the relation
$relation->save();
The Cypher performed by this statement will be as follows:
MATCH (account:`Account`), (user:`User`)
WHERE id(account) = 1986 AND id(user) = 9862
MERGE (account)<-[rel_user_account:ACCOUNT]-(user)
RETURN rel_user_account;
The Cypher performed by this statement will be as follows:
MATCH (phone:Phone) (phone)<-[:PHONE]-(user:User)
WHERE id(phone) = 1006
RETURN user;
class User extends NeoEloquent {
public function posts()
{
return $this->hasMany('Post', 'POSTED');
}
}
This represents an OUTGOING
relationship direction
from the :User
node to the :Post
node.
$user = User::find(1);
$post = new Post(['title' => 'The Title', 'body' => 'Hot Body']);
$user->posts()->save($post);
Similar to One-To-One
relationships the returned value from a save()
statement is an
Edge[In|Out]
The Cypher performed by this statement will be as follows:
MATCH (user:`User`)
WHERE id(user) = 1
CREATE (user)-[rel_user_post:POSTED]->(post:`Post` {title: 'The Title', body: 'Hot Body', created_at: '15-05-2014', updated_at: '15-05-2014'})
RETURN rel_user_post;
class Post extends NeoEloquent {
public function author()
{
return $this->belongsTo('User', 'POSTED');
}
}
This represents an INCOMING
relationship direction from
the :User
node to this :Post
node.
class User extends NeoEloquent {
public function followers()
{
return $this->belongsToMany('User', 'FOLLOWS');
}
}
This represents an OUTGOING
relationship between a :User
node and another :User
.
$jd = User::find(1012);
$mc = User::find(1013);
$jd
follows $mc
:
$jd->followers()->save($mc);
Or using the attach()
method:
$jd->followers()->attach($mc);
// Or..
$jd->followers()->attach(1); // 1 being the id of $mc ($mc->getKey())
The Cypher performed by this statement will be as follows:
MATCH (user:`User`), (followers:`User`)
WHERE id(user) = 1012 AND id(followers) = 1013
CREATE (user)-[:FOLLOWS]->(followers)
RETURN rel_follows;
$mc
follows $jd
back:
$mc->followers()->save($jd);
The Cypher performed by this statement will be as follows:
MATCH (user:`User`), (followers:`User`)
WHERE id(user) = 1013 AND id(followers) = 1012
CREATE (user)-[rel_user_followers:FOLLOWS]->(followers)
RETURN rel_follows;
get the followers of $jd
$followers = $jd->followers;
The Cypher performed by this statement will be as follows:
MATCH (user:`User`), (followers:`User`), (user)-[rel_user_followers:FOLLOWS]-(followers)
WHERE id(user) = 1012
RETURN rel_follows;
class Phone extends Eloquent {
public function user()
{
return $this->belongsTo('User');
}
}
$phone = Phone::find(1006);
$user = $phone->user;
// or getting an attribute out of the related model
$name = $phone->user->name;
The concept behind Polymocrphic relations is purely relational to the bone but when it comes to graph we are representing it as a HyperEdge.
Hyper edges involves three models, the parent model, hyper model and related model represented in the following figure:
Similarly in code this will be represented by three models User
Comment
and Post
where a User
with id 1 posts a Post
and a User
with id 6 COMMENTED
a Comment
ON
that Post
as follows:
class User extends NeoEloquent {
public function comments($morph = null)
{
return $this->hyperMorph($morph, 'Comment', 'COMMENTED', 'ON');
}
}
In order to keep things simple but still involving the three models we will have to pass the
$morph
which is any commentable
model, in our case it's either a Video
or a Post
model.
Note: Make sure to have it defaulting to
null
so that we can Dynamicly or Eager load with$user->comments
later on.
Creating a Comment
with the create()
method.
$user = User::find(6);
$post = Post::find(2);
$user->comments($post)->create(['text' => 'Totally agree!', 'likes' => 0, 'abuse' => 0]);
As usual we will have returned an Edge, but this time it's not directed it is an instance of
HyperEdge
, read more about HyperEdges here.
Or you may save a Comment instance:
$comment = new Comment(['text' => 'Magnificent', 'likes' => 0, 'abuse' => 0]);
$user->comments($post)->save($comment);
Also all the functionalities found in a BelongsToMany
relationship are supported like
attaching models by Ids:
$user->comments($post)->attach([$id, $otherId]);
Or detaching models:
$user->comments($post)->detach($comment); // or $comment->id
Sync too:
$user->comments($post)->sync([$id, $otherId, $someId]);
From our previous example we will use the Video
model to retrieve their comments:
class Video extends NeoEloquent {
public function comments()
{
return $this->morphMany('Comment', 'ON');
}
}
$video = Video::find(3);
$comments = $video->comments;
$video = Video::with('comments')->find(3);
foreach ($video->comments as $comment)
{
//
}
class Comment extends NeoEloquent {
public function commentable()
{
return $this->morphTo();
}
}
$postComment = Comment::find(7);
$post = $comment->commentable;
$videoComment = Comment::find(5);
$video = $comment->commentable;
// You can also eager load them
Comment::with('commentable')->get();
You may also specify the type of morph you would like returned:
class Comment extends NeoEloquent {
public function post()
{
return $this->morphTo('Post', 'ON');
}
public function video()
{
return $this->morphTo('Video', 'ON');
}
}
To drill things down here's how our three models involved in a Polymorphic relationship connect:
class User extends NeoEloquent {
public function comments($morph = null)
{
return $this->hyperMorph($morph, 'Comment', 'COMMENTED', 'ON');
}
}
class Post extends NeoEloquent { // Video is the same as this one
public function comments()
{
return $this->morphMany('Comment', 'ON');
}
}
class Comment extends NeoEloquent {
public function commentable()
{
return $this->morphTo();
}
}
class Book extends Eloquent {
public function author()
{
return $this->belongsTo('Author');
}
}
Loading authors with their books with the least performance overhead possible.
foreach (Book::with('author')->get() as $book)
{
echo $book->author->name;
}
Only two Cypher queries will be run in the loop above:
MATCH (book:`Book`) RETURN *;
MATCH (book:`Book`), (book)<-[:WROTE]-(author:`Author`) WHERE id(book) IN [1, 2, 3, 4, 5, ...] RETURN book, author;
Due to the fact that relationships in Graph are much different than other database types so we will have to handle them accordingly. Relationships have directions that can vary between In and Out respectively towards the parent node.
Represents an INCOMING
direction relationship from the related model towards the parent model.
class Location extends NeoEloquent {
public function user()
{
return $this->belongsTo('User', 'LOCATED_AT');
}
}
To associate a User
to a Location
:
$location = Location::find(1922);
$user = User::find(3876);
$relation = $location->associate($user);
which in Cypher land will map to (:Location)<-[:LOCATED_AT]-(:User)
and $relation
being an instance of EdgeIn
representing an incoming relationship towards the parent.
And you can still access the models from the edge:
$relation = $location->associate($user);
$location = $relation->parent();
$user = $relation->related();
Represents an OUTGOING
direction relationship from the parent model to the related model.
class User extends NeoEloquent {
public function posts()
{
return $this->hasMany('Post', 'POSTED');
}
}
To save an outgoing edge from :User
to :Post
it goes like:
$post = new Post(['...']);
$posted = $user->posts()->save($post);
Which in Cypher would be (:User)-[:POSTED]->(:Post)
and $posted
being the EdgeOut
instance.
And fetch the related models:
$edge = $user->posts()->save($post);
$user = $edge->parent();
$post = $edge->related();
This edge comes as a result of a Polymorphic Relation representing an edge involving
tow other edges left and right that can be accessed through the left()
and right()
methods.
This edge is treated a bit different than the others since it is not a direct relationship between two models which means it has no specific direction.
$edge = $user->comments($post)->attach($comment);
// Access the left and right edges
$left = $edge->left();
$user = $left->parent();
$comment = $left->related();
$right = $edge->right();
$comment = $right->parent();
$post = $right->related();
As stated earlier Edges are entities to Graph unlike SQL where they are a matter of a
foreign key having the value of the parent model as an attribute on the belonging model or in
Documents where they are either embeds or ids as references. So we developed them to be light
models which means you can work with them as if you were working with an Eloquent
instance - to a certain extent,
except HyperEdges.
// Create a new relationship
$relation = $location->associate($user); // Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn
// Save the relationship to the database
$relation->save(); // true
In the case of a HyperEdge
you can access all three models as follows:
$edge = $user->comments($post)->save($comment);
$user = $edge->parent();
$comment = $edge->hyper();
$post = $edge->related();
By default, edges will have the timestamps created_at
and updated_at
automatically set and updated only if timestamps are enabled by setting $timestamps
to true
on the parent model.
$located_at = $location->associate($user);
$located_at->since = 1966;
$located_at->present = true;
$located_at->save();
// $created_at and $updated_at are Carbon\Carbon instances
$created_at = $located_at->created_at;
$updated_at = $located_at->updated_at;
The same way an association will create an EdgeIn
relationship we can retrieve
the edge between two models by calling the edge($model)
method on the belongsTo
relationship.
$location = Location::find(1892);
$edge = $location->user()->edge();
You may also specify the model at the other side of the edge.
Note: By default NeoEloquent will try to pefrorm the
$location->user
internally to figure out the related side of the edge based on the relation function name, in this case it'suser()
.
$location = Location::find(1892);
$edge = $location->user()->edge($location->user);
Here you will find NeoEloquent-specific methods and implementations that with the wonderful Eloquent methods would make working with Graph and Neo4j a blast!
This method will "kind of" fill the gap between relational and document databases, it allows the creation of multiple related models with one database hit.
Here's an example of creating a post with attached photos and videos:
class Post extends NeoEloquent {
public function photos()
{
return $this->hasMany('Photo', 'PHOTO');
}
public function videos()
{
return $this->hasMany('Video', 'VIDEO');
}
}
Post::createWith(['title' => 'the title', 'body' => 'the body'], [
'photos' => [
[
'url' => 'http://url',
'caption' => '...',
'metadata' => '...'
],
[
'url' => 'http://other.url',
'caption' => 'the bay',
'metadata' => '...'
]
],
'videos' => [
'title' => 'Boats passing us by',
'description' => '...'
]
]);
The keys
photos
andvideos
must be the same as the relation method names in thePost
model.
The Cypher query performed by the example above is:
CREATE (post:`Post` {title: 'the title', body: 'the body'}),
(post)-[:PHOTO]->(:`Photo` {url: 'http://url', caption: '...', metadata: '...'}),
(post)-[:PHOTO]->(:`Photo` {url: 'http://other', caption: 'the bay', metadata: '...'}),
(post)-[:VIDEO]->(:`Video` {title: 'Boats passing us by', description: '...'});
We will get the nodes created with their relations as such:
You may also mix models and attributes as relation values but it is not necessary
since NeoEloquent will pass the provided attributes through the $fillable
filter pipeline:
$videos = new Video(['title' => 'foo', 'description' => 'bar']);
Post::createWith($info, compact('videos'));
You may also use a single array of attributes as such:
class User extends NeoEloquent {
public function account()
{
return $this->hasOne('Account');
}
}
User::createWith(['name' => 'foo'], ['account' => ['guid' => 'bar', 'email' => '[email protected]']]);
createWith
is intelligent enough to know the difference when you pass an existing model,
a model Id or new records that you need to create which allows mixing new records with existing ones.
class Post extends NeoEloquent {
public function tags()
{
return $this->hasMany('Tag', 'TAG');
}
}
$tag1 = Tag::create(['title' => 'php']);
$tag2 = Tag::create(['title' => 'dev']);
$post = Post::createWith(['title' => 'foo', 'body' => 'bar'], ['tags' => [$tag1, $tag2]]);
And we will get the Post
related to the existing Tag
nodes.
Or using the id
of the model:
Post::createWith(['title' => 'foo', 'body' => 'bar'], ['tags' => 1, 'privacy' => 2]);
The Cypher for the query that attaches records would be:
CREATE (post:`Post` {title: 'foo', 'body' => 'bar'})
WITH post
MATCH (tag:`Tag`)
WHERE id(tag) IN [1, 2]
CREATE (post)-[:TAG]->(tag);
In addition to the Eloquent builder aggregates, NeoEloquent also has support for Neo4j specific aggregates like percentile and standard deviation, keeping the same function names for convenience. Check the docs for more.
table()
represents the label of the model
$users = DB::table('User')->count();
$distinct = DB::table('User')->countDistinct('points');
$price = DB::table('Order')->max('price');
$price = DB::table('Order')->min('price');
$price = DB::table('Order')->avg('price');
$total = DB::table('User')->sum('votes');
$disc = DB::table('User')->percentileDisc('votes', 0.2);
$cont = DB::table('User')->percentileCont('votes', 0.8);
$deviation = DB::table('User')->stdev('sex');
$population = DB::table('User')->stdevp('sex');
$emails = DB::table('User')->collect('email');
You can read more about migration on:
http://laravel.com/docs/migrations
Because Neo4j is dynamic database you don't need to predefine types of properties for each label. However by few limitations on Neo4j - NeoEloquent comes to rescue to make them as pain-less as possible.
####Commands NeoEloquent introduces new commands just for Neo4j. You can use Laravel's default migration commands side by side with NeoEloquent migration ones:
neo4j:make Create a new migration file
neo4j:migrate Run the database migrations
neo4j:refresh Reset and re-run all migrations
neo4j:reset Rollback all database migrations
neo4j:rollback Rollback the last database migration
###Creating Migrations
Like in Laravel you can create new migration by using added command: neo4j:make
on the Artisan CLI:
php artisan neo4j:make create_user_label
Label migrations will be placed app/database/labels
(remember about registering new autoload classmap on composer.json
!(and running composer dumpautoload
after)
You can add additional options to commands like:
php artisan neo4j:make foo --path=app/labels
php artisan neo4j:make create_user_label --create=User
php artisan neo4j:make create_user_label --label=User
###Running Migrations
#####Running All Outstanding Migrations
php artisan neo4j:migrate
#####Running All Outstanding Migrations For A Path
php artisan neo4j:migrate --path=app/foo/labels
#####Running All Outstanding Migrations For A Package
php artisan neo4j:migrate --package=vendor/package
Note: If you receive a "class not found" error when running migrations, try running the composer
dump-autoload
command.
####Forcing Migrations In Production To force running migration on production database you can use:
php artisan neo4j:migrate --force
###Rolling Back Migrations
#####Rollback The Last Migration Operation
php artisan neo4j:rollback
#####Rollback all migrations
php artisan neo4j:reset
#####Rollback all migrations and run them all again
php artisan neo4j:refresh
php artisan neo4j:refresh --seed
You can read more about schema on:
http://laravel.com/docs/schema
To manipulate label's schema, the Neo4jSchema::label
is used:
Neo4jSchema::label('User', function(Blueprint $label)
{
$label->unique('uuid');
});
Currently Neo4j supports UNIQUE
constraint and INDEX
on properties. You can read more about them:
http://docs.neo4j.org/chunked/stable/graphdb-neo4j-schema.html
You can use them by adding $label->{KEY}({PROPERTY_NAME}):
Command | Description |
---|---|
$label->unique('email') | Adding a unique constraint on property |
$label->dropUnique('email') | Dropping a unique constraint from property |
$label->index('uuid') | Adding a index on property |
$label->dropUnique('uuid') | Dropping a index from property |
###Droping Labels
To drop a label, you may use the Neo4jSchema::drop method:
Neo4jSchema::drop('User');
Neo4jSchema::dropIfExists('User');
###Renaming Labels
Neo4jSchema::renameLabel($from, $to);
###Checking Existence Of Label:
if (Neo4jSchema::hasLabel('User'))
{
//
}
else
{
//
}
###Checking Existence Of Relation:
if (Neo4jSchema::hasRelation('FRIEND_OF'))
{
//
}
else
{
//
}
Check the Releases for details.
Here are some constraints and Graph-specific gotchas, a list of features that are either not supported or not recommended.
- They make no sense for Graph, plus Graph hates them!
Which makes them unsupported on purpose. If migrating from an
SQL
-based app they will be your boogie monster.
This is not supported, instead we will be using Edges to work with relationships between models.
- Due to the limitations imposed by the objects map types that can be stored in a single, you can never have nested arrays or objects in a single model, make sure it's flat. Example:
// Don't
User::create(['name' => 'Some Name', 'location' => ['lat' => 123, 'lng'=> -123 ] ]);
Check out the createWith() method on how you can achieve this in a Graph way.
Tests will actually wipe database, so never run tests on populated database!
- install a Neo4j instance and run it with the default configuration
localhost:7474
- make sure the database graph is empty to avoid conflicts
- after running
composer install
there should be/vendor/bin/phpunit
- run
./vendor/bin/phpunit
after making sure that the Neo4j instance is running
Tests marked as incomplete means they are either known issues or non-supported features, check included messages for more info.