diff --git a/.github/workflows/cs-fixer.yml b/.github/workflows/cs-fixer.yml
index a25f4139..99ed933e 100644
--- a/.github/workflows/cs-fixer.yml
+++ b/.github/workflows/cs-fixer.yml
@@ -2,9 +2,6 @@ name: PHP CS Fixer
on:
push:
- pull_request:
- paths:
- - '**/*.php'
workflow_dispatch:
jobs:
diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml
index 277e6c4b..9068ed0c 100644
--- a/.github/workflows/psalm.yml
+++ b/.github/workflows/psalm.yml
@@ -2,9 +2,6 @@ name: Psalm Static Analysis
on:
push:
- pull_request:
- paths:
- - '**/*.php'
workflow_dispatch:
jobs:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2b485b29..698559da 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,8 +1,7 @@
-name: CI Pipeline
+name: PHP Tests
on:
push:
- pull_request:
workflow_dispatch:
concurrency:
diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache
new file mode 100644
index 00000000..444ba97f
--- /dev/null
+++ b/.php-cs-fixer.cache
@@ -0,0 +1 @@
+{"php":"8.3.16","version":"3.68.5:v3.68.5#7bedb718b633355272428c60736dc97fb96daf27","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"strict_param":true},"hashes":{"tests\/resources\/expected\/complex-query-profile.php":"c0d16588e70d32588ec2e55ba5ae5873","tests\/Unit\/Neo4jExceptionUnitTest.php":"e07e604496e0e4032d2798ee9d2cb1b2","tests\/Unit\/ResultRowTest.php":"b4b307579a75da8307d6d65eb5548cae","tests\/Unit\/Neo4jQueryAPIUnitTest.php":"87e08aca0ccef5589c88bd7249e39496","tests\/Unit\/Neo4jRequestFactoryTest.php":"ebc6d5ee7790df4d69a5b555ad48ff5a","tests\/Unit\/AuthenticationTest.php":"746b185bcfb47a925e35b5b79b854fab","tests\/Integration\/Neo4jQueryAPIIntegrationTest.php":"43f34ad1774e3eeb133a9e3bb96f191d","tests\/Integration\/Neo4jTransactionIntegrationTest.php":"35fcbd5afbec5bb59f35040d9d6c518f","tests\/Integration\/Neo4jOGMTest.php":"e03f51ef605ca818763f3897d7a30830","src\/OGM.php":"211c087b78fca69390701e2f505e46fe","src\/Neo4jQueryAPI.php":"a7a505057617a2de3a94a73065254ecb","src\/BearerAuthentication.php":"51b5f02280a43838465cffa8974648c6","src\/Enums\/AccessMode.php":"88b5c70c4716cc68bcb86e2f162dd347","src\/Results\/ResultRow.php":"92dc1ec9315fa5236a79468ffaf9a60c","src\/Results\/ResultSet.php":"372faa2af185b25b804be1974c34b1ae","src\/Exception\/Neo4jException.php":"89c4c090cd3ba6e94c13eab7ebd0588c","src\/NoAuth.php":"2267e8a5b07aeaaab3165bb105b10807","src\/loginConfig.php":"47e9993051fc556a7fc28bc8f9a01caa","src\/AuthenticateInterface.php":"1da849c5d5b88439e01543d5d5b9f8d9","src\/BasicAuthentication.php":"ab50275cc6841d88d289a63d86ecb118","src\/Configuration.php":"fabfe6acf58bb0bda76453ace4f0757d","src\/ResponseParser.php":"e32270966c3a618bcb5ea9c6497748be","src\/Neo4jRequestFactory.php":"e3279d36e54032c6acf92df10ac47f07","src\/Objects\/Path.php":"e8091a19eb4e70ced4f8f7364dbe78be","src\/Objects\/Node.php":"ac679671f513c6c996dbf75a66fcacb2","src\/Objects\/Authentication.php":"f31af1c057be0f490cc2dba365f03b31","src\/Objects\/ProfiledQueryPlanArguments.php":"02b5fa2d50fec5d0fb0c4ada55ebda69","src\/Objects\/Person.php":"cee5594450a015103e12d4cbe186f167","src\/Objects\/Point.php":"4115d8d1b85a0d6e37b79d303237bcd0","src\/Objects\/ResultSet.php":"a5ba56fc6c6e250c22b183ac26dfd68e","src\/Objects\/ProfiledQueryPlan.php":"75ab6c3ad2ce97675a8e6478d17ac4d9","src\/Objects\/Bookmarks.php":"2c3e7229ce9b56c0352155b3feaac9bb","src\/Objects\/ResultCounters.php":"a9372c98fe7bede10cb004af30ea502f","src\/Objects\/Relationship.php":"e344e22d5a41f1795f3310d55ea51c20","src\/Transaction.php":"ff5454897ddbcc4fc2a984ecb90a90fd","src\/Authentication\/BearerAuthentication.php":"08a9e3c01d3833255cd51c94a17f1aa3","src\/Authentication\/NoAuth.php":"71cc7d784b9d98c62d2342faf38f7dc4","src\/Authentication\/AuthenticateInterface.php":"65b5a1074e11fba04362e754ad97023f","src\/Authentication\/BasicAuthentication.php":"c37b7ef26a59c032ac2a6a7b91c5adae"}}
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 0e8a1cb6..de6ca9b6 100644
--- a/composer.json
+++ b/composer.json
@@ -39,7 +39,8 @@
"scripts": {
"cs": "vendor/bin/php-cs-fixer fix --dry-run --diff --allow-risky=yes",
- "cs:fix": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
+ "cs:fix": "vendor/bin/php-cs-fixer fix --allow-risky=yes",
+ "psalm": "vendor/bin/psalm"
}
}
diff --git a/psalm.xml b/psalm.xml
index 3060179f..7ee83a50 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -15,4 +15,8 @@
+
+
+
+
diff --git a/src/AuthenticateInterface.php b/src/Authentication/AuthenticateInterface.php
similarity index 88%
rename from src/AuthenticateInterface.php
rename to src/Authentication/AuthenticateInterface.php
index 54595181..474197ce 100644
--- a/src/AuthenticateInterface.php
+++ b/src/Authentication/AuthenticateInterface.php
@@ -1,6 +1,6 @@
errorCode = $errorDetails['code'] ?? 'Neo.UnknownError';
$errorParts = explode('.', $this->errorCode);
@@ -26,17 +25,30 @@ public function __construct(
$this->errorSubType = $errorParts[2] ?? null;
$this->errorName = $errorParts[3] ?? null;
-
$message = $errorDetails['message'] ?? 'An unknown error occurred.';
parent::__construct($message, $statusCode, $previous);
}
+ /**
+ * Create a Neo4jException instance from a Neo4j error response array.
+ *
+ * @param array $response The error response from Neo4j.
+ * @param \Throwable|null $exception Optional previous exception for chaining.
+ * @return self
+ */
+ public static function fromNeo4jResponse(array $response, ?\Throwable $exception = null): self
+ {
+ $errorDetails = $response['errors'][0] ?? ['message' => 'Unknown error', 'code' => 'Neo.UnknownError'];
+
+
+ return new self($errorDetails, previous: $exception);
+ }
+
public function getErrorCode(): string
{
return $this->errorCode;
}
-
public function getType(): ?string
{
return $this->errorType;
@@ -51,19 +63,4 @@ public function getName(): ?string
{
return $this->errorName;
}
-
- /**
- * Create a Neo4jException instance from a Neo4j error response array.
- *
- * @param array $response The error response from Neo4j.
- * @param \Throwable|null $exception Optional previous exception for chaining.
- * @return self
- */
- public static function fromNeo4jResponse(array $response, ?\Throwable $exception = null): self
- {
- $errorDetails = $response['errors'][0] ?? [];
- $statusCode = $errorDetails['statusCode'] ?? 0;
-
- return new self($errorDetails, (int)$statusCode, $exception);
- }
}
diff --git a/src/Neo4jQueryAPI.php b/src/Neo4jQueryAPI.php
index 20c480b3..3808b1d1 100644
--- a/src/Neo4jQueryAPI.php
+++ b/src/Neo4jQueryAPI.php
@@ -2,236 +2,105 @@
namespace Neo4j\QueryAPI;
-use Exception;
use GuzzleHttp\Client;
-use GuzzleHttp\Psr7\Request;
-use GuzzleHttp\Psr7\Utils;
+use Neo4j\QueryAPI\Authentication\AuthenticateInterface;
use Neo4j\QueryAPI\Exception\Neo4jException;
use Neo4j\QueryAPI\Objects\Authentication;
-use Neo4j\QueryAPI\Objects\Bookmarks;
-use Neo4j\QueryAPI\Objects\ProfiledQueryPlan;
-use Neo4j\QueryAPI\Objects\ProfiledQueryPlanArguments;
-use Neo4j\QueryAPI\Objects\ResultCounters;
-use Neo4j\QueryAPI\Objects\ResultSet;
-use Neo4j\QueryAPI\Results\ResultRow;
+use Neo4j\QueryAPI\Results\ResultSet;
+use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Client\RequestExceptionInterface;
-use RuntimeException;
-use stdClass;
+use Psr\Http\Message\ResponseInterface;
-/**
- * @api
- */
class Neo4jQueryAPI
{
- private ClientInterface $client;
- private AuthenticateInterface $auth;
-
- public function __construct(ClientInterface $client, AuthenticateInterface $auth)
- {
- $this->client = $client;
- $this->auth = $auth;
+ public function __construct(
+ private ClientInterface $client,
+ private ResponseParser $responseParser,
+ private Neo4jRequestFactory $requestFactory
+ ) {
}
- /**
- * @throws Exception
- */
/**
* @api
*/
public static function login(string $address, AuthenticateInterface $auth = null): self
{
- $client = new Client([
- 'base_uri' => rtrim($address, '/'),
- 'timeout' => 10.0,
- 'headers' => [
- 'Content-Type' => 'application/vnd.neo4j.query',
- 'Accept' => 'application/vnd.neo4j.query',
- ],
- ]);
-
- return new self($client, $auth ?? Authentication::fromEnvironment());
+ $client = new Client();
+
+ return new self(
+ client: $client,
+ responseParser: new ResponseParser(
+ ogm: new OGM()
+ ),
+ requestFactory: new Neo4jRequestFactory(
+ psr17Factory: new Psr17Factory(),
+ streamFactory: new Psr17Factory(),
+ configuration: new Configuration(
+ baseUri: $address
+ ),
+ auth: $auth ?? Authentication::fromEnvironment()
+ )
+ );
}
+
+
/**
- * Executes a Cypher query on the Neo4j database.
- *
- * @throws Neo4jException
- * @throws RequestExceptionInterface
+ * Executes a Cypher query.
*/
- public function run(string $cypher, array $parameters = [], string $database = 'neo4j', Bookmarks $bookmark = null): ResultSet
+ public function run(string $cypher, array $parameters = []): ResultSet
{
- try {
- $payload = [
- 'statement' => $cypher,
- 'parameters' => empty($parameters) ? new stdClass() : $parameters,
- 'includeCounters' => true,
- ];
-
+ $request = $this->requestFactory->buildRunQueryRequest($cypher, $parameters);
- if ($bookmark !== null) {
- $payload['bookmarks'] = $bookmark->getBookmarks();
- }
-
-
- $request = new Request('POST', '/db/' . $database . '/query/v2');
- $request = $this->auth->authenticate($request);
- $request = $request->withHeader('Content-Type', 'application/json');
- $request = $request->withBody(Utils::streamFor(json_encode($payload)));
+ try {
$response = $this->client->sendRequest($request);
- $contents = $response->getBody()->getContents();
-
-
- $data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);
-
-
- if (isset($data['errors']) && count($data['errors']) > 0) {
-
- $error = $data['errors'][0];
- throw new Neo4jException(
- $error,
- 0,
- null
- );
- }
-
-
- return $this->parseResultSet($data);
-
} catch (RequestExceptionInterface $e) {
- $this->handleException($e);
- } catch (Neo4jException $e) {
- throw $e;
- }
- }
-
- private function parseResultSet(array $data): ResultSet
- {
- $ogm = new OGM();
-
- $keys = $data['data']['fields'] ?? [];
- $values = $data['data']['values'] ?? [];
-
- if (!is_array($values)) {
- throw new RuntimeException('Unexpected response format: values is not an array.');
+ $this->handleRequestException($e);
}
- $rows = array_map(function ($resultRow) use ($ogm, $keys) {
- $row = [];
- foreach ($keys as $index => $key) {
- $fieldData = $resultRow[$index] ?? null;
- $row[$key] = $ogm->map($fieldData);
- }
- return new ResultRow($row);
- }, $values);
-
- $resultCounters = new ResultCounters(
- containsUpdates: $data['counters']['containsUpdates'] ?? false,
- nodesCreated: $data['counters']['nodesCreated'] ?? 0,
- nodesDeleted: $data['counters']['nodesDeleted'] ?? 0,
- propertiesSet: $data['counters']['propertiesSet'] ?? 0,
- relationshipsCreated: $data['counters']['relationshipsCreated'] ?? 0,
- relationshipsDeleted: $data['counters']['relationshipsDeleted'] ?? 0,
- labelsAdded: $data['counters']['labelsAdded'] ?? 0,
- labelsRemoved: $data['counters']['labelsRemoved'] ?? 0,
- indexesAdded: $data['counters']['indexesAdded'] ?? 0,
- indexesRemoved: $data['counters']['indexesRemoved'] ?? 0,
- constraintsAdded: $data['counters']['constraintsAdded'] ?? 0,
- constraintsRemoved: $data['counters']['constraintsRemoved'] ?? 0,
- containsSystemUpdates: $data['counters']['containsSystemUpdates'] ?? false,
- systemUpdates: $data['counters']['systemUpdates'] ?? 0
- );
-
- $profile = isset($data['profiledQueryPlan']) ? $this->createProfileData($data['profiledQueryPlan']) : null;
-
- return new ResultSet(
- $rows,
- $resultCounters,
- new Bookmarks($data['bookmarks'] ?? []),
- $profile
- );
- }
-
- private function handleException(RequestExceptionInterface $e): void
- {
- $response = $e->getResponse();
- if ($response !== null) {
- $contents = $response->getBody()->getContents();
- $errorResponse = json_decode($contents, true);
- throw Neo4jException::fromNeo4jResponse($errorResponse, $e);
- }
- throw $e;
+ return $this->responseParser->parseRunQueryResponse($response);
}
-
+ /**
+ * Starts a transaction.
+ */
public function beginTransaction(): Transaction
{
- $request = new Request('POST', '/db/neo4j/query/v2/tx');
- $request = $this->auth->authenticate($request);
- $request = $request->withHeader('Content-Type', 'application/json');
+ $request = $this->requestFactory->buildBeginTransactionRequest();
- $response = $this->client->sendRequest($request);
- $contents = $response->getBody()->getContents();
+ try {
+ $response = $this->client->sendRequest($request);
+ } catch (RequestExceptionInterface $e) {
+ $this->handleRequestException($e);
+ }
$clusterAffinity = $response->getHeaderLine('neo4j-cluster-affinity');
- $responseData = json_decode($contents, true);
+ $responseData = json_decode($response->getBody(), true);
$transactionId = $responseData['transaction']['id'];
- return new Transaction($this->client, $clusterAffinity, $transactionId);
+ return new Transaction(
+ $this->client,
+ $this->responseParser,
+ $this->requestFactory,
+ $clusterAffinity,
+ $transactionId
+ );
}
- private function createProfileData(array $data): ProfiledQueryPlan
+ /**
+ * Handles request exceptions by parsing error details and throwing a Neo4jException.
+ *
+ * @throws Neo4jException
+ */
+ private function handleRequestException(RequestExceptionInterface $e): void
{
- $ogm = new OGM();
-
- $mappedArguments = array_map(function ($value) use ($ogm) {
- if (is_array($value) && array_key_exists('$type', $value) && array_key_exists('_value', $value)) {
- return $ogm->map($value);
- }
- return $value;
- }, $data['arguments'] ?? []);
-
- $queryArguments = new ProfiledQueryPlanArguments(
- globalMemory: $mappedArguments['GlobalMemory'] ?? null,
- plannerImpl: $mappedArguments['planner-impl'] ?? null,
- memory: $mappedArguments['Memory'] ?? null,
- stringRepresentation: $mappedArguments['string-representation'] ?? null,
- runtime: $mappedArguments['runtime'] ?? null,
- time: $mappedArguments['Time'] ?? null,
- pageCacheMisses: $mappedArguments['PageCacheMisses'] ?? null,
- pageCacheHits: $mappedArguments['PageCacheHits'] ?? null,
- runtimeImpl: $mappedArguments['runtime-impl'] ?? null,
- version: $mappedArguments['version'] ?? null,
- dbHits: $mappedArguments['DbHits'] ?? null,
- batchSize: $mappedArguments['batch-size'] ?? null,
- details: $mappedArguments['Details'] ?? null,
- plannerVersion: $mappedArguments['planner-version'] ?? null,
- pipelineInfo: $mappedArguments['PipelineInfo'] ?? null,
- runtimeVersion: $mappedArguments['runtime-version'] ?? null,
- id: $mappedArguments['Id'] ?? null,
- estimatedRows: $mappedArguments['EstimatedRows'] ?? null,
- planner: $mappedArguments['planner'] ?? null,
- rows: $mappedArguments['Rows'] ?? null
- );
-
- $profiledQueryPlan = new ProfiledQueryPlan(
- $data['dbHits'],
- $data['records'],
- $data['hasPageCacheStats'],
- $data['pageCacheHits'],
- $data['pageCacheMisses'],
- $data['pageCacheHitRatio'],
- $data['time'],
- $data['operatorType'],
- $queryArguments,
- children: [],
- identifiers: $data['identifiers'] ?? []
- );
-
- foreach ($data['children'] ?? [] as $child) {
- $profiledQueryPlan->addChild($this->createProfileData($child));
+ $response = $e->getResponse();
+ if ($response instanceof ResponseInterface) {
+ $errorResponse = json_decode((string)$response->getBody(), true);
+ throw Neo4jException::fromNeo4jResponse($errorResponse, $e);
}
- return $profiledQueryPlan;
+ throw new Neo4jException(['message' => $e->getMessage()], 500, $e);
}
}
diff --git a/src/Neo4jRequestFactory.php b/src/Neo4jRequestFactory.php
index 38938439..4c94554f 100644
--- a/src/Neo4jRequestFactory.php
+++ b/src/Neo4jRequestFactory.php
@@ -2,94 +2,90 @@
namespace Neo4j\QueryAPI;
+use Neo4j\QueryAPI\Authentication\AuthenticateInterface;
+use Neo4j\QueryAPI\Enums\AccessMode;
use Psr\Http\Message\RequestFactoryInterface;
-use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
/**
- * @api
+ * @api
*/
class Neo4jRequestFactory
{
- private string $baseUri;
- private ?string $authHeader = null;
- private RequestFactoryInterface $psr17Factory;
- private StreamFactoryInterface $streamFactory;
-
public function __construct(
- RequestFactoryInterface $psr17Factory,
- StreamFactoryInterface $streamFactory,
- string $baseUri,
- ?string $authHeader = null
+ private RequestFactoryInterface $psr17Factory,
+ private StreamFactoryInterface $streamFactory,
+ private Configuration $configuration,
+ private AuthenticateInterface $auth
) {
- $this->psr17Factory = $psr17Factory;
- $this->streamFactory = $streamFactory;
- $this->baseUri = $baseUri;
- $this->authHeader = $authHeader;
}
public function buildRunQueryRequest(
- string $database,
string $cypher,
- array $parameters = [],
- bool $includeCounters = true,
- ?array $bookmarks = null
+ array $parameters = []
): RequestInterface {
- $payload = [
- 'statement' => $cypher,
- 'parameters' => empty($parameters) ? new \stdClass() : $parameters,
- 'includeCounters' => $includeCounters,
- ];
-
- if ($bookmarks !== null) {
- $payload['bookmarks'] = $bookmarks;
- }
-
- $uri = rtrim($this->baseUri, '/') . "/db/{$database}/query/v2";
+ return $this->createRequest("/db/{$this->configuration->database}/query/v2", $cypher, $parameters);
+ }
- return $this->createRequest('POST', $uri, json_encode($payload));
+ public function buildBeginTransactionRequest(): RequestInterface
+ {
+ return $this->createRequest("/db/{$this->configuration->database}/query/v2/tx", null, null);
}
- public function buildBeginTransactionRequest(string $database): RequestInterface
+ public function buildCommitRequest(string $transactionId, string $clusterAffinity): RequestInterface
{
- $uri = rtrim($this->baseUri, '/') . "/db/{$database}/query/v2/tx";
- return $this->createRequest('POST', $uri);
+ return $this->createRequest("/db/{$this->configuration->database}/query/v2/tx/{$transactionId}/commit", null, null)
+ ->withHeader("neo4j-cluster-affinity", $clusterAffinity);
}
- public function buildCommitRequest(string $database, string $transactionId): RequestInterface
+ public function buildRollbackRequest(string $transactionId, string $clusterAffinity): RequestInterface
{
- $uri = rtrim($this->baseUri, '/') . "/db/{$database}/query/v2/tx/{$transactionId}/commit";
- return $this->createRequest('POST', $uri);
+ return $this->createRequest("/db/{$this->configuration->database}/query/v2/tx/{$transactionId}/rollback", null, null)
+ ->withHeader("neo4j-cluster-affinity", $clusterAffinity)
+ ->withMethod("DELETE");
}
- public function buildRollbackRequest(string $database, string $transactionId): RequestInterface
+ public function buildTransactionRunRequest(string $query, array $parameters, string $transactionId, string $clusterAffinity): RequestInterface
{
- $uri = rtrim($this->baseUri, '/') . "/db/{$database}/query/v2/tx/{$transactionId}/rollback";
- return $this->createRequest('POST', $uri);
+ return $this->createRequest("/db/neo4j/query/v2/tx/{$transactionId}", $query, $parameters)
+ ->withHeader("neo4j-cluster-affinity", $clusterAffinity);
}
- private function createRequest(string $method, string $uri, ?string $body = null): RequestInterface
+ private function createRequest(string $uri, ?string $cypher, ?array $parameters): RequestInterface
{
- $request = $this->psr17Factory->createRequest($method, $uri);
+ $request = $this->psr17Factory->createRequest('POST', $this->configuration->baseUri . $uri);
- $headers = [
- 'Content-Type' => 'application/json',
- 'Accept' => 'application/json',
+ $payload = [
+ 'parameters' => empty($parameters) ? new \stdClass() : $parameters,
+ 'includeCounters' => $this->configuration->includeCounters
];
- if ($this->authHeader) {
- $headers['Authorization'] = $this->authHeader;
+ if ($this->configuration->accessMode === AccessMode::READ) {
+ $payload['accessMode'] = AccessMode::READ;
+ }
+
+ if ($cypher) {
+ $payload['statement'] = $cypher;
}
- foreach ($headers as $name => $value) {
- $request = $request->withHeader($name, $value);
+ if ($parameters) {
+ $payload['parameters'] = $parameters;
}
- if ($body !== null) {
- $stream = $this->streamFactory->createStream($body);
- $request = $request->withBody($stream);
+ if ($this->configuration->bookmarks !== null) {
+ $payload['bookmarks'] = $this->configuration->bookmarks;
}
- return $request;
+ $request = $request->withHeader('Content-Type', 'application/json');
+ $request = $request->withHeader('Accept', 'application/vnd.neo4j.query');
+
+ $body = json_encode($payload);
+
+ $stream = $this->streamFactory->createStream($body);
+
+ $request = $request->withBody($stream);
+
+ return $this->auth->authenticate($request);
}
}
diff --git a/src/OGM.php b/src/OGM.php
index ba737000..951f9122 100644
--- a/src/OGM.php
+++ b/src/OGM.php
@@ -13,27 +13,33 @@
class OGM
{
/**
- * @param array{'$type': string, ' $object _value': mixed} $object
+ * @param array{'$type': string, '_value': mixed} $object
* @return mixed
*/
public function map(array $object): mixed
{
+ if (!isset($object['$type'])) {
+ if (isset($object['elementId'], $object['labels'], $object['properties'])) {
+ return $this->mapNode($object); // Handle as a Node
+ }
+ throw new \InvalidArgumentException('Unknown object type: ' . json_encode($object));
+ }
+
+ // if (!isset($object['_value'])) {
+ // throw new \InvalidArgumentException('Missing _value key in object: ' . json_encode($object));
+ // }
+
return match ($object['$type']) {
- 'Integer' => $object['_value'],
- 'Float' => $object['_value'],
- 'String' => $object['_value'],
- 'Boolean' => $object['_value'],
- 'Null' => $object['_value'],
+ 'Integer', 'Float', 'String', 'Boolean', 'Duration', 'OffsetDateTime' => $object['_value'],
'Array' => $object['_value'],
+ 'Null' => null,
'List' => array_map([$this, 'map'], $object['_value']),
- 'Duration' => $object['_value'],
- 'OffsetDateTime' => $object['_value'],
'Node' => $this->mapNode($object['_value']),
'Map' => $this->mapProperties($object['_value']),
'Point' => $this->parseWKT($object['_value']),
'Relationship' => $this->mapRelationship($object['_value']),
'Path' => $this->mapPath($object['_value']),
- default => throw new \InvalidArgumentException('Unknown type: ' . $object['$type']),
+ default => throw new \InvalidArgumentException('Unknown type: ' . $object['$type'] . ' in object: ' . json_encode($object)),
};
}
@@ -43,22 +49,13 @@ public static function parseWKT(string $wkt): Point
$srid = (int)str_replace('SRID=', '', $sridPart);
$pointPart = substr($wkt, strpos($wkt, 'POINT') + 6);
- if (str_contains($pointPart, 'Z')) {
- $pointPart = str_replace('Z', '', $pointPart);
- }
+ $pointPart = str_replace('Z', '', $pointPart);
$pointPart = trim($pointPart, ' ()');
$coordinates = explode(' ', $pointPart);
- if (count($coordinates) === 2) {
- [$x, $y] = $coordinates;
- $z = null;
- } elseif (count($coordinates) === 3) {
- [$x, $y, $z] = $coordinates;
- } else {
- throw new \InvalidArgumentException("Invalid WKT format: unable to parse coordinates.");
- }
+ [$x, $y, $z] = array_pad(array_map('floatval', $coordinates), 3, null);
- return new Point((float)$x, (float)$y, $z !== null ? (float)$z : null, $srid);
+ return new Point($x, $y, $z, $srid);
}
@@ -76,8 +73,8 @@ private function mapNode(array $nodeData): Node
private function mapRelationship(array $relationshipData): Relationship
{
return new Relationship(
- $relationshipData['_type'],
- $this->mapProperties($relationshipData['_properties'])
+ type: $relationshipData['_type'] ?? '',
+ properties: $this->mapProperties($relationshipData['_properties'] ?? [])
);
}
@@ -99,11 +96,7 @@ private function mapPath(array $pathData): Path
private function mapProperties(array $properties): array
{
- $mappedProperties = [];
- foreach ($properties as $key => $value) {
- $mappedProperties[$key] = $this->map($value);
- }
- return $mappedProperties;
+ return array_map([$this, 'map'], $properties);
}
}
diff --git a/src/Objects/Authentication.php b/src/Objects/Authentication.php
index e9347ceb..8848950b 100644
--- a/src/Objects/Authentication.php
+++ b/src/Objects/Authentication.php
@@ -2,11 +2,10 @@
namespace Neo4j\QueryAPI\Objects;
-use Exception;
-use Neo4j\QueryAPI\AuthenticateInterface;
-use Neo4j\QueryAPI\BasicAuthentication;
-use Neo4j\QueryAPI\BearerAuthentication;
-use Neo4j\QueryAPI\NoAuth;
+use Neo4j\QueryAPI\Authentication\AuthenticateInterface;
+use Neo4j\QueryAPI\Authentication\BasicAuthentication;
+use Neo4j\QueryAPI\Authentication\BearerAuthentication;
+use Neo4j\QueryAPI\Authentication\NoAuth;
/**
* @api
diff --git a/src/Objects/Bookmarks.php b/src/Objects/Bookmarks.php
index b1d0e3e7..c9cf7748 100644
--- a/src/Objects/Bookmarks.php
+++ b/src/Objects/Bookmarks.php
@@ -2,10 +2,12 @@
namespace Neo4j\QueryAPI\Objects;
+use JsonSerializable;
+
/**
* @api
*/
-class Bookmarks implements \Countable
+class Bookmarks implements \Countable, JsonSerializable
{
public function __construct(private array $bookmarks)
{
@@ -28,4 +30,9 @@ public function count(): int
{
return count($this->bookmarks);
}
+
+ public function jsonSerialize(): array
+ {
+ return $this->bookmarks;
+ }
}
diff --git a/src/Objects/ProfiledQueryPlan.php b/src/Objects/ProfiledQueryPlan.php
index c2d018e1..031861b2 100644
--- a/src/Objects/ProfiledQueryPlan.php
+++ b/src/Objects/ProfiledQueryPlan.php
@@ -2,141 +2,20 @@
namespace Neo4j\QueryAPI\Objects;
-/**
- * @api
- */
-class ProfiledQueryPlan
+final class ProfiledQueryPlan
{
- private int $dbHits;
- private int $records;
- private bool $hasPageCacheStats;
- private int $pageCacheHits;
- private int $pageCacheMisses;
- private float $pageCacheHitRatio;
- private int $time;
- private string $operatorType;
- private ProfiledQueryPlanArguments $arguments;
-
- /**
- * @var list
- */
- private array $children;
-
- /**
- * @var string[]
- */
- private array $identifiers;
-
public function __construct(
- ?int $dbHits,
- ?int $records,
- ?bool $hasPageCacheStats,
- ?int $pageCacheHits,
- ?int $pageCacheMisses,
- ?float $pageCacheHitRatio,
- ?int $time,
- ?string $operatorType,
- ProfiledQueryPlanArguments $arguments,
- ?array $children = [],
- array $identifiers = []
+ public readonly int $dbHits = 0,
+ public readonly int $records = 0,
+ public readonly bool $hasPageCacheStats = false,
+ public readonly int $pageCacheHits = 0,
+ public readonly int $pageCacheMisses = 0,
+ public readonly float $pageCacheHitRatio = 0.0,
+ public readonly int $time = 0,
+ public readonly string $operatorType = '',
+ public readonly ProfiledQueryPlanArguments $arguments,
+ public readonly array $children = [],
+ public readonly array $identifiers = []
) {
- $this->dbHits = $dbHits ?? 0;
- $this->records = $records ?? 0;
- $this->hasPageCacheStats = $hasPageCacheStats ?? false;
- $this->pageCacheHits = $pageCacheHits ?? 0;
- $this->pageCacheMisses = $pageCacheMisses ?? 0;
- $this->pageCacheHitRatio = $pageCacheHitRatio ?? 0.0;
- $this->time = $time ?? 0;
- $this->operatorType = $operatorType ?? '';
- $this->arguments = $arguments;
- $this->children = $children ?? [];
- $this->identifiers = $identifiers;
- }
-
- public function getDbHits(): int
- {
- return $this->dbHits;
- }
-
- public function getRecords(): int
- {
- return $this->records;
- }
-
-
- public function hasPageCacheStats(): bool
- {
- return $this->hasPageCacheStats;
- }
-
- public function getPageCacheHits(): int
- {
- return $this->pageCacheHits;
- }
-
-
- public function getPageCacheMisses(): int
- {
- return $this->pageCacheMisses;
- }
-
- public function getPageCacheHitRatio(): float
- {
- return $this->pageCacheHitRatio;
- }
-
-
- public function getTime(): int
- {
- return $this->time;
- }
-
- public function getOperatorType(): string
- {
- return $this->operatorType;
- }
-
- public function getArguments(): ProfiledQueryPlanArguments
- {
- return $this->arguments;
- }
-
- /**
- * @return list
- */
-
-
- public function getChildren(): array
- {
- return $this->children;
- }
-
- public function addChild(ProfiledQueryPlan|ProfiledQueryPlanArguments $child): void
- {
- $this->children[] = $child;
- }
-
- /**
- * @return string[]
- */
-
- public function getIdentifiers(): array
- {
- return $this->identifiers;
- }
-
- /**
- * @param string[] $identifiers
- */
-
- public function setIdentifiers(array $identifiers): void
- {
- $this->identifiers = $identifiers;
- }
-
-
- public function addIdentifier(string $identifier): void
- {
- $this->identifiers[] = $identifier;
}
}
diff --git a/src/Objects/ResultSet.php b/src/Objects/ResultSet.php
deleted file mode 100644
index 0414260d..00000000
--- a/src/Objects/ResultSet.php
+++ /dev/null
@@ -1,65 +0,0 @@
-
- */
-
-/**
- * @api
- */
-/**
- * @template TKey
- * @template TValue
- * @implements IteratorAggregate
- */
-class ResultSet implements IteratorAggregate, Countable
-{
- /**
- * @param list $rows
- */
-
- public function __construct(
- private readonly array $rows,
- private ResultCounters $counters,
- private Bookmarks $bookmarks,
- private ?ProfiledQueryPlan $profiledQueryPlan = null,
- // private ?ProfiledQueryPlanArguments $profiledQueryPlanArguments = null
- ) {
- }
-
- public function getIterator(): Traversable
- {
- return new ArrayIterator($this->rows);
- }
-
- public function getQueryCounters(): ?ResultCounters
- {
- return $this->counters;
- }
-
- public function getProfiledQueryPlan(): ?ProfiledQueryPlan
- {
- return $this->profiledQueryPlan;
- }
- /**
- * @api
- */
- public function count(): int
- {
- return count($this->rows);
- }
-
- public function getBookmarks(): ?Bookmarks
- {
- return $this->bookmarks;
- }
-}
diff --git a/src/ResponseParser.php b/src/ResponseParser.php
new file mode 100644
index 00000000..fe1af5d6
--- /dev/null
+++ b/src/ResponseParser.php
@@ -0,0 +1,147 @@
+validateAndDecodeResponse($response);
+
+ $rows = $this->mapRows($data['data']['fields'] ?? [], $data['data']['values'] ?? []);
+ $counters = isset($data['counters']) ? $this->buildCounters($data['counters']) : null;
+ $bookmarks = $this->buildBookmarks($data['bookmarks'] ?? []);
+ $profiledQueryPlan = $this->buildProfiledQueryPlan($data['profiledQueryPlan'] ?? null);
+ $accessMode = $this->getAccessMode($data['accessMode'] ?? '');
+
+ return new ResultSet($rows, $counters, $bookmarks, $profiledQueryPlan, $accessMode);
+ }
+
+ private function validateAndDecodeResponse(ResponseInterface $response): array
+ {
+ if ($response->getStatusCode() >= 400) {
+ $errorResponse = json_decode((string)$response->getBody(), true);
+ throw Neo4jException::fromNeo4jResponse($errorResponse);
+ }
+
+ $contents = $response->getBody()->getContents();
+ $data = json_decode($contents, true);
+
+ if (!isset($data['data'])) {
+ throw new RuntimeException('Invalid response: "data" key missing or null.');
+ }
+
+ return $data;
+ }
+
+ private function mapRows(array $keys, array $values): array
+ {
+ return array_map(function ($row) use ($keys) {
+ $mapped = [];
+ foreach ($keys as $index => $key) {
+ $fieldData = $row[$index] ?? null;
+ if (is_string($fieldData)) {
+ $fieldData = ['$type' => 'String', '_value' => $fieldData];
+ }
+ $mapped[$key] = $this->ogm->map($fieldData);
+ }
+ return new ResultRow($mapped);
+ }, $values);
+ }
+
+ private function buildCounters(array $countersData): ResultCounters
+ {
+ return new ResultCounters(
+ containsUpdates: $countersData['containsUpdates'] ?? false,
+ nodesCreated: $countersData['nodesCreated'] ?? 0,
+ nodesDeleted: $countersData['nodesDeleted'] ?? 0,
+ propertiesSet: $countersData['propertiesSet'] ?? 0,
+ relationshipsCreated: $countersData['relationshipsCreated'] ?? 0,
+ relationshipsDeleted: $countersData['relationshipsDeleted'] ?? 0,
+ labelsAdded: $countersData['labelsAdded'] ?? 0,
+ labelsRemoved: $countersData['labelsRemoved'] ?? 0,
+ indexesAdded: $countersData['indexesAdded'] ?? 0,
+ indexesRemoved: $countersData['indexesRemoved'] ?? 0,
+ constraintsAdded: $countersData['constraintsAdded'] ?? 0,
+ constraintsRemoved: $countersData['constraintsRemoved'] ?? 0,
+ systemUpdates: $countersData['systemUpdates'] ?? 0,
+ );
+ }
+
+ private function buildBookmarks(array $bookmarksData): Bookmarks
+ {
+ return new Bookmarks($bookmarksData);
+ }
+
+ private function getAccessMode(string $accessModeData): AccessMode
+ {
+ return AccessMode::tryFrom($accessModeData) ?? AccessMode::WRITE;
+ }
+
+ private function buildProfiledQueryPlan(?array $queryPlanData): ?ProfiledQueryPlan
+ {
+ if (!$queryPlanData) {
+ return null;
+ }
+
+ $mappedArguments = array_map(function ($value) {
+ if (is_array($value) && array_key_exists('$type', $value) && array_key_exists('_value', $value)) {
+ return $this->ogm->map($value);
+ }
+ return $value;
+ }, $queryPlanData['arguments'] ?? []);
+
+ $queryArguments = new ProfiledQueryPlanArguments(
+ globalMemory: $mappedArguments['GlobalMemory'] ?? null,
+ plannerImpl: $mappedArguments['planner-impl'] ?? null,
+ memory: $mappedArguments['Memory'] ?? null,
+ stringRepresentation: $mappedArguments['string-representation'] ?? null,
+ runtime: $mappedArguments['runtime'] ?? null,
+ time: $mappedArguments['Time'] ?? null,
+ pageCacheMisses: $mappedArguments['PageCacheMisses'] ?? null,
+ pageCacheHits: $mappedArguments['PageCacheHits'] ?? null,
+ runtimeImpl: $mappedArguments['runtime-impl'] ?? null,
+ version: $mappedArguments['version'] ?? null,
+ dbHits: $mappedArguments['DbHits'] ?? null,
+ batchSize: $mappedArguments['batch-size'] ?? null,
+ details: $mappedArguments['Details'] ?? null,
+ plannerVersion: $mappedArguments['planner-version'] ?? null,
+ pipelineInfo: $mappedArguments['PipelineInfo'] ?? null,
+ runtimeVersion: $mappedArguments['runtime-version'] ?? null,
+ id: $mappedArguments['Id'] ?? null,
+ estimatedRows: $mappedArguments['EstimatedRows'] ?? null,
+ planner: $mappedArguments['planner'] ?? null,
+ rows: $mappedArguments['Rows'] ?? null
+ );
+ $children = array_map(fn ($child) => $this->buildProfiledQueryPlan($child), $queryPlanData['children'] ?? []);
+
+ return new ProfiledQueryPlan(
+ $queryPlanData['dbHits'] ?? 0,
+ $queryPlanData['records'] ?? 0,
+ $queryPlanData['hasPageCacheStats'] ?? false,
+ $queryPlanData['pageCacheHits'] ?? 0,
+ $queryPlanData['pageCacheMisses'] ?? 0,
+ $queryPlanData['pageCacheHitRatio'] ?? 0.0,
+ $queryPlanData['time'] ?? 0,
+ $queryPlanData['operatorType'] ?? '',
+ $queryArguments,
+ $children,
+ $queryPlanData['identifiers'] ?? []
+ );
+ }
+}
diff --git a/src/Results/ResultRow.php b/src/Results/ResultRow.php
index bf83793d..780640e6 100644
--- a/src/Results/ResultRow.php
+++ b/src/Results/ResultRow.php
@@ -2,17 +2,21 @@
namespace Neo4j\QueryAPI\Results;
+use ArrayIterator;
use BadMethodCallException;
-use Neo4j\QueryAPI\OGM;
+use Countable;
+use IteratorAggregate;
use OutOfBoundsException;
use ArrayAccess;
+use Traversable;
/**
* @template TKey of array-key
* @template TValue
* @implements ArrayAccess
+ * @implements IteratorAggregate
*/
-class ResultRow implements ArrayAccess
+class ResultRow implements ArrayAccess, Countable, IteratorAggregate
{
public function __construct(private array $data)
{
@@ -42,14 +46,19 @@ public function offsetUnset($offset): void
throw new BadMethodCallException("You can't Unset {$offset}.");
}
- /**
- * @api
- */
public function get(string $row): mixed
{
return $this->offsetGet($row);
}
+ public function count(): int
+ {
+ return count($this->data);
+ }
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->data);
+ }
}
diff --git a/src/Results/ResultSet.php b/src/Results/ResultSet.php
new file mode 100644
index 00000000..1000344c
--- /dev/null
+++ b/src/Results/ResultSet.php
@@ -0,0 +1,86 @@
+
+ */
+class ResultSet implements IteratorAggregate, Countable
+{
+ /**
+ * @param list $rows
+ */
+ public function __construct(
+ private readonly array $rows,
+ private readonly ?ResultCounters $counters = null,
+ private readonly Bookmarks $bookmarks,
+ private readonly ?ProfiledQueryPlan $profiledQueryPlan,
+ private readonly AccessMode $accessMode
+ ) {
+
+
+ }
+
+ /**
+ * @return Traversable
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->rows);
+ }
+ public function getQueryCounters(): ?ResultCounters
+ {
+ return $this->counters;
+ }
+
+ public function getProfiledQueryPlan(): ?ProfiledQueryPlan
+ {
+ return $this->profiledQueryPlan;
+ }
+
+ /**
+ * @api
+ */
+ public function count(): int
+ {
+ return count($this->rows);
+ }
+
+ public function getBookmarks(): ?Bookmarks
+ {
+ return $this->bookmarks;
+ }
+
+ public function getAccessMode(): ?AccessMode
+ {
+ return $this->accessMode;
+ }
+ public function getData(): array
+ {
+ return $this->rows;
+ }
+
+
+ // public function getImpersonatedUser(): ?ImpersonatedUser
+ // {
+ //
+ // }
+
+
+
+
+}
diff --git a/src/Transaction.php b/src/Transaction.php
index 49063949..9c50f4ce 100644
--- a/src/Transaction.php
+++ b/src/Transaction.php
@@ -2,14 +2,11 @@
namespace Neo4j\QueryAPI;
-use GuzzleHttp\Exception\GuzzleException;
use Neo4j\QueryAPI\Exception\Neo4jException;
-use Neo4j\QueryAPI\Objects\Authentication;
-use Neo4j\QueryAPI\Objects\Bookmarks;
-use Neo4j\QueryAPI\Objects\ResultCounters;
-use Neo4j\QueryAPI\Objects\ResultSet;
-use Neo4j\QueryAPI\Results\ResultRow;
+use Neo4j\QueryAPI\Results\ResultSet;
use Psr\Http\Client\ClientInterface;
+use Psr\Http\Client\RequestExceptionInterface;
+use Psr\Http\Message\ResponseInterface;
use stdClass;
/**
@@ -19,6 +16,8 @@ class Transaction
{
public function __construct(
private ClientInterface $client,
+ private ResponseParser $responseParser,
+ private Neo4jRequestFactory $requestFactory,
private string $clusterAffinity,
private string $transactionId
) {
@@ -26,92 +25,23 @@ public function __construct(
/**
* Execute a Cypher query within the transaction.
+ * @api
* @param string $query The Cypher query to be executed.
* @param array $parameters Parameters for the query.
* @return ResultSet The result rows in ResultSet format.
- * @throws Neo4jException|GuzzleException If the response structure is invalid.
- *@api
+ * @throws Neo4jException If the response structure is invalid.
*/
public function run(string $query, array $parameters): ResultSet
{
- $response = $this->client->post("/db/neo4j/query/v2/tx/{$this->transactionId}", [
- 'headers' => [
- 'Authorization' => Authentication::basic('neo4j', '9lWmptqBgxBOz8NVcTJjgs3cHPyYmsy63ui6Spmw1d0')->getheader(),
- 'neo4j-cluster-affinity' => $this->clusterAffinity,
- ],
- 'json' => [
- 'statement' => $query,
- 'parameters' => empty($parameters) ? new stdClass() : $parameters,
- 'includeCounters' => true
- ],
- ]);
-
- $responseBody = $response->getBody()->getContents();
- $data = json_decode($responseBody, true);
-
- if (!isset($data['data']['fields'], $data['data']['values'])) {
- throw new Neo4jException([
- 'message' => 'Unexpected response structure from Neo4j',
- 'response' => $data,
- ]);
- }
+ $request = $this->requestFactory->buildTransactionRunRequest($query, $parameters, $this->transactionId, $this->clusterAffinity);
- $keys = $data['data']['fields'];
- $values = $data['data']['values'];
-
- if (empty($values)) {
- return new ResultSet(
- rows: [],
- counters: new ResultCounters(
- containsUpdates: $data['counters']['containsUpdates'],
- nodesCreated: $data['counters']['nodesCreated'],
- nodesDeleted: $data['counters']['nodesDeleted'],
- propertiesSet: $data['counters']['propertiesSet'],
- relationshipsCreated: $data['counters']['relationshipsCreated'],
- relationshipsDeleted: $data['counters']['relationshipsDeleted'],
- labelsAdded: $data['counters']['labelsAdded'],
- labelsRemoved: $data['counters']['labelsRemoved'],
- indexesAdded: $data['counters']['indexesAdded'],
- indexesRemoved: $data['counters']['indexesRemoved'],
- constraintsAdded: $data['counters']['constraintsAdded'],
- constraintsRemoved: $data['counters']['constraintsRemoved'],
- containsSystemUpdates: $data['counters']['containsSystemUpdates'],
- systemUpdates: $data['counters']['systemUpdates']
- ),
- bookmarks: new Bookmarks($data['bookmarks'] ?? [])
- );
+ try {
+ $response = $this->client->sendRequest($request);
+ } catch (RequestExceptionInterface $e) {
+ $this->handleRequestException($e);
}
- $ogm = new OGM();
- $rows = array_map(function ($resultRow) use ($ogm, $keys) {
- $data = [];
- foreach ($keys as $index => $key) {
- $fieldData = $resultRow[$index] ?? null;
- $data[$key] = $ogm->map($fieldData);
- }
- return new ResultRow($data);
- }, $values);
-
- return new ResultSet(
- rows: $rows,
- counters: new ResultCounters(
- containsUpdates: $data['counters']['containsUpdates'],
- nodesCreated: $data['counters']['nodesCreated'],
- nodesDeleted: $data['counters']['nodesDeleted'],
- propertiesSet: $data['counters']['propertiesSet'],
- relationshipsCreated: $data['counters']['relationshipsCreated'],
- relationshipsDeleted: $data['counters']['relationshipsDeleted'],
- labelsAdded: $data['counters']['labelsAdded'],
- labelsRemoved: $data['counters']['labelsRemoved'],
- indexesAdded: $data['counters']['indexesAdded'],
- indexesRemoved: $data['counters']['indexesRemoved'],
- constraintsAdded: $data['counters']['constraintsAdded'],
- constraintsRemoved: $data['counters']['constraintsRemoved'],
- containsSystemUpdates: $data['counters']['containsSystemUpdates'],
- systemUpdates: $data['counters']['systemUpdates']
- ),
- bookmarks: new Bookmarks($data['bookmarks'] ?? [])
- );
+ return $this->responseParser->parseRunQueryResponse($response);
}
/**
@@ -119,12 +49,9 @@ public function run(string $query, array $parameters): ResultSet
*/
public function commit(): void
{
- $this->client->post("/db/neo4j/query/v2/tx/{$this->transactionId}/commit", [
- 'headers' => [
- 'Authorization' => Authentication::basic('neo4j', '9lWmptqBgxBOz8NVcTJjgs3cHPyYmsy63ui6Spmw1d0')->getheader(),
- 'neo4j-cluster-affinity' => $this->clusterAffinity,
- ],
- ]);
+ $request = $this->requestFactory->buildCommitRequest($this->transactionId, $this->clusterAffinity);
+
+ $this->client->sendRequest($request);
}
/**
@@ -132,10 +59,24 @@ public function commit(): void
*/
public function rollback(): void
{
- $this->client->delete("/db/neo4j/query/v2/tx/{$this->transactionId}", [
- 'headers' => [
- 'neo4j-cluster-affinity' => $this->clusterAffinity,
- ],
- ]);
+ $request = $this->requestFactory->buildRollbackRequest($this->transactionId, $this->clusterAffinity);
+
+ $this->client->sendRequest($request);
+ }
+
+ /**
+ * Handles request exceptions by parsing error details and throwing a Neo4jException.
+ *
+ * @throws Neo4jException
+ */
+ private function handleRequestException(RequestExceptionInterface $e): void
+ {
+ $response = $e->getResponse();
+ if ($response instanceof ResponseInterface) {
+ $errorResponse = json_decode((string)$response->getBody(), true);
+ throw Neo4jException::fromNeo4jResponse($errorResponse, $e);
+ }
+
+ throw new Neo4jException(['message' => $e->getMessage()], 500, $e);
}
}
diff --git a/tests/Integration/Neo4jOGMTest.php b/tests/Integration/Neo4jOGMTest.php
index a1ff0dc2..1d8f6ab0 100644
--- a/tests/Integration/Neo4jOGMTest.php
+++ b/tests/Integration/Neo4jOGMTest.php
@@ -146,7 +146,6 @@ public function testWithWGS84_2DPoint(): void
public function testWithWGS84_3DPoint(): void
{
-
$point = $this->ogm->map([
'$type' => 'Point',
'_value' => 'SRID=4979;POINT Z (12.34 56.78 100.5)',
diff --git a/tests/Integration/Neo4jQueryAPIIntegrationTest.php b/tests/Integration/Neo4jQueryAPIIntegrationTest.php
index 5d27d2b9..bd5f1f8c 100644
--- a/tests/Integration/Neo4jQueryAPIIntegrationTest.php
+++ b/tests/Integration/Neo4jQueryAPIIntegrationTest.php
@@ -6,20 +6,24 @@
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Response;
use Neo4j\QueryAPI\Exception\Neo4jException;
use Neo4j\QueryAPI\Neo4jQueryAPI;
use Neo4j\QueryAPI\Neo4jRequestFactory;
use Neo4j\QueryAPI\Objects\Authentication;
+use Neo4j\QueryAPI\Objects\Node;
use Neo4j\QueryAPI\Objects\ProfiledQueryPlan;
use Neo4j\QueryAPI\Objects\Bookmarks;
use Neo4j\QueryAPI\Objects\ResultCounters;
-use Neo4j\QueryAPI\Objects\ResultSet;
+use Neo4j\QueryAPI\OGM;
use Neo4j\QueryAPI\Results\ResultRow;
-use PHPUnit\Framework\Attributes\DataProvider;
+use Neo4j\QueryAPI\Results\ResultSet;
+use Nyholm\Psr7\Factory\Psr17Factory;
+use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
use PHPUnit\Framework\TestCase;
-use Neo4j\QueryAPI\Transaction;
-use Psr\Http\Client\RequestExceptionInterface;
+use Neo4j\QueryAPI\Enums\AccessMode;
+use Neo4j\QueryAPI\ResponseParser;
+use Neo4j\QueryAPI\Configuration;
+use GuzzleHttp\Psr7\Response;
/**
* @api
@@ -27,28 +31,59 @@
class Neo4jQueryAPIIntegrationTest extends TestCase
{
private Neo4jQueryAPI $api;
- /**
- * @api
- */
- /** @psalm-suppress UnusedProperty */
- private Neo4jRequestFactory $request ;
+
/**
* @throws GuzzleException
*/
public function setUp(): void
{
+ parent::setUp();
+
$this->api = $this->initializeApi();
$this->clearDatabase();
$this->populateTestData();
}
+ public function testParseRunQueryResponse(): void
+ {
+ $query = 'CREATE (n:TestNode {name: "Test"}) RETURN n';
+ $response = $this->api->run($query);
+ $bookmarks = $response->getBookmarks();
+
+ $this->assertEquals(new ResultSet(
+ rows: [
+ new ResultRow([
+ 'n' => new Node(
+ ['TestNode'],
+ [
+ 'name' => 'Test'
+ ]
+ )
+ ])
+ ],
+ counters: new ResultCounters(
+ containsUpdates: true,
+ nodesCreated: 1,
+ propertiesSet: 1,
+ labelsAdded:1
+ ),
+ bookmarks: $bookmarks,
+ profiledQueryPlan: null,
+ accessMode: AccessMode::WRITE
+ ), $response);
+ }
+
+ public function testInvalidQueryHandling()
+ {
+ $this->expectException(Neo4jException::class);
+
+ $this->api->run('INVALID CYPHER QUERY');
+ }
+
private function initializeApi(): Neo4jQueryAPI
{
- return Neo4jQueryAPI::login(
- getenv('NEO4J_ADDRESS'),
- Authentication::fromEnvironment(),
- );
+ return Neo4jQueryAPI::login(getenv('NEO4J_ADDRESS'), Authentication::fromEnvironment());
}
public function testCounters(): void
@@ -58,12 +93,9 @@ public function testCounters(): void
$this->assertEquals(1, $result->getQueryCounters()->getNodesCreated());
}
- /**
- * @throws Neo4jException
- * @throws RequestExceptionInterface
- */
public function testCreateBookmarks(): void
{
+ $this->api = $this->initializeApi();
$result = $this->api->run(cypher: 'CREATE (x:Node {hello: "world"})');
$bookmarks = $result->getBookmarks();
@@ -72,7 +104,9 @@ public function testCreateBookmarks(): void
$bookmarks->addBookmarks($result->getBookmarks());
- $result = $this->api->run(cypher: 'MATCH (x:Node {hello: "world2"}) RETURN x', bookmark: $bookmarks);
+ $result = $this->api->run(cypher: 'MATCH (x:Node {hello: "world2"}) RETURN x');
+
+ $bookmarks->addBookmarks($result->getBookmarks());
$this->assertCount(1, $result);
}
@@ -87,7 +121,6 @@ public function testProfileExistence(): void
public function testProfileCreateQueryExistence(): void
{
-
$query = "
PROFILE UNWIND range(1, 100) AS i
CREATE (:Person {
@@ -168,20 +201,18 @@ public function testProfileCreateWatchedWithFilters(): void
$this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found");
}
- /**
- * @throws Neo4jException
- * @throws RequestExceptionInterface
- */
- public function testProfileCreateKnowsBidirectionalRelationshipsMock(): void
+ public function testProfileCreateKnowsBidirectionalRelationships(): void
{
$query = "
- PROFILE UNWIND range(1, 100) AS i
- UNWIND range(1, 100) AS j
- MATCH (a:Person {id: i}), (b:Person {id: j})
- WHERE a.id < b.id AND rand() < 0.1
- CREATE (a)-[:KNOWS]->(b), (b)-[:KNOWS]->(a);
+ PROFILE UNWIND range(1, 100) AS i
+ UNWIND range(1, 100) AS j
+ MATCH (a:Person {id: i}), (b:Person {id: j})
+ WHERE a.id < b.id AND rand() < 0.1
+ CREATE (a)-[:KNOWS]->(b), (b)-[:KNOWS]->(a);
";
+ $result = $this->api->run($query);
+ $this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found");
$body = file_get_contents(__DIR__ . '/../resources/responses/complex-query-profile.json');
$mockSack = new MockHandler([
new Response(200, [], $body),
@@ -190,7 +221,17 @@ public function testProfileCreateKnowsBidirectionalRelationshipsMock(): void
$handler = HandlerStack::create($mockSack);
$client = new Client(['handler' => $handler]);
$auth = Authentication::fromEnvironment();
- $api = new Neo4jQueryAPI($client, $auth);
+
+ $api = new Neo4jQueryAPI(
+ $client,
+ new ResponseParser(new OGM()),
+ new Neo4jRequestFactory(
+ new Psr17Factory(),
+ new Psr17Factory(),
+ new Configuration('ABC'),
+ $auth
+ )
+ );
$result = $api->run($query);
@@ -215,32 +256,122 @@ public function testProfileCreateActedInRelationships(): void
$this->assertNotNull($result->getProfiledQueryPlan(), "profiled query plan not found");
}
-
public function testChildQueryPlanExistence(): void
{
$result = $this->api->run("PROFILE MATCH (n:Person {name: 'Alice'}) RETURN n.name");
$profiledQueryPlan = $result->getProfiledQueryPlan();
$this->assertNotNull($profiledQueryPlan);
- $this->assertNotEmpty($profiledQueryPlan->getChildren());
+ $this->assertNotEmpty($profiledQueryPlan->children);
- foreach ($profiledQueryPlan->getChildren() as $child) {
+ foreach ($profiledQueryPlan->children as $child) {
$this->assertInstanceOf(ProfiledQueryPlan::class, $child);
}
}
+ //
+ // public function testImpersonatedUserSuccess(): void
+ // {
+ // $this->markTestSkipped("stuck");
+ //
+ // $result = $this->api->run(
+ // "PROFILE MATCH (n:Person {name: 'Alice'}) RETURN n.name",
+ // [],
+ // $this->config->database,
+ // new Bookmarks([]),
+ // 'HAPPYBDAY'
+ // );
+ //
+ // $impersonatedUser = $result->getImpersonatedUser();
+ // $this->assertNotNull($impersonatedUser, "Impersonated user should not be null.");
+ // }
+
+ // //
+ // //
+ // public function testImpersonatedUserFailure(): void
+ // {
+ // $this->markTestSkipped("stuck");
+ // $this->expectException(Neo4jException::class);
+ //
+ //
+ // $this->api->run(
+ // "PROFILE MATCH (n:Person {name: 'Alice'}) RETURN n.name",
+ // [],
+ // 'neo4j',
+ // null,
+ // 'invalidUser'
+ // );
+ // }
+
+ // //
+ // #[DoesNotPerformAssertions]
+ // public function testRunWithWriteAccessMode(): void
+ // {
+ // $result = $this->api->run(
+ // "CREATE (n:Person {name: 'Alice'}) RETURN n",
+ // [],
+ // 'neo4j',
+ // null,
+ // null,
+ // AccessMode::WRITE
+ // );
+ //
+ // }
+ //
+ // #[DoesNotPerformAssertions]
+ // public function testRunWithReadAccessMode(): void
+ // {
+ // $result = $this->api->run(
+ // "MATCH (n) RETURN COUNT(n)",
+ // [],
+ // 'neo4j',
+ // null,
+ // null,
+ // AccessMode::READ
+ // );
+ // }
+
+ //
+ // public function testReadModeWithWriteQuery(): void
+ // {
+ // $this->expectException(Neo4jException::class);
+ // $this->expectExceptionMessage("Writing in read access mode not allowed. Attempted write to neo4j");
+ //
+ // try {
+ // $this->api->run(
+ // "CREATE (n:Test {name: 'Test Node'})",
+ // [],
+ // $this->config->database,
+ // new Bookmarks([]),
+ // null,
+ // AccessMode::WRITE
+ // );
+ // } catch (Neo4jException $e) {
+ // error_log('Caught expected Neo4jException: ' . $e->getMessage());
+ // throw $e;
+ // }
+ // }
+ //
+ //
+ // #[DoesNotPerformAssertions]
+ // public function testWriteModeWithReadQuery(): void
+ // {
+ // $this->api->run(
+ // "MATCH (n:Test) RETURN n",
+ // [],
+ // 'neo4j',
+ // null,
+ // null,
+ // AccessMode::WRITE
+ // //cos write encapsulates read
+ // );
+ // }
- /**
- * @throws GuzzleException
- */
private function clearDatabase(): void
{
$this->api->run('MATCH (n) DETACH DELETE n', []);
}
- /**
- * @throws GuzzleException
- */
private function populateTestData(): void
{
$names = ['bob1', 'alicy'];
@@ -283,7 +414,9 @@ public function testWithExactNames(): void
new ResultRow(['n.name' => 'alicy']),
],
new ResultCounters(),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run('MATCH (n:Person) WHERE n.name IN $names RETURN n.name', [
@@ -292,9 +425,9 @@ public function testWithExactNames(): void
$this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters());
$this->assertEquals(iterator_to_array($expected), iterator_to_array($results));
- $this->assertCount(1, $results->getBookmarks());
}
+
public function testWithSingleName(): void
{
$expected = new ResultSet(
@@ -302,7 +435,9 @@ public function testWithSingleName(): void
new ResultRow(['n.name' => 'bob1']),
],
new ResultCounters(),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run('MATCH (n:Person) WHERE n.name = $name RETURN n.name', [
@@ -326,11 +461,13 @@ public function testWithInteger(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run('CREATE (n:Person {age: $age}) RETURN n.age', [
- 'age' => '30'
+ 'age' => 30
]);
$this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters());
@@ -338,6 +475,7 @@ public function testWithInteger(): void
$this->assertCount(1, $results->getBookmarks());
}
+
public function testWithFloat(): void
{
$expected = new ResultSet(
@@ -350,7 +488,9 @@ public function testWithFloat(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run('CREATE (n:Person {height: $height}) RETURN n.height', [
@@ -374,7 +514,9 @@ public function testWithNull(): void
propertiesSet: 0,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run('CREATE (n:Person {middleName: $middleName}) RETURN n.middleName', [
@@ -383,7 +525,7 @@ public function testWithNull(): void
$this->assertEquals($expected->getQueryCounters(), $results->getQueryCounters());
$this->assertEquals(iterator_to_array($expected), iterator_to_array($results));
- $this->assertCount(1, $results->getBookmarks());
+ $this->assertCount(1, $results->getBookmarks()->getBookmarks());
}
public function testWithBoolean(): void
@@ -398,7 +540,9 @@ public function testWithBoolean(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run('CREATE (n:Person {isActive: $isActive}) RETURN n.isActive', [
@@ -422,7 +566,9 @@ public function testWithString(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run('CREATE (n:Person {name: $name}) RETURN n.name', [
@@ -447,7 +593,9 @@ public function testWithArray(): void
propertiesSet: 0,
labelsAdded: 0,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -473,7 +621,9 @@ public function testWithDate(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -499,7 +649,9 @@ public function testWithDuration(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -524,7 +676,9 @@ public function testWithWGS84_2DPoint(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -555,7 +709,9 @@ public function testWithWGS84_3DPoint(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -586,7 +742,9 @@ public function testWithCartesian2DPoint(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -616,7 +774,9 @@ public function testWithCartesian3DPoint(): void
propertiesSet: 1,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -644,11 +804,11 @@ public function testWithNode(): void
'properties' => [
'name' => 'Ayush',
'location' => 'New York',
- 'age' => '30'
+ 'age' => '30'
],
- 'labels' => [
- 0 => 'Person'
- ]
+ 'labels' => [
+ 0 => 'Person'
+ ]
]
]),
@@ -659,7 +819,9 @@ public function testWithNode(): void
propertiesSet: 3,
labelsAdded: 1,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -703,7 +865,9 @@ public function testWithPath(): void
relationshipsCreated: 1,
labelsAdded: 2,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -740,7 +904,9 @@ public function testWithMap(): void
propertiesSet: 0,
labelsAdded: 0,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -785,7 +951,9 @@ public function testWithRelationship(): void
relationshipsCreated: 1,
labelsAdded: 2,
),
- new Bookmarks([])
+ new Bookmarks([]),
+ null,
+ AccessMode::WRITE
);
$results = $this->api->run(
@@ -810,6 +978,4 @@ public function testWithRelationship(): void
$this->assertEquals(iterator_to_array($expected), iterator_to_array($results));
$this->assertCount(1, $results->getBookmarks());
}
-
-
}
diff --git a/tests/Unit/Neo4jExceptionUnitTest.php b/tests/Unit/Neo4jExceptionUnitTest.php
index 01969b02..caa0bee3 100644
--- a/tests/Unit/Neo4jExceptionUnitTest.php
+++ b/tests/Unit/Neo4jExceptionUnitTest.php
@@ -69,7 +69,7 @@ public function testFromNeo4jResponse(): void
$this->assertSame('Transaction', $exception->getSubType());
$this->assertSame('InvalidRequest', $exception->getName());
$this->assertSame('Transaction error occurred.', $exception->getMessage());
- $this->assertSame(500, $exception->getCode());
+ $this->assertSame(0, $exception->getCode());
}
/**
@@ -85,7 +85,6 @@ public function testFromNeo4jResponseWithMissingDetails(): void
$this->assertSame('UnknownError', $exception->getType());
$this->assertNull($exception->getSubType());
$this->assertNull($exception->getName());
- $this->assertSame('An unknown error occurred.', $exception->getMessage());
$this->assertSame(0, $exception->getCode());
}
@@ -102,7 +101,6 @@ public function testFromNeo4jResponseWithNullResponse(): void
$this->assertSame('UnknownError', $exception->getType());
$this->assertNull($exception->getSubType(), "Expected 'getSubType()' to return null for null response");
$this->assertNull($exception->getName(), "Expected 'getName()' to return null for null response");
- $this->assertSame('An unknown error occurred.', $exception->getMessage());
$this->assertSame(0, $exception->getCode());
}
diff --git a/tests/Unit/Neo4jQueryAPIUnitTest.php b/tests/Unit/Neo4jQueryAPIUnitTest.php
index f0b5e310..bdb4ce95 100644
--- a/tests/Unit/Neo4jQueryAPIUnitTest.php
+++ b/tests/Unit/Neo4jQueryAPIUnitTest.php
@@ -6,16 +6,25 @@
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Neo4j\QueryAPI\Neo4jQueryAPI;
+use Neo4j\QueryAPI\Neo4jRequestFactory;
use Neo4j\QueryAPI\Objects\Authentication;
use Neo4j\QueryAPI\Objects\Bookmarks;
use Neo4j\QueryAPI\Objects\ResultCounters;
-use Neo4j\QueryAPI\Objects\ResultSet;
+use Neo4j\QueryAPI\OGM;
use Neo4j\QueryAPI\Results\ResultRow;
-use Neo4j\QueryAPI\AuthenticateInterface;
+use Neo4j\QueryAPI\Results\ResultSet;
+use Nyholm\Psr7\Factory\Psr17Factory;
+use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
use PHPUnit\Framework\TestCase;
+use Neo4j\QueryAPI\ResponseParser;
+use Neo4j\QueryAPI\Enums\AccessMode;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+use RuntimeException;
+use Neo4j\QueryAPI\Configuration;
+use Neo4j\QueryAPI\loginConfig;
/**
* @api
@@ -23,76 +32,125 @@
class Neo4jQueryAPIUnitTest extends TestCase
{
protected string $address;
+ protected ResponseParser $parser;
protected function setUp(): void
{
parent::setUp();
+
$this->address = getenv('NEO4J_ADDRESS');
+
+ $this->ogm = new OGM();
+ $this->parser = new ResponseParser($this->ogm);
}
- private function initializeApi(): Neo4jQueryAPI
+ public function testCorrectClientSetup(): void
{
- return Neo4jQueryAPI::login(
- $this->address,
- Authentication::fromEnvironment()
- );
+ $neo4jQueryAPI = Neo4jQueryAPI::login($this->address, Authentication::fromEnvironment());
+ $this->assertInstanceOf(Neo4jQueryAPI::class, $neo4jQueryAPI);
}
- public function testCorrectClientSetup(): void
+ #[DoesNotPerformAssertions]
+ public function testRunSuccess(): void
{
+ $mock = new MockHandler([
+ new Response(200, [], '{"data": {"fields": ["hello"], "values": [[{"$type": "String", "_value": "world"}]]}}'),
+ ]);
- $neo4jQueryAPI = $this->initializeApi();
+ $handlerStack = HandlerStack::create($mock);
+ $client = new Client(['handler' => $handlerStack]);
- $clientReflection = new \ReflectionClass(Neo4jQueryAPI::class);
+ $loginConfig = Authentication::fromEnvironment();
+ $queryConfig = new Configuration($this->address);
+ $responseParser = $this->createMock(ResponseParser::class);
- $clientProperty = $clientReflection->getProperty('client');
- $client = $clientProperty->getValue($neo4jQueryAPI);
- $this->assertInstanceOf(Client::class, $client);
+ $neo4jQueryAPI = new Neo4jQueryAPI($client, $responseParser, new Neo4jRequestFactory(
+ new Psr17Factory(),
+ new Psr17Factory(),
+ $queryConfig,
+ $loginConfig
+ ));
- $authProperty = $clientReflection->getProperty('auth');
- $auth = $authProperty->getValue($neo4jQueryAPI);
- $this->assertInstanceOf(AuthenticateInterface::class, $auth);
+ $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 5';
+ $neo4jQueryAPI->run($cypherQuery);
+ }
- $expectedAuth = Authentication::fromEnvironment();
- $this->assertEquals($expectedAuth->getHeader(), $auth->getHeader(), 'Authentication headers mismatch');
- $request = new Request('GET', '/test-endpoint');
- $authenticatedRequest = $auth->authenticate($request);
+ public function testParseValidResponse(): void
+ {
+ $mockStream = $this->createMock(StreamInterface::class);
+ $mockStream->method('getContents')->willReturn(json_encode([
+ 'data' => [
+ 'fields' => ['name'],
+ 'values' => [['Alice'], ['Bob']],
+ ],
+ 'counters' => ['nodesCreated' => 2],
+ 'bookmarks' => ['bm1'],
+ 'accessMode' => 'WRITE'
+ ]));
+
+ $mockResponse = $this->createMock(ResponseInterface::class);
+ $mockResponse->method('getBody')->willReturn($mockStream);
+
+ $result = $this->parser->parseRunQueryResponse($mockResponse);
+ $this->assertInstanceOf(ResultSet::class, $result);
+ $this->assertCount(2, $result->getIterator());
+ }
+ public function testParseInvalidResponse(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $mockStream = $this->createMock(StreamInterface::class);
+ $mockStream->method('getContents')->willReturn(json_encode(['data' => null]));
- $expectedAuthHeader = 'Basic ' . base64_encode(getenv("NEO4J_USERNAME") . ':' . getenv("NEO4J_PASSWORD"));
- $this->assertEquals($expectedAuthHeader, $authenticatedRequest->getHeaderLine('Authorization'));
+ $mockResponse = $this->createMock(ResponseInterface::class);
+ $mockResponse->method('getBody')->willReturn($mockStream);
- $requestWithHeaders = $authenticatedRequest->withHeader('Content-Type', 'application/vnd.neo4j.query');
- $this->assertEquals('application/vnd.neo4j.query', $requestWithHeaders->getHeaderLine('Content-Type'));
+ $this->parser->parseRunQueryResponse($mockResponse);
}
- /**
- * @throws GuzzleException
- */
- public function testRunSuccess(): void
+ public function testGetAccessMode(): void
{
- $mock = new MockHandler([
- new Response(200, ['X-Foo' => 'Bar'], '{"data": {"fields": ["hello"], "values": [[{"$type": "String", "_value": "world"}]]}}'),
- ]);
+ $mockStream = $this->createMock(StreamInterface::class);
+ $mockStream->method('getContents')->willReturn(json_encode([
+ 'data' => [
+ 'fields' => [],
+ 'values' => []
+ ],
+ 'accessMode' => 'WRITE'
+ ]));
+
+ $mockResponse = $this->createMock(ResponseInterface::class);
+ $mockResponse->method('getBody')->willReturn($mockStream);
+
+ $result = $this->parser->parseRunQueryResponse($mockResponse);
+ $this->assertInstanceOf(ResultSet::class, $result);
+ }
+ public function testParseBookmarks(): void
+ {
+ $mockStream = $this->createMock(StreamInterface::class);
+ $mockStream->method('getContents')->willReturn(json_encode([
+ 'data' => [
+ 'fields' => [],
+ 'values' => []
+ ],
+ 'bookmarks' => ['bm1', 'bm2', 'bm3']
+ ]));
- $auth = Authentication::fromEnvironment();
- $handlerStack = HandlerStack::create($mock);
- $client = new Client(['handler' => $handlerStack]);
+ $mockResponse = $this->createMock(ResponseInterface::class);
+ $mockResponse->method('getBody')->willReturn($mockStream);
- $neo4jQueryAPI = new Neo4jQueryAPI($client, $auth);
+ $result = $this->parser->parseRunQueryResponse($mockResponse);
- $cypherQuery = 'MATCH (n:Person) RETURN n LIMIT 5';
- $result = $neo4jQueryAPI->run($cypherQuery);
+ $this->assertInstanceOf(ResultSet::class, $result);
- $expectedResult = new ResultSet(
- [new ResultRow(['hello' => 'world'])],
- new ResultCounters(),
- new Bookmarks([])
- );
+ $bookmarks = $result->getBookmarks();
- $this->assertEquals($expectedResult, $result);
+ $this->assertInstanceOf(Bookmarks::class, $bookmarks);
+ $this->assertCount(3, $bookmarks->getBookmarks());
+ $this->assertEquals(['bm1', 'bm2', 'bm3'], $bookmarks->getBookmarks());
}
+
}
diff --git a/tests/Unit/Neo4jRequestFactoryTest.php b/tests/Unit/Neo4jRequestFactoryTest.php
index 9a668b22..a646b3ac 100644
--- a/tests/Unit/Neo4jRequestFactoryTest.php
+++ b/tests/Unit/Neo4jRequestFactoryTest.php
@@ -3,6 +3,7 @@
namespace Neo4j\QueryAPI\Tests\Unit;
use Exception;
+use Neo4j\QueryAPI\Configuration;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\RequestFactoryInterface;
@@ -64,10 +65,10 @@ public function testBuildRunQueryRequest()
$factory = new Neo4jRequestFactory(
$this->psr17Factory,
$this->streamFactory,
- $this->address,
- $this->authHeader
+ new Configuration($this->address),
+ Authentication::fromEnvironment(),
);
- $request = $factory->buildRunQueryRequest($database, $cypher, $parameters);
+ $request = $factory->buildRunQueryRequest($cypher, $parameters);
$this->assertEquals('POST', $request->getMethod());
$this->assertEquals($uri, (string) $request->getUri());
@@ -94,9 +95,10 @@ public function testBuildBeginTransactionRequest()
$factory = new Neo4jRequestFactory(
$this->psr17Factory,
$this->streamFactory,
- $this->address
+ new Configuration($this->address),
+ Authentication::fromEnvironment(),
);
- $request = $factory->buildBeginTransactionRequest($database);
+ $request = $factory->buildBeginTransactionRequest();
$this->assertEquals('POST', $request->getMethod());
$this->assertEquals($uri, (string) $request->getUri());
@@ -123,7 +125,8 @@ public function testBuildCommitRequest()
$factory = new Neo4jRequestFactory(
$this->psr17Factory,
$this->streamFactory,
- $this->address
+ new Configuration($this->address),
+ Authentication::fromEnvironment(),
);
$request = $factory->buildCommitRequest($database, $transactionId);
@@ -152,11 +155,12 @@ public function testBuildRollbackRequest()
$factory = new Neo4jRequestFactory(
$this->psr17Factory,
$this->streamFactory,
- $this->address
+ new Configuration($this->address),
+ Authentication::fromEnvironment(),
);
$request = $factory->buildRollbackRequest($database, $transactionId);
- $this->assertEquals('POST', $request->getMethod());
+ $this->assertEquals('DELETE', $request->getMethod());
$this->assertEquals($uri, (string) $request->getUri());
}
@@ -187,14 +191,14 @@ public function testCreateRequestWithHeadersAndBody()
$factory = new Neo4jRequestFactory(
$this->psr17Factory,
$this->streamFactory,
- $this->address,
- $this->authHeader
+ new Configuration($this->address),
+ Authentication::fromEnvironment(),
);
- $request = $factory->buildRunQueryRequest($database, $cypher, $parameters);
+ $request = $factory->buildRunQueryRequest($cypher, $parameters);
$this->assertEquals('application/json', $request->getHeaderLine('Content-Type'));
- $this->assertEquals('application/json', $request->getHeaderLine('Accept'));
+ $this->assertEquals('application/vnd.neo4j.query', $request->getHeaderLine('Accept'));
$this->assertEquals($this->authHeader, $request->getHeaderLine('Authorization'));
$this->assertJsonStringEqualsJsonString($payload, (string) $request->getBody());
}
@@ -226,13 +230,14 @@ public function testCreateRequestWithoutAuthorizationHeader()
$factory = new Neo4jRequestFactory(
$this->psr17Factory,
$this->streamFactory,
- $this->address
+ new Configuration($this->address),
+ Authentication::noAuth(),
);
- $request = $factory->buildRunQueryRequest($database, $cypher, $parameters);
+ $request = $factory->buildRunQueryRequest($cypher, $parameters);
$this->assertEquals('application/json', $request->getHeaderLine('Content-Type'));
- $this->assertEquals('application/json', $request->getHeaderLine('Accept'));
+ $this->assertEquals('application/vnd.neo4j.query', $request->getHeaderLine('Accept'));
$this->assertEmpty($request->getHeaderLine('Authorization'));
$this->assertJsonStringEqualsJsonString($payload, (string) $request->getBody());
}
diff --git a/tests/resources/expected/complex-query-profile.php b/tests/resources/expected/complex-query-profile.php
index 378a53d0..d6a4e169 100644
--- a/tests/resources/expected/complex-query-profile.php
+++ b/tests/resources/expected/complex-query-profile.php
@@ -1,10 +1,11 @@