Skip to content

Commit

Permalink
Merge pull request #134 from neo4j-php/beta_v7
Browse files Browse the repository at this point in the history
v7
  • Loading branch information
stefanak-michal authored Dec 18, 2023
2 parents a256e7f + 04ddeaa commit 04fc9c8
Show file tree
Hide file tree
Showing 99 changed files with 1,250 additions and 1,336 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ cert/
*.lock
*.cache
phpunit.dev.xml
temp/
tmp/
147 changes: 73 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

# Bolt

PHP library for communication with graph database over TCP socket with Bolt protocol specification. Bolt protocol was
created by [Neo4j](https://neo4j.com/) and documentation is available
at [https://www.neo4j.com/](https://www.neo4j.com/docs/bolt/current/). This library is aimed to be low level, support
PHP library for communication with graph database over TCP socket with Bolt protocol specification. Bolt protocol was created by [Neo4j](https://neo4j.com/) and documentation is available at [https://www.neo4j.com/](https://www.neo4j.com/docs/bolt/current/). This library is aimed to be low level, support
all available versions and keep up with protocol messages architecture and specifications.

[![](https://img.shields.io/github/stars/stefanak-michal/Bolt)](https://github.com/neo4j-php/Bolt/stargazers)
Expand All @@ -16,28 +14,24 @@ all available versions and keep up with protocol messages architecture and speci

<a href='https://jb.gg/OpenSourceSupport' target='_blank'><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo." width="100" /></a>

## Version support
## :label: Version support

We are trying to keep up and this library supports **Bolt <= 5.4**.

https://www.neo4j.com/docs/bolt/current/bolt-compatibility/

## Requirements
## :white_check_mark: Requirements

Keep up with [PHP supported versions](https://www.php.net/supported-versions.php) means we are at **PHP^8**.
This library keep up with [PHP supported versions](https://www.php.net/supported-versions.php) what means it is at **PHP^8.1**.

_If you need support for PHP < 7.4 you can use latest v3.x release and if you need support for PHP 7.4 you can use v5.x.
Not all new features are implement backwards and this readme is updated to latest released version._

### Extensions
### PHP Extensions

- [mbstring](https://www.php.net/manual/en/book.mbstring.php)
- [sockets](https://www.php.net/manual/en/book.sockets.php) (optional) - Required when you use Socket connection class
- [openssl](https://www.php.net/manual/en/book.openssl.php) (optional) - Required when you use StreamSocket connection
class with enabled SSL
- [phpunit](https://phpunit.de/) (development)

## Installation
## :floppy_disk: Installation

You can use composer or download this repository from github and manually implement it.

Expand All @@ -55,47 +49,14 @@ Run the following command in your project to install the latest applicable versi
2. Unpack
3. Copy content of `src` directory into your project

## Usage
## :desktop_computer: Usage

Concept of usage is based on Bolt messages. Available protocol methods depends on Bolt version. Communication works
in [pipeline](https://www.neo4j.com/docs/bolt/current/bolt/message/#pipelining) and you can chain multiple Bolt messages
before fetching response from the server.
Concept of usage is based on Bolt messages. Bolt messages are mapped 1:1 as protocol methods. Available protocol methods depends on Bolt version. Communication works in [pipeline](https://www.neo4j.com/docs/bolt/current/bolt/message/#pipelining) and you can chain multiple Bolt messages before consuming response from the server.

Main `Bolt` class serves as Factory design pattern and it returns instance of protocol class by requested Bolt version. Query execution and fetching response is split in two methods. First message `run` is for
sending queries. Second message `pull` is for fetching response from last executed query on database.
Response from database for Bolt message `pull` always contains n+1 rows because last entry is `success` message with
Main `Bolt` class serves as Factory design pattern and it returns instance of protocol class by requested Bolt version. Basic usage consist of query execution and fetching response which is split in two methods. First message `run` is for sending queries. Second message `pull` is for fetching response from executed query on database. Response from database for Bolt message `pull` always contains n+1 rows because last entry is `success` message with
meta informations.

More info about available Bolt messages: https://www.neo4j.com/docs/bolt/current/bolt/message/

### Example

```php
// Create connection class and specify target host and port.
$conn = new \Bolt\connection\Socket('127.0.0.1', 7687);
// Create new Bolt instance and provide connection object.
$bolt = new \Bolt\Bolt($conn);
// Set requested protocol versions
$bolt->setProtocolVersions(5.1, 5, 4.4);
// Build and get protocol version instance which creates connection and executes handshake.
$protocol = $bolt->build();
// Connect and login into database
$protocol->hello();
$protocol->logon(['scheme' => 'basic', 'principal' => 'neo4j', 'credentials' => 'neo4j']);

// Pipeline two messages. One to execute query with parameters and second to pull records.
$protocol
->run('RETURN $a AS num, $b AS str', ['a' => 123, 'b' => 'text'])
->pull();

// Fetch waiting server responses for pipelined messages.
foreach ($protocol->getResponses() as $response) {
// $response is instance of \Bolt\protocol\Response.
// First response is SUCCESS message for RUN message.
// Second response is RECORD message for PULL message.
// Third response is SUCCESS message for PULL message.
}
```
:information_source: More info about available Bolt messages: https://www.neo4j.com/docs/bolt/current/bolt/message/

### Available methods

Expand Down Expand Up @@ -132,12 +93,12 @@ foreach ($protocol->getResponses() as $response) {
| pullAll | @see pull | |
| discardAll | @see discard | |

Many methods accept argument called `$extra`. This argument can contain any of key-value by Bolt specification. This
Multiple methods accept argument called `$extra`. This argument can contain any of key-value by Bolt specification. This
argument was extended during Neo4j development which means the content of it changed. You should keep in mind what
version you are working with when using this argument. You can read more about extra parameter in Bolt documentation
where you can look into your version and bolt message.

Annotation of methods in protocol classes contains direct link to specific version and message from mentioned
:information_source: Annotation of methods in protocol classes contains direct link to specific version and message from mentioned
documentation website.

### Authentification
Expand Down Expand Up @@ -179,51 +140,77 @@ of data. To learn more you can
check [performance test](https://github.com/neo4j-php/Bolt/blob/master/tests/PerformanceTest.php)
or [packer test](https://github.com/neo4j-php/Bolt/blob/master/tests/PackStream/v1/PackerTest.php).

Structures `Node`, `Relationship`, `UnboundRelationship` and `Path` cannot be used as parameter. They are available only
:warning: Structures `Node`, `Relationship`, `UnboundRelationship` and `Path` cannot be used as parameter. They are available only
as received data from database.

Server state is not available from server but we assume it. Library contains `\Bolt\helpers\ServerState` and you can get
used instance of this class with `$bolt->serverState` or `$protocol->serverState` (after you call `build()`).
### Example

### Autoload
```php
// Choose and create connection class and specify target host and port.
$conn = new \Bolt\connection\Socket('127.0.0.1', 7687);
// Create new Bolt instance and provide connection object.
$bolt = new \Bolt\Bolt($conn);
// Set requested protocol versions ..you can add up to 4 versions
$bolt->setProtocolVersions(5.4);
// Build and get protocol version instance which creates connection and executes handshake.
$protocol = $bolt->build();

Directory `src` contains autoload file which accepts only Bolt library namespaces. Main Bolt namespace points to this
directory. If you have installed this project with composer, you have to load `vendor/autoload.php`.
// Initialize communication with database
$response = $protocol->hello()->getResponse();
// verify $response for successful initialization

## Server state
// Login into database
$response = $protocol->logon(['scheme' => 'basic', 'principal' => 'neo4j', 'credentials' => 'neo4j'])->getResponse();
// verify $response for successful login

If assumed server state is different than expected, library does not throw exception. This logic is silent but you can
change it and if you would like to implement own logic when assumed server state is different than expected you can
assign callable into class property `$serverState->expectedServerStateMismatchCallback`.
// Pipeline two messages. One to execute query with parameters and second to pull records.
$protocol
->run('RETURN $a AS num, $b AS str', ['a' => 123, 'b' => 'text'])
->pull();

// Fetch waiting server responses for pipelined messages.
foreach ($protocol->getResponses() as $response) {
// $response is instance of \Bolt\protocol\Response.
// First response is SUCCESS message for RUN message.
// Second response is RECORD message for PULL message.
// Third response is SUCCESS message for PULL message.
}
```

## Connection
### Autoload

Bolt class constructor accepts connection argument. This argument has to be instance of class which implements
IConnection interface. Currently exists two predefined classes `Socket` and `StreamSocket`.
Directory `src` contains autoload file which accepts only Bolt library namespaces. Main Bolt namespace points to this
directory. If you have installed this project with composer, you have to load `vendor/autoload.php`.

## :chains: Connection

_We provide two connection classes. `Socket` was created first and it has better memory usage. `StreamSocket` was made
because of need to accept TLS._
Bolt class constructor accepts connection argument. This argument has to be instance of class which implements IConnection interface. Library offers few options.

**\Bolt\connection\Socket**

This class use php extension sockets. More informations
This class use php extension sockets and has better memory usage. More informations
here: [https://www.php.net/manual/en/book.sockets.php](https://www.php.net/manual/en/book.sockets.php)

**\Bolt\connection\StreamSocket**

This class uses php stream functions. Which is a part of php and there is no extensions needed. More informations
here: [https://www.php.net/manual/en/ref.stream.php](https://www.php.net/manual/en/ref.stream.php)

StreamSocket besides of implemented methods from interface has method to configure SSL. When you want to activate SSL
StreamSocket besides of implemented methods from interface has method to configure SSL. SSL option requires php extension openssl. When you want to activate SSL
you have to call method `setSslContextOptions`. This method accept array by php specification available
here: [https://www.php.net/manual/en/context.ssl.php](https://www.php.net/manual/en/context.ssl.php).

_If you want to use it, you have to enable openssl php extension._
**\Bolt\connection\PStreamSocket**

This class extends StreamSocket and adds support for persistent connections. Upon reuse of connection remaining buffer is consumed and message RESET is automatically sent. PHP is stateless therefore using this connection class requires storing meta information about active TCP connection. Default storage is `\Bolt\helper\FileCache` which you can change with method `setCache` (PSR-16 Simple Cache).

:warning: If your system reuse persistent connection and meta information about it was lost for some reason, your attemt to connect will end with ConnectionTimeoutException. Repeated attempt to connect will succeed.

## :lock: SSL

### Neo4j Aura

Connecting to Aura requires encryption which is provided with SSL. To connect to Aura you have to use `StreamSocket`
connection class and enable SSL.
Connecting to Aura requires encryption which is provided with SSL. To connect to Aura you have to use `StreamSocket` connection class and enable SSL.

```php
$conn = new \Bolt\connection\StreamSocket('helloworld.databases.neo4j.io');
Expand Down Expand Up @@ -251,18 +238,22 @@ $conn->setSslContextOptions([
$bolt = new \Bolt\Bolt($conn);
```

You can also take a look at my article on how to implement SSL for Neo4j running on localhost
:bookmark: You can also take a look at my article on how to implement SSL for Neo4j running on localhost
at [Neo4j and self signed certificate](https://ko-fi.com/post/Neo4j-and-self-signed-certificate-on-Windows-S6S2I0KQT).

### Timeout
## :stopwatch: Timeout

Connection class constructor contains `$timeout` argument. This timeout is for established socket connection. To set up
timeout for establishing socket connection itself you have to set ini directive `default_socket_timeout`.

_Setting up ini directive isn't part of connection class because function `ini_set` can be disabled on production
environments for security reasons._

## Another solutions
## :vertical_traffic_light: Server state

Server state is not reported by server but it is evaluated by received response. You can access current state through property `$protocol->serverState`. This property is updated with every call `getResponse(s)`.

## :pushpin: More solutions

If you need simple class to cover basic functionality you can
use: [neo4j-bolt-wrapper](https://packagist.org/packages/stefanak-michal/neo4j-bolt-wrapper)
Expand All @@ -273,3 +264,11 @@ on: [php-client](https://packagist.org/packages/laudis/neo4j-php-client)
PDO implementation is available at [pdo-bolt](https://github.com/stefanak-michal/pdo-bolt)

More informations can be found at: https://neo4j.com/developer/php/

## :recycle: Old versions

If you need support for end-of-life PHP versions, here is a short info list. Not all new features are implement backwards and this readme is updated to latest released version.

* PHP < 7.4 - v3.x
* PHP 7.4 - v5.x
* PHP 8.0 - v6.x
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
"license": "MIT",
"minimum-stability": "stable",
"require": {
"php": "^8",
"ext-mbstring": "*"
"php": "^8.1",
"ext-mbstring": "*",
"psr/simple-cache": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^9"
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
</testsuites>
<php>
<var name="NEO_USER" value="neo4j"/>
<var name="NEO_PASS" value="nothing"/>
<var name="NEO_PASS" value="nothing123"/>
</php>
</phpunit>
74 changes: 55 additions & 19 deletions src/Bolt.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

use Bolt\error\ConnectException;
use Bolt\error\BoltException;
use Bolt\protocol\{AProtocol, ServerState};
use Bolt\protocol\AProtocol;
use Bolt\enum\{Signature, ServerState};
use Bolt\connection\IConnection;

/**
Expand All @@ -22,11 +23,10 @@ final class Bolt
private int $packStreamVersion = 1;

public static bool $debug = false;
public ServerState $serverState;

public function __construct(private IConnection $connection)
{
$this->setProtocolVersions(5, 4.4);
$this->setProtocolVersions(5.4, 5, 4.4);
}

/**
Expand All @@ -35,27 +35,69 @@ public function __construct(private IConnection $connection)
*/
public function build(): AProtocol
{
$this->serverState = new ServerState();
$this->serverState->is(ServerState::DISCONNECTED, ServerState::DEFUNCT);
$protocol = null;

try {
if (!$this->connection->connect()) {
throw new ConnectException('Connection failed');
}

$version = $this->handshake();
if ($this->connection instanceof \Bolt\connection\PStreamSocket) {
$protocol = $this->persistentBuild();
}

$protocolClass = "\\Bolt\\protocol\\V" . str_replace('.', '_', $version);
if (!class_exists($protocolClass)) {
throw new ConnectException('Requested Protocol version (' . $version . ') not yet implemented');
if (empty($protocol)) {
$protocol = $this->normalBuild();
}
} catch (ConnectException $e) {
$this->serverState->set(ServerState::DEFUNCT);
} catch (BoltException $e) {
$this->connection->disconnect();
throw $e;
}

$this->serverState->set(ServerState::CONNECTED);
return new $protocolClass($this->packStreamVersion, $this->connection, $this->serverState);
if ($this->connection instanceof \Bolt\connection\PStreamSocket) {
$this->connection->getCache()->set($this->connection->getIdentifier(), $protocol->getVersion());
}

return $protocol;
}

private function normalBuild(): AProtocol
{
$version = $this->handshake();

$protocolClass = "\\Bolt\\protocol\\V" . str_replace('.', '_', $version);
if (!class_exists($protocolClass)) {
throw new ConnectException('Requested Protocol version (' . $version . ') not yet implemented');
}

$protocol = new $protocolClass($this->packStreamVersion, $this->connection);
$protocol->serverState = version_compare($version, '5.1', '>=') ? ServerState::NEGOTIATION : ServerState::CONNECTED;
return $protocol;
}

private function persistentBuild(): ?AProtocol
{
$version = $this->connection->getCache()->get($this->connection->getIdentifier());
if (empty($version)) {
return null;
}

$protocolClass = "\\Bolt\\protocol\\V" . str_replace('.', '_', $version);
if (!class_exists($protocolClass)) {
throw new ConnectException('Requested Protocol version (' . $version . ') not yet implemented');
}

/** @var AProtocol $protocol */
$protocol = new $protocolClass($this->packStreamVersion, $this->connection);
$protocol->serverState = ServerState::INTERRUPTED;

if ($protocol->reset()->getResponse()->signature != Signature::SUCCESS) {
$this->connection->disconnect();
$this->connection->connect();
return null;
}

return $protocol;
}

public function setProtocolVersions(int|float|string ...$v): Bolt
Expand All @@ -72,12 +114,6 @@ public function setPackStreamVersion(int $version = 1): Bolt
return $this;
}

public function setConnection(IConnection $connection): Bolt
{
$this->connection = $connection;
return $this;
}

/**
* @link https://www.neo4j.com/docs/bolt/current/bolt/handshake/
* @throws BoltException
Expand Down
Loading

0 comments on commit 04fc9c8

Please sign in to comment.