Install via composer:
composer require laudis/neo4j-php-client
The HTTP protocol requires psr-7, psr-17 and psr-18 implementations. If there are not any available, composer can install them.
composer require nyholm/psr7 nyholm/psr7-server kriswallsmith/buzz
$client = Laudis\Neo4j\ClientBuilder::create()
->addHttpConnection('backup', 'http://neo4j:password@localhost')
->addBoltConnection('default', 'bolt://neo4j:password@localhost')
->setDefaultConnection('default')
->build();
The default connection is the first registered connection. setDefaultConnection
overrides this behaviour.
Sending a query is done by sending the cypher with optional parameters and a connection alias.
$client->run(
'MERGE (user {email: $email})', //The query is a required parameter
['email' => 'abc@hotmail.com'], //Parameters can be optionally added
'backup' //The default connection can be overridden
);
Or by using a statement object.
use Laudis\Neo4j\Databags\Statement;
$statement = new Statement('MERGE (user {email: $email})', ['email' => 'abc@hotmail.com']);
$client->runStatement($statement, 'default');
A result is a simple vector, with hashmaps representing a record.
foreach ($client->run('UNWIND range(1, 9) as x RETURN x') as $item) {
echo $item->get('x');
}
will echo 123456789
.
The Map representing the Record can only contain null, scalar or array values. Each array can then only contain null, scalar or array values, ad infinitum.
An example project exists on the neo4j github. It uses Slim and neo4j-php-client to build an api for the classic movies example of neo4j.
The runStatements
method will run all the statements at once. This method is an essential tool to reduce the number of database calls.
use Laudis\Neo4j\Databags\Statement;
$results = $client->runStatements([
Statement::create('MATCH (x) RETURN x LIMIT 100'),
Statement::create('MERGE (x:Person {email: $email})', ['email' => 'abc@hotmail.com'])
]);
The returned value is a vector containing result vectors.
$results->first(); //Contains the first result vector
$results->get(0); //Contains the first result vector
$result->get(1); //Contains the second result vector
The openTransaction
method will start a transaction over the relevant connection.
use Laudis\Neo4j\Databags\Statement;
$tsx = $client->openTransaction(
// This is an optional set of statements to execute while opening the transaction
[Statement::create('MERGE (x:Person({email: $email})', ['email' => 'abc@hotmail.com'])],
'backup' // This is the optional connection alias
);
Note that openTransaction
only returns the transaction object, not the results of the provided statements.
The transaction can run statements just like the client object as long as it is still open.
$result = $tsx->run('MATCH (x) RETURN x LIMIT 100');
$result = $tsx->runStatement(Statement::create('MATCH (x) RETURN x LIMIT 100'));
$results = $tsx->runStatements([Statement::create('MATCH (x) RETURN x LIMIT 100')]);
They can be committed or rolled back at will:
$tsx->rollback();
$tsx->commit([Statement::create('MATCH (x) RETURN x LIMIT 100')]);
Cypher has lists and maps. This notion can be problematic as the standard php arrays encapsulate both. When you provide an empty array as a parameter, it will be impossible to determine if it is an empty list or map.
The ParameterHelper
class is the ideal companion for this:
use Laudis\Neo4j\ParameterHelper;
$client->run('MATCH (x) WHERE x.slug in $listOrMap RETURN x', ['listOrMap' => ParameterHelper::asList([])]); // will return an empty set
$client->run('MATCH (x) WHERE x.slug in $listOrMap RETURN x', ['listOrMap' => ParameterHelper::asMap([])]); // will error
$client->run('MATCH (x) WHERE x.slug in $listOrMap RETURN x', ['listOrMap' => []]); // will return an empty set
This helper can also be used to make intent explicit.
addHttpConnection
and addBoltConnection
each accept their respective injections.
use GuzzleHttp\Client;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Laudis\Neo4j\Network\Bolt\BoltInjections;
use Laudis\Neo4j\Network\Http\HttpInjections;
$client = Laudis\Neo4j\ClientBuilder::create()
->addHttpConnection('backup', 'http://neo4j:password@localhost', HttpInjections::create()->withClient(static function () {
$handler = HandlerStack::create(new CurlHandler());
$handler->push(Middleware::cookies());
return new Client(['handler' => $handler]);
}))
->addBoltConnection('default', 'neo4j:password@localhost', BoltInjections::create()->withDatabase('tags'))
->build();
Wrapping the injections in a callable will enforce lazy initialization.
Cluster support is always available when directly connecting to the responsible servers. If you do not need to fine tune these connections, autorouting is available for all connections:
use Laudis\Neo4j\Network\Bolt\BoltInjections;
use Laudis\Neo4j\Network\Http\HttpInjections;
$client = Laudis\Neo4j\ClientBuilder::create()
->addBoltConnection(
'main',
'bolt://neo4j:password@any-server-with-routing-information',
BoltInjections::create()->withAutoRouting(true)
)
->addHttpConnection(
'http',
'http://neo4j:password@any-server-with-routing-information',
HttpInjections::create()->withAutoRouting(true)
)
->addBoltConnection('backup', 'bolt://neo4j:password@non-replicated-server')
->setDefaultConnection('main')
->build();
$client->run('RETURN 1'); // will run over a random follower found in the routing table
$client->run('MERGE (x:Y {z: 0)'); //will run over a random leader found in the routing table
$client->run('RETURN 1', [], 'backup'); //will run over the non replicated server
$client->run('RETURN 1', [], 'main'); //will be autorouted
$client->run('RETURN 1', [], 'http'); //will be autorouted
This client tries to strike a balance between extensibility, performance and clean code. All elementary classes have an interface. These provide infinite options to extend or change the implementation.
This library does not use any custom result classes but uses php-ds instead. These data structures are competent, flexible and fast. It furthermore provides a consistent interface and works seamlessly with other iterables.
Flexibility is maintained where possible by making all parameters iterables if they are a container of sorts. This means you can pass parameters as an array, \Ds\Map or any other object which implements the \Iterator or \IteratorAggregate. These examples are all valid:
use Ds\Map;
// Vanilla flavour
$client->run('MATCH (x {slug: $slug})', ['slug' => 'a']);
// php-ds implementation
$client->run('MATCH (x {slug: $slug})', new Map(['slug' => 'a']));
// laravel style
$client->run('MATCH (x {slug: $slug})', collect(['slug' => 'a']));
Version | Tested |
---|---|
3.0 + | Yes |
4.0 + | Yes |
Feature | Supported? |
---|---|
Auth | Yes |
Transactions | Yes |
Http Protocol | Yes |
Bolt Protocol | Yes |
Cluster | Yes |
Graph Representation | Roadmap |
- PHP >= 7.4
- A Neo4j database (minimum version 2.3)
- ext-bcmath *
- ext-sockets *
- ext-json **
- ext-ds ***
(*) Needed to implement the bolt protocol
(**) Needed to implement the http protocol
(***) Needed for optimal performance
Version 2.0 will have graph representation support. The interface for this is not yet set in stone, but will be somthing akin to this:
$client = $clientBuilder->withGraph($client);
$result = $client->run('MATCH (x:Article) - [:ContainsReferenceTo] -> (y:Article)');
$node = $result->getGraph()->enter(HasLabel::create('Article')->and(HasAttribute::create('slug', 'neo4j-is-awesome')))->first();
foreach ($node->relationships() as $relationship) {
if (!$relationship->endNode()->relationships()->isEmpty()) {
echo 'multi level path detected' . "\n";
}
}
Neo4j has the option to return statement statistics as well. These will be supported in version 2.0 with an api like this:
// This will create a client which will wrap the results and statistics in a single object.
$client = $clientBuilder->withStatistics($client);
$result = $client->run('MERGE (x:Node {id: $id}) RETURN x.id as id', ['id' => Uuid::v4()]);
echo $result->getStatistics()->getNodesCreated() . "\n"; // will echo 1 or 0.
echo $result->getResults()->first()->get('id') . "\n"; // will echo the id generated by the Uuid::v4() method
Statistics aggregate like this:
$results = $client->runStatements([
Statement::create('MERGE (x:Node {id: $id}) RETURN x', ['id' => Uuid::v4()]),
Statement::create('MERGE (p:Person {email: $email})', ['email' => 'abc@hotmail.com'])
]);
$total = Statistics::aggregate($results);
echo $total->getNodesCreated(); // will echo 0, 1 or 2.
Statistics and graph representation are ways of decorating a result. They can be chained like this:
$client = $clientBuilder->withGraph($client);
$client = $clientBuilder->withStatistics($client);
$result = $client->run('MATCH (x:Article) RETURN x.slug as slug LIMIT 1');
$statistics = $result->getStatistics();
$graph = $result->getResults()->getGraph();
$records = $result->getResults()->getResults();
This way maximum flexibility is guaranteed. Type safety will be enforced by using psalm templates.