Skip to content

Commit 79cdb30

Browse files
Implement GetServerInfo Handler with Routing Validation Fix and Full Testkit Integration #277 (#278)
- Implemented Driver-Level executeQuery() Implementation - Implemented GetSErverInfo Where appropriate - Session::close() now discards unconsumed results on all used connections for proper resource cleanup - Improved Testkit Coverage Co-authored-by: p123-stack <[email protected]>
1 parent b0c3676 commit 79cdb30

File tree

15 files changed

+580
-1
lines changed

15 files changed

+580
-1
lines changed

psalm.xml

100755100644
File mode changed.

src/Basic/Driver.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Laudis\Neo4j\Contracts\AuthenticateInterface;
1717
use Laudis\Neo4j\Contracts\DriverInterface;
1818
use Laudis\Neo4j\Databags\DriverConfiguration;
19+
use Laudis\Neo4j\Databags\ServerInfo;
1920
use Laudis\Neo4j\Databags\SessionConfiguration;
2021
use Laudis\Neo4j\DriverFactory;
2122
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
@@ -44,6 +45,11 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool
4445
return $this->driver->verifyConnectivity($config);
4546
}
4647

48+
public function getServerInfo(?SessionConfiguration $config = null): ServerInfo
49+
{
50+
return $this->driver->getServerInfo($config);
51+
}
52+
4753
public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null): self
4854
{
4955
$driver = DriverFactory::create($uri, $configuration, $authenticate, SummarizedResultFormatter::create());

src/Bolt/BoltDriver.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Laudis\Neo4j\Contracts\DriverInterface;
2626
use Laudis\Neo4j\Contracts\SessionInterface;
2727
use Laudis\Neo4j\Databags\DriverConfiguration;
28+
use Laudis\Neo4j\Databags\ServerInfo;
2829
use Laudis\Neo4j\Databags\SessionConfiguration;
2930
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
3031
use Psr\Http\Message\UriInterface;
@@ -97,6 +98,23 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool
9798
return true;
9899
}
99100

101+
public function getServerInfo(?SessionConfiguration $config = null): ServerInfo
102+
{
103+
$config ??= SessionConfiguration::default();
104+
105+
$connection = GeneratorHelper::getReturnFromGenerator($this->pool->acquire($config));
106+
107+
$serverInfo = new ServerInfo(
108+
$connection->getServerAddress(),
109+
$connection->getProtocol(),
110+
$connection->getServerAgent()
111+
);
112+
113+
$this->pool->release($connection);
114+
115+
return $serverInfo;
116+
}
117+
100118
public function closeConnections(): void
101119
{
102120
$this->pool->close();

src/Bolt/ProtocolFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function createProtocol(IConnection $connection): V4_4|V5|V5_1|V5_2|V5_3|
3333
}
3434

3535
$bolt = new Bolt($connection);
36+
// Offer protocol versions from newest to oldest (only 4.4 and above are supported)
3637
$bolt->setProtocolVersions('5.4.4', 4.4);
3738
$protocol = $bolt->build();
3839

src/Contracts/DriverInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace Laudis\Neo4j\Contracts;
1515

16+
use Laudis\Neo4j\Databags\ServerInfo;
1617
use Laudis\Neo4j\Databags\SessionConfiguration;
1718
use Laudis\Neo4j\Types\CypherList;
1819
use Laudis\Neo4j\Types\CypherMap;
@@ -35,6 +36,11 @@ public function createSession(?SessionConfiguration $config = null): SessionInte
3536
*/
3637
public function verifyConnectivity(?SessionConfiguration $config = null): bool;
3738

39+
/**
40+
* Returns server information by establishing a connection.
41+
*/
42+
public function getServerInfo(?SessionConfiguration $config = null): ServerInfo;
43+
3844
/**
3945
* Closes all connections in the pool.
4046
*/

src/Neo4j/Neo4jDriver.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
use Laudis\Neo4j\Contracts\DriverInterface;
2929
use Laudis\Neo4j\Contracts\SessionInterface;
3030
use Laudis\Neo4j\Databags\DriverConfiguration;
31+
use Laudis\Neo4j\Databags\ServerInfo;
3132
use Laudis\Neo4j\Databags\SessionConfiguration;
33+
use Laudis\Neo4j\Enum\AccessMode;
3234
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
3335
use Psr\Http\Message\UriInterface;
3436
use Psr\Log\LogLevel;
@@ -99,6 +101,28 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool
99101
return true;
100102
}
101103

104+
public function getServerInfo(?SessionConfiguration $config = null): ServerInfo
105+
{
106+
$config ??= SessionConfiguration::default();
107+
108+
// Use READ access mode to connect to a follower (read server)
109+
if ($config->getAccessMode() === null) {
110+
$config = $config->withAccessMode(AccessMode::READ());
111+
}
112+
113+
$connection = GeneratorHelper::getReturnFromGenerator($this->pool->acquire($config));
114+
115+
$serverInfo = new ServerInfo(
116+
$connection->getServerAddress(),
117+
$connection->getProtocol(),
118+
$connection->getServerAgent()
119+
);
120+
121+
$this->pool->release($connection);
122+
123+
return $serverInfo;
124+
}
125+
102126
public function closeConnections(): void
103127
{
104128
$this->pool->close();
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Neo4j PHP Client and Driver package.
7+
*
8+
* (c) Nagels <https://nagels.tech>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Laudis\Neo4j\TestkitBackend\Handlers;
15+
16+
use Exception;
17+
use Laudis\Neo4j\Databags\Neo4jError;
18+
use Laudis\Neo4j\Databags\SessionConfiguration;
19+
use Laudis\Neo4j\Enum\AccessMode;
20+
use Laudis\Neo4j\Exception\Neo4jException;
21+
use Laudis\Neo4j\Exception\TransactionException;
22+
use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface;
23+
use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface;
24+
use Laudis\Neo4j\TestkitBackend\MainRepository;
25+
use Laudis\Neo4j\TestkitBackend\Requests\ExecuteQueryRequest;
26+
use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse;
27+
use Laudis\Neo4j\TestkitBackend\Responses\EagerResultResponse;
28+
use Symfony\Component\Uid\Uuid;
29+
30+
/**
31+
* @implements RequestHandlerInterface<ExecuteQueryRequest>
32+
*/
33+
final class ExecuteQuery implements RequestHandlerInterface
34+
{
35+
public function __construct(
36+
private MainRepository $repository,
37+
) {
38+
}
39+
40+
/**
41+
* @param ExecuteQueryRequest $request
42+
*/
43+
public function handle($request): TestkitResponseInterface
44+
{
45+
try {
46+
$driver = $this->repository->getDriver($request->getDriverId());
47+
48+
if (method_exists($driver, 'executeQuery')) {
49+
return $this->handleWithExecuteQuery($driver, $request);
50+
}
51+
52+
return $this->handleWithSession($driver, $request);
53+
} catch (Exception $e) {
54+
$uuid = Uuid::v4();
55+
56+
if ($e instanceof Neo4jException || $e instanceof TransactionException) {
57+
return new DriverErrorResponse($uuid, $e);
58+
}
59+
60+
$neo4jError = new Neo4jError(
61+
$e->getMessage(),
62+
(string) $e->getCode(),
63+
'DatabaseError',
64+
'Service',
65+
'Service Unavailable'
66+
);
67+
68+
return new DriverErrorResponse($uuid, new Neo4jException([$neo4jError], $e));
69+
}
70+
}
71+
72+
private function handleWithExecuteQuery($driver, ExecuteQueryRequest $request): TestkitResponseInterface
73+
{
74+
$config = $this->buildExecutionConfig($request->getConfig());
75+
$params = $request->getParams() ?? [];
76+
77+
$eagerResult = $driver->executeQuery(
78+
$request->getCypher(),
79+
$params,
80+
$config
81+
);
82+
83+
$resultId = Uuid::v4();
84+
$this->repository->addEagerResult($resultId, $eagerResult);
85+
86+
return new EagerResultResponse($resultId, $eagerResult);
87+
}
88+
89+
private function handleWithSession($driver, ExecuteQueryRequest $request): TestkitResponseInterface
90+
{
91+
$config = $request->getConfig();
92+
93+
$sessionConfig = SessionConfiguration::default();
94+
95+
if (array_key_exists('database', $config)) {
96+
$sessionConfig = $sessionConfig->withDatabase($config['database']);
97+
}
98+
99+
$accessMode = AccessMode::READ();
100+
if (array_key_exists('routing', $config) && $config['routing'] === 'w') {
101+
$accessMode = AccessMode::WRITE();
102+
}
103+
$sessionConfig = $sessionConfig->withAccessMode($accessMode);
104+
105+
$session = $driver->createSession($sessionConfig);
106+
107+
try {
108+
$result = $session->run(
109+
$request->getCypher(),
110+
$request->getParams() ?? []
111+
);
112+
113+
$resultId = Uuid::v4();
114+
$this->repository->addEagerResult($resultId, $result);
115+
116+
return new EagerResultResponse($resultId, $result);
117+
} finally {
118+
$session->close();
119+
}
120+
}
121+
122+
private function buildExecutionConfig(?array $config): array
123+
{
124+
if ($config === null) {
125+
return [];
126+
}
127+
128+
$executionConfig = [];
129+
130+
if (array_key_exists('database', $config) && $config['database'] !== null) {
131+
$executionConfig['database'] = $config['database'];
132+
}
133+
134+
if (array_key_exists('routing', $config) && $config['routing'] !== null) {
135+
$executionConfig['routing'] = $config['routing'];
136+
}
137+
138+
if (array_key_exists('impersonatedUser', $config) && $config['impersonatedUser'] !== null) {
139+
$executionConfig['impersonatedUser'] = $config['impersonatedUser'];
140+
}
141+
142+
if (array_key_exists('txMeta', $config) && $config['txMeta'] !== null) {
143+
$executionConfig['txMeta'] = $config['txMeta'];
144+
}
145+
146+
if (array_key_exists('timeout', $config) && $config['timeout'] !== null) {
147+
$executionConfig['timeout'] = $config['timeout'] / 1000;
148+
}
149+
150+
if (array_key_exists('authorizationToken', $config) && $config['authorizationToken'] !== null) {
151+
$authToken = $config['authorizationToken'];
152+
if (array_key_exists('data', $authToken)) {
153+
$executionConfig['auth'] = $this->convertAuthToken($authToken['data']);
154+
}
155+
}
156+
157+
return $executionConfig;
158+
}
159+
160+
private function convertAuthToken(array $tokenData): array
161+
{
162+
$auth = [];
163+
164+
if (array_key_exists('scheme', $tokenData)) {
165+
$auth['scheme'] = $tokenData['scheme'];
166+
}
167+
168+
if (array_key_exists('principal', $tokenData)) {
169+
$auth['principal'] = $tokenData['principal'];
170+
}
171+
172+
if (array_key_exists('credentials', $tokenData)) {
173+
$auth['credentials'] = $tokenData['credentials'];
174+
}
175+
176+
if (array_key_exists('realm', $tokenData)) {
177+
$auth['realm'] = $tokenData['realm'];
178+
}
179+
180+
if (array_key_exists('parameters', $tokenData)) {
181+
$auth['parameters'] = $tokenData['parameters'];
182+
}
183+
184+
return $auth;
185+
}
186+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Neo4j PHP Client and Driver package.
7+
*
8+
* (c) Nagels <https://nagels.tech>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Laudis\Neo4j\TestkitBackend\Handlers;
15+
16+
use Exception;
17+
use Laudis\Neo4j\Databags\Neo4jError;
18+
use Laudis\Neo4j\Exception\Neo4jException;
19+
use Laudis\Neo4j\Exception\TransactionException;
20+
use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface;
21+
use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface;
22+
use Laudis\Neo4j\TestkitBackend\MainRepository;
23+
use Laudis\Neo4j\TestkitBackend\Requests\GetServerInfoRequest;
24+
use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse;
25+
use Laudis\Neo4j\TestkitBackend\Responses\ServerInfoResponse;
26+
use Symfony\Component\Uid\Uuid;
27+
28+
/**
29+
* @implements RequestHandlerInterface<GetServerInfoRequest>
30+
*/
31+
final class GetServerInfo implements RequestHandlerInterface
32+
{
33+
public function __construct(
34+
private MainRepository $repository,
35+
) {
36+
}
37+
38+
/**
39+
* @param GetServerInfoRequest $request
40+
*/
41+
public function handle($request): TestkitResponseInterface
42+
{
43+
try {
44+
$driver = $this->repository->getDriver($request->getDriverId());
45+
46+
$serverInfo = $driver->getServerInfo();
47+
48+
return new ServerInfoResponse($serverInfo);
49+
} catch (Neo4jException|TransactionException $e) {
50+
return new DriverErrorResponse(Uuid::v4(), $e);
51+
} catch (Exception $e) {
52+
$neo4jError = new Neo4jError(
53+
$e->getMessage(),
54+
(string) $e->getCode(),
55+
'DatabaseError',
56+
'Service',
57+
'Service Unavailable'
58+
);
59+
60+
return new DriverErrorResponse(Uuid::v4(), new Neo4jException([$neo4jError], $e));
61+
}
62+
}
63+
}

testkit-backend/src/MainRepository.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ public function addDriver(Uuid $id, DriverInterface $driver): void
6868

6969
public function removeDriver(Uuid $id): void
7070
{
71+
$driver = $this->drivers[$id->toRfc4122()] ?? null;
72+
if ($driver !== null) {
73+
$driver->closeConnections();
74+
}
7175
unset($this->drivers[$id->toRfc4122()]);
7276
}
7377

testkit-backend/src/RequestFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Laudis\Neo4j\TestkitBackend\Requests\ForcedRoutingTableUpdateRequest;
2323
use Laudis\Neo4j\TestkitBackend\Requests\GetFeaturesRequest;
2424
use Laudis\Neo4j\TestkitBackend\Requests\GetRoutingTableRequest;
25+
use Laudis\Neo4j\TestkitBackend\Requests\GetServerInfoRequest;
2526
use Laudis\Neo4j\TestkitBackend\Requests\NewDriverRequest;
2627
use Laudis\Neo4j\TestkitBackend\Requests\NewSessionRequest;
2728
use Laudis\Neo4j\TestkitBackend\Requests\ResolverResolutionCompletedRequest;
@@ -70,6 +71,7 @@ final class RequestFactory
7071
'RetryableNegative' => RetryableNegativeRequest::class,
7172
'ForcedRoutingTableUpdate' => ForcedRoutingTableUpdateRequest::class,
7273
'GetRoutingTable' => GetRoutingTableRequest::class,
74+
'GetServerInfo' => GetServerInfoRequest::class,
7375
];
7476

7577
/**

0 commit comments

Comments
 (0)