Skip to content
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ foreach ($results as $result) {
}
```

`peek()` returns the next row without moving the cursor (or `null` if there is none). It can pull the next record from the server into the buffer if needed, but does not advance past it—unlike `next()` in a `foreach`, which always consumes. Repeated calls to `peek()` return the same value until you advance the iterator (for example with `next()` or by continuing a `foreach`).

Cypher values and types map to these php types and classes:

| Cypher | Php |
Expand Down
8 changes: 4 additions & 4 deletions src/Authentication/BasicAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
/**
* @throws Exception
*
* @return array{server: string, connection_id: string, hints: list}

Check failure on line 38 in src/Authentication/BasicAuth.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

InvalidReturnType

src/Authentication/BasicAuth.php:38:16: InvalidReturnType: The declared return type 'array{connection_id: string, hints: list<mixed>, server: string}' for Laudis\Neo4j\Authentication\BasicAuth::authenticateBolt is incorrect, got 'array{connection_id: string, hints: list<mixed>, patch_bolt?: list<string>, server: string}' which is different due to additional array shape fields (patch_bolt) (see https://psalm.dev/011)
*/
public function authenticateBolt(BoltConnection $connection, string $userAgent): array
{
Expand All @@ -55,21 +55,21 @@

$response = $factory->createLogonMessage($credentials)->send()->getResponse();

/** @var array{server: string, connection_id: string, hints: list} */
/** @var array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>} */
return array_merge($responseHello->content, $response->content);

Check failure on line 59 in src/Authentication/BasicAuth.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

InvalidReturnStatement

src/Authentication/BasicAuth.php:59:20: InvalidReturnStatement: The inferred type 'array{connection_id: string, hints: list<mixed>, patch_bolt?: list<string>, server: string}' does not match the declared return type 'array{connection_id: string, hints: list<mixed>, server: string}' for Laudis\Neo4j\Authentication\BasicAuth::authenticateBolt due to additional array shape fields (patch_bolt) (see https://psalm.dev/128)
}

$helloMetadata = [
$helloMetadata = BoltHelloMetadata::withUtcPatchIfSupported($connection, [
'user_agent' => $userAgent,
'scheme' => 'basic',
'principal' => $this->username,
'credentials' => $this->password,
];
]);

$response = $factory->createHelloMessage($helloMetadata)->send()->getResponse();

/** @var array{server: string, connection_id: string, hints: list} */
/** @var array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>} */
return $response->content;

Check failure on line 72 in src/Authentication/BasicAuth.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

InvalidReturnStatement

src/Authentication/BasicAuth.php:72:16: InvalidReturnStatement: The inferred type 'array{connection_id: string, hints: list<mixed>, patch_bolt?: list<string>, server: string}' does not match the declared return type 'array{connection_id: string, hints: list<mixed>, server: string}' for Laudis\Neo4j\Authentication\BasicAuth::authenticateBolt due to additional array shape fields (patch_bolt) (see https://psalm.dev/128)
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/Authentication/KerberosAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,17 @@ public function __construct(
/**
* @throws Exception
*
* @return array{server: string, connection_id: string, hints: list}
* @return array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>}
*/
public function authenticateBolt(BoltConnection $connection, string $userAgent): array
{
$factory = $this->createMessageFactory($connection);

$this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]);

$factory->createHelloMessage(['user_agent' => $userAgent])->send()->getResponse();
$helloMetadata = BoltHelloMetadata::withUtcPatchIfSupported($connection, ['user_agent' => $userAgent]);

$factory->createHelloMessage($helloMetadata)->send()->getResponse();

$this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'kerberos', 'principal' => '']);

Expand All @@ -56,7 +58,7 @@ public function authenticateBolt(BoltConnection $connection, string $userAgent):
])->send()->getResponse();

/**
* @var array{server: string, connection_id: string, hints: list}
* @var array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>}
*/
return $response->content;
}
Expand Down
12 changes: 6 additions & 6 deletions src/Authentication/NoAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,31 @@ public function __construct(
/**
* @throws Exception
*
* @return array{server: string, connection_id: string, hints: list}
* @return array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>}
*/
public function authenticateBolt(BoltConnection $connection, string $userAgent): array
{
$factory = $this->createMessageFactory($connection);

if ($connection->getProtocol()->compare(ConnectionProtocol::BOLT_V5_1()) >= 0) {
$helloMetadata = ['user_agent' => $userAgent];
$helloMetadata = BoltHelloMetadata::withUtcPatchIfSupported($connection, ['user_agent' => $userAgent]);

$factory->createHelloMessage($helloMetadata)->send()->getResponse();

$response = $factory->createLogonMessage(['scheme' => 'none'])->send()->getResponse();

/** @var array{server: string, connection_id: string, hints: list} */
/** @var array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>} */
return $response->content;
}

$helloMetadata = [
$helloMetadata = BoltHelloMetadata::withUtcPatchIfSupported($connection, [
'user_agent' => $userAgent,
'scheme' => 'none',
];
]);

$response = $factory->createHelloMessage($helloMetadata)->send()->getResponse();

/** @var array{server: string, connection_id: string, hints: list} */
/** @var array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>} */
return $response->content;
}

Expand Down
8 changes: 5 additions & 3 deletions src/Authentication/OpenIDConnectAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,17 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s
/**
* @throws Exception
*
* @return array{server: string, connection_id: string, hints: list}
* @return array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>}
*/
public function authenticateBolt(BoltConnection $connection, string $userAgent): array
{
$factory = $this->createMessageFactory($connection);

$this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]);

$factory->createHelloMessage(['user_agent' => $userAgent])->send()->getResponse();
$helloMetadata = BoltHelloMetadata::withUtcPatchIfSupported($connection, ['user_agent' => $userAgent]);

$factory->createHelloMessage($helloMetadata)->send()->getResponse();

$this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'bearer']);

Expand All @@ -61,7 +63,7 @@ public function authenticateBolt(BoltConnection $connection, string $userAgent):
])->send()->getResponse();

/**
* @var array{server: string, connection_id: string, hints: list}
* @var array{server: string, connection_id: string, hints: list, patch_bolt?: list<string>}
*/
return $response->content;
}
Expand Down
24 changes: 20 additions & 4 deletions src/Bolt/BoltConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Bolt\error\BoltException;
use Bolt\error\ConnectException as BoltConnectException;
use Bolt\protocol\Response;
use Bolt\protocol\V4_2;
use Bolt\protocol\V4_3;
use Bolt\protocol\V4_4;
use Bolt\protocol\V5;
use Bolt\protocol\V5_1;
Expand Down Expand Up @@ -45,7 +47,7 @@
use WeakReference;

/**
* @implements ConnectionInterface<array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection}>
* @implements ConnectionInterface<array{0: V4_2|V4_3|V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection}>
*
* @psalm-import-type BoltMeta from SummarizedResultFormatter
*/
Expand Down Expand Up @@ -81,8 +83,12 @@

private int $messagesInPipeline = 0;

private bool $boltUtcPatchNegotiated = false;

private bool $boltUtcPatchNegotiated = false;

Check failure on line 88 in src/Bolt/BoltConnection.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

DuplicateProperty

src/Bolt/BoltConnection.php:88:5: DuplicateProperty: Property Laudis\Neo4j\Bolt\BoltConnection::$boltUtcPatchNegotiated has already been defined (see https://psalm.dev/325)

/**
* @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection}
* @return array{0: V4_2|V4_3|V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection}
*/
public function getImplementation(): array
{
Expand All @@ -93,7 +99,7 @@
* @psalm-mutation-free
*/
public function __construct(
private V4_4|V5|V5_1|V5_2|V5_3|V5_4|null $boltProtocol,
private V4_2|V4_3|V4_4|V5|V5_1|V5_2|V5_3|V5_4|null $boltProtocol,
private readonly Connection $connection,
private readonly AuthenticateInterface $auth,
private readonly string $userAgent,
Expand Down Expand Up @@ -142,10 +148,20 @@
return $this->config->getProtocol();
}

public function setBoltUtcPatchNegotiated(bool $negotiated): void
{
$this->boltUtcPatchNegotiated = $negotiated;
}

public function isBoltUtcPatchNegotiated(): bool
{
return $this->boltUtcPatchNegotiated;
}

/**
* @psalm-mutation-free
*/
public function isBoltUtcPatchNegotiated(): bool

Check failure on line 164 in src/Bolt/BoltConnection.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

DuplicateMethod

src/Bolt/BoltConnection.php:164:5: DuplicateMethod: Method Laudis\Neo4j\Bolt\BoltConnection::isboltutcpatchnegotiated has already been defined in /home/runner/work/neo4j-php-client/neo4j-php-client/src/Bolt/BoltConnection.php (see https://psalm.dev/178)
{
return $this->config->isBoltUtcPatchNegotiated();
}
Expand Down Expand Up @@ -323,7 +339,7 @@
$this->assertNoFailure($response);
}

public function protocol(): V4_4|V5|V5_1|V5_2|V5_3|V5_4
public function protocol(): V4_2|V4_3|V4_4|V5|V5_1|V5_2|V5_3|V5_4
{
if (!isset($this->boltProtocol)) {
throw new Exception('Connection is closed');
Expand Down
5 changes: 3 additions & 2 deletions src/Bolt/BoltMessageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Laudis\Neo4j\Bolt\Messages\BoltRollbackMessage;
use Laudis\Neo4j\Bolt\Messages\BoltRunMessage;
use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Contracts\Neo4jBookmarkManagerHooksInterface;
use Laudis\Neo4j\Databags\BookmarkHolder;

/**
Expand Down Expand Up @@ -59,9 +60,9 @@ public function createRunMessage(string $text, array $parameters, array $extra):
return new BoltRunMessage($this->connection, $text, $parameters, $extra, $this->logger);
}

public function createCommitMessage(BookmarkHolder $bookmarkHolder): BoltCommitMessage
public function createCommitMessage(BookmarkHolder $bookmarkHolder, ?Neo4jBookmarkManagerHooksInterface $bookmarkManagerHooks = null, bool $neo4jSharedManagedBookmarks = false): BoltCommitMessage
{
return new BoltCommitMessage($this->connection, $this->logger, $bookmarkHolder);
return new BoltCommitMessage($this->connection, $this->logger, $bookmarkHolder, $bookmarkManagerHooks, $neo4jSharedManagedBookmarks);
}

public function createRollbackMessage(): BoltRollbackMessage
Expand Down
5 changes: 5 additions & 0 deletions src/Bolt/BoltResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@
{
$this->networkPullOccurred = true;

$deferred = $this->connection->takeDeferredPullFailure();

Check failure on line 148 in src/Bolt/BoltResult.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

UndefinedMethod

src/Bolt/BoltResult.php:148:40: UndefinedMethod: Method Laudis\Neo4j\Bolt\BoltConnection::takeDeferredPullFailure does not exist (see https://psalm.dev/022)
if ($deferred !== null) {
throw $deferred;
}

try {
$meta = $this->connection->pull($this->qid, $this->effectivePullSize());
} catch (BoltConnectException|BoltException $e) {
Expand Down
15 changes: 12 additions & 3 deletions src/Bolt/Messages/BoltCommitMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Laudis\Neo4j\Bolt\BoltConnection;
use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Contracts\BoltMessage;
use Laudis\Neo4j\Contracts\Neo4jBookmarkManagerHooksInterface;
use Laudis\Neo4j\Databags\Bookmark;
use Laudis\Neo4j\Databags\BookmarkHolder;
use Psr\Log\LogLevel;
Expand All @@ -28,6 +29,8 @@ public function __construct(
BoltConnection $connection,
private readonly ?Neo4jLogger $logger,
private readonly BookmarkHolder $bookmarks,
private readonly ?Neo4jBookmarkManagerHooksInterface $bookmarkManagerHooks = null,
private readonly bool $neo4jSharedManagedBookmarks = false,
) {
parent::__construct($connection);
}
Expand All @@ -54,9 +57,15 @@ public function getResponse(): Response
/** @var array{bookmark?: string} $content */
$content = $response->content;
$bookmark = $content['bookmark'] ?? '';

if (trim($bookmark) !== '') {
$this->bookmarks->setBookmark(new Bookmark([$bookmark]));
$trimmed = trim($bookmark);
if ($trimmed !== '') {
$incoming = new Bookmark([$trimmed]);
if ($this->neo4jSharedManagedBookmarks) {
$this->bookmarks->neo4jApplyCommittedServerBookmark($incoming);
} else {
$this->bookmarks->setBookmark($incoming);
}
$this->bookmarkManagerHooks?->notifyBookmarksUpdated([$trimmed]);
}

$this->connection->protocol()->serverState = ServerState::READY;
Expand Down
12 changes: 7 additions & 5 deletions src/Bolt/ProtocolFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

use Bolt\Bolt;
use Bolt\connection\IConnection;
use Bolt\protocol\V4_2;
use Bolt\protocol\V4_3;
use Bolt\protocol\V4_4;
use Bolt\protocol\V5;
use Bolt\protocol\V5_1;
Expand All @@ -25,20 +27,20 @@

class ProtocolFactory
{
public function createProtocol(IConnection $connection): V4_4|V5|V5_1|V5_2|V5_3|V5_4
public function createProtocol(IConnection $connection): V4_2|V4_3|V4_4|V5|V5_1|V5_2|V5_3|V5_4
{
$boltOptoutEnv = getenv('BOLT_ANALYTICS_OPTOUT');
if ($boltOptoutEnv === false) {
putenv('BOLT_ANALYTICS_OPTOUT=1');
}

$bolt = new Bolt($connection);
// Offer protocol versions from newest to oldest (only 4.4 and above are supported)
$bolt->setProtocolVersions('5.4.4', 4.4);
// Four Bolt version suggestions (library maximum); include 4.2/4.3 for TestKit stubs and older servers.
$bolt->setProtocolVersions('5.4.4', '4.4.4', '4.3.3', '4.2.2');
$protocol = $bolt->build();

if (!($protocol instanceof V4_4 || $protocol instanceof V5 || $protocol instanceof V5_1 || $protocol instanceof V5_2 || $protocol instanceof V5_3 || $protocol instanceof V5_4)) {
throw new RuntimeException('Client only supports bolt version 4.4 to 5.4');
if (!($protocol instanceof V4_2 || $protocol instanceof V4_3 || $protocol instanceof V4_4 || $protocol instanceof V5 || $protocol instanceof V5_1 || $protocol instanceof V5_2 || $protocol instanceof V5_3 || $protocol instanceof V5_4)) {
throw new RuntimeException('Client only supports Bolt protocol 4.2 through 5.4');
}

return $protocol;
Expand Down
3 changes: 2 additions & 1 deletion src/Bolt/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
*/
private readonly SummarizedResultFormatter $formatter,
) {
$this->bookmarkHolder = new BookmarkHolder(Bookmark::from($config->getBookmarks()));
$this->bookmarkHolder = $config->getBookmarkHolder()

Check failure on line 65 in src/Bolt/Session.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

UndefinedMethod

src/Bolt/Session.php:65:42: UndefinedMethod: Method Laudis\Neo4j\Databags\SessionConfiguration::getBookmarkHolder does not exist (see https://psalm.dev/022)

Check warning on line 65 in src/Bolt/Session.php

View workflow job for this annotation

GitHub Actions / Lint & Analyse

MixedAssignment

src/Bolt/Session.php:65:9: MixedAssignment: Unable to determine the type that $this->bookmarkHolder is being assigned to (see https://psalm.dev/032)
?? new BookmarkHolder(Bookmark::from($config->getBookmarks()));
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/BoltFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
use Laudis\Neo4j\Enum\SocketType;

/**
* Small wrapper around the bolt library to easily guarantee only bolt version 3 and up will be created and authenticated.
* Small wrapper around the bolt library to create and authenticate Bolt connections (protocol 4.2+).
*/
class BoltFactory
{
Expand Down Expand Up @@ -83,6 +83,11 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati

$response = $data->getAuth()->authenticateBolt($connection, $data->getUserAgent());

$patchBolt = $response['patch_bolt'] ?? null;
$connection->setBoltUtcPatchNegotiated(
is_array($patchBolt) && in_array('utc', $patchBolt, true)
);

$config->setServerAgent($response['server'] ?? '');

// Timeout precedence: 1) driver config, 2) server hint, 3) default 30s.
Expand Down
35 changes: 35 additions & 0 deletions src/Contracts/Neo4jBookmarkManagerHooksInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Neo4j PHP Client and Driver package.
*
* (c) Nagels <https://nagels.tech>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Laudis\Neo4j\Contracts;

/**
* Optional hooks used when a session is tied to a Neo4j-style bookmark manager
* (supplier / consumer callbacks).
*/
interface Neo4jBookmarkManagerHooksInterface
{
/**
* Additional bookmarks merged only for the next wire message (RUN / BEGIN / ROUTE).
*
* @return list<string>
*/
public function getSupplierBookmarks(): array;

/**
* Called after the server reports new bookmark(s) for the session bookmark store.
*
* @param list<string> $bookmarks
*/
public function notifyBookmarksUpdated(array $bookmarks): void;
}
Loading
Loading