Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/neo4j.2025.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Tests with Neo4j^2025 on PHP^8

on:
pull_request:
branches: [ master ]

jobs:
db-tests-2025-2204:
runs-on: ubuntu-22.04
name: "Running Integration tests for PHP ${{ matrix.php-version }} on Neo4j ${{ matrix.neo4j-version }}"
strategy:
fail-fast: false
matrix:
neo4j-version: ['2025.07', '2025']
php-version: ['8.1', '8.2', '8.3', '8.4']

services:
neo4j:
image: neo4j:${{ matrix.neo4j-version }}
env:
NEO4J_AUTH: neo4j/nothing123
NEO4J_PLUGINS: '["apoc"]'
ports:
- 7687:7687
- 7474:7474
options: >-
--health-cmd "wget http://localhost:7474 || exit 1"

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, sockets
coverage: xdebug
ini-values: max_execution_time=0

- name: Install dependencies
run: composer install --no-progress

- name: Test with phpunit
env:
GDB_USERNAME: neo4j
GDB_PASSWORD: nothing123
run: vendor/bin/phpunit --configuration phpunit.xml --testsuite "Database"
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
neo4j-version: ['5.4', '5.6', '5.8', '5.12', '5.13', '5.23', '5.26', '2025']
neo4j-version: ['5.4', '5.6', '5.8', '5.12', '5.22', '5.25', '5.26']
php-version: ['8.1', '8.2', '8.3', '8.4']

services:
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Website: https://stefanak.serv00.net/

## :label: Version support

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

## :books: Supported ecosystems

Expand Down Expand Up @@ -198,7 +198,7 @@ foreach ($protocol->getResponses() as $response) {
}
```

:information_source: Default settings for bolt protocol version is 4.3, 4.4 and 5.0 to 5.8. If you are within this list you can ommit calling `$bolt->setProtocolVersions();`.
:information_source: Default settings for bolt protocol version is 4.3, 4.4, 5.0 to 5.8 and 6. If you are within this list you can ommit calling `$bolt->setProtocolVersions();`.

### Autoload

Expand Down
2 changes: 1 addition & 1 deletion src/Bolt.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function __construct(private IConnection $connection)
$this->track();
}

$this->setProtocolVersions('5.8.8', '4.4.4');
$this->setProtocolVersions(6, '5.8.8', '4.4.4');
}

private function track(): void
Expand Down
5 changes: 2 additions & 3 deletions src/packstream/v1/Packer.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ private function packString(string $str): iterable

private function packFloat(float $value): iterable
{
$packed = pack('d', $value);
yield chr(0xC1) . ($this->littleEndian ? strrev($packed) : $packed);
yield chr(0xC1) . pack('E', $value);
}

/**
Expand Down Expand Up @@ -233,7 +232,7 @@ private function packStructure(IStructure $structure): iterable
}
$packerMethod = 'pack' . ucfirst($packerMethod);

yield from [$this, $packerMethod]($structure->{$parameter->getName()});
yield from call_user_func([$this, $packerMethod], $structure->{$parameter->getName()});
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/packstream/v1/Unpacker.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ private function u(): mixed
if ($output !== null) {
return $output;
}
$output = $this->unpackStruct($marker);
$output = $this->unpackStructure($marker);
if ($output !== null) {
return $output;
}
Expand All @@ -120,7 +120,7 @@ private function u(): mixed
* @return array|IStructure|null
* @throws UnpackException
*/
private function unpackStruct(int $marker): array|IStructure|null
private function unpackStructure(int $marker): array|IStructure|null
{
if ($marker >> 4 == 0b1011) { //TINY_STRUCT
$size = 0b10110000 ^ $marker;
Expand Down Expand Up @@ -238,7 +238,7 @@ private function unpackFloat(int $marker): ?float
{
if ($marker == 0xC1) {
$value = $this->next(8);
return (float)unpack('d', $this->littleEndian ? strrev($value) : $value)[1];
return (float)unpack('E', $value)[1];
} else {
return null;
}
Expand Down
32 changes: 32 additions & 0 deletions src/protocol/V6.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Bolt\protocol;

/**
* Class Protocol version 6
*
* @author Michal Stefanak
* @link https://github.com/neo4j-php/Bolt
* @see https://www.neo4j.com/docs/bolt/current/bolt/message/
* @package Bolt\protocol
*/
class V6 extends AProtocol
{
use \Bolt\protocol\v6\AvailableStructures;
use \Bolt\protocol\v4\ServerStateTransition;

use \Bolt\protocol\v1\ResetMessage;

use \Bolt\protocol\v3\RunMessage;
use \Bolt\protocol\v3\BeginMessage;
use \Bolt\protocol\v3\CommitMessage;
use \Bolt\protocol\v3\RollbackMessage;
use \Bolt\protocol\v3\GoodbyeMessage;

use \Bolt\protocol\v4\PullMessage;
use \Bolt\protocol\v4\DiscardMessage;

use \Bolt\protocol\v4_1\HelloMessage;

use \Bolt\protocol\v4_4\RouteMessage;
}
63 changes: 63 additions & 0 deletions src/protocol/v6/AvailableStructures.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Bolt\protocol\v6;

use Bolt\protocol\v1\structures\{
Date,
Duration,
LocalDateTime,
LocalTime,
Path,
Point2D,
Point3D,
Time,
};
use Bolt\protocol\v5\structures\{
DateTime,
DateTimeZoneId,
Node,
Relationship,
UnboundRelationship
};
use Bolt\protocol\v6\structures\Vector;

/**
* Trait to set available structures
*
* @author Michal Stefanak
* @link https://github.com/neo4j-php/Bolt
* @see https://www.neo4j.com/docs/bolt/current/bolt/structure-semantics/
* @package Bolt\protocol
*/
trait AvailableStructures
{
protected array $packStructuresLt = [
0x44 => Date::class,
0x54 => Time::class,
0x74 => LocalTime::class,
0x49 => DateTime::class,
0x69 => DateTimeZoneId::class,
0x64 => LocalDateTime::class,
0x45 => Duration::class,
0x58 => Point2D::class,
0x59 => Point3D::class,
0x56 => Vector::class,
];

protected array $unpackStructuresLt = [
0x4E => Node::class,
0x52 => Relationship::class,
0x72 => UnboundRelationship::class,
0x50 => Path::class,
0x44 => Date::class,
0x54 => Time::class,
0x74 => LocalTime::class,
0x49 => DateTime::class,
0x69 => DateTimeZoneId::class,
0x64 => LocalDateTime::class,
0x45 => Duration::class,
0x58 => Point2D::class,
0x59 => Point3D::class,
0x56 => Vector::class,
];
}
143 changes: 143 additions & 0 deletions src/protocol/v6/structures/Vector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

namespace Bolt\protocol\v6\structures;

use Bolt\packstream\Bytes;
use Bolt\protocol\IStructure;

/**
* Class Vector
* Immutable
*
* @author Michal Stefanak
* @link https://github.com/neo4j-php/Bolt
* @link https://www.neo4j.com/docs/bolt/current/bolt/structure-semantics/#structure-vector
* @package Bolt\protocol\v6\structures
*/
class Vector implements IStructure
{
public function __construct(
public readonly Bytes $type_marker,
public readonly Bytes $data
) {
}

public function __toString(): string
{
return json_encode([(string)$this->type_marker, (string)$this->data]);
}

private static array $formats = ['s', 'l', 'q'];

/**
* Encode array as vector structure
* @param int[]|float[] $data
* @return self
* @throws \InvalidArgumentException
*/
public static function encode(array $data): self
{
if (count($data) === 0) {
throw new \InvalidArgumentException('Vector cannot be empty');
}
if (count($data) > 4096) {
throw new \InvalidArgumentException('Vector cannot have more than 4096 elements');
}

$allIntegers = array_reduce($data, fn($carry, $item) => $carry && is_int($item), true);
$allFloats = array_reduce($data, fn($carry, $item) => $carry && is_float($item), true);

// Check if all values are integer or float
if (!$allIntegers && !$allFloats) {
throw new \InvalidArgumentException('All values in the vector must be integer xor float');
}

$minValue = min($data);
$maxValue = max($data);
$marker = 0;
$packed = [];
$packFormat = '';

if ($allIntegers) {
if ($minValue >= -128 && $maxValue <= 127) { // INT_8
$marker = 0xC8;
$packFormat = 'c';
} elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16
$marker = 0xC9;
$packFormat = 's';
} elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32
$marker = 0xCA;
$packFormat = 'l';
} else { // INT_64
$marker = 0xCB;
$packFormat = 'q';
}
} elseif ($allFloats) {
if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32)
$marker = 0xC6;
$packFormat = 'G';
} else { // Double precision float (FLOAT_64)
$marker = 0xC1;
$packFormat = 'E';
}
}

if ($marker === 0) {
throw new \InvalidArgumentException('Unsupported data type for vector');
}

// Pack the data
$littleEndian = unpack('S', "\x01\x00")[1] === 1;
foreach ($data as $entry) {
$value = pack($packFormat, $entry);
$packed[] = in_array($packFormat, self::$formats) && $littleEndian ? strrev($value) : $value;
}

return new self(new Bytes([chr($marker)]), new Bytes($packed));
}

/**
* Decode vector structure .. returns binary $this->data as array
* @return int[]|float[]
* @throws \InvalidArgumentException
*/
public function decode(): array
{
switch (ord($this->type_marker[0])) {
case 0xC8: // INT_8
$size = 1;
$unpackFormat = 'c';
break;
case 0xC9: // INT_16
$size = 2;
$unpackFormat = 's';
break;
case 0xCA: // INT_32
$size = 4;
$unpackFormat = 'l';
break;
case 0xCB: // INT_64
$size = 8;
$unpackFormat = 'q';
break;
case 0xC6: // FLOAT_32
$size = 4;
$unpackFormat = 'G';
break;
case 0xC1: // FLOAT_64
$size = 8;
$unpackFormat = 'E';
break;
default:
throw new \InvalidArgumentException('Unknown vector type marker: ' . $this->type_marker[0]);
}

$output = [];
$littleEndian = unpack('S', "\x01\x00")[1] === 1;
foreach(mb_str_split((string)$this->data, $size, '8bit') as $value) {
$output[] = unpack($unpackFormat, in_array($unpackFormat, self::$formats) && $littleEndian ? strrev($value) : $value)[1];
}

return $output;
}
}
2 changes: 2 additions & 0 deletions tests/TestLayer.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ protected function getCompatibleBoltVersion(string $url = null): float|int

$neo4jVersion = $decoded['neo4j_version'];

if (version_compare($neo4jVersion, '2025.08', '>='))
return 6;
if (version_compare($neo4jVersion, '5.26', '>='))
return 5.8;
if (version_compare($neo4jVersion, '5.23', '>='))
Expand Down
Loading