primd / fluidgraph
A Doctrine-inspired OGM for Bolt/Cypher Graph Databases
Requires
- php: ^8.4
- ramsey/uuid: ^4.7
- stefanak-michal/bolt: ^7.2
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-06-13 02:04:07 UTC
README
FluidGraph is an Object Graph Manager (OGM) for memgraph (though in principle could also work with Neo4j). It borrows a lot of concepts from Doctrine (a PHP ORM for relational databases), but aims to "rethink" many of those concepts in the graph paradigm.
This project is part of the Primd application stack and is copyright Primd Cooperative, licensed MIT. Primd Cooperative is a worker-owned start-up aiming to revolutionize hiring, learning, and work itself. For more information, or to support our work:
- See what we do: https://primd.app
- Become a Patreon Member: https://patreon.com/primd
- Star and Share this Repository
Installation
composer require primd/fluidgraph
Basic Concepts
FluidGraph borrows a bit of a "spiritual ontology" in order to talk about its core concepts. The Entity world is effectively the natural world. These are your concrete models and the things you interact with.
The Element world is the spiritual world. Entities are fastened to an Element. A single Element can be expressed in one or more Entities. These are the underlying graph data and not generally meant to be interacted with (unless you're doing more advanced development).
Both Entities and Elements have different forms, namely Nodes and Edges, i.e. there is a "Node Element" as well as a "Node Entity."
NOTE: FluidGraph is still alpha and is subject to fairly rapid changes, though we'll try not to break documented APIs.
Basic Usage
Instantiating a graph:
use Bolt\Bolt; use Bolt\connection\StreamSocket; $graph = new FluidGraph\Graph( [ 'scheme' => 'basic', 'principal' => 'memgraph', 'credentials' => 'password' ], new Bolt(new StreamSocket()) );
Create a Node class:
class Person extends FluidGraph\Node { use FluidGraph\Entity\Id\Uuid7; public function __construct( public ?string $firstName = NULL, public ?string $lastName = NULL, ) {} }
Traits are used to provide built-in common functionality and usually represent hooks. The example above uses the Uuid7
trait to identify the entity. This will provide a key of id
and automatically generate the UUID onCreate
.
Create an Edge class:
class FriendsWith extends FluidGraph\Edge { use FluidGraph\Entity\DateCreated; use FluidGraph\Entity\DateModified; public string $description; }
Add a relationship between people:
class Person extends FluidGraph\Node { use FluidGraph\Entity\Id\Uuid7; public protected(set) FluidGraph\Relationship\ToMany $friends; public function __construct( public ?string $firstName = NULL, public ?string $lastName = NULL, ) { $this->friendships = new FluidGraph\Relationship\ToMany( $this, FriendsWith::class, [ self::class ] ); } }
Note: All properties on your entities MUST be publicly readable. They can have
protected(set)
orprivate(set)
, however, note that you CANNOT use property hooks. FluidGraph avoids reflection where possible, but due to how it uses per-property references, hooks are not viable.
Instantiate nodes:
$matt = new Person(firstName: 'Matt'); $jill = new Person(firstName: 'Jill');
Set the relationship between them:
$matt->friendships->set($jill, [ 'description' => 'Best friends forever!' ]);
Attach, merge changes into the queue, and execute it:
$graph->attach($matt)->queue->merge()->run();
Note: There is no need to attach
$jill
or theFriendsWith
edge, as these are cascaded from$matt
being attached. Without the relationship,$jill
would need to be attached separately to persist.
Find a person:
$matt = $graph->query->matchOne(Person::class, ['firstName' => 'Matt']);
Get their friends:
$friends = $matt->friendships->get(Person::class);
Get the friendships (The actual FriendsWith
edges):
$friendships = $matt->friendships->all();
Note: The available methods and return values depend on the relationship type. A
ToMany
hasall()
while aToOne
hasany()
for example.
Working with Entities and Elements
Status
To determine the status of an Entity or Element you can use the status()
method which, with no arguments will return a FluidGraph\Status
or NULL
if somehow an Entity has not been fastened.
$entity_or_element->status()
Status types:
FluidGraph\Status::* | Description |
---|---|
FASTENED | The entity or element is bound to its other half, that's it. |
INDUCTED | The entity or element is ready and waiting to be merged with the graph. |
ATTACHED | The entity or element has been merged with and is attached to the graph |
RELEASED | The entity or element is ready and waiting to be removed from the graph. |
DETACHED | The entity or element has been merged with and is detached from the graph |
You can easily check if the status is of one or more types by passing arguments, in which case status()
will return TRUE
if the status is any one of the types, FALSE
otherwise:
$entity_or_element->status(FluidGraph\Status::ATTACHED, ...)
Is
Determine whether or not an Entity or Element is the same as another-ish:
$entity_or_element->is($entity_or_element_or_class);
This returns TRUE
in given the following modes and outcomes:
Entities share the same Element
$entity->is($entity);
Entity expresses a given Element
$entity->is($element);
Element is the same as another Element
$element->is($element);
Entity's Element is Labeled as a Class
$entity->is(Person::class);
Element is Labeled as a Class
$element->is(Person::class);
Because Entities can express the same Element without the need for polymorphism you can, for example, have a totally different Node class, such as Author
and check whether or not they are the same as a Person
:
if ($person->is(Author::class)) { // Do things knowing the person is an author }
Like
Available for Nodes only (as they can have more than one label), is the like()
methods which will observe both classes as well as arbitrary labels that may be common. Like will also accept Nodes and Node Elements:
$entity->like(Archivable::ARCHIVED);
It is strongly recommended that you use constants for labels. How or where you implement them depends on how they are shared across Nodes. In the example above we have a separate Archivable
Trait which could be used by various classes.
As
As mentioned before, different Entities can express the same Element. Because Nodes can carry multiple distinct labels, this effectively means that you can transform one Node into another (adding properties and relationships) in a dynamic an horizontal fashion.
A person becomes an author:
$author = $person->as(Author::class, ['penName' => 'Hairy Poster']); $author->is(Person::class); // TRUE $person->is(Author::class); // TRUE
NOTE: The
Person
object is not changed, rather, in this example a newAuthor
object is created and the person/author share the same graph Node, the same Element (in FluidGraph). When working with aPerson
you only have access to the properties and relationships of aPerson
. Theas()
method allows you to gracefully change the Entity type.
When using as()
to create a new expression of an existing Node, you need to pass any required arguments for instantiations (required by it's __construct()
method) as the second parameter. If no properties are required, this can be excluded. If the Author
object is already fastened to the underlying Node Element, then you can simply switch between them.
A subsequent merge/run of the queue will persist the Author
Label as well as the properties to the database, assuming the original $person
is already attached:
$graph->queue->merge()->run();
NOTE: At present
as()
exists on Edges as well, however, edges cannot have more than one Label, so the behavior is not particularly defined. On approach that may be taken is to allow an$edge->as()
call to create a new type of Edge between the same source and target Entities. Another would be to change the type entirely.
Working with Relationships
Relationships are collections of Edges. To understand these better, we'll give a bit more definition to our Author
class:
<?php class Author extends FluidGraph\Node { public FluidGraph\Relationship\ToMany $writings; public function __construct( public string $penName ) { $this->writings = new FluidGraph\Relationship\ToMany( $this, Wrote::class, [ Book::class ] ); } }
Relationships have a subject (the Node from which they originate, $this
when defined), a kind (the class of their Edge Entities), and a list of concerns (the classes of their related Node Entities).
In order to add this relationship, we need to define our Edge Entity Wrote
. Edges can have their own properties, but in this case we'll keep it simple:
<?php use FluidGraph\Edge; class Wrote extends FluidGraph\Edge {}
We can add our corresponding Book
node, as well:
class Book extends FluidGraph\Node { public function __construct( public string $name, public int $pages ) { } }
With these options in place, we can now define books on our Author
:
$book = new Book(name: 'The Cave of Blunder', pages: 13527); $author->writings->set($book);
If our Edge had properties, we could define those properties when we set the $book
:
$author->writings->set($book, [ 'dateStarted' => new DateTime('September 17th, 1537'), 'dateFinished' => new DateTime('June 1st, 1804') ]);
Similar to using as()
, when we set an Entity on a given relationship, any arguments required to __construct()
the edge would need to be passed. You can update the existing edge using the same method.
To get Nodes out of a relationship you use the corresponding get()
method. For example, to get every Book
written by an Author
:
foreach($author->writings->get(Book::class) as $book) { // Do things with Books }
When you unset()
on a relationship the corresponding Edge is Released (and if the relationship is an owning relationship, related Nodes can be Released automatically):
$author->writings->unset($book);
If the Relationship is a ToOne
the Entity argument is excluded.
NOTE: It's possible to have multiple Edges to/from the same nodes. While not yet supported, there would be additional methods for releasing individual edges. Which leads us to our next subject...
Getting Edges
Because Edges can have their own properties and/or you may need to remove a specific Edge from a relationship without destroying all relationships between two Nodes you occasionally may need to be able to obtain the edges themselves. In our running example these would be the Wrote
object(s).
If you need to get all Edge Entities from a ToMany
or FromMany
relationships you can use the all()
method:
foreach($author->writings->all() as $wrote) { // Do things with $wrote }
NOTE: the return value of
all()
is aFluidGraph\Result
objects, which has additional filtering and other abilities, but generally speaking operates like an array by extendingArrayObject
.
To get the Edge Entity from a ToOne
or FromOne
relationship you can use the any()
method which will return either the Edge Entity or NULL
if there is no relationship.
if ($edge = $entity->relationship->any()) { // Do things with the $edge }
In order to discover the Edges associated only with specific Nodes, Node Types, and Labels, you can use the of()
and for()
method. Both take multiple arguments of either Node Entities, Node Elements, or strings and collected the Edges that correspond to Nodes like()
the argument. The only distinctions are as follows:
- The
of()
method only returns Edges whose Node corresponds to all arguments. - The
for()
method returns Edges whose Node corresponds to any arguments.
Accordingly, for a single argument, these methods are effectively equivalent.
Finding Edges for a specific $person
:
foreach($person->friendships->of($person) as $friends_with) { // Working with an Edge to a specific friend }
Finding Edges to all friends who are of type Author
:
foreach($person->friendships->of(Author::class) as $friends_with) { // Working with an Edge to a friend who's like() an Author }
Note: No validation is done against the relationships concerns, because even though it will allow setting Nodes of the supported types, different Nodes Entities can share a common Node Element.
Finding Edges to all friends who are of type Author
and labeled as Archived
using of()
:
foreach($person->friendships->of(Author::class, Archivable::ARCHIVED) as $friends_with) { // Working with an edge to friend who's like() an Author AND like() ARCHIVED }
Using the same argument with for()
would result in finding Edges to all friends who are of type Author
or labeled as Archived
:
foreach($person->friendships->for(Author::class, Archivable::ARCHIVED) as $friends_with) { // Working with an edge to friend who's like() an Author OR like() ARCHIVED }
Generally speaking, of()
is likely what you want most of the time.