Neo4j-PHP-OGM is an Object Graph Mapper for Neo4j in PHP.
It uses the RepositoryPattern and is very similar to the Doctrine2 ORM, also it makes uses of Doctrine Annotations and Collection library.
This getting started guide is based on the Neo4j movies example you can load by running the :play movies
in the neo4j browser.
Require the OGM via composer :
composer require graphaware/neo4j-php-ogm:^1.0@beta
Let's take a look at the movie graph and define what our domain objects will look like :
We can identify the following entities :
- a Person having a
name
andborn
properties - a Movie having a
title
,tagline
andrelease
properties
Also, the following relationships can be identified :
a Person
acted in a Movie
a Person
wrote a Movie
Mapping definition is done by using Annotations on your domain object entities, let's build the Person model :
<?php
namespace Movies;
use GraphAware\Neo4j\OGM\Annotations as OGM;
/**
* @OGM\Node(label="Person")
*/
class Person
{
/**
* @OGM\GraphId()
* @var int
*/
protected $id;
/**
* @OGM\Property(type="string")
* @var string
*/
protected $name;
/**
* @OGM\Property(type="born")
* @var int
*/
protected $born;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return int
*/
public function getBorn()
{
return $this->born;
}
}
First off, you'll need to import the GraphAware\Neo4j\OGM\Annotations
directory with the use
statement.
Secondly, you'll need to declare your model as a graph entity, by adding the @OGM\Node()
annotation on the class.
The @OGM\Node()
annotation must contain the name of the label representing the person nodes in the database.
The @OGM\GraphId
annotation defines the property on which the internal neo4j node id will be mapped. This property and annotation
is mandatory.
As of now, the only allowed property name is id
, in the future you'll be able to specify a custom property name.
The @OGM\Property
annotation defines which entity properties will be managed by the OGM. You can have properties without this
annotation and they will not be saved / loaded to / from the database.
The type argument defines the internal type (php) of the property, common types are string
, 'int', float
, ...
Currently, the exact property name used in your domain model is used as property key on the database node. (This will evolve).
As of now, we are able to load / save Person
entities to the database, as well as handling updates. Before we need to create
the entity manager which will be the central point of operations.
Creating the manager is just instantiating a new GraphAware\Neo4j\OGM\EntityManager
object and passing your neo4j host url :
use GraphAware\Neo4j\OGM\EntityManager;
$manager = EntityManager::create('http://localhost:7474');
Finding nodes from the database is done via their repository, retrieving the corresponding repository is done by passing the
entity class name to the getRepository
method :
use GraphAware\Neo4j\OGM\EntityManager;
use Movies\Person;
$manager = EntityManager::create('http://localhost:7474');
$personRepository = $manager->getRepository(Person::class);
Once you have the repository, you can retrieve node from the database, let's find Tom Hanks
:
use GraphAware\Neo4j\OGM\EntityManager;
use Movies\Person;
$manager = EntityManager::create('http://localhost:7474');
$personRepository = $manager->getRepository(Person::class);
$tomHanks = $personRepository->findOneBy('name', 'Tom Hanks');
The available methods on the repository are :
findAll()
findOneBy($propertyKey, $propertyValue)
findBy($property, $propertyValue)
findOneById($id)
Persistence is handled by the OGM with two main methods, persist()
and flush()
.
To briefly summarize the difference, the objects you pass to the persist
method become managed
by the Entity EntityManager,
keeping track of their changes and reflecting the changes at the next flush()
operation.
Let's create a new actor, named Kevin Ross
and born in 1976
:
$actor = new Person('Kevin Ross', 1976);
$manager->persist($actor);
$manager->flush();
And verify our database :
The entity remains to be managed by the Entity EntityManager, this means that any update to your object will be reflected on next flush. This is also the case when you load entities from the database, they become automtically managed, let's modify Tom Hank's year of birth :
// adding the setter to the model
/**
* @param int $year
*/
public function setBorn($year)
{
$this->born = $year;
}
$tomHanks->setBorn(1990);
$manager->flush();
He is quite younger now :)
For finishing this first part, let's create our Movie entity before opening the chapter of relationships :
<?php
namespace Movies;
use GraphAware\Neo4j\OGM\Annotations as OGM;
/**
* @OGM\Node(label="Movie")
*/
class Movie
{
/**
* @OGM\GraphId()
* @var int
*/
protected $id;
/**
* @OGM\Property(type="string")
* @var string
*/
protected $title;
/**
* @OGM\Property(type="string")
* @var string
*/
protected $tagline;
/**
* @OGM\Property(type="int")
* @var int
*/
protected $release;
/**
* @param string $title
* @param string|null $release
*/
public function __construct($title, $release = null)
{
$this->title = $title;
$this->release = $release;
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @return string
*/
public function getTagline()
{
return $this->tagline;
}
/**
* @param string $tagline
*/
public function setTagline($tagline)
{
$this->tagline = $tagline;
}
/**
* @return int
*/
public function getRelease()
{
return $this->release;
}
}
Mapping relationship to an domain object property is done with the @OGM\Relationship
annotation. There are two types of relationships managed
by the OGM.
- Simple relationships, where the property will reflect another node
- Relationships entities, where the property will reflect a
RelationshipEntity
mapped object.
The first one is generally used for relationships where you don't have properties or don't need them in your domain model.
The latter is used when you need to filter on the relationships and need them in your business logic.
An example of a simple relationship can be a FOLLOWS
relationship while a RANKED
relationship with a score property is better
handled by a RelationshipEntity.
Let's add the ACTED_IN
relationship to our Person model, this will be a simple relationship :
<?php
namespace Movies;
use Doctrine\Common\Collections\ArrayCollection;
use GraphAware\Neo4j\OGM\Annotations as OGM;
/**
* @OGM\Node(label="Person")
*/
class Person
{
/...
/**
* @OGM\Relationship(type="ACTED_IN", direction="OUTGOING", targetEntity="Movie", collection=true)
* @var ArrayCollection|Movie[]
*/
protected $movies;
/**
* @param string $name
* @param int|null $born
*/
public function __construct($name, $born = null)
{
$this->name = $name;
$this->born = $born;
$this->movies = new ArrayCollection();
}
...
/**
* @return \Doctrine\Common\Collections\ArrayCollection|\Movies\Movie[]
*/
public function getMovies()
{
return $this->movies;
}
/**
* @param \Movies\Movie $movie
*/
public function addMovie(Movie $movie)
{
if (!$this->movies->contains($movie)) {
$this->movies->add($movie);
}
}
/**
* @param \Movies\Movie $movie
*/
public function removeMovie(Movie $movie)
{
if ($this->movies->contains($movie)) {
$this->movies->removeElement($movie);
}
}
}
Let's explain the annotation parameters :
/**
* @OGM\Relationship(type="ACTED_IN", direction="OUTGOING", targetEntity="Movie", collection=true)
* @var ArrayCollection|Movie[]
*/
protected $movies;
type
is the relationship typedirection
is the direction of the relationship, can be ofOUTGOING
,INCOMING
orBOTH
targetEntity
defines the classname of the entity representing the node on the other side of the relationship.collection
defines whether or not there can be multiple relationships of the same type connected to this entity.
Note : targetEntity
takes the **fully qualified class name` as argument, you can pass only the classname if both of the
entities lives in the same namespace.
Simply with this annotation, the nodes connected by an outgoing ACTED_IN relationship to this entity will be returned, let's take Tom Hanks again and all his movies :
$tomHanks = $manager->getRepository(Person::class)->findOneBy('name', 'Tom Hanks');
echo sprintf('Tom Hanks played in %d movies', count($tomHanks->getMovies())) . PHP_EOL;
foreach ($tomHanks->getMovies() as $movie) {
echo $movie->getTitle() . PHP_EOL;
}
$ php app.php
Tom Hanks played in 12 movies
Charlie Wilson's War
The Polar Express
A League of Their Own
Cast Away
Apollo 13
The Green Mile
The Da Vinci Code
Cloud Atlas
That Thing You Do
Joe Versus the Volcano
You've Got Mail
Sleepless in Seattle
Great, we can retrieve the related movies for an actor, but once we own the Movie object, there is no way to retrieve back the actor, let's tackle this by adding the appropriate mapping to the Movie domain object class.
<?php
namespace Movies;
use Doctrine\Common\Collections\ArrayCollection;
use GraphAware\Neo4j\OGM\Annotations as OGM;
/**
* @OGM\Node(label="Movie")
*/
class Movie
{
...
/**
* @OGM\Relationship(type="ACTED_IN", direction="OUTGOING", targetEntity="Person", collection=true)
* @var ArrayCollection|Person[]
*/
protected $actors;
/**
* @param string $title
* @param string|null $release
*/
public function __construct($title, $release = null)
{
$this->title = $title;
$this->release = $release;
$this->actors = new ArrayCollection();
}
...
/**
* @return \Doctrine\Common\Collections\ArrayCollection|\Movies\Person[]
*/
public function getActors()
{
return $this->actors;
}
/**
* @param \Movies\Person $person
*/
public function addActor(Person $person)
{
if (!$this->actors->contains($person)) {
$this->actors->add($person);
}
}
/**
* @param \Movies\Person $person
*/
public function removeActor(Person $person)
{
if ($this->actors->contains($person)) {
$this->actors->removeElement($person);
}
}
}
We need also to add a parameter to the Person
entity mapping for the movies
relationship annotation.
/**
* @OGM\Relationship(type="ACTED_IN", direction="OUTGOING", targetEntity="Movie", collection=true, mappedBy="actors")
* @var ArrayCollection|Movie[]
*/
protected $movies;
The mappedBy
argument defines the name of the property of the other entity where the relationship is bound to. This is to ensure
that the relationships are mapped to the right property, maybe the Movie entity will have incoming relationships from other objects
than a Person.
Let's modify the related Cast Away
movie related to Tom Hanks to a new Cast Away 2
movie name.
// Find Tom Hanks, filter his movies to find Cast Away and rename it to Cast Away 2
/** @var Person $tomHanks */
$tomHanks = $manager->getRepository(Person::class)->findOneBy('name', 'Tom Hanks');
$filter = array_values(array_filter($tomHanks->getMovies()->toArray(), function(\Movies\Movie $movie) {
return 'Cast Away' === $movie->getTitle();
}));
/** @var \Movies\Movie $castAway */
$castAway = $filter[0];
$castAway->setTitle('Cast Away 2');
$manager->flush();
And verify our database :
All good !
For the sake of the example, we will create another entity, called User
that will represent a user visiting the movies application
and rating the movies he saw :
<?php
namespace Movies;
use GraphAware\Neo4j\OGM\Annotations as OGM;
/**
* @OGM\Node(name="User")
*/
class User
{
/**
* @OGM\GraphId()
* @var int
*/
protected $id;
/**
* @OGM\Property(type="string")
* @var string
*/
protected $login;
/**
* @param string $login
*/
public function __construct($login)
{
$this->login = $login;
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getLogin()
{
return $this->login;
}
}
In order to relate the user and the movie with a RATED
relationship having a score property, we need a different type of object, where
we could use actually this relationship as first class citizen in our application.
This type of object is called a RelationshipEntity
, let's create it :
<?php
namespace Movies;
use GraphAware\Neo4j\OGM\Annotations as OGM;
/**
* @OGM\RelationshipEntity(type="RATED")
*/
class Rating
{
/**
* @OGM\GraphId()
* @var int
*/
protected $id;
/**
* @OGM\StartNode(targetEntity="User")
* @var User
*/
protected $user;
/**
* @OGM\EndNode(targetEntity="Movie")
* @var Movie
*/
protected $movie;
/**
* @OGM\Property(type="float")
* @var float
*/
protected $score;
/**
* Rating constructor.
* @param \Movies\User $user
* @param \Movies\Movie $movie
* @param float $score
*/
public function __construct(User $user, Movie $movie, $score)
{
$this->user = $user;
$this->movie = $movie;
$this->score = $score;
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @return Movie
*/
public function getMovie()
{
return $this->movie;
}
/**
* @return float
*/
public function getScore()
{
return $this->score;
}
}
Let's also add the corresponding annotation in the User class :
class User
{
...
/**
* @OGM\Relationship(relationshipEntity="Rating", type="RATED", direction="OUTGOING", collection=true)
* @var Rating[]|ArrayCollection
*/
protected $ratings;
/**
* @param string $login
*/
public function __construct($login)
{
$this->login = $login;
$this->ratings = new ArrayCollection();
}
...
/**
* @return \Doctrine\Common\Collections\ArrayCollection|\Movies\Rating[]
*/
public function getRatings()
{
return $this->ratings;
}
/**
* @param \Movies\Movie $movie
* @param float $score
*/
public function rateMovie(Movie $movie, $score)
{
$this->ratings->add(new Rating($this, $movie, $score));
}
Now let's create a new User, find 'The Matrix' movie and create a rating :
$user = new User('cypher666');
/** @var Movie $movie */
$movie = $manager->getRepository(Movie::class)->findOneBy('title', 'The Matrix');
$user->rateMovie($movie, '4.5');
$manager->persist($user);
$manager->flush();
And check our graph :