|
| 1 | +# Laudis Neo4j PHP Client |
| 2 | + |
| 3 | +## Installation and basic usage |
| 4 | + |
| 5 | +### installing |
| 6 | + |
| 7 | +Install via composer: |
| 8 | + |
| 9 | +```bash |
| 10 | +composer require laudis/neo4j-php-client |
| 11 | +``` |
| 12 | + |
| 13 | +If you want to use the http protocol but haven't installed any of the psr7, psr17 and psr18 implementations yet: |
| 14 | + |
| 15 | +```bash |
| 16 | +composer require guzzlehttp/guzzle guzzlehttp/psr7 http-interop/http-factory-guzzle |
| 17 | +``` |
| 18 | + |
| 19 | +### Initializing client |
| 20 | + |
| 21 | +```php |
| 22 | +$client = Laudis\Neo4j\ClientBuilder::create() |
| 23 | + ->addHttpConnection('backup', 'http://neo4j:password@localhost') |
| 24 | + ->addBoltConnection('default', 'neo4j:password@localhost') |
| 25 | + ->setDefaultConnection('default') |
| 26 | + ->build(); |
| 27 | +``` |
| 28 | + |
| 29 | +The default connection is the first registered connection, unless it is overridden with the `setDefaultConnection` method. |
| 30 | + |
| 31 | +### Sending a Cypher Query |
| 32 | + |
| 33 | +Sending a query is done by sending the cypher with optional parameters and a connection alias |
| 34 | + |
| 35 | +```php |
| 36 | +$client->run( |
| 37 | + 'MERGE (user {email: $email})', //The query is a required parameter |
| 38 | + ['email' => ' [email protected]'], //Parameters can be optionally added |
| 39 | + 'backup' //The default connection can be overridden |
| 40 | +); |
| 41 | +``` |
| 42 | + |
| 43 | +Or by using a statement object |
| 44 | + |
| 45 | +```php |
| 46 | +use Laudis\Neo4j\Databags\Statement; |
| 47 | +$statement = new Statement('MERGE (user {email: $email})', ['email' => ' [email protected]']); |
| 48 | +$client->runStatement($statement, 'default'); |
| 49 | +``` |
| 50 | + |
| 51 | +### Reading a Result |
| 52 | + |
| 53 | +A result is a simple vector, with hashmaps representing a record. |
| 54 | + |
| 55 | +```php |
| 56 | +foreach ($client->run('UNWIND range(1, 9) as x RETURN x') as $item) { |
| 57 | + echo $item->get('x'); |
| 58 | +} |
| 59 | +``` |
| 60 | +will echo `123456789` |
| 61 | + |
| 62 | +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. |
| 63 | + |
| 64 | +## Diving Deeper |
| 65 | + |
| 66 | +### Running multiple queries at once |
| 67 | + |
| 68 | +You can run multiple statements at once by simple wrapping a bunch of statements in an iterable object and passing it to the `runStatements` method. |
| 69 | + |
| 70 | +```php |
| 71 | +use Laudis\Neo4j\Databags\Statement; |
| 72 | + |
| 73 | +$results = $client->runStatements([ |
| 74 | + Statement::create('MATCH (x) RETURN x LIMIT 100'), |
| 75 | + Statement::create('MERGE (x:Person {email: $email})', ['email' => ' [email protected]']) |
| 76 | +]); |
| 77 | +``` |
| 78 | + |
| 79 | +The results of each query will be wrapped in another vector: |
| 80 | + |
| 81 | +```php |
| 82 | +$results->first(); //Contain the first result vector |
| 83 | +$results->get(0); //Contain the first result vector |
| 84 | +$result->get(1); //Contains the second result vector |
| 85 | +``` |
| 86 | + |
| 87 | +### Opening a transaction |
| 88 | + |
| 89 | +Transactions can be started by opening one with the client. |
| 90 | +```php |
| 91 | +use Laudis\Neo4j\Databags\Statement; |
| 92 | + |
| 93 | +$tsx = $client->openTransaction( |
| 94 | + // This is an optional set of statements to execute while opening the transaction |
| 95 | + [Statement::create('MERGE (x:Person({email: $email})', ['email' => ' [email protected]'])], |
| 96 | + 'backup' // This is the optional connection alias |
| 97 | +); |
| 98 | +``` |
| 99 | + |
| 100 | +**Note that the optional set of statements will not have their result returned to you, as the transaction will be returned instead** |
| 101 | + |
| 102 | +You can then further execute statements on the transaction. |
| 103 | + |
| 104 | +```php |
| 105 | +$result = $tsx->runStatement('MATCH (x) RETURN x LIMIT 100'); |
| 106 | +$result = $tsx->runStatement(Statement::create('MATCH (x) RETURN x LIMIT 100')); |
| 107 | +$results = $tsx->runStatements([Statement::create('MATCH (x) RETURN x LIMIT 100')]); |
| 108 | +``` |
| 109 | + |
| 110 | +They can be committed or rolled back at will: |
| 111 | + |
| 112 | +```php |
| 113 | +$tsx->rollback(); |
| 114 | +``` |
| 115 | + |
| 116 | +```php |
| 117 | +$tsx->commit([Statement::create('MATCH (x) RETURN x LIMIT 100')]); |
| 118 | +``` |
| 119 | + |
| 120 | +*Note that the optional set of statements provided while comitting the transaction will not return the results. |
| 121 | + |
| 122 | +### Providing custom injections |
| 123 | + |
| 124 | +Each connection can be configured with custom injections. |
| 125 | + |
| 126 | +```php |
| 127 | +use GuzzleHttp\Client; |
| 128 | +use GuzzleHttp\Handler\CurlHandler; |
| 129 | +use GuzzleHttp\HandlerStack; |
| 130 | +use GuzzleHttp\Middleware; |
| 131 | +use Laudis\Neo4j\Network\Bolt\BoltInjections;use Laudis\Neo4j\Network\Http\HttpInjections; |
| 132 | + |
| 133 | +$client = Laudis\Neo4j\ClientBuilder::create() |
| 134 | + ->addHttpConnection('backup', 'http://neo4j:password@localhost', HttpInjections::create()->withClient(static function () { |
| 135 | + $handler = HandlerStack::create(new CurlHandler()); |
| 136 | + $handler->push(Middleware::cookies()); |
| 137 | + return new Client(['handler' => $handler]); |
| 138 | + })) |
| 139 | + ->addBoltConnection('default', 'neo4j:password@localhost', BoltInjections::create()->withDatabase('tags')) |
| 140 | + ->build(); |
| 141 | +``` |
| 142 | + |
| 143 | +Custom injections can be wrapped around a callable for lazy initialization as can be found in the example above. |
| 144 | + |
| 145 | +## Final Remarks |
| 146 | + |
| 147 | +### Filosophy |
| 148 | + |
| 149 | +This client tries to strike a balance between extensibility, performance and clean code. All classes are marked final but where there is an interface, injections and decorators can be used. |
| 150 | + |
| 151 | +I also chose not to implement custom resultsets but use the php-ds extension or polyfill instead. This is because these datastructures are a lot more capable than I will ever be able to make them. Php ds has a consistent interface, works nicely with psalm, has all features you can really want from a simple container and is incredibly fast. |
| 152 | + |
| 153 | +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 are all valid: |
| 154 | + |
| 155 | +```php |
| 156 | +// Vanilla flavour |
| 157 | +$client->run('MATCH (x {slug: $slug})', ['slug' => 'a']); |
| 158 | +// php-ds implementation |
| 159 | +$client->run('MATCH (x {slug: $slug})', new \Ds\Map(['slug' => 'a'])); |
| 160 | +// laravel style |
| 161 | +$client->run('MATCH (x {slug: $slug})', collect(['slug' => 'a'])); |
| 162 | +``` |
| 163 | + |
| 164 | +### Neo4j Version Support |
| 165 | + |
| 166 | +| **Version** | **Tested** | |
| 167 | +|-------------|-------------| |
| 168 | +| 2.3 | Yes | |
| 169 | +| 3.0 + | Yes | |
| 170 | +| 4.0 + | Yes | |
| 171 | + |
| 172 | +### Neo4j Feature Support |
| 173 | + |
| 174 | +| **Feature** | **Supported?** | |
| 175 | +|----------------------|----------------| |
| 176 | +| Auth | Yes | |
| 177 | +| Transactions | Yes | |
| 178 | +| Http Protocol | Yes | |
| 179 | +| Bolt Protocol | Yes | |
| 180 | +| Cluster | Roadmap | |
| 181 | +| Graph Representation | Roadmap | |
| 182 | + |
| 183 | +## Requirements |
| 184 | + |
| 185 | +* PHP >= 7.4 |
| 186 | +* A Neo4j database (minimum version 2.3) |
| 187 | +* ext-bcmath * |
| 188 | +* ext-sockets * |
| 189 | +* ext-json ** |
| 190 | +* ext-ds *** |
| 191 | + |
| 192 | +(*) Needed to implement the bolt protocol |
| 193 | + |
| 194 | +(**) Needed to implement the http protocol |
| 195 | + |
| 196 | +(***) Needed for optimal performance |
| 197 | + |
| 198 | +## Roadmap |
| 199 | + |
| 200 | +### Cluster support |
| 201 | + |
| 202 | +Version 2.0 will have cluster support. The interface for this is not yet set in stone. |
| 203 | + |
| 204 | +### Support for graph representation instead of simple records. |
| 205 | + |
| 206 | +Version 2.0 will have graph representation suppport. The inteface for this is not yet set in stone, but will be somthing akin to this: |
| 207 | + |
| 208 | +```php |
| 209 | +$graph = $client->graphOf('MATCH (x:Article) - [:ContainsReferenceTo] -> (y:Article)'); |
| 210 | + |
| 211 | +$node = $graph->enter(HasLabel::create('Article')->and(HasAttribute::create('slug', 'neo4j-is-awesome'))); |
| 212 | + |
| 213 | +foreach ($node->relationships() as $relationship) { |
| 214 | + if (!$relationship->endNode()->relationships()->isEmpty()) { |
| 215 | + echo 'multi level reference detected' . "\n"; |
| 216 | + } |
| 217 | +} |
| 218 | +``` |
0 commit comments